# GatewayWorker 入门指南

GatewayWorker 是基于 Workerman 的一个实时通信框架,专门解决服务端主动推送。平时做 Web 开发,都是客户端主动请求服务器(HTTP 请求-响应模式),服务器没办法主动找客户端。GatewayWorker 解决了这个问题------客户端和服务器之间建一条 WebSocket 长连接,想推就推。客户端可以是网页、小程序、App,只要支持 WebSocket 就行。

真实用法:报名登记页面,家长填完表单提交,服务器处理完后直接推一条"报名成功"的通知到页面右下角,不用刷新不用轮询。


一、从一个需求出发

拿报名登记页举例:家长填完信息提交 → 后台处理 → 主动推一条通知到页面右下角。整个过程涉及四个角色:

markdown 复制代码
                     ① WebSocket 长连接(建好就一直连着)
客户端  ←────────────────────────────────────────→  GatewayWorker
  │                                                    ↑
  │  ② HTTP 请求                                       │ ③ GatewayClient
  │  (提交数据、绑定身份、查数据库)                     │    (后台调它来操控推送)
  ↓                                                    │
后台(ThinkPHP) ───────────────────────────────────────┘
角色 是什么 在这个场景里
GatewayWorker 推送引擎 维持长连接、转发消息
客户端 家长打开的报名页面 连 WebSocket 等推送
后台 ThinkPHP 接口 处理报名逻辑
GatewayClient 后台调的一个 PHP 库 让后台能操控 GatewayWorker

记住这句话就够了:客户端连上 GatewayWorker 保持长连接,后台把事情处理完后通过 GatewayWorker 把结果推回去。


二、内部三个"员工"------启动后到底跑了什么

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

yaml 复制代码
                   Register(注册中心,端口 1238)
                   干的事:告诉 Gateway 和 BusinessWorker 彼此在哪
                   启动后撮合完就没它事了
                   ╱                        ╲
          Gateway(门卫)             BusinessWorker(工人)
          监听 8282 端口,接客        不对外暴露,纯内部干活
          维持客户端长连接            跑你的 Events.php
          消息来了转发给工人           处理完告诉门卫推给谁
             ↑
         客户端连这个端口

简单记:Register 管撮合、Gateway 管连接、BusinessWorker 管逻辑。

⚠️ Register 端口(1238)只能监 127.0.0.1。它用的是纯文本协议,没有任何鉴权------任何人连上来都能假装自己是 Gateway 或 BusinessWorker 发指令,从而劫持整个推送系统。所以必须限制只允许本机进程访问。

配置文件对应关系

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 你的业务代码,跑在 BusinessWorker 里面

关键看 start_gateway.php

php 复制代码
$gateway = new Gateway("tcp://0.0.0.0:8282");  // 对外端口,0.0.0.0 表示接受任何 IP 的连接
$gateway->count = 2;                            // 开 2 个进程,多进程分摊连接压力
$gateway->lanIp = '127.0.0.1';                  // 内网通信用的 IP
$gateway->startPort = 2900;                     // 内网通信端口从 2900 起
$gateway->registerAddress = '127.0.0.1:1238';   // 去哪找 Register 登记

start_businessworker.php

php 复制代码
$worker = new BusinessWorker();
$worker->count = 4;                             // 4 个进程并行处理业务
$worker->registerAddress = '127.0.0.1:1238';    // 也找同一个 Register

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

count 数量怎么定:

进程类型 建议 count 理由
Register 永远 1 就一个登记簿,无状态,多开没意义
Gateway 1 ~ CPU核数 主要吃内存(每连接 ~4KB),连接数上去了优先加它
BusinessWorker CPU核数 ~ 2×CPU核数 跑业务逻辑,吃 CPU,设多了上下文切换反而慢

实际项目:一台 4 核机器,5000 并发连接 → gateway.count=2businessworker.count=8

你到手后实际要做的就三件事:

  1. 改端口 ------ start_gateway.php 里把 8282 换成你的端口,防火墙放行
  2. 调 count ------ 按上面表格改 gateway.countbusinessworker.count,不用纠结,默认值先跑着也行
  3. Events.php ------ 这是你唯一需要写代码的地方,后面第六节会展开

另外三个配置(lanIpstartPortregisterAddress)单机部署不用改,保持默认就行。start_register.phpstart_businessworker.php 也几乎不需要动------框架模板就是对的。


三、跟一条连接走完全程

把每个阶段拆开看------从连接建立到消息送达,一共有四个阶段。

阶段 1:连接建立

