基于 Swoole 的简单多人聊天室示例
下面是一个基于 Swoole 的简单多人聊天室示例。它使用 WebSocket 协议,所有连接的用户都可以看到彼此发送的消息。
1. 核心原理
- Swoole 启动一个 WebSocket 服务器,常驻内存。
- 每当有新的客户端连接,服务器保存该连接的文件描述符(
$fd)到一个全局数组(或使用Swoole\Table等共享内存结构)。 - 当某个客户端发送消息时,服务器遍历所有保存的连接,将消息广播出去(包括发送者自己,或者可以排除发送者,此处示例是发给所有人)。
2. 服务端代码
创建文件 chat_server.php:
<?php
// chat_server.php
use Swoole\WebSocket\Server;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
// 创建 WebSocket 服务器,监听 0.0.0.0:9501
$server = new Server("0.0.0.0", 9501);
// 存储所有客户端连接的文件描述符(fd)
$connections = new Swoole\Table(1024);
$connections->column('fd', Swoole\Table::TYPE_INT);
$connections->create();
// 监听 WebSocket 连接打开事件
$server->on('open', function (Server $server, Request $request) use ($connections) {
$fd = $request->fd;
$connections->set((string)$fd, ['fd' => $fd]);
echo "新连接: fd={$fd}\n";
// 可选:广播用户进入消息
$server->push($fd, "欢迎加入聊天室!");
// 通知其他人
foreach ($connections as $conn) {
if ($conn['fd'] != $fd) {
$server->push($conn['fd'], "用户 {$fd} 进入房间");
}
}
});
// 监听 WebSocket 消息事件
$server->on('message', function (Server $server, Frame $frame) use ($connections) {
$fd = $frame->fd;
$data = $frame->data;
echo "收到消息: fd={$fd}, data={$data}\n";
// 广播给所有客户端
foreach ($connections as $conn) {
$server->push($conn['fd'], "用户 {$fd} 说: {$data}");
}
});
// 监听 WebSocket 连接关闭事件
$server->on('close', function (Server $server, int $fd) use ($connections) {
$connections->del((string)$fd);
echo "连接关闭: fd={$fd}\n";
// 广播离开消息
foreach ($connections as $conn) {
$server->push($conn['fd'], "用户 {$fd} 离开了聊天室");
}
});
// 启动服务器
$server->start();说明:
- 这里使用
Swoole\Table来存储连接的$fd,因为Swoole\Table是共享内存的,可以在多个 Worker 进程间共享。如果只用一个 Worker 进程(默认是1个),也可以直接用普通数组$connections = [],但多进程时需要共享内存结构。- 广播时遍历所有连接并推送消息,注意如果某连接已断开,
push可能会失败,可以忽略或加入异常处理。
3. 客户端代码
创建 index.html,放在 Web 服务器可访问的目录(或直接用 php -S 0.0.0.0:8080 启动一个简单 HTTP 服务)。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Swoole 多人聊天室</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
#messages { list-style: none; padding: 0; height: 300px; overflow-y: auto; border: 1px solid #ccc; margin-bottom: 10px; }
#messages li { padding: 5px; border-bottom: 1px solid #eee; }
#form { display: flex; }
#input { flex: 1; padding: 8px; }
button { padding: 8px 15px; }
</style>
</head>
<body>
<h2>Swoole 多人聊天室</h2>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" />
<button>发送</button>
</form>
<script>
// 连接 WebSocket 服务器,替换成你的实际地址
const ws = new WebSocket("ws://你的服务器IP:9501");
const messages = document.getElementById('messages');
const input = document.getElementById('input');
const form = document.getElementById('form');
// 收到消息时追加到列表
ws.onmessage = function(event) {
const li = document.createElement('li');
li.textContent = event.data;
messages.appendChild(li);
messages.scrollTop = messages.scrollHeight;
};
// 发送消息
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
ws.send(input.value);
input.value = '';
}
});
</script>
</body>
</html>注意:如果客户端和服务器不在同一台机器上,需要将 ws://你的服务器IP:9501 替换为服务器的实际 IP 或域名。同时确保服务器防火墙开放 9501 端口。4. 运行与测试
安装 Swoole
pecl install swoole或在 Docker 等环境中使用带有 Swoole 扩展的 PHP 镜像。
启动服务端
php chat_server.php看到
Server started之类的输出即成功。- 启动客户端
用浏览器打开index.html(可以通过php -S 0.0.0.0:8080启动一个简单 HTTP 服务器,或者直接双击打开但某些浏览器可能限制 WebSocket 连接)。
多个浏览器标签页或不同设备打开该页面,即可模拟多人聊天。
5. 扩展想法
- 区分用户昵称:在
open事件中让用户发送昵称,服务端存储昵称与fd的映射,广播时使用昵称。 - 房间支持:引入房间 ID,只向同一房间的用户广播。
- 心跳保活:定期发送 ping/pong 帧保持连接。
- 持久化消息:将聊天记录存入 Redis 或数据库。
- 使用协程:在消息处理中如需 I/O(如数据库查询),可以使用
Co\run或go创建协程,避免阻塞。
这个例子展示了 Swoole WebSocket 服务器的基本用法,以及如何实现一个多人聊天室。通过常驻内存、多进程(默认单进程)和事件驱动,它能支撑大量并发连接。
以上写法只是入门演示,存在的问题点及解决思路:
一、这种写法存在的主要问题
单进程处理所有事件,无法利用多核 CPU
代码中没有设置 worker_num,Swoole 默认只启动 1 个 Worker 进程。这意味着:
服务器只能使用一个 CPU 核心,并发处理能力受限。
如果某个连接的 push 操作因网络延迟而阻塞(虽然 Swoole 的 push 是非阻塞的,但遍历大量连接仍会占用 CPU),会拖慢所有其他客户端消息的处理。
无法水平扩展进程内的事件处理能力。
广播操作同步遍历,会阻塞事件循环
在 onMessage 回调中,使用 foreach ($connections as $conn) { $server->push(...); } 直接遍历所有连接。当连接数达到数千甚至上万时:
这条 foreach 循环会长时间占用当前 Worker 进程,导致其他客户端的连接事件、消息事件被延迟处理。
如果某个 push 因对方连接已关闭而抛异常(代码中未处理),可能中断广播。
连接存储使用 Swoole\Table,但未设置进程间共享的合理结构
Swoole\Table 本身是共享内存的,可以在多 Worker 进程间共享。但当前示例中:
由于只有 1 个 Worker,实际上未发挥多进程优势。
广播时直接遍历整个 Table,如果未来增加 Worker 数量,每个 Worker 都会持有完整的连接表副本(共享内存是同一份),但每个 Worker 仍在自己的进程内遍历全表,造成冗余。
没有利用 Swoole 内置的 $server->connections 迭代器(它本身就提供了遍历所有连接的能力,且更高效)。
缺少必要的错误处理和资源限制
没有对 push 失败做处理(如连接已断开)。
没有设置 heartbeat_idle_time 和 heartbeat_check_interval,空闲连接不会自动清理,可能导致资源浪费。
没有限制最大连接数,可能被恶意连接耗尽内存。
客户端代码存在单点隐患
前端直接连接 WebSocket 服务器,没有通过 HTTP 升级或负载均衡,不利于扩展和安全性。
没有重连机制,网络波动后页面需要手动刷新。
二、针对问题的改进方案
配置多进程,利用多核 CPU
$server = new Swoole\WebSocket\Server("0.0.0.0", 9501); $server->set([ 'worker_num' => swoole_cpu_num(), // 通常设置为 CPU 核心数 'heartbeat_idle_time' => 600, // 10分钟无心跳则断开 'heartbeat_check_interval' => 60, // 每分钟检查一次 'max_connection' => 10000, // 根据服务器内存设置 ]);- 使用 Swoole 内置的 $server->connections 遍历,并采用异步广播
Swoole 提供了 $server->connections 迭代器,它是全局的,且在多进程下由主进程维护。推荐在 onMessage 中使用 foreach ($server->connections as $fd) 来广播,但注意避免在遍历过程中执行阻塞操作。如果广播耗时较长,可以将广播任务投递到 Task 进程 中异步处理:
$server->on('message', function ($server, $frame) {
// 将广播任务投递到 task 进程
$server->task(['type' => 'broadcast', 'data' => $frame->data, 'sender_fd' => $frame->fd]);
});
$server->on('task', function ($server, $task_id, $src_worker_id, $data) {
if ($data['type'] === 'broadcast') {
foreach ($server->connections as $fd) {
if ($fd !== $data['sender_fd']) { // 可选:不发给发送者
$server->push($fd, $data['data']);
}
}
}
$server->finish($data);
});这样广播逻辑不会阻塞 Worker 进程接收新消息。
- 使用更高效的连接管理
如果确实需要跨进程共享更复杂的连接元数据(如用户ID、房间ID),可以使用 Swoole\Table 存储扩展信息,但遍历仍建议通过 $server->connections 进行。 增加错误处理与连接限制
$server->on('message', function ($server, $frame) { try { // 业务逻辑 foreach ($server->connections as $fd) { if (!$server->isEstablished($fd)) { continue; } $server->push($fd, $data); } } catch (\Throwable $e) { // 记录错误日志,避免崩溃 error_log($e->getMessage()); } });- 前端增强与架构优化
前端增加 WebSocket 重连逻辑(如 ws.onclose 中延迟重连)。
生产环境建议在 WebSocket 服务器前加一层 Nginx 做负载均衡和 SSL 卸载,使用 proxy_pass 到 Swoole 服务器。
三、单机可支撑的规模估算
在改进后(多进程、异步广播、合理配置),单机(8核16GB)能支撑的规模大致如下:
场景 连接数 消息吞吐量 说明
简单广播聊天室 3万~5万 每秒处理 1万~2万条消息 假设消息频率不高,广播遍历成本随连接数线性增长,CPU是主要瓶颈。
低频推送(如股票行情) 8万~10万 每秒广播 1000次 主要受内存(每个连接约占用 2-4KB)和网络带宽限制。
高交互游戏 1万~2万 每秒广播 5万条 消息频率高时,广播遍历开销大,需要更精细的优化(如分组广播、使用 sendMessage 等)。
实际影响因素:
CPU 核心数:Worker 进程数通常等于核心数,更多核心可并行处理不同连接的消息。
广播模式:全量广播最耗 CPU;如果改用按房间广播(连接分片),可大幅提升容量。
网络带宽:若消息体较大(如 1KB 以上),千兆网卡可能在 1万连接时就达到瓶颈。
内存:每个连接占用内存(Swoole 约 2-4KB,加上业务数据),16GB 内存可支撑约 5-8 万连接。
四、关于“手动创建进程和协程”的补充
在 Swoole 的 WebSocket 应用中,通常不需要手动创建进程,因为 Swoole 的 Server 已经通过 worker_num 配置帮你管理了多进程模型。只有在以下场景才考虑手动控制:
需要独立于请求生命周期的后台任务(如定时清理、数据聚合)→ 使用 Swoole\Process 或 Swoole\Timer。
需要在 Worker 内部并行执行多个 I/O 任务(如同时查询多个 API)→ 使用 go() 创建协程,避免阻塞。
需要实现自定义的进程间通信(IPC)或守护进程 → 使用 Swoole\Process。
控制使用 CPU 的核心方式:
通过 worker_num 设置 Worker 进程数量,一般设为 CPU 核心数(swoole_cpu_num())。
通过 task_worker_num 设置 Task 进程数量,用于处理异步任务。
协程是自动调度的,你只需在可能阻塞的地方(如 Co\run 或 go)中使用协程客户端(如 Co\Http\Client),避免使用同步阻塞的 I/O 函数。
版权属于:Joyber
本文链接:https://blog.qqvbc.com/default/1442.html
转载时须注明出处及本声明