利用 HTTP 長連線(Long Polling / Comet),瀏覽器發起請求後伺服器保持不回應,直到有新訊息或逾時,實現「伺服器推播」效果。本範例採用 iframe + 無限循環讀取文字檔的極簡架構,適合學習原理,生產環境建議使用 WebSocket 或 Server-Sent Events。
<?php
header('Cache-Control: private');
header('Content-Type: text/html; charset=utf-8');
?>
<html>
<script>
function submitChat(form) {
form.submit();
form.content.value = '';
}
</script>
<iframe src="chat_content.php" height="300" width="100%"></iframe>
<iframe name="say" height="0" width="0"></iframe>
<form method="POST" target="say" action="say.php" onsubmit="submitChat(this)">
<input type="text" size="30" name="content" />
<input type="button" value="說" onclick="submitChat(this.form)" />
</form>
</html>
<?php
$content = trim($_POST['content']);
if ($content) {
$fp = fopen('./chat.txt', 'a');
fwrite($fp, $content . "\n");
fclose($fp);
clearstatcache();
}
?>
<?php
header('Cache-Control: private');
header('Content-Type: text/html; charset=utf-8');
set_time_limit(30); // 測試用 30 秒,實際應設更長或 0
echo str_repeat(' ', 256); // IE 需先輸出 256 字元才會顯示後續內容
ob_flush(); flush();
$fp = new SplFileObject('./chat.txt', 'r+');
$totalLine = 0;
while (!$fp->eof()) { $fp->current(); $totalLine++; $fp->next(); }
$fp->seek($totalLine); // 移至檔案末尾
$lastLine = $totalLine - 1;
while (true) {
if (!$fp->eof()) {
$content = trim($fp->current());
if ($content !== '') {
echo '<div>' . htmlspecialchars($content) . '</div>';
flush(); // 立即推送給瀏覽器
$fp->next();
$lastLine++;
}
} else {
$fp->seek($lastLine - 1); // 避免 eof 狀態卡住
$fp->next();
}
// 這裡可加入心跳檢測:若客戶端斷線超過 N 秒則 break
usleep(1000); // 降低 CPU 佔用
}
?>
Timeout、Nginx proxy_read_timeout、PHP-FPM request_terminate_timeout 等多層設定,需同步調整。ob_flush(); flush(); 強制將資料送至瀏覽器,否則會被緩衝區攔截。seek()、current()、next()、eof() 等介面,比傳統 fgets() 更適合隨機存取大檔案。flock() 或改用 Redis List / MySQL 防止併發寫入損毀。MaxClients。建議改用 Nginx + PHP-FPM 或 Swoole/Workerman 事件驅動架構。iframe 為 fetch + ReadableStream 或 EventSource(SSE),體驗更佳。本範例已整理為獨立專案,包含心跳檢測、Redis 支援、Docker 部署腳本,歡迎至 GitHub 取用。