webman的tcp服务需手动处理粘包、心跳等底层逻辑,须在config/server.php中配置tcp监听器并实现自定义frameparser解析二进制帧,协程启用后所有i/o操作必须替换为协程版本。

Webman 的 TCP 服务不是开箱即用的 HTTP 扩展,而是基于 Workerman 底层的长连接能力构建的——这意味着你得亲手处理粘包、心跳、协议解析和连接生命周期,否则设备一连就断、一发就乱。
如何在 Webman 中启动一个基础 TCP 服务器
Webman 本身不内置 TCP 监听入口,必须通过 config/server.php 显式添加 TCP 类型监听器,并绑定回调逻辑。它不走路由系统,也不经过中间件,所有数据收发都在 onConnect、onReceive、onClose 这三个事件里完成。
- 在
config/server.php的servers数组中新增一项,type设为'tcp',listen指定地址端口(如'tcp://0.0.0.0:8383') -
setting中必须关闭默认分包行为:'open_eof_split' => false,否则会误切二进制帧 -
callback键下挂载三个闭包:分别处理onConnect(连接建立)、onReceive(收到原始字节流)、onClose(连接断开) - 不要在
onConnect里做耗时操作(如查数据库),协程未启用时会阻塞整个进程
为什么自定义 FrameParser 是绕不开的一步
物联网设备发来的数据是裸二进制流,没有换行或固定分隔符,onReceive 可能一次收到半包、两包拼接,或一包被拆成多次回调——这就是粘包/拆包。Webman 不像 Netty 那样自动帮你识别消息边界,必须自己实现帧解析逻辑。
Webman 2.2.0版本强化了 TCP/UDP 服务支持,优化路由组管理,并增强异步任务处理能力。结合协程与连接池技术,Webman 能轻松应对高并发场景,适用于网站、接口服务、即时通讯、物联网及游戏开发,兼具高性能、灵活扩展与稳定可靠,是多场景 PHP 服务开发的理想选择。
- 推荐做法:在
app/Protocol/CustomProtocol.php中写一个类,实现parse(string $buffer): int|array和pack(array $data): string -
parse()要先读前 4 字节作为包长度(unpack('N', $buffer)),再判断剩余缓冲区是否 ≥ 该长度;不足则返回0等待更多数据,足够则截取并返回解包后数组 - 务必校验长度值是否合理(比如 > 1MB 或
- 别用
json_encode()直接序列化二进制 payload,某些字段含 \x00 会导致截断;改用msgpack_pack()或自定义二进制编码
设备心跳与连接超时管理的实际写法
Webman 没有内置心跳机制,TCP 连接空闲时不会自动探测设备是否掉线。若不主动维护,几百个“假在线”连接会持续占用内存和文件描述符,最终触发 Too many open files。
- 在
onConnect回调中,为每个$connection设置唯一 ID(如md5($connection->getRemoteIp() . ':' . $connection->getRemotePort())),存入静态数组或 Redis - 调用
Timer::add(5, function() use ($connection) { ... })启动 5 秒定时器,每次检查$connection->lastActiveTime是否超过阈值(如 30 秒) - 在
onReceive中识别心跳包(例如 opcode =0x01或固定 JSON 字段),更新$connection->lastActiveTime = time(),并用Timer::del($timerId)清除旧定时器再重建 - 注意:定时器 ID 必须绑定到连接对象上(如
$connection->heartbeatTimerId = $id),否则无法精准清除
协程启用后 TCP 处理逻辑要重写吗
要。协程不是魔法开关,它只改变 I/O 调用的阻塞方式。如果你在 onReceive 里用了 file_get_contents()、curl_exec() 或同步数据库查询,即使开了协程,也会让整个 worker 进程卡住——因为这些函数不支持协程化。
- 确认已按规范配置
config/process.php,为 TCP 进程指定eventLoop(如Workerman\Events\Swoole::class) - 所有外部调用必须换成协程版:用
Swoole\Coroutine\Http\Client替代 cURL,用co\mysql_connect()或webman/database的协程驱动替代 PDO -
onReceive回调内不能直接sleep()或usleep(),改用Co::sleep(0.1) - 别在协程中使用全局变量或静态属性保存连接状态,它们会被多个协程共享,引发竞态;改用
$connection对象属性或协程上下文Context::set()
真正难的不是写通第一条指令,而是当 5000 台设备同时重连、心跳包错序、CRC 校验失败率突然升到 12% 时,你还得从日志里一眼定位是帧解析逻辑缺陷,还是网关 NAT 超时导致的 ACK 丢失——这些细节不会出现在文档里,但每天都在生产环境发生。










