学完这篇,你能从一个空目录开始,跑起一个带鉴权、心跳和广播的 PHP WebSocket 服务端,并用 Nginx 配好 wss 域名入口。
第一步:确认版本和端口
Workerman 4.x 要求 PHP ≥ 7.2,5.x 要求 PHP ≥ 8.1。先看版本:
php -v
端口示例用 2346(WebSocket)和 2347(内部推送接口),确认没被占用:
ss -tlnp | grep -E '2346|2347'
注意:宝塔等面板默认开了防火墙,TCP 2346 要在安全组和面板里都放行,否则本地 curl 通、外部连不上。
第二步:安装 Workerman
mkdir /www/ws && cd /www/ws
composer require workerman/workerman
没有 Composer 的话,去 Workerman 官网下载压缩包解压,把 Workerman/ 目录放进来,改成 require_once __DIR__ . '/Workerman/Autoloader.php'; 即可。
第三步:写最小可用的服务端
新建 start.php:
<?php
use Workerman\Worker;
use Workerman\Timer;
require_once __DIR__ . '/vendor/autoload.php';
$ws = new Worker('websocket://0.0.0.0:2346');
$ws->name = 'chat';
$ws->count = 1; // 先用 1 个进程,见下面注意
$ws->onConnect = function ($conn) {
$conn->uid = 0; // 自定义属性,连接级
};
$ws->onMessage = function ($conn, $data) use ($ws) {
$msg = json_decode($data, true);
if (!is_array($msg) || empty($msg['type'])) return;
if ($msg['type'] === 'login') {
// 生产环境这里必须校验 token,不能直接信前端传的 uid
$conn->uid = (int)($msg['uid'] ?? 0);
$conn->send(json_encode(['type' => 'ok']));
}
if ($msg['type'] === 'say' && $conn->uid) {
$out = json_encode([
'type' => 'msg',
'uid' => $conn->uid,
'text' => (string)($msg['text'] ?? ''),
], JSON_UNESCAPED_UNICODE);
foreach ($ws->connections as $c) {
if ($c->uid) $c->send($out);
}
}
};
// 每 30 秒踢掉 90 秒没消息的死连接
Timer::add(30, function () use ($ws) {
foreach ($ws->connections as $c) {
if (time() - $c->lastMessageTime > 90) $c->close();
}
});
Worker::runAll();
$conn->lastMessageTime 是 Workerman 内置属性,收到任何数据自动刷新,不用自己维护。
注意:$ws->count > 1 时,$ws->connections 只包含当前进程的连接,广播不会跨进程。要多进程广播,用 GatewayWorker,或者装 workerman/channel 做进程间订阅。
第四步:启动与运维命令
php start.php start # 前台跑,看报错用这个
php start.php start -d # 守护进程
php start.php status # 看连接数、内存
php start.php reload # 平滑重载业务代码,不断连接
php start.php restart -d # 整体重启
php start.php stop
改了代码用 reload,改了 count、端口这类配置必须 restart。
注意:守护进程模式下 echo 不会打印到终端,日志在 start.php 同级的 workerman.log。调试阶段一律先前台跑。
第五步:Nginx 反代成 wss
页面是 https 时,前端只能连 wss://,需要 Nginx 转发:
location /ws {
proxy_pass http://127.0.0.1:2346;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 600s;
}
前端:
const sock = new WebSocket('wss://你的域名/ws');
sock.onopen = () => sock.send(JSON.stringify({ type: 'login', uid: 1 }));
sock.onmessage = e => console.log(JSON.parse(e.data));
注意:缺 proxy_read_timeout 时,Nginx 默认 60 秒无数据就断开,表现为"页面放着不动连接就掉"。配合前端每 30 秒发一次 {"type":"ping"} 更稳。
第六步:让 PHP 业务代码往里推消息
在同一份 start.php 里再开一个只监听本机的 HTTP 端口:
$api = new Worker('http://127.0.0.1:2347');
$api->onMessage = function ($req, $conn) use ($ws) {
if (($req->header('x-token') ?? '') !== '改成你的长随机串') {
return $conn->send('forbidden');
}
$payload = $req->post('payload');
foreach ($ws->connections as $c) {
if ($c->uid) $c->send($payload);
}
$conn->send('ok');
};
业务侧(发帖、收到私信、有人回复时)用 curl 推:
curl -X POST http://127.0.0.1:2347 \
-H 'x-token: 改成你的长随机串' \
-d 'payload={"type":"notice","text":"你有新回复"}'
注意:这个端口只能绑 127.0.0.1,且必须带 token。一旦暴露公网,等于给了所有人广播权限。
第七步:避开常驻内存的坑
- 回调里不要
sleep()、不要同步 curl 外部接口、不要跑慢 SQL。一个连接阻塞,整个进程一起卡住。
- PHP 常驻内存不会自动释放变量,全局数组要记得在
onClose 里清理,否则内存只涨不降。
- 用户身份校验放在服务端做:客户端连上后发的 token 拿去查库或验签,验证不通过直接
$conn->close()。
小结
- 装
workerman/workerman,new Worker('websocket://0.0.0.0:2346'),Worker::runAll() 就能跑。
- 调试用前台
php start.php start,上线用 -d,改代码用 reload。
- 心跳靠定时器遍历
lastMessageTime 清理死连接。
- 多进程时连接不共享,广播要么单进程,要么上 GatewayWorker / Channel。
- 生产必须走 Nginx 反代 wss,记得设
proxy_read_timeout 和 Upgrade 头。
- 业务推送另开
127.0.0.1 内网 HTTP 端口,加 token 鉴权。