使用 Swoole WebSocket 服务需安装 v4.8+ 扩展,监听 0.0.0.0:9501 并配置心跳,避免手写握手;前端连接失败多因监听地址、代理头缺失或协议不匹配;广播应协程化避免阻塞;Workerman 可用但性能与功能弱于 Swoole。

用 swoole_websocket_server 启动一个可用的 WebSocket 服务
PHP 原生不支持 WebSocket 服务端,必须依赖扩展。目前最稳定、生产可用的选择是 Swoole(v4.8+ 或 v5.x),它内置了 swoole_websocket_server 类,无需额外协议解析就能处理握手、帧解包和心跳。
常见错误是试图用 fsockopen 或 stream_socket_server 手写握手——不仅容易出错(比如忽略 Sec-WebSocket-Key 转换、缺少 101 Switching Protocols 响应),还无法自动处理分片、ping/pong、关闭帧等细节。
实操建议:
- 确认已安装
swoole扩展:php -m | grep swoole,且版本 ≥ 4.8 - 监听地址建议用
0.0.0.0:9501(而非127.0.0.1),否则前端浏览器可能因跨域或连接拒绝失败 - 必须设置
$server->set(['heartbeat_check_interval' => 60, 'heartbeat_idle_time' => 600]),否则客户端断连后连接不会自动清理 - 启动前确保端口未被占用:
lsof -i :9501或netstat -tuln | grep 9501
$server = new Swoole\WebSocket\Server('0.0.0.0', 9501);
$server->set([
'heartbeat_check_interval' => 60,
'heartbeat_idle_time' => 600,
]);
$server->on('open', function ($server, $request) {
echo "Client {$request->fd} connected\n";
});
$server->on('message', function ($server, $frame) {
$server->push($frame->fd, "Echo: {$frame->data}");
});
$server->on('close', function ($server, $fd) {
echo "Client {$fd} disconnected\n";
});
$server->start();
前端用 WebSocket 连接时为什么报 Connection closed before receiving a handshake response
这个错误几乎都指向服务端未正确响应 WebSocket 握手请求,而不是网络问题。Swoole 默认会处理,但以下情况会导致失败:
立即学习“PHP免费学习笔记(深入)”;
- 服务未运行,或监听在
127.0.0.1而前端从http://localhost:8080发起连接(浏览器认为是跨网段) - 反向代理(如 Nginx)未透传 WebSocket 头部:
Upgrade和Connection: upgrade必须显式配置,否则 Swoole 收不到握手请求 - HTTPS 站点尝试连
ws://(明文)——浏览器会直接拦截,必须用wss://+ SSL 终止(Nginx 做 TLS 终结,后端仍走ws://) - 客户端 URL 写错,例如漏掉端口:
new WebSocket('ws://127.0.0.1')→ 应为new WebSocket('ws://127.0.0.1:9501')
验证方式:用 curl -i -N -H "Upgrade: websocket" -H "Connection: Upgrade" http://127.0.0.1:9501/ 看是否返回 HTTP/1.1 101 Switching Protocols。
如何在 Swoole WebSocket 服务中广播消息而不阻塞
调用 $server->push() 是同步操作,但遍历所有 $server->connections 并逐个 push 会卡住事件循环——尤其连接数多时,单次广播可能耗时上百毫秒,导致新连接堆积、心跳超时断连。
正确做法是利用 Swoole 的协程调度或异步投递:
- 避免
foreach ($server->connections as $fd)——$server->connections是迭代器,底层会锁表,高并发下性能骤降 - 改用
$server->getClientInfo()配合$server->exist()判断连接有效性,再批量push - 更推荐用
go(function () use ($server, $data) { ... })包裹广播逻辑,让其在独立协程中执行,不阻塞主循环 - 若需高频广播(如聊天室),建议把消息发到 Redis Pub/Sub,由另一个 Worker 进程订阅后分发,实现读写分离
简单非阻塞广播示例:
go(function () use ($server, $message) {
foreach ($server->connections as $fd) {
if ($server->isEstablished($fd)) {
$server->push($fd, $message);
}
}
});
用 Workerman 替代 Swoole 是否可行
可以,但要注意 Workerman 是纯 PHP 实现,无内核级异步 I/O 支持,靠 stream_select 轮询,在 Linux 上连接数超过 1000 后性能明显下降;而 Swoole 基于 epoll/kqueue,轻松支撑数万连接。
如果你的场景是小规模内部工具、开发调试、或已有 Workerman 项目想快速加 WebSocket,它确实上手更快(不用装扩展),但以下限制必须清楚:
- 不支持 HTTP/2、QUIC 等新协议,也无法与 Swoole 的协程 MySQL/Redis 客户端无缝集成
- 热更新困难:Workerman 没有类似
reload的优雅重启机制,改代码要手动 kill 进程 - 日志和异常捕获粒度粗,
onMessage抛异常会导致整个连接中断,而 Swoole 可通过try/catch在回调内捕获 - 调试时
var_dump输出会混在守护进程日志里,不如 Swoole 的swoole_error_log清晰
真要选 Workerman,请至少用 workerman/websocket-server 4.x 版本,并禁用 daemonize 开发时看实时输出。
root 启动后 fork 出 worker,再切到普通用户,可能导致 opcache 或 shm 共享内存权限异常;另外,docker 中没设 --cap-add=SYS_PTRACE 时,strace 调试会失败——但这和 WebSocket 本身无关,只是排障时卡住的点。**



















