☰
PHP全开源聊天室源码实战:WebSocket实时消息与高并发架构
2026/10/9 14:51:51 网站建设 项目流程

简介:这是一套基于PHP与WebSocket技术构建的全开源H5聊天室源码,面向需要为网站或应用快速集成即时通讯功能的开发者,尤其适合具备一定PHP基础、希望省去从零搭建实时通信框架的人群。资源包共19个文件,约1.5MB,以9个php核心脚本为主,涵盖WebSocket服务端、用户与消息模型及上传接口,另含4个sql建表脚本、1个db数据库文件、1个css样式、1个js脚本以及搭建文档与说明文件,结构紧凑便于二次开发。目前已有126人学习下载。源码同时支持数据库与无数据库两种运行模式,可灵活适配不同存储需求,并借助WebSocket实现客户端与服务器的全双工实时通信。开发者可自由查看和修改全部代码,按需扩展功能、优化界面或提升性能,也可参考社区贡献快速定位问题,是搭建定制化聊天室的实用起点。

1. 一套 PHP 全开源聊天室源码,到底能扛住多少人同时在线

很多开发者第一次接触 PHP 全开源聊天室源码,都是被“H5 聊天室源码、实时消息聊天源码”这几个词吸引进来的——想着拿过来改改就能上线,结果一压测就翻车。我最早做的一个模拟项目,单机 Apache + PHP-FPM 跑轮询方案,50 个人同时发消息就开始出现明显延迟,消息乱序、重复推送全来了。问题不在 PHP 本身,而在于“实时消息”这四个字背后的技术选型:你是用 HTTP 轮询、长轮询,还是 WebSocket?存储用文件、MySQL 还是 Redis?这套源码能不能直接商用,取决于它把实时链路做在哪一层。这篇文章面向想用 PHP 全开源聊天室源码快速搭一套 H5 聊天室的开发者,从选型、部署、核心代码到压测避坑,把一条能复现的落地路径讲清楚,新手能照着跑通,熟手能看到参数边界和性能天花板。

2. 先定实时方案:轮询、长轮询还是 WebSocket,选错后面全白干

2.1 三种实时消息方案的延迟与资源账

在动手改任何一行 PHP 全开源聊天室源码之前,先把实时消息的传输方案定下来,这是整个 H5 聊天室的地基。常见做法有三种,我按实际压测数据给你算一笔账。

第一种是短轮询(Polling)。前端每隔 N 秒发一次 HTTP 请求问“有没有新消息”。实现最简单,任何 PHP 环境都能跑,但延迟等于轮询间隔,设 3 秒就是平均 1.5 秒延迟,设 1 秒服务器请求量直接翻倍。100 人在线、1 秒轮询,就是 100 QPS 的空请求,其中 95% 是“没有新消息”的无效查询。这种方案只适合演示,不适合真实聊天室。

第二种是长轮询(Long Polling)。前端发请求后,服务端 hold 住连接,有新消息才返回,超时再重连。延迟能压到接近实时,但每个在线用户会长期占用一个 PHP-FPM 进程。PHP-FPM 默认pm.max_children也就几十到几百,意味着同时在线人数直接被进程数卡死。我见过有人把max_children调到 2000,结果内存直接爆掉,这就是典型的血泪经验。

第三种是 WebSocket。一次握手后全双工通信,服务端主动推送,延迟最低、连接开销最小。但 PHP 本身不擅长常驻进程,需要配合 Swoole、Workerman 这类常驻内存的扩展或框架,或者用独立的 WebSocket 服务(比如 Node.js、Go)来扛连接,PHP 只负责业务逻辑和存储。

方案平均延迟1000 人在线资源占用实现难度适用场景
短轮询1~3 秒高(大量无效请求)低演示、低频通知
长轮询0.5~1 秒极高(进程被占满)中小规模、兼容性优先
WebSocket50~200 毫秒低(单进程多连接)高真实聊天室、生产环境

选型结论很明确:要做真正的实时消息聊天源码,WebSocket 是唯一能规模化的路。如果你的 PHP 全开源聊天室源码里只有 AJAX 轮询,那它本质是个“伪实时”Demo,别指望直接上线。

