☰
GatewayWorker 从零到一完整指南
2026/10/12 16:18:35 网站建设 项目流程

一、这玩意儿解决什么问题

平时做网站是"请求-响应"模式:

浏览器:"服务器,给我这个页面" → 服务器:"给你" → 结束

服务器永远被动,没法主动找浏览器说话。

但现实中有大量场景需要跨客户端通知——A 做了一件事,B 要立刻知道:

场景谁操作谁收到通知HTTP 能搞定?
聊天A 发消息B 收到消息❌ B 没发请求,服务器没法主动找 B
班级大屏后台审核通过教室大屏实时刷新❌ 大屏只是展示页,不知道什么时候有更新
后台推送通知管理员发公告所有在线用户弹窗❌ 用户没请求,服务器推不了

判断标准很简单:自己操作、自己等结果(比如提交报名后弹"提交成功")→ HTTP 请求-响应就够了。别人操作、自己收到通知(比如后台审核通过后大屏自动刷新)→ 才需要 WebSocket。

GatewayWorker 就是干这个的:在浏览器和服务器之间建一条 WebSocket 长连接,连接不断开,服务器想什么时候推给谁就什么时候推。

传统 HTTP: 浏览器 ──问──→ 服务器 ──答──→ 浏览器 (结束,各走各的) WebSocket: 浏览器 ←─────长连接─────→ 服务器 (一直连着,随时推)

客户端可以是网页、小程序、App,只要支持 WebSocket 协议就行。

拿"班级报名 + 大屏实时刷新"当例子贯穿全文:

角色干什么
报名页(家长用,走 HTTP)填表单、提交 → HTTP 请求-响应,和普通网页一样
教室大屏(需要 WebSocket)连上 GatewayWorker 保持长连接,等后台推送新报名数据
ThinkPHP 后台家长提交后处理报名、存数据库、然后推通知给大屏
GatewayWorker维持大屏的 WebSocket 连接,后台告诉它推给大屏
GatewayClient(一个 PHP 库)让 ThinkPHP 能操控 GatewayWorker

一句话:家长走 HTTP 提交报名 → 后台处理完 → 通过 GatewayWorker 推给教室大屏。家长和大屏是两拨人,推送解决的是"一个角色操作,另一个角色收到通知"。


二、启动后到底跑了什么 —— 三种进程

GatewayWorker 启动后,实际起了三种进程,各自干活不打架:

Register(注册中心,端口 1238) 告诉 Gateway 和 BusinessWorker 彼此在哪 撮合完就没它事了 ╱ ╲ Gateway(门卫) BusinessWorker(工人) 监听 8282 端口 不对外暴露,纯内部干活 接待客户端连接 跑 Events.php 维持长连接 处理完告诉门卫推给谁 ↑ 客户端连这个端口

进程比喻端口对外?职责
Register花名册1238❌ 仅本机登记所有 Gateway 和 Worker 的地址,进程间互相发现全靠它
Gateway门卫8282✅ 浏览器连这个接客、维持长连接、转发消息
BusinessWorker工人无❌跑 Events.php,处理全部业务逻辑

⚠️Register 必须监听127.0.0.1,绝不能暴露到外网。它用的是纯文本协议,没有任何鉴权——任何人连上来都能假装自己是 Gateway 或 Worker 发指令,从而劫持整个推送系统。Gateway 必须监听0.0.0.0,否则外部客户端连不进来。

启动顺序:Register → Gateway → BusinessWorker

Register 必须先起来,不然后面两个启动直接报错——它们一启动就要向 Register 报到。

配置文件对应关系

Applications/YourApp/下四个文件:

文件起什么关键配置
start_register.phpRegistertext://127.0.0.1:1238
start_gateway.phpGatewaytcp://0.0.0.0:8282,2 个进程
start_businessworker.phpBusinessWorker4 个进程,指向 Events.php
Events.php唯一要写的业务逻辑全在这

start_gateway.php核心代码:

