PHP 做个简单的聊天室

wen PHP项目 3

从零构建:用PHP打造一个轻量级实时聊天室(长轮询实战指南)


目录导读

  1. 为什么用PHP写聊天室?——技术选型与适用场景
  2. 架构设计:没有WebSocket,如何实现“伪实时”?
  3. 数据库设计:MySQL与文件存储的权衡
  4. 核心代码拆解:从登录到消息广播的完整链路
  5. 安全性加固:防XSS与SQL注入的必备姿势
  6. 压力测试与性能优化:当人数超过100人怎么办?
  7. 高频问答(FAQ):踩坑与解决方案

为什么用PHP写聊天室?——技术选型与适用场景

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.idcreated_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环境,动手试试吧!

抱歉,评论功能暂时关闭!