从零构建:用PHP打造一个轻量级实时聊天室(长轮询实战指南)
目录导读
- 为什么用PHP写聊天室?——技术选型与适用场景
- 架构设计:没有WebSocket,如何实现“伪实时”?
- 数据库设计:MySQL与文件存储的权衡
- 核心代码拆解:从登录到消息广播的完整链路
- 安全性加固:防XSS与SQL注入的必备姿势
- 压力测试与性能优化:当人数超过100人怎么办?
- 高频问答(FAQ):踩坑与解决方案
为什么用PHP写聊天室?——技术选型与适用场景

尽管Node.js和Go常被视为实时应用的“标准答案”,但PHP依然在特定场景下拥有不可替代的价值,对于中小型项目(如企业内部工具、在线教育答疑、官网客服系统),PHP的部署简单性(无需编译环境)和生态成熟度(直接兼容主流虚拟主机)是巨大优势,通过长轮询(Long Polling) 机制,PHP完全可以实现接近WebSocket的体验,且无需额外安装扩展——这正是本教程的核心。
架构设计:没有WebSocket,如何实现“伪实时”?
聊天室的核心痛点是“消息推送”,本文采用长轮询方案:客户端发送请求到服务器后,服务器保持连接挂起(默认30秒),直到有新消息或超时才返回结果,客户端收到响应后立即发起下一次请求,形成“准实时”循环,相比WebSocket,这种方案的优点是兼容所有PHP环境,缺点是服务器资源占用稍高,但在百人级别下表现稳定。
关键流程:
- 客户端A发消息 →
chat_send.php写入数据库并标记未读 - 客户端B的
chat_poll.php检测到新消息ID → 立即返回JSON数据
数据库设计:MySQL与文件存储的权衡
为兼顾可靠性与简易性,推荐使用MySQL存储消息(字段:msg_id INT自增主键、user_name VARCHAR、content TEXT、created_at TIMESTAMP),而在线用户列表使用文件缓存(如 online.txt)存储用户ID与最后心跳时间,每10秒更新一次,这种混合策略避免了频繁的数据库写操作。
表结构示例:
CREATE TABLE messages ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
核心代码拆解:从登录到消息广播的完整链路
(1)用户登录与Session管理:用户输入昵称后,PHP生成唯一user_token存入Cookie,同步写入online.txt文件(格式:用户ID|时间戳)。
(2)发送消息(send.php):
// 防XSS + 过滤空消息
$content = htmlspecialchars(trim($_POST['content']), ENT_QUOTES, 'UTF-8');
if (mb_strlen($content) > 500) { exit('消息过长'); }
// 插入数据库
$stmt = $pdo->prepare("INSERT INTO messages (username, content) VALUES (?, ?)");
$stmt->execute([$_SESSION['username'], $content]);
(3)长轮询接收(poll.php)——这是核心中的核心:
$lastId = (int)$_GET['last_id']; // 客户端已知的最后消息ID
$timeout = 30; // 最大挂起30秒
$start = time();
while (time() - $start < $timeout) {
// 查询比lastId更大的新消息
$sql = "SELECT * FROM messages WHERE id > ? ORDER BY id ASC LIMIT 50";
$stmt = $pdo->prepare($sql);
$stmt->execute([$lastId]);
$newMessages = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($newMessages)) {
// 将新消息转换为JSON并返回
echo json_encode(['status' => 'ok', 'messages' => $newMessages]);
exit; // 结束脚本
}
// 没新消息则休眠1秒后继续循环,节省CPU
sleep(1);
}
// 超时无新消息,返回空数组
echo json_encode(['status' => 'timeout', 'messages' => []]);
(4)前端JS轮询循环(使用fetch):
function poll() {
fetch(`poll.php?last_id=${lastId}`)
.then(response => response.json())
.then(data => {
if (data.messages.length > 0) {
// 追加消息到页面
lastId = data.messages[data.messages.length-1].id;
}
poll(); // 递归调用,形成无限循环
}).catch(() => setTimeout(poll, 3000)); // 错误重试
}
安全性加固:防XSS与SQL注入的必备姿势
- XSS防护:所有用户输入在输出前必须经过
htmlspecialchars()处理,包括昵称和消息内容。 - SQL注入:严格使用PDO预处理语句(如上文代码所示),禁止拼接SQL。
- CSRF令牌:为发送消息的表单添加一次性随机令牌,防止跨站请求伪造。
- 速率限制:记录用户IP,同一IP每10秒只能发3条消息,防止刷屏。
压力测试与性能优化:当人数超过100人怎么办?
使用Apache ab 工具模拟并发连接,当并发数达到200时,默认配置会出现连接超时,优化策略:
- 开启OPcache:将PHP代码编译缓存到内存,可提升30%性能。
- 数据库索引:为
messages.id和created_at添加索引,加速查询。 - 改用Redis:将消息暂存于Redis的List结构中,利用其阻塞式
BRPOP命令替代MySQL长轮询,可轻松支持2000人并发。
高频问答(FAQ)
Q1:长轮询和WebSocket到底差在哪? A:WebSocket是双向长连接,服务器可主动推送;长轮询本质是HTTP请求的“变体”,每次都需要重新建立连接,但长轮询的兼容性极好(IE8都能跑),且PHP实现WebSocket需要安装Swoole等扩展,成本更高。
Q2:用户关闭页面后,服务器还在挂起连接吗?
A:是的,PHP会继续执行直到超时(30秒)或返回数据,建议设置 ignore_user_abort(false) 让脚本在客户端断开时立即终止,避免资源浪费。
Q3:如何显示“XX正在输入...”的状态?
A:可以复用 online.txt 文件,通过监听输入框的 keydown 事件,每3秒发送一个心跳包(包含打字状态标志),轮询接口读取该状态并广播。
Q4:如何清理历史消息?
A:编写一个Cron定时任务,每天凌晨删除3天前的数据:DELETE FROM messages WHERE created_at < NOW() - INTERVAL 3 DAY。
这样一个基于PHP的长轮询聊天室,已经可以完美支撑百人级别的实时交流,如果未来业务快速增长,只需将轮询层替换为Redis或WebSocket,底层数据库架构不需要大改,启动你的PHP环境,动手试试吧!