ThinkPHP 6.0 本质上是一个 HTTP 框架,专注于处理短生命周期的 Web 请求,因此原生不支持纯 TCP/UDP 长连接服务端并不意外。然而,如果需要在 TP6 中实现实时通信(如即时通讯、设备指令下发、长连接通道),则必须借助底层网络扩展来提供辅助。
需要明确的是:think-swoole 和 think-worker 这两种方式都无法直接实现需求。think-swoole 专为 HTTP/WebSocket 场景设计,而 think-worker 本质上是 Workerman 的封装,与 Swoole 的原生协程模型不兼容,无法直接构建标准的 TCP/UDP Server。
真正可行的方向仅有一条:绕过 TP6 的生命周期,使用原生 Swoole 启动独立服务,然后通过 Redis、Swoole Table 等中间件与 TP6 应用进行数据联动。听起来有些复杂,但拆分后其实并不难理解。

一、为什么不能直接在 TP6 里写 Swoole TCP Server?
TP6 的 think-swoole 扩展仅封装了 SwooleHttpServer 和 SwooleWebSocketServer,其启动逻辑完全绑定在 HTTP 请求的生命周期内。具体而言:
- 启动时注册的是
onRequest事件,而非onConnect或onReceive; - 所有回调均运行在“HTTP 上下文”中,无法直接访问
$fd,不支持自定义协议解析,也缺少连接元信息管理的能力; - 若强行在
onReceive中调用 TP6 容器或 ORM,极易引发协程上下文丢失、数据库连接复用混乱、内存泄漏等问题。
简单来说:不要在框架的河道里强行航行一艘海船。
二、正确的做法:分离架构 + 协同通信
将长连接服务与 Web 业务解耦,各司其职:
- TCP/UDP 服务端:使用原生
SwooleServer实现,负责连接管理、心跳保活、协议解析及广播路由; - TP6 Web 层:专注于用户认证、消息持久化、权限校验和 API 接口提供;
- 协同枢纽:借助
SwooleTable(跨 Worker 共享连接状态)、Redis(发布/订阅以通知业务变更)、MySQL(存储消息和关系数据)来打通两端。
这就是关键——各尽其责,不要期望一个进程完成所有任务。
三、TCP 长连接服务端(Swoole 原生)代码实战
来看一个生产级的 TCP 长连接最小可行实现,已经适配了 SLB 和高并发场景:
// server/tcp_server.php
use SwooleServer;
use SwooleTable;
// 1. 创建跨 Worker 连接表
$table = new Table(2048);
$table->column('ip', SwooleTable::TYPE_STRING, 46);
$table->column('port', SwooleTable::TYPE_INT, 2);
$table->column('connect_time', SwooleTable::TYPE_INT, 8);
$table->column('last_active', SwooleTable::TYPE_INT, 8);
$table->create();
$serv = new Server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_TCP);
$serv->set([
'worker_num' => swoole_cpu_num() * 2,
'heartbeat_idle_time' => 300, // 必须 > SLB 空闲超时(如阿里云默认 300s)
'heartbeat_check_interval' => 60, // 内核级心跳检测间隔
'open_tcp_nodelay' => true,
'package_max_length' => 10 * 1024 * 1024,
]);
$serv->on('Start', function ($serv) {
echo "TCP server started at port {$serv->port}\n";
});
$serv->on('Connect', function ($serv, $fd, $from_id) use ($table) {
$info = $serv->getClientInfo($fd);
$table->set($fd, [
'ip' => $info['remote_ip'],
'port' => $info['remote_port'],
'connect_time' => time(),
'last_active' => time(),
]);
});
$serv->on('Receive', function ($serv, $fd, $from_id, $data) use ($table) {
// 无条件刷新活跃时间(哪怕收到空包或非法帧)
$table->set($fd, ['last_active' => time()]);
// 示例:识别 PING 心跳并立即回 PONG
if (trim($data) === 'PING') {
$serv->send($fd, "PONG\n");
return;
}
// 解析业务协议(如 JSON / 自定义二进制),投递到 Task 进程避免阻塞
$serv->task(['fd' => $fd, 'data' => $data]);
});
$serv->on('Task', function ($serv, $task_id, $from_id, $data) {
// 在此处做 DB 查询、Redis 推送、权限校验等耗时操作
// 注意:不可调用 $serv->send(),需用 finish 或 onFinish 回传
$result = handleBusinessLogic($data['data']);
$serv->finish(['fd' => $data['fd'], 'result' => $result]);
});
$serv->on('Finish', function ($serv, $task_id, $data) {
if (isset($data['fd']) && $serv->exist($data['fd'])) {
$serv->send($data['fd'], json_encode($data['result']) . "\n");
}
});
$serv->on('Close', function ($serv, $fd) use ($table) {
$table->del($fd); // 清理连接元信息
});
$serv->start();这段代码有几个值得注意的技术细节:心跳检测采用内核级机制,避免了 PHP 层的轮询开销;Task 进程专门处理耗时的业务逻辑,不会阻塞主 Reactor 线程;连接表直接存储在内存中,具备极快的读取速度。
四、UDP 服务端(轻量设备通信场景)
UDP 无连接、低开销,适合物联网终端上报、日志采集这类场景。不过要牢记:UDP 不保证送达,应用层需要自行实现重传和确认机制。
// server/udp_server.php
$serv = new SwooleServer('0.0.0.0', 9601, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);
$serv->on('Packet', function ($serv, $data, $client_info) {
// $client_info 包含 address/port,可作为唯一标识(类似 fd)
$addr = $client_info['address'] . ':' . $client_info['port'];
// 记录活跃时间(可用 Redis hash 存)
redis()->hSet('udp_clients', $addr, time());
redis()->expire('udp_clients', 300);
// 解析并响应
$response = processUdpPacket($data);
$serv->sendto($client_info['address'], $client_info['port'], $response);
});
$serv->start();UDP 服务端的优势在于连接开销几乎为零,但相应的代价是需要自行处理丢包、乱序、重复等问题。
五、与 TP6 项目如何联动
核心原则是:不要让 TP6 直接启动 Swoole Server,而是让两者并行独立运行。
- 启动方式:分别执行
php server/tcp_server.php和php think serve(或者部署 Nginx + PHP-FPM); - 共享数据:
- 连接状态 →
SwooleTable(内存级,毫秒级响应); - 用户会话/消息 → Redis(支持发布订阅,TP6 可监听事件进行异步处理);
- 持久记录 → MySQL(TP6 负责写入,TCP Server 读取群组关系等配置);
- 连接状态 →
- 安全协同:TCP Server 收到设备登录请求后,调用 TP6 提供的 REST API(例如
POST /api/auth/bind)完成身份核验,再将授权状态写入 Redis,后续通信直接查询缓存即可。
该方案本身并不复杂,但容易忽略各组件之间的数据一致性保障。例如连接状态和认证令牌存放于不同位置,如何确保它们同步?建议将 TP6 的鉴权逻辑封装为独立服务,通过 Redis 或 HTTP 接口双向共享,避免出现“连接仍在、权限已过期”的尴尬情况。