$gateway = new Gateway("tcp://0.0.0.0:8282"); // 对外端口 $gateway->count = 2; // 开 2 个进程分摊连接 $gateway->lanIp = '127.0.0.1'; // 内网通信 IP $gateway->startPort = 2900; // 内部通信起始端口 $gateway->registerAddress = '127.0.0.1:1238'; // 去哪找 Register

start_businessworker.php:

$worker = new BusinessWorker(); $worker->count = 4; // 4 个进程并行处理业务 $worker->registerAddress = '127.0.0.1:1238'; // 找同一个 Register $worker->eventHandler = 'Events'; // 指向业务类

所以启动后一共1 + 2 + 4 = 7 个进程。

count 怎么调

进程类型建议理由
Register永远 1无状态,多开没意义
Gateway按连接数,不按 CPU每进程可处理 5000~10000 连接;开多了反而加大广播开销
BusinessWorkerCPU核数 ~ 2×核数吃 CPU,多了上下文切换反而慢

Gateway 的 count不是越多越好。Gateway 进程增多后,Register 向所有 Gateway 广播寻址的开销会变大(参考第五章 sendToUid)。官方推荐:1 万连接以内开 2 个,每多 1 万连接加 1 个。因为它主要是 I/O 密集型(维持连接、转发数据),不是 CPU 密集型。

实际:4 核机器、5000 并发 →gateway.count=2,businessworker.count=8。不纠结的话默认值也能跑。

拿到代码后实际就做三件事

  1. 改端口 ——start_gateway.php里把8282换成对应端口,防火墙放行
  2. 调 count —— 按上面表格改,不纠结就用默认值
  3. 写Events.php——唯一需要写代码的地方,后面会展开

lanIp、startPort、registerAddress单机部署不用改。start_register.php和start_businessworker.php也几乎不需要动。


三、一条连接的全过程

把从页面打开到收到推送拆成四个阶段。

阶段 1:连接建立 → 服务端返回 client_id

new WebSocket("ws://192.168.1.6:8282") │ ├── TCP 三次握手 → 连接通了 │ ├── Gateway 分配一个全局唯一编号:client_id │ 比如 "7f0000010a5400000001" │ 注意:这个编号断开重连就会变 │ ├── 自动触发 Events::onConnect($client_id) │ 连接已通,此时可以主动发消息给客户端(sendToClient) │ 但客户端还没发过任何数据(onMessage 尚未触发) │ └── onConnect 里主动推 client_id 回去: Gateway::sendToClient($client_id, '{"type":"connected","client_id":"7f0000..."}') 这条消息走 WebSocket 到客户端,触发 ws.onmessage ✅ 至此客户端才知道自己的 client_id

阶段 2:身份绑定

客户端收到 onConnect 推过来的 { type:'connected', client_id:'7f0000...' } │ ↑ 这条消息来自上一步 onConnect 里的 sendToClient │ ├── 发 HTTP 给后台 /api/bind │ 带两个东西:client_id + 业务身份(如 classroom_id=1) │ ├── 后台调 Gateway::bindUid("7f0000...", "classroom_1") │ └── GatewayWorker 内部记下映射: _uidConnections["classroom_1"] = ["7f0000010a5400000001"] 以后说"给 classroom_1 发",就知道往哪推了

阶段 3:消息推送

家长提交报名(走 HTTP,普通请求-响应) │ ↑ ├── 后台存数据库 | 注意:家长和后面的大屏是两拨人! │ | 家长用 HTTP,大屏用 WebSocket ├── 后台调:Gateway::sendToUid("classroom_1", json数据) → 推给大屏 │ └── 内部怎么找到大屏的连接? → 构造指令包 → 广播给所有 Gateway 进程(不知道 classroom_1 连在哪个 Gateway 上) → 每个 Gateway 查自己的 _uidConnections → 谁管着这个连接谁负责推出,没管着的忽略

为什么广播?后台只知道 uid,不知道这个用户连在哪个 Gateway 进程上,所以告诉所有人,各自认领。

阶段 4:断线

关闭页面 / 网络断开 │ └── 自动触发 Events::onClose($client_id) GatewayWorker 自动清除绑定关系 可以记日志、通知其他人 "xx 下线了"