2.2 用 Workerman 给 PHP 聊天室接上 WebSocket 的最小改造

假设你手上这套 PHP 全开源聊天室源码是传统的 PHP + MySQL 结构,前端用 jQuery 发 AJAX。要把它升级成 WebSocket 实时推送,最省事的路径是引入 Workerman——纯 PHP 写的常驻内存框架,不需要装额外扩展,composer require workerman/workerman就能用。

下面是一个最小可跑的 WebSocket 服务端,负责接收消息、广播给所有在线连接,并把消息落库:

<?php // chat_server.php require_once __DIR__ . '/vendor/autoload.php'; use Workerman\Worker; use Workerman\Connection\TcpConnection; // 创建 WebSocket 服务,监听 0.0.0.0:8282 $ws_worker = new Worker("websocket://0.0.0.0:8282"); // 启动 4 个进程,利用多核 $ws_worker->count = 4; // 用连接 id 作为 key 维护在线用户 $ws_worker->onConnect = function (TcpConnection $connection) { // 连接建立时先不绑定用户,等客户端发登录消息 $connection->uid = null; }; $ws_worker->onMessage = function (TcpConnection $connection, $data) { $msg = json_decode($data, true); if (!$msg || !isset($msg['type'])) { return; } // 登录消息:绑定 uid if ($msg['type'] === 'login') { $connection->uid = (int)$msg['uid']; $connection->send(json_encode(['type' => 'login_ok'])); return; } // 聊天消息:落库 + 广播 if ($msg['type'] === 'chat' && $connection->uid) { $content = trim($msg['content'] ?? ''); if ($content === '') { return; } // 这里换成你的 PDO 连接,写入消息表 $pdo = new PDO('mysql:host=127.0.0.1;dbname=chat', 'user', 'pass'); $stmt = $pdo->prepare( 'INSERT INTO messages (uid, content, created_at) VALUES (?, ?, ?)' ); $stmt->execute([$connection->uid, $content, time()]); // 广播给所有在线连接 $payload = json_encode([ 'type' => 'chat', 'uid' => $connection->uid, 'content' => $content, 'time' => date('H:i:s'), ]); foreach ($ws_worker->connections as $conn) { if ($conn->uid !== null) { $conn->send($payload); } } } }; $ws_worker->onClose = function (TcpConnection $connection) { $connection->uid = null; }; Worker::runAll();

逻辑说明:onConnect阶段不急着绑定用户,因为 WebSocket 握手时拿不到可靠的用户身份,必须等客户端发第一条login消息再绑定,这是防止身份伪造的基本操作。onMessage里区分消息类型,chat类型先落库再广播,保证消息不丢。广播时遍历$ws_worker->connections,只推给已登录连接。

参数说明:count = 4表示开 4 个进程,一般设成 CPU 核数;websocket://0.0.0.0:8282里的端口要和前端连接地址一致,生产环境记得在前面挂 Nginx 做 TLS 卸载。启动命令是php chat_server.php start,调试用start -d后台运行,改代码后restart。

前端连接就三行核心代码:

// 前端建立 WebSocket 连接 const ws = new WebSocket('ws://your-domain:8282'); ws.onopen = () => { // 连接成功后发送登录消息,绑定用户身份 ws.send(JSON.stringify({ type: 'login', uid: CURRENT_UID })); }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); if (msg.type === 'chat') { // 把消息追加到聊天列表 appendMessage(msg); } };

注意:生产环境必须用wss://,否则浏览器在 HTTPS 页面下会拦截ws://连接。这一步是新手最容易踩的坑,本地测试好好的,一上线就“连不上”,八成是混合内容被拦了。

3. 消息存储与历史记录:MySQL 表结构怎么设计才不拖慢实时链路

3.1 消息表、会话表、用户表的字段与索引

实时链路跑通后,下一个瓶颈就是存储。很多 PHP 全开源聊天室源码把消息直接写文件,单机小规模能用,一旦要查历史记录、做分页、支持多房间,文件方案立刻崩。正确做法是 MySQL 存消息,Redis 做在线状态和最近消息缓存。

