GatewayWorker 从零到一完整指南

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

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

复制代码

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

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

但现实中有大量场景需要跨客户端通知------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.php Register text://127.0.0.1:1238
start_gateway.php Gateway tcp://0.0.0.0:8282,2 个进程
start_businessworker.php BusinessWorker 4 个进程,指向 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 连接;开多了反而加大广播开销
BusinessWorker CPU核数 ~ 2×核数 吃 CPU,多了上下文切换反而慢

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

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

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

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

lanIpstartPortregisterAddress 单机部署不用改。start_register.phpstart_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 进程,能 use ThinkPHP 的 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 直接 use ThinkPHP 的类:

复制代码

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.onopenws.onclosevar myUidws.onmessagesendMsg() 保持不变。

复制代码

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 必须接受外部连接
1238 Gateway / Worker 进程 127.0.0.1 安全,不对外暴露
复制代码

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


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

# 现象 原因 解决
1 stream_socket_client 报错 Register 端口不对或进程没启动 检查 registerAddress 端口和 Register 实际端口是否一致
2 ?? 语法报错 Web 服务器 PHP < 7.1 phpStudy 切换网站 PHP 版本到 7.1+
3 CLI PHP 8.3 但网页报语法错 CLI 和 Web 的 PHP 版本不一致 CLI 和网站必须用同一版本
4 Composer 装了 gateway-worker 4.x,网站跑 PHP 5.6 报错 CLI 是 8.3,composer 自动选了最新版 composer.json^3.0composer update
5 sendToUid 没反应但 bindUid 正常 Register 端口配错 检查 registerAddress 配置和 Register 实际监听端口一致(bindUid 自带地址能通,掩盖了问题)
6 客户端连不上 防火墙拦了 / IP 配置错 Gateway 必须 0.0.0.0;先 telnet IP 8282
7 调 GatewayClient 没反应 registerAddress 和 Register 不一致 两边都检查,确认 Register 进程在跑
8 消息发出后客户端收不到 没调 bindUid 或客户端已断 isUidOnline() 确认在线
9 return 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}
12 Windows 部署: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 装的包能跑,网页跑不动
相关推荐
脉动数据行情1 小时前
Java OkHttp 完整封装 GC COMEX 美黄金实时盘口定时拉取工具类
java·okhttp·comex 黄金·外盘贵金属
敖行客 Allthinker1 小时前
IM 融合专题:后端架构师的核心修炼
java·开发语言·数据库
HAPPY酷1 小时前
【ROS2】16G 内存笔记本跑 ROS2 仿真?VMware 虚拟机“保姆级”配置指南 (R9 7940HX + RTX 4060)
java·linux·ide·windows·pycharm
折哥的程序人生 · 物流技术专研10 小时前
第4篇:Lambda 简化策略模式(Java 8+)
java·设计模式·策略模式·函数式编程·lambda·代码简化·扩充系列
researcher-Jiang10 小时前
高性能计算之OpenMP——超算习堂学习1
android·java·学习
西门吹-禅12 小时前
java springboot N+1问题
java·开发语言·spring boot
DLYSB_12 小时前
生命通道:如何用 HIS 医疗系统直连网络声光终端,打造“零延误”的危急值响应网关?
java·网络·数据库·报警灯
weixin_BYSJ198712 小时前
SpringBoot + MySQL 乒乓球运动员信息管理系统项目实战--附源码04954
java·javascript·spring boot·python·django·flask·php
AI小码12 小时前
LLM 应用的缓存工程:当每次 API 调用都在燃烧成本
java·人工智能·spring·计算机·llm·编程·api