php 复制代码
页面打开
  │
  ├── ① new WebSocket("ws://192.168.1.6:8282")
  │     TCP 三次握手 → 连接通了
  │
  ├── ② Gateway 给这个连接分配一个全局唯一的编号:client_id
  │     比如 "7f0000010a5400000001"
  │     注意:这个编号断开重连就会变,只给内部用
  │
  └── ③ 自动触发 Events::onConnect($client_id)
        这时候连接已通,但客户端还没发过消息
        你能主动推:Gateway::sendToClient($client_id, "...")
        但不能收------客户端还没调 ws.send()

阶段 2:身份绑定

bash 复制代码
客户端收到 { type:'connected', client_id:'7f0000...' }
  │
  ├── ④ 发 HTTP 给后台 /api/bind
  │     带两个东西:client_id + 业务身份(如 classroom_id=1)
  │
  ├── ⑤ 后台调 GatewayClient::bindUid("7f0000...", "classroom_1")
  │
  └── ⑥ GatewayWorker 内部记下映射关系:
        _uidConnections["classroom_1"] = ["7f0000010a5400000001"]
        以后说"给 classroom_1 发",就知道往哪推了

阶段 3:消息推送

rust 复制代码
家长提交报名
  │
  ├── ⑦ 后台存数据库
  │
  ├── ⑧ 后台调:
  │    GatewayClient::sendToUid("classroom_1", '{"type":"notify",...}')
  │
  └── ⑨ GatewayWorker 内部怎么找到连接?
        → 构造内部指令包
        → 广播给所有 Gateway 进程(因为不知道 classroom_1 连在哪个 Gateway 上)
        → 每个 Gateway 查自己的 _uidConnections
        → 谁管着 classroom_1 的连接,谁就负责推出去
        → 没管着的自然忽略

为什么广播给所有 Gateway?因为后台不知道具体连在哪个 Gateway 上了,所以告诉所有人,各自认领。

阶段 4:断线

php 复制代码
关闭页面 / 网络断开
  │
  └── ⑩ 自动触发 Events::onClose($client_id)
         GatewayWorker 自动清除 _uidConnections 里的绑定
         你可以在这里记日志、通知其他人"xx 下线了"

常用的几个 API(备忘)

php 复制代码
Gateway::sendToClient($client_id, $data);    // 发给指定连接
Gateway::sendToAll($data);                   // 广播给所有人
Gateway::sendToUid($uid, $data);             // 发给指定用户(重点)
Gateway::bindUid($client_id, $uid);          // 绑定 client_id ↔ uid
Gateway::unbindUid($client_id, $uid);        // 解绑
Gateway::isUidOnline($uid);                  // 查在线状态
Gateway::getClientIdListByUid($uid);         // 查 uid 对应的所有连接

四、两种用法------怎么选

模式一:纯 GatewayWorker(练手用)

复制代码
客户端 ←→ WebSocket ←→ GatewayWorker(Events.php 里写所有逻辑)

Events.php 的 onMessage 自己处理业务。简单直接,但没数据库、没法做鉴权,改需求要重启。

适合:原型验证、IoT 设备通信。

模式二:GatewayWorker + 后台框架(正经项目用)

scss 复制代码
客户端 ──── HTTP ────→ 后台(ThinkPHP) ──── GatewayClient ────→ GatewayWorker
客户端 ←── WebSocket ←──────────────────── 推送 ────────────────┘

Events.php 极简化,onMessage 甚至可以留空:

php 复制代码
public static function onMessage($client_id, $message)
{
    // 什么都不写,消息全走 HTTP 了
}

优点:数据库、鉴权、业务逻辑全在框架里,和普通 Web 开发没区别。

为什么走 HTTP 而不是 WebSocket 转后台?

技术上可以做------WebSocket 收到消息,GatewayWorker 转给后台。但代价很大:

走 WebSocket → 后台 走 HTTP → 后台
消息不持久化,丢了没了 后台收到后可存库
没法做登录校验 正常走 cookie/token
Events.php 越写越乱 业务集中在框架
发出去的消息撤不回 有记录就能撤

多一跳 HTTP,换来全套业务基础设施,生产环境必须这么干。


五、client_id 和 uid------这俩最容易搞混

上面已经反复出现了,这里专门拎清楚:

client_id uid
谁给的 GatewayWorker 自动分配 你自己定的
长啥样 7f0000010a5400000001 user_123 / classroom_1
断了重连 不变
用来干啥 内部定位一个连接 代表业务上的一个人/物

类比:

  • client_id = 快递单号,每次寄件不一样
  • uid = 身份证号,一辈子不变
  • bindUid = 把"这单快递是张三的"登记一下

实际场景:班级大屏绑定 classroom_1,后台调 sendToUid('classroom_1', ...),大屏就收到推送。你不需要关心 classrom_1 的 client_id 是什么,绑定后内部自动查。


六、完整流程代码------从前端到后端跑通

步骤一:客户端建立 WebSocket 连接