消息表设计要围绕“按会话查最近 N 条”这个最高频查询来建索引。下面是我一般会用的表结构:

-- 消息表:只增不改,按会话 + 时间查询 CREATE TABLE `messages` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `room_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '房间/会话 id', `uid` INT UNSIGNED NOT NULL COMMENT '发送者用户 id', `content` TEXT NOT NULL COMMENT '消息内容', `msg_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1文本 2图片 3系统', `created_at` INT UNSIGNED NOT NULL COMMENT 'Unix 时间戳', PRIMARY KEY (`id`), KEY `idx_room_time` (`room_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 在线状态放 Redis,不落 MySQL -- key: online:room:{room_id} value: set of uid -- key: user:last_seen:{uid} value: timestamp

逻辑说明:idx_room_time这个联合索引是关键,查询“某房间最近 50 条”时WHERE room_id = ? ORDER BY created_at DESC LIMIT 50能直接走索引,不用全表扫。content用 TEXT 而不是 VARCHAR(255),因为聊天消息可能带表情、链接,长度不可控。created_at用 INT 存时间戳而不是 DATETIME,索引体积更小,范围查询更快。

参数说明:如果消息量很大(日增百万级),单表会越来越慢,常见做法是按月分表,比如messages_202501,查询时根据时间路由到对应表。分表键选room_id还是时间,取决于你的查询模式——如果主要是查最近消息,按时间分表更合适。

3.2 历史消息分页:游标分页替代 offset 分页

新手写历史消息分页,第一反应是LIMIT offset, size。这个写法在消息表上会越翻越慢,因为offset很大时 MySQL 要扫描并丢弃前面所有行。100 万条消息翻到第 1000 页,查询直接卡死。

正确做法是游标分页(Cursor Pagination),用上一页最后一条的id作为游标:

<?php // 拉取历史消息:传入 last_id(上一页最后一条的 id),首次传 0 function getHistory(PDO $pdo, int $roomId, int $lastId = 0, int $size = 20): array { if ($lastId > 0) { // 有游标:查比 last_id 更早的消息 $sql = 'SELECT id, uid, content, created_at FROM messages WHERE room_id = ? AND id < ? ORDER BY id DESC LIMIT ?'; $stmt = $pdo->prepare($sql); $stmt->execute([$roomId, $lastId, $size]); } else { // 首次加载:查最新消息 $sql = 'SELECT id, uid, content, created_at FROM messages WHERE room_id = ? ORDER BY id DESC LIMIT ?'; $stmt = $pdo->prepare($sql); $stmt->execute([$roomId, $size]); } $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); // 返回时反转,让前端按时间正序渲染 return array_reverse($rows); }

逻辑说明:用id < last_id代替offset,每次查询都从索引定位,翻到第几页都是同样的速度。ORDER BY id DESC配合主键索引,效率最高。返回前array_reverse是因为查询是倒序取最新,但前端渲染要正序。

参数说明:size建议 20~50,太大单次响应体大,太小请求频繁。last_id由前端保存,每次加载更多时带上。这套分页方式在千万级消息表上依然稳定,是实时消息聊天源码必须做对的一环。

4. 避坑与排查:PHP 聊天室上线后最容易翻车的 5 个点

4.1 消息重复推送与乱序

现象:用户收到两条一样的消息,或者后发的消息先显示。原因通常是广播逻辑里对同一连接发了多次,或者多进程模式下各进程的connections不共享。Workerman 多进程时,每个进程维护自己的连接列表,A 进程收到的消息广播时只能推给 A 进程的连接,B 进程的连接收不到,于是有人收不到、有人收到重复。

解决:多进程广播必须走进程间通信。Workerman 提供GatewayWorker或Channel组件做跨进程推送。简单做法是把count设为 1 先验证逻辑,确认无误后再引入 Channel 组件。乱序问题则要在前端按消息id或时间戳排序后再渲染,不能收到就 append。

4.2 连接数上不去,卡在 1024

