一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

GatewayWorker 入门手册

时间:2026-07-09 11:36:07 编辑:袖梨 来源:一聚教程网

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

GatewayWorker 入门指南

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


一、从一个需求出发

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

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

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


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

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

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

简单记:Register 管撮合、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

 复制代码$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

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

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

count 数量怎么定:

进程类型建议 count理由
Register永远 1就一个登记簿,无状态,多开没意义
Gateway1 ~ CPU核数主要吃内存(每连接 ~4KB),连接数上去了优先加它
BusinessWorkerCPU核数 ~ 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:连接建立

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

阶段 2:身份绑定

 复制代码客户端收到 { 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:消息推送

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

阶段 4:断线

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

常用的几个 API(备忘)

 复制代码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 + 后台框架(正经项目用)

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

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

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

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

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

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

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

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


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

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

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

类比:

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

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


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

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

 复制代码// ========== 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);
    // 一般网络不通或端口没开会触发这里
};

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

 复制代码// 上面 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

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

步骤三:后台绑定 + 推送

 复制代码// 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
    ]));
}

流程小结

 复制代码页面打开
  → 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 查一下

九、全景图

 复制代码┌──────────────────────────────────────────────────────────────┐
│                         服务器                                │
│                                                               │
│  ┌──────────┐      ┌──────────┐      ┌────────────────────┐  │
│  │ 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 对接框架后可以几乎留空——这是正确的,不浪费

热门栏目