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 参数将与携带 hosttokensocket 键的负载发生致命冲突。保持前缀,您的负载键将保持自由。

14.4: 认证流程

phloWS 实现了一个两步握手:

  1. 浏览器打开 wss://<host>/websocket
  2. phloWS 调用 wsConnect。如果返回 false:关闭连接。
  3. 第一个传入消息必须是认证负载(通常是 {type: 'auth', token: '<string>'})。
  4. phloWS 调用 wsAuth($wsHost, $wsToken, $wsSocket)。应用程序根据 %user%session->token 或自定义查找进行验证。
  5. 如果返回 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 注入一个脚本,该脚本:

如果您想从 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: 已知限制

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 跟随请求,广播跟随连接的队列。

我们使用必要的cookie来使该网站正常工作。在您的许可下,我们还使用分析工具来改善网站。