现象:在线人数一到 1000 左右就大量掉线。原因:Linux 默认单进程文件描述符限制是 1024,WebSocket 每个连接占一个 fd。解决:调大ulimit -n,在/etc/security/limits.conf里加* soft nofile 65535和* hard nofile 65535,重启后ulimit -n确认生效。同时 Workerman 启动时可以设$ws_worker->count配合系统 fd 上限,别让单进程扛太多连接。

4.3 消息落库拖慢广播

现象:消息一多,广播延迟明显上升。原因:onMessage里同步执行了 MySQL 写入,写库慢就阻塞广播。解决:把落库改成异步,用 Redis 队列缓冲,后台进程消费入库;或者至少把广播放在落库之前,先保证实时性,落库失败再补偿。我一般会先广播再落库,因为聊天场景里“实时看到”比“绝对不丢”优先级更高。

4.4 前端断线不重连

现象:网络抖动后用户收不到消息,刷新页面才恢复。原因:前端没做 WebSocket 断线重连。解决:在onclose里加指数退避重连,第一次 1 秒后重连,失败则 2 秒、4 秒、8 秒,上限 30 秒。重连成功后要重新发login绑定身份,并拉取断线期间的历史消息补上。

let retry = 0; function connect() { const ws = new WebSocket('wss://your-domain/ws'); ws.onopen = () => { retry = 0; ws.send(JSON.stringify({type:'login', uid: CURRENT_UID})); }; ws.onclose = () => { // 指数退避重连,上限 30 秒 const delay = Math.min(1000 * Math.pow(2, retry), 30000); retry++; setTimeout(connect, delay); }; ws.onmessage = handleMessage; } connect();

4.5 敏感词与 XSS 没过滤

现象:用户发<script>标签,别人页面弹窗;或者发违规内容没人管。原因:消息内容直接渲染到 DOM,且没有服务端过滤。解决:服务端入库前做敏感词过滤,前端渲染用textContent而不是innerHTML,或者用 DOMPurify 清洗。这两步缺一不可,服务端过滤防存储型 XSS,前端转义防即时渲染漏洞。

5. 压测与容量估算:你的 PHP 聊天室到底能撑多少人

5.1 用 websocket-bench 做一次真实压测

上线前必须压测,别靠猜。我一般用websocket-bench这个工具,Node.js 写的,能模拟大量并发连接发消息。

# 安装 npm install -g websocket-bench # 模拟 2000 个连接,每个连接每秒发 1 条消息,持续 60 秒 websocket-bench -a 2000 -c 200 -t 60 \ -m '{"type":"chat","content":"load test"}' \ ws://127.0.0.1:8282

参数说明:-a是总连接数,-c是并发建立连接的批次,-t是持续秒数,-m是发送的消息体。压测时重点看三个指标:连接建立成功率、消息平均延迟、服务端 CPU 和内存。如果延迟随连接数线性上升,说明广播逻辑有 O(n) 遍历瓶颈,需要优化成房间维度广播。

5.2 单机容量估算与水平扩展思路

基于我的实测经验,一台 4 核 8G 的机器,Workerman 开 4 进程,单机能稳定支撑 5000~8000 个 WebSocket 连接,消息广播延迟在 100 毫秒以内。超过这个量级,要么升配,要么水平扩展。

水平扩展的核心是解决“跨机广播”。常见做法是引入 Redis 发布订阅:每台机器上的 WebSocket 服务订阅同一个 Redis 频道,消息统一发到 Redis,各机器收到后推给自己持有的连接。这样加机器就能线性提升容量。存储层 MySQL 可以做主从,读走从库,写走主库;Redis 做集群。这套架构下,PHP 全开源聊天室源码的实时消息能力可以做到十万级在线,关键看你有没有把广播和存储解耦。

我自己的习惯是:任何聊天室项目上线前,先用 2000 连接压一遍,把延迟和错误率记下来,作为基线。后面每次改代码都对比这个基线,延迟涨了 20% 以上就回头查。这个习惯帮我提前发现过好几次广播逻辑的性能退化。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询