javascript 复制代码
// ========== 1. 建立 WebSocket 连接 ==========
// new WebSocket(url) 一执行,浏览器立刻开始 TCP 握手
// 注意协议前缀是 ws://,不是 http://
var ws = new WebSocket('ws://192.168.1.6:8282');
//                               ↑              ↑
//                          GatewayWorker      Gateway 端口
//                          所在服务器的 IP
var clientId = null; // 后面存 GatewayWorker 分配给我的连接编号

// ========== 2. 连接成功后的回调(TCP 握手完成) ==========
ws.onopen = function() {
    console.log('WebSocket 连上了');
    // 注意:这里还不知道 client_id
    // client_id 要等服务端 Events::onConnect 回调里主动发过来
    // 所以不要在这里做绑定,等收到消息再说
};

// ========== 3. 收到服务端消息的回调 ==========
ws.onmessage = function(e) {
    // e.data 是服务端发过来的原始字符串
    // GatewayWorker 底层帮你封装好了,这里直接拿到 JSON 字符串
    var msg = JSON.parse(e.data); // 把 JSON 字符串转成 JS 对象

    // 情况 A:收到了连接确认消息(包含 client_id)
    if (msg.type === 'connected') {
        clientId = msg.client_id; // 拿到自己的连接编号
        console.log('我的 client_id:', clientId);

        // 拿着 client_id 去后台登记身份
        // fetch() 是浏览器内置的 HTTP 请求方法,替代老旧的 XMLHttpRequest
        fetch('/api/bind', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            // 告诉后台:我是 classroom_1 的大屏,我的连接编号是 xxx
            body: JSON.stringify({
                client_id: clientId,
                classroom_id: 1
            })
        });
    }

    // 情况 B:收到了推送消息
    if (msg.type === 'batch_send') {
        // 这里是后台主动推过来的通知,比如"有新学生报名"
        alert(msg.message);
    }
};

// ========== 4. 连接断开的回调 ==========
ws.onclose = function() {
    console.log('WebSocket 断开了');
    clientId = null; // 清除旧的 client_id,重连后会重新分配
    // 实际项目里这里应该写重连逻辑,见下方"断线重连"
};

// ========== 5. 连接出错的回调 ==========
ws.onerror = function(err) {
    console.log('WebSocket 出错:', err);
    // 一般网络不通或端口没开会触发这里
};

断线重连(实际项目必备)

javascript 复制代码
// 上面 onclose 里提到重连,完整写法是这样:
var ws = null;
var reconnectTimer = null;

function connect() {
    // 如果之前有旧连接,先关掉(防止多个连接残留)
    if (ws) {
        ws.onclose = null; // 先解绑回调,不然关旧连接也会触发重连
        ws.close();
    }

    ws = new WebSocket('ws://192.168.1.6:8282');

    ws.onopen = function() {
        console.log('连上了');
        // 连上后清除重连定时器
        if (reconnectTimer) {
            clearInterval(reconnectTimer);
            reconnectTimer = null;
        }
        // 发绑定请求...
    };

    ws.onclose = function() {
        console.log('断了,5 秒后重连');
        // 断线后启动定时器,每隔 5 秒尝试重连
        if (!reconnectTimer) {
            reconnectTimer = setInterval(connect, 5000);
        }
    };
}

// 页面加载时第一次连接
connect();

步骤二:Events.php 返回 client_id

php 复制代码
// 客户端连上后,GatewayWorker 会自动调这个函数
public static function onConnect($client_id)
{
    // 把 client_id 告诉客户端,客户端收到后才能去后台绑定
    Gateway::sendToClient($client_id, json_encode([
        'type' => 'connected',
        'client_id' => $client_id  // 这个值是 GatewayWorker 分配的内部编号
    ]));
}

步骤三:后台绑定 + 推送

php 复制代码
// ThinkPHP 控制器 ------ 绑定接口
function bind() {
    $client_id = $_POST['client_id'];
    $classroom_id = $_POST['classroom_id'];

    // 告诉 GatewayWorker:这个 client_id 对应 classroom_1
    // 之后给 classroom_1 发消息,GatewayWorker 就知道推到哪个连接
    Gateway::bindUid($client_id, 'classroom_' . $classroom_id);

    return json(['code' => 1, 'msg' => '绑定成功']);
}

// ThinkPHP 控制器 ------ 报名接口
function register() {
    // ... 保存报名信息到数据库 ...

    // 推送通知给该班级的大屏
    Gateway::sendToUid('classroom_' . $classroom_id, json_encode([
        'type' => 'batch_send',
        'message' => '有新学生报名: ' . $student_name
    ]));
}

流程小结

