聊天室是 Web 即時通訊的經典入門專案。本文以傳統「iframe 輪詢」架構為基礎,完整呈現資料庫設計、後端 PHP 邏輯、前端框架結構,並探討現代 WebSocket/SSE 替代方案。
chat 表。CREATE DATABASE `chatroom` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `chatroom`;
CREATE TABLE `chat` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`nickname` VARCHAR(20) NOT NULL COMMENT '暱稱',
`content` VARCHAR(500) NOT NULL COMMENT '發言內容',
`created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_created` (`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
// login.php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['nickname'])) {
$nick = trim($_POST['nickname']);
// 簡單過濾
$nick = htmlspecialchars($nick, ENT_QUOTES, 'UTF-8');
setcookie('chat_nick', $nick, time() + 86400, '/'); // 1 天有效
header('Location: main.php');
exit;
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>聊天室登入</title></head>
<body>
<h2>請輸入暱稱進入聊天室</h2>
<form method="post">
<input type="text" name="nickname" maxlength="20" required placeholder="暱稱">
<button type="submit">登入</button>
</form>
</body></html>
<?php
// main.php
if (empty($_COOKIE['chat_nick'])) {
header('Location: login.php');
exit;
}
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>PHP 聊天室</title>
<style>
html, body { height: 100%; margin: 0; overflow: hidden; }
frameset { border: none; }
</style></head>
<frameset rows="80%, *" cols="*">
<frame src="display.php" name="display">
<frame src="speak.php" name="speak">
</frameset>
</html>
<?php
// display.php
header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-cache, must-revalidate');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
$pdo = new PDO('mysql:host=localhost;dbname=chatroom;charset=utf8mb4', 'root', '');
// 取得最新 20 筆 (按時間倒序,再反轉顯示)
$stmt = $pdo->query("
SELECT nickname, content, created
FROM (
SELECT * FROM chat ORDER BY created DESC LIMIT 20
) t ORDER BY created ASC
");
$messages = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 清理舊資料 (保留最近 100 筆)
$pdo->exec("DELETE FROM chat WHERE id NOT IN (SELECT id FROM (SELECT id FROM chat ORDER BY created DESC LIMIT 100) t)");
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8">
<meta http-equiv="refresh" content="3"> <!-- 3 秒自動重新整理 -->
<style>
body { font-family: sans-serif; padding: 10px; background: #f9f9f9; }
.msg { margin: 5px 0; padding: 5px; background: #fff; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,.1); }
.nick { color: #0066cc; font-weight: bold; }
.time { color: #999; font-size: 0.85em; margin-left: 8px; }
</style></head>
<body>
<div id="chat-area">
<?php foreach ($messages as $m): ?>
<div class="msg">
<span class="nick"><?php echo htmlspecialchars($m['nickname']); ?></span>
<span class="time"><?php echo date('H:i:s', strtotime($m['created'])); ?></span>
<br><?php echo nl2br(htmlspecialchars($m['content'])); ?>
</div>
<?php endforeach; ?>
</div>
<script>
// 自動捲動到底部
window.onload = function() { window.scrollTo(0, document.body.scrollHeight); };
</script>
</body></html>
<?php
// speak.php
session_start(); // 可選:用 session 驗證防灌水
header('Content-Type: text/html; charset=utf-8');
$pdo = new PDO('mysql:host=localhost;dbname=chatroom;charset=utf8mb4', 'root', '');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['content'])) {
$nick = isset($_COOKIE['chat_nick']) ? $_COOKIE['chat_nick'] : '匿名';
$content = trim($_POST['content']);
$content = mb_substr($content, 0, 500); // 限制長度
$stmt = $pdo->prepare("INSERT INTO chat (nickname, content) VALUES (?, ?)");
$stmt->execute([$nick, $content]);
// 重導回 speak.php 避免重複提交 (PRG 模式)
header('Location: speak.php');
exit;
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>發言</title>
<style>
body { padding: 10px; margin: 0; background: #eee; }
form { display: flex; gap: 5px; }
input[type=text] { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 8px 16px; background: #0066cc; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #0055aa; }
</style></head>
<body>
<form method="post">
<input type="text" name="content" placeholder="輸入訊息..." maxlength="500" required autofocus>
<button type="submit">發送</button>
</form>
</body></html>
| 問題 | 解決方案 |
|---|---|
| iframe 輪詢造成閃爍/歷史紀錄污染 | 使用 meta refresh 或 JavaScript setInterval + AJAX 局部更新 (單頁應用模式)。 |
| 訊息延遲 (3 秒輪詢間隔) | 採用 長輪詢 或 Server-Sent Events (SSE) 實現準即時推送。 |
| 資料表無限增大 | 定期清理 (如保留最近 1000 筆)、分表、或啟用 MySQL 事件排程自動歸檔。 |
| XSS 攻擊 | 輸出時 htmlspecialchars(),儲存前 strip_tags() 或使用 HTMLPurifier。 |
| 灌水/頻率限制 | Session/IP 記錄最後發言時間,限制最小間隔 (如 2 秒)。 |