四、client_id 的秘密 —— 核心原理

上一节反复出现了client_id,现在把它拆开看——这是理解整个 GatewayWorker 的关键。

client_id不是随机字符串,而是编码后的门牌号:

// 每当有客户端连上 Gateway,Gateway 自动生成: client_id = bin2hex(pack('NnN', $local_ip, $local_port, $connection_id)) // ↑ ↑ ↑ // Gateway 自己的 IP Gateway 内部端口 自增计数器 // (如 127.0.0.1) (如 2900,来自 (每来一个新连接 +1) // startPort 配置)

重点:这里的 IP 和端口都是 Gateway 进程自己的,跟客户端的 IP、端口没有任何关系。客户端的地址不参与编码。

解码回去:

$local_ip → Gateway 进程的内网 IP(如 127.0.0.1) $local_port → Gateway 的内部通信端口(如 2900,来自 start_gateway.php 的 startPort) $connection_id → 该 Gateway 进程上第几个连接(自增数字)

一个真实例子:

client_id = "7f0000010b5400000001" 解码 → Gateway IP: 127.0.0.1 内部端口: 2900 连接号: 1

为什么同一个客户端重连后 client_id 会变

因为connection_id是一个自增计数器,不是客户端的标识:

同一个人,同一台电脑,断开后立刻重连: 第 1 次连接:Gateway IP=127.0.0.1, port=2900, conn_id=1 → "7f0000010b5400000001" 第 2 次连接:Gateway IP=127.0.0.1, port=2900, conn_id=2 → "7f0000010b5400000002" ↑ 只有这里变了

Gateway 的 IP 和端口没变(还是那个进程),但计数器 +1 了,client_id 就不同了。这就是为什么断开重连后必须重新bindUid——旧的 client_id 已经作废,新连接拿到了新的编号。

既然 client_id 会变,怎么定位目标 Gateway

虽然connection_id不同,但Gateway 的 IP 和端口不变——这两个字段足以让任何进程直接 TCP 连到目标 Gateway。

这意味着:拿到 client_id,就等于拿到了目标 Gateway 的地址。后面的 bindUid 就是利用这一点实现"不走 Register 直达"的。

这是后面一切理解的基础——既然地址都写在里面了,有些操作自然就不需要去 Register 查了。

类比快递单号和人名,后文会展开。


五、bindUid 和 sendToUid —— 最核心的区别

这是 GatewayWorker 里问得最多、最容易搞混的地方。

bindUid:直达(不走 Register)

Gateway::bindUid($client_id, 'user_123');

bindUid(client_id, uid) → 解码 client_id → 拿到 Gateway 的 IP + 端口 → 直接 TCP 连过去 ✅ → 在目标 Gateway 的内存里记下:uid → client_id → Register 全程不知道这件事

sendToUid:广播寻人(必须走 Register)

Gateway::sendToUid('user_123', $message);

sendToUid(uid, msg) → 只有 uid,不知道他在哪个 Gateway 上 → 去 Register 问:所有 Gateway 的地址列表给我 📋 → 挨个发给每一个 Gateway → 每个 Gateway 查自己的内存:uid 在我这吗? → 在的那个负责推送,不在的忽略

一句话判断标准

参数里有通信方式走 Register?为什么
client_id直达目标 Gateway❌ 不走client_id 自带 IP+端口
uid广播问所有 Gateway✅ 必须走uid 只是身份,不含地址

跟是前端还是后端、HTTP 还是命令行都无关,只看参数里带没带地址。

快递单号比喻(三个概念一次分清)

  • client_id=快递单号,上面写着收件地址,快递员直接送
  • uid=人名,不知住哪,只能去查通讯录然后挨家挨户问
  • bindUid= 登记"这单快递是张三的",以后找张三就知道往哪送了

一个经典误判

开发时可能会遇到:sendToUid没反应,但bindUid正常。

别急着怀疑代码——如果 Register 端口配错了(比如代码写的 1236,实际启动的 1238),bindUid 因为client_id自带地址所以能通,容易误以为一切正常。但实际上 sendToUid 需要走 Register,端口不对它就找不到人了。

