14: WebSocket
在 Phlo 中,实时功能通过 phloWS 运行,这是一个独立的 Node.js 服务器(phloWS.js),它在多个虚拟主机之间复用 WebSocket 连接,并将每个传入消息作为一次性 PHP CLI 调用传递。您的应用实现了四个钩子函数,并通过 wsCast() 从 PHP 广播。
14.1: phloWS 是什么
phloWS 是一个用 Node.js 编写的轻量级代理(ws 库,约 9 KB 的代码)。一个在端口 3001 上运行的进程服务于整个堆栈:一个单一的 phloWS 通过 WS 握手的 Host 头路由到正确的应用程序。
对于每个传入的消息,phloWS 启动一个 一次性 PHP 进程(php-zts <app>/www/app.php ws::<event>)。这大约需要 50-100 毫秒每条消息,但为每个处理程序提供了完整的请求生命周期:数据库、会话、资源,一切都可以简单地使用。
在消息之间没有持久的工作状态。如果您想共享状态,请通过您的数据库或 apcu 来实现。
14.2: 安装
phloWS 生活在 Phlo 框架之外。选择一个路径,/srv/websocket、/opt/phloWS、~/code/phloWS,都可以。我们使用 <ws> 作为占位符:
git clone https://github.com/q-ainl/phlo-websocket.git <ws>
cd <ws>
npm install
在 <ws>/websocket.js 中,你将 vhost → app.php 路径映射:
require('./phloWS.js')(3001, '/usr/bin/php-zts', {
'app.example.com': '<app>/release/www/app.php',
'dev.app.example.com': '<app>/www/app.php',
})
将其作为服务运行(systemd / pm2 / supervisord);phlo-websocket README 描述了 pm2 运行模式和 /message 桥接合同:
node <ws>/websocket.js
对于生产环境:通过你的反向代理(Caddy、Nginx、FrankenPHP)将 wss:// 转发到 127.0.0.1:3001,路径为 /websocket。
14.3: 应用钩子
在您的应用源代码中,您定义了四个函数;Phlo 的 websocket 资源会在它们存在时调用它们。将它们放在一个像 app.ws.phlo 的文件中:不要将文件命名为 websocket.phlo,因为该类名在加载时与引擎的 websocket 资源冲突。
function wsConnect($wsHost, $wsToken, $wsSocket){
%log->info('ws connect', socket: $wsSocket)
return true
}
function wsAuth($wsHost, $wsToken, $wsSocket){
$user = %user->byToken($wsToken)
if (!$user) return false
%session->user = $user
return true
}
function wsReceive($wsHost, $wsToken, $wsSocket, ...$data){
$type = $data['type'] ?? null
if ($type === 'ping') return wsCast(wsTarget: $wsSocket, pong: time())
if ($type === 'chat.send') chat::send($data['text'], from: %session->user->id)
}
function wsClose($wsHost, $wsToken, $wsSocket){
%log->info('ws close', socket: $wsSocket)
}
| 钩子 | 何时 | 返回 |
|---|---|---|
wsConnect |
WS 握手后立即 | true 接受,false 关闭 |
wsAuth |
客户端的第一个认证消息 | true 认证,false 关闭 |
wsReceive |
每个后续消息(JSON 解码并展开) | 无关紧要,使用 wsCast() 响应 |
wsClose |
连接关闭 | 无关紧要 |
$wsSocket 是一个不透明的字符串标识符,您可以用它来向确切的这个客户端广播。
连接上下文参数是 按约定以 ws 为前缀($wsHost,$wsToken,$wsSocket),与 wsCast 完全相同。这不是装饰性的:wsReceive 将 JSON 负载展开为命名参数(...$data),因此未加前缀的 $host/$token/$socket 参数将与携带 host、token 或 socket 键的负载发生致命冲突。保持前缀,您的负载键将保持自由。
14.4: 认证流程
phloWS 实现了一个两步握手:
- 浏览器打开
wss://<host>/websocket。 - phloWS 调用
wsConnect。如果返回false:关闭连接。 - 第一个传入消息必须是认证负载(通常是
{type: 'auth', token: '<string>'})。 - phloWS 调用
wsAuth($wsHost, $wsToken, $wsSocket)。应用程序根据%user、%session->token或自定义查找进行验证。 - 如果返回
false:关闭连接。如果返回true:套接字被标记为已认证;之后的所有内容都通过wsReceive进行处理。
令牌通常来自 %user->token(每个登录用户)或 API 密钥。客户端可以通过 cookie 或作为第一个 WS 消息发送它。
14.5: 从 PHP 广播
wsCast() 是一个常规函数(资源 wsCast)。它向 phloWS 的内部 HTTP 桥发送 POST 请求,将其推送到正确的套接字。
wsCast(wsTarget: 'all', toast: 'New message received')
wsCast(wsTarget: $wsSocket, path: '/inbox')
wsCast(wsTarget: ['s1', 's2'], inner: ['#count' => $newCount])
| 参数 | 默认值 | 说明 |
|---|---|---|
wsTarget |
'all' |
'all'、'token:<id>'、'token:not:<id>'、单个套接字 ID 或套接字 ID 数组 |
wsHost |
host |
广播适用的虚拟主机(默认:当前主机) |
wsPort |
websocket(来自应用配置的常量) |
phloWS 端口 |
...$data |
无 | 命名参数成为有效负载,通常是 apply() 命令 |
有效负载通过 phlo.js 自动传递给客户端并应用于 DOM:与您从异步路由中了解的相同 apply() 协议。
没有重试,没有死信,没有确认。 如果 phloWS 关闭,POST 将静默失败。对于保证交付(财务事件):与数据库队列结合使用。
14.6: 客户端
客户端本身没有做什么特别的事情。将 DOM/websocket 添加到您的 data/app.json 中的资源:
{
"resources": [..., "DOM/websocket", "wsCast"]
}
DOM/websocket 注入一个脚本,该脚本:
- 自动连接到
wss://<host>/websocket - 将传入的消息直接通过
apply()、inner:、outer:、class:、toast:、path:处理,工作方式与 async routes 相同 - 以指数退避的方式重新连接(333 毫秒 → 999 毫秒 → ...)
如果您想从 JS 发送消息:app.websocket.send({type: 'chat.send', text: 'hi'})。
14.7: 迷你示例:presence
显示“谁在线”而不进行轮询。
function wsConnect($wsHost, $wsToken, $wsSocket){
%apcu->set("presence:$wsSocket", time(), 3600)
wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
return true
}
function wsClose($wsHost, $wsToken, $wsSocket){
%apcu->delete("presence:$wsSocket")
wsCast(wsTarget: 'all', inner: ['#online-count' => static::count()])
}
static count(){
$keys = %apcu->keys('presence:')
return count($keys)
}
服务器不保持状态;APCu 根据主机计算套接字。在 PHP 重启时,缓存会自动清空,这很好,因为空的存在状态是可以接受的降级状态。
14.8: 已知限制
- 每个事件的一次性 CLI,每条消息都需要 PHP 启动。适合收件箱、在线状态和通知;不适合高频遥测或实时交易。
- 负载没有版本控制,在重构时:一次性迁移所有客户端。
- 单点故障,整个堆栈只有一个 phloWS 进程。在崩溃时:所有实时功能都将停止,直到重启。请在进程监控器下运行 phloWS。
- 没有内置加密,请使用您的反向代理进行 TLS 终止(
wss://)。
14.9: 流式响应:没有 phloWS 的实时轻量级
并非每个渐进更新都需要 WebSocket。在一个 route 中设置 %res->streaming = true,每个后续的 apply(...) 都会立即作为一行 JSON 打印并刷新到同一个 HTTP 响应中;phlo.js 会在命令到达时继续应用这些命令:
route async POST report::generate {
%res->streaming = true
foreach ($this->steps AS $i => $step){
$step->run
apply(inner: arr('#progress' => $i + 1 .'/'. count($this->steps)))
}
apply(toast: 'Done')
}
当一个客户端触发工作并监视自己的进度时,请使用 streaming(AI 令牌流、导入、批处理作业)。当其他客户端也必须接收更新时,请使用 phloWS:streaming 跟随请求,广播跟随连接的队列。