php 复制代码
页面打开
  → new WebSocket() 连上 GatewayWorker
  → onConnect 触发,返回 client_id
  → 客户端收到,拿 client_id + 班级号 发 HTTP 到后台 /api/bind
  → 后台调 Gateway::bindUid,登记身份
  → (用户提交报名)后台调 sendToUid,推送到对应大屏

七、Events.php 五个回调------备忘

回调 什么时候触发 频率 用来干嘛
onConnect 有人连上 ⭐⭐ 返回 client_id,记连接日志
onMessage 有人发了数据 ⭐⭐⭐ 处理消息(模式一才用)
onClose 有人断开 ⭐⭐ 清理资源,通知其他人
onWorkerStart 进程启动 初始化数据库连接、定时器
onWorkerStop 进程退出 保存状态、清理连接

记住一个细节:onConnect 时连接已通,能推不能收 ------客户端还没调 ws.send()


八、踩坑记录 & 排查思路

客户端连不上

按顺序查:

  1. GatewayWorker 起来了吗? php start.php status 看进程
  2. 防火墙拦了 8282 端口吗?
  3. Gateway 监听的 IP 对了吗? 0.0.0.0 才能接受外部连接,127.0.0.1 只能本机连
  4. telnet 服务器IP 8282 试试,通了再查代码

后台调 GatewayClient 没反应

  1. GatewayClient 的 registerAddress 和你 Register 的地址一致吗?默认都是 127.0.0.1:1238
  2. Register 进程在跑吗?
  3. 是不是没绑定就直接调 sendToUid 了?必须先 bindUidsendToUid

消息发出去了但客户端没收到

  1. Gateway::getClientIdListByUid($uid) 看看绑成功了没有
  2. Gateway::isUidOnline($uid) 确认客户端还在线
  3. 检查消息格式------客户端 JSON.parse 解析成功了吗?

Windows 下坑最多

  • ❌ 不能用 php start.php start,要用 start_for_win.bat
  • count 配置无效,只会起一个进程(没 pcntl)
  • ❌ 端口被占用?netstat -ano | findstr 8282 查一下

九、全景图

ini 复制代码
┌──────────────────────────────────────────────────────────────┐
│                         服务器                                │
│                                                               │
│  ┌──────────┐      ┌──────────┐      ┌────────────────────┐  │
│  │ Register │      │ Gateway  │      │ BusinessWorker     │  │
│  │ :1238    │◄────►│ :8282    │◄────►│ (Events.php)       │  │
│  │ 撮合双方 │      │ count=2  │      │ count=4            │  │
│  │ 只127.0.0.1    │ 接客户端  │      │ 纯内部             │  │
│  └──────────┘      └────┬─────┘      └────────────────────┘  │
│                          │                                     │
└──────────────────────────┼─────────────────────────────────────┘
                      WebSocket :8282
    ┌──────────────────────┼──────────────────────────┐
    ▼                      ▼                          ▼
┌────────┐          ┌────────────┐           ┌──────────────┐
│ 客户端  │   HTTP   │ ThinkPHP   │ 调        │ GatewayClient │
│        │─────────→│            │─────────→│              │
│        │          │ 业务后台    │           │ 一个 PHP 库   │
└────────┘          └────────────┘           └──────────────┘

十、核心记住这几句

  1. GatewayWorker 是根推送管道------不存数据、不管业务,只管送达
  2. client_id 是内部编号,uid 是你定的业务身份------绑定后只管 uid
  3. Register 撮合、Gateway 接客、BusinessWorker 干活------三种进程各司其职
  4. 生产环境消息走 HTTP 进后台,不要走 WebSocket------持久化 + 鉴权 + 集中管理
  5. Events.php 对接框架后可以几乎留空------这是正确的,不浪费
相关推荐
2601_963771376 小时前
Optimizing Logistics and Transportation Sites on WordPress
前端·数据库·windows·php
草邦设计开发团队_媒体资源平台7 小时前
Linux 稳定常驻启动 PHP 队列,服务器重启自动恢复
linux·服务器·php·队列执行
2601_963771377 小时前
Speeding Up Creative Agency Portfolios: A Performance Guide
前端·数据库·php
七夜zippoe8 小时前
OpenClaw 数据加密:敏感信息保护的完整方案
数据库·mysql·php·openclaw·敏感保护
烤代码的吐司君1 天前
Redis IO 多路复用原理与引入原因深度解析
数据库·redis·php
鹏易灵1 天前
C++——8.移动语义初探(移动构造、移动赋值)
开发语言·c++·php
从零开始的代码生活_1 天前
Linux epoll 多路转接详解
linux·运维·网络·后端·tcp/ip·计算机网络·php
不吃辣4902 天前
前端应用的离线暂停更新策略:构建稳定可靠的渐进式更新方案
开发语言·前端·php
钟智强2 天前
我警告了 329 天
git·php·wordpress·webshell