排查口诀:bindUid 能通不代表 Register 没问题,先拿 sendToUid 验证。


六、两种开发模式 —— 怎么选

模式一:纯 GatewayWorker(练手 / IoT)

浏览器 ←→ WebSocket ←→ GatewayWorker └─ Events.php 写所有逻辑

Events.php 的onMessage自己处理业务:

public static function onMessage($client_id, $message) { $data = json_decode($message, true); switch ($data['type']) { case 'login': Gateway::bindUid($client_id, $data['uid']); Gateway::sendToClient($client_id, json_encode(['type' => 'login_ok'])); break; case 'chat': Gateway::sendToUid($data['to_uid'], json_encode([ 'type' => 'msg', 'from' => $data['from_uid'], 'content' => $data['content'] ])); break; } }

优点:简单直接,客户端和服务端全走 WebSocket,少一跳。

不是说模式一不能连数据库、不能做鉴权。BusinessWorker 本质就是一个 PHP 进程,能useThinkPHP 的 Model、能查数据库、能做 Session 校验——下面模式一的鉴权示例就是最好的证明。所以技术上两种模式都能干所有事,区别不在能不能,而在代码怎么组织。

模式二:GatewayWorker + 后台框架

客户端 ─── HTTP ───→ ThinkPHP 后台 ─── GatewayClient ───→ GatewayWorker ──推送──→ 客户端

Events.php 极简化,onMessage甚至可以留空。业务逻辑全部放在 ThinkPHP 控制器里:

// Events.php - 只负责返回 client_id public static function onConnect($client_id) { Gateway::sendToClient($client_id, json_encode([ 'type' => 'connected', 'client_id' => $client_id ])); } // ThinkPHP 控制器 - 绑定 function bind() { Gateway::bindUid($_POST['client_id'], 'classroom_' . $_POST['classroom_id']); return json(['code' => 1]); } // ThinkPHP 控制器 - 推送 function register() { // ... 存数据库 ... Gateway::sendToUid('classroom_1', json_encode(['type' => 'notify', 'msg' => '报名成功'])); }

两种模式真正的区别

重申:技术上没有高下之分,两种都能连数据库、做鉴权。区别在哪:

模式一(纯 WebSocket)模式二(HTTP 推送)
业务代码在哪Events.php(长驻进程)ThinkPHP 控制器(请求-响应)
改业务代码后需要重启 Worker不需要重启,即时生效
鉴权方式在 onMessage 里自己做 Session / Token 校验复用框架的中间件、cookie
适合什么聊天、即时通讯——收发消息是高频双向的推送通知、大屏刷新——客户端主要是被动接收
调试难度稍高,日志要靠 Worker 输出和普通 Web 开发一样,请求可见

一句话判断:如果客户端主要是发消息(聊天),用模式一;如果客户端主要是收消息(通知推送),用模式二。实际项目里两种经常混用。

模式一里做鉴权的真实写法

举个例子,在 ThinkPHP 里把 GatewayWorker 放到 addons 目录,Events.php 直接useThinkPHP 的类:

public static function onMessage($client_id, $message) { $message_data = json_decode($message, true); if (!$message_data) return; // Session 鉴权——跟 HTTP 接口里写的一样 if (!isset($_SESSION['user_id']) || !$_SESSION['user_id']) { return; // 没登录,忽略 } // 调 ThinkPHP 的 Chat 模型处理业务 $user_id = $_SESSION['user_id']; $chat = new \addons\xilujob\chatprograms\library\Chat(); $ret = $chat->{$message_data['type']}($user_id, $message_data); // 回复发送方 Gateway::sendToClient($client_id, json_encode($ret)); // 如果接收人在线,转发 if ($ret['code'] == 1 && in_array($message_data['type'], ['say'])) { $receive_uid = 'user_' . $ret['data']['to_uid']; if (Gateway::isUidOnline($receive_uid)) { $ret['type'] = 'new_msg'; Gateway::sendToUid($receive_uid, json_encode($ret)); } } }

这就是模式一的正确打开方式——并不是 Events.php 就只能写switch-case转发,它能做任何 ThinkPHP 能做的事。

混合模式(实际项目最常用)

两种模式共存:客户端之间的即时聊天走 WebSocket(模式一),服务器主动推送通知走 HTTP(模式二)。GatewayWorker 天然支持,不用做任何额外配置。


七、完整代码实战 —— 从零搭聊天系统

这一段是可以照抄跑通的代码,从前端到后端全覆盖。

需要创建 / 修改的文件

文件操作说明
start_register.php不动框架模板就是对的
start_gateway.php不动最多改个端口
start_businessworker.php🆕 新建启动 Worker
Events.php🆕在这写代码业务逻辑入口
前端 HTML🆕 新建聊天页面

1. start_businessworker.php

<?php use Workerman\Worker; use GatewayWorker\BusinessWorker; require_once __DIR__ . '/vendor/autoload.php'; $worker = new BusinessWorker(); $worker->name = 'BusinessWorker'; $worker->count = 4; $worker->registerAddress = '127.0.0.1:1238'; $worker->eventHandler = 'Events'; // 指向业务处理类 Worker::runAll();

2. Events.php

<?php use GatewayWorker\Lib\Gateway; class Events { // 连接建立 → 返回 client_id 给客户端 public static function onConnect($client_id) { Gateway::sendToClient($client_id, json_encode([ 'type' => 'connected', 'client_id' => $client_id ])); } // 收到客户端消息 → 根据 type 分发 public static function onMessage($client_id, $message) { $data = json_decode($message, true); switch ($data['type']) { case 'login': Gateway::bindUid($client_id, $data['uid']); Gateway::sendToClient($client_id, json_encode(['type' => 'login_ok'])); break; case 'chat': Gateway::sendToUid($data['to_uid'], json_encode([ 'type' => 'msg', 'from' => $data['from_uid'], 'content' => $data['content'] ])); break; } } // 连接断开 public static function onClose($client_id) { // 记日志、通知其他人 } }

3. Events.php 五个回调速查

回调触发时机频率用途
onConnect($client_id)客户端连上⭐⭐返回 client_id。可推不可收
onMessage($client_id, $data)收到客户端消息⭐⭐⭐处理业务:登录、聊天、绑定
onClose($client_id)客户端断开⭐⭐清理资源,通知"xx 下线"
onWorkerStart($worker)进程启动⭐初始化定时器、连接池(别放 Worker::runAll() 之前)
onWorkerStop($worker)进程退出⚪保存状态、清理连接

4. 前端 HTML(聊天页面)

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>聊天 Demo</title> </head> <body> <p id="status">连接中...</p> <div id="messages"></div> <input type="text" id="input" placeholder="输入消息"> <button onclick="sendMsg()">发送</button> <script> var ws = new WebSocket('ws://127.0.0.1:8282'); var myUid = 0; ws.onopen = function() { document.getElementById('status').innerHTML = '已连接'; }; ws.onmessage = function(e) { var data = JSON.parse(e.data); switch (data.type) { case 'connected': // 拿到 client_id → 发登录消息绑定身份 myUid = Date.now(); // 用时间戳当临时 uid ws.send(JSON.stringify({ type: 'login', uid: myUid })); break; case 'login_ok': document.getElementById('status').innerHTML = '已登录 (uid=' + myUid + ')'; break; case 'msg': document.getElementById('messages').innerHTML += '<p>[' + data.from + '] ' + data.content + '</p>'; break; } }; ws.onclose = function() { document.getElementById('status').innerHTML = '已断开'; }; function sendMsg() { var text = document.getElementById('input').value; ws.send(JSON.stringify({ type: 'chat', from_uid: myUid, to_uid: prompt('发给谁?输入对方的 uid'), content: text })); document.getElementById('input').value = ''; } </script> </body> </html>

5. 断线重连

把上面 HTML 里这三部分替换成下面的:var ws = new WebSocket(...)、ws.onopen、ws.onclose。var myUid、ws.onmessage和sendMsg()保持不变。

var ws = null; var reconnectTimer = null; function connect() { if (ws) { ws.onclose = null; // 先解绑回调,防止关旧连接也触发重连 ws.close(); } ws = new WebSocket('ws://127.0.0.1:8282'); ws.onopen = function() { console.log('连上了'); if (reconnectTimer) { clearInterval(reconnectTimer); reconnectTimer = null; } // 把上面第4节 onopen 里的逻辑搬过来(显示状态、发 login 等) }; ws.onclose = function() { console.log('断了,5 秒后重连'); if (!reconnectTimer) { reconnectTimer = setInterval(connect, 5000); } }; } connect();

6. ThinkPHP 后台(生产模式)

// 绑定接口 function bind() { $client_id = $_POST['client_id']; $classroom_id = $_POST['classroom_id']; Gateway::bindUid($client_id, 'classroom_' . $classroom_id); return json(['code' => 1, 'msg' => '绑定成功']); } // 报名接口(处理业务 + 推送通知) function register() { $classroom_id = input('post.classroom_id'); $student_name = input('post.student_name'); // ... 存数据库、校验 ... Gateway::sendToUid('classroom_' . $classroom_id, json_encode([ 'type' => 'notify', 'message' => '有新学生报名: ' . $student_name ])); }

7. 消息协议规范

所有消息统一走 JSON,用type字段区分:

// 服务端 → 客户端 {"type":"connected", "client_id":"7f0000010a5400000001"} // 连接确认 {"type":"login_ok"} // 登录成功 {"type":"msg", "from":123, "content":"你好"} // 聊天消息 {"type":"notify", "message":"报名成功"} // 服务端通知 // 客户端 → 服务端 {"type":"login", "uid":123} // 身份登记 {"type":"chat", "from_uid":123, "to_uid":456, "content":"你好"} // 聊天

统一格式的好处:Events.php 里一个switch($data['type'])就分完了,不猜消息内容。


八、前端 WebSocket API 速查

这是浏览器原生 API,不是 GatewayWorker 的,文档去 MDN 搜 "WebSocket"。

var ws = new WebSocket('ws://IP:8282'); // 注意:不是 http:// // 4 个事件 ws.onopen = function(e) { /* 连接成功 */ }; ws.onmessage = function(e) { var data = JSON.parse(e.data); }; ws.onclose = function(e) { /* 断开 */ }; ws.onerror = function(e) { /* 出错 */ }; // 2 个方法 ws.send(JSON.stringify({ type: 'chat', content: '你好' })); ws.close(); // 1 个属性 ws.readyState // 0=连接中 1=已连接 2=关闭中 3=已关闭

注意:JS 大小写敏感,WebSocket不能写成websocket。


九、GatewayWorker API 速查

// ===== 配置(调任何方法前必须设好) ===== Gateway::$registerAddress = '127.0.0.1:1238'; // ===== 连接级别(参数带 client_id,不走 Register,直达) ===== Gateway::sendToClient($client_id, $data); // 发给指定连接 Gateway::closeClient($client_id); // 踢人下线 // ===== 用户级别(参数只有 uid,走 Register 寻址) ===== Gateway::bindUid($client_id, $uid); // 绑定 client_id ↔ uid Gateway::unbindUid($client_id, $uid); // 解绑 Gateway::sendToUid($uid, $data); // 发给指定用户 Gateway::isUidOnline($uid); // 用户在线吗 Gateway::getClientIdListByUid($uid); // 查 uid 对应所有连接 // ===== 广播 ===== Gateway::sendToAll($data); // 发给所有连接的客户端


十、端口说明

端口谁连监听 IP原因
8282浏览器 / 客户端0.0.0.0必须接受外部连接
1238Gateway / Worker 进程127.0.0.1安全,不对外暴露

8282 = 大门,谁来都行 1238 = 内部花名册,只有自己人能看


十一、踩坑记录(12 个真实坑)

#现象原因解决
1stream_socket_client报错Register 端口不对或进程没启动检查registerAddress端口和 Register 实际端口是否一致
2??语法报错Web 服务器 PHP < 7.1phpStudy 切换网站 PHP 版本到 7.1+
3CLI PHP 8.3 但网页报语法错CLI 和 Web 的 PHP 版本不一致CLI 和网站必须用同一版本
4Composer 装了 gateway-worker 4.x,网站跑 PHP 5.6 报错CLI 是 8.3,composer 自动选了最新版composer.json改^3.0,composer update
5sendToUid没反应但bindUid正常Register 端口配错检查registerAddress配置和 Register 实际监听端口一致(bindUid 自带地址能通,掩盖了问题)
6客户端连不上防火墙拦了 / IP 配置错Gateway 必须0.0.0.0;先telnet IP 8282试
7调 GatewayClient 没反应registerAddress 和 Register 不一致两边都检查,确认 Register 进程在跑
8消息发出后客户端收不到没调 bindUid 或客户端已断先isUidOnline()确认在线
9return 1前端 res.json() 失败ThinkPHP 返回了非 JSON必须return json([...])
10打印Promise{pending}fetch 没写.then()post(...).then(data => console.log(data))
11{e.client_id}JS 语法报错点号不是合法对象 key写{'client_id': e.client_id}
12Windows 部署:count 无效、start.php start报错没 pcntl 扩展用start_for_win.bat,只能单进程

十二、完整数据流 —— 两用户聊天的全过程

把前面所有知识点串起来,追踪一条消息从头到尾走的路径:

浏览器A (uid=123) 浏览器B (uid=456) │ │ │ ws://127.0.0.1:8282 │ ws://127.0.0.1:8282 ↓ ↓ Gateway ───→ Register 报到 Gateway ───→ Register 报到 │ 分配到 client_id_A │ 分配到 client_id_B ↓ ↓ onConnect → 返回 {type:'connected', ...} onConnect → 返回 {type:'connected', ...} ↓ ↓ 浏览器A 收到 connected 浏览器B 收到 connected │ │ │ ws.send({type:'login', uid:123}) │ ws.send({type:'login', uid:456}) ↓ ↓ BusinessWorker.onMessage BusinessWorker.onMessage └→ bindUid(client_id_A, 123) └→ bindUid(client_id_B, 456) └→ 返回 login_ok └→ 返回 login_ok ↓ ↓ ╔══════════════ A 给 B 发消息 ═══════════════╗ 浏览器A: ws.send({type:'chat', to_uid:456, content:'你好'}) ↓ BusinessWorker.onMessage └→ sendToUid(456, '{"type":"msg","from":123,"content":"你好"}') │ ├→ 去 Register(:1238) 问:所有 Gateway 地址列表 ├→ 遍历每个 Gateway:uid=456 在你这吗? ├→ Gateway-1 查内存:没有 → 跳过 ├→ Gateway-2 查内存:有!→ 推送 └→ 浏览器B 收到消息 ✅

关键看点:

  • 前两步(连接、登录)不走 Register,client_id 自带地址
  • 最后一步(sendToUid)必须走 Register,只有 uid 不知道在哪
  • 流程中判断标准始终是"参数里有没有地址"

十三、一句不漏 —— 七句话记全部

  1. Gateway 管连接,Register 管地址簿,BusinessWorker 写业务—— 三种进程各司其职
  2. client_id 是编码后的门牌号(IP + 端口 + 连接号),能直接定位;uid 只是名字,没有地址
  3. bindUid 直达(不走 Register),sendToUid 广播寻人(必须走 Register)—— 跟谁调无关,看参数有没有地址
  4. 模式一和模式二都能连数据库、做鉴权—— 区别不在技术能不能,在业务代码放哪维护、是不是需要重启 Worker
  5. Events.php 就是全部业务入口—— 数据库、鉴权、推送都在这里
  6. WebSocket API 是浏览器标准,去 MDN 查—— 别和 GatewayWorker 的方法混了
  7. CLI PHP 和网站 PHP 版本必须对齐—— 不然 composer 装的包能跑,网页跑不动

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询