news 2026/9/3 19:46:31

Workerman+ThinkPHP5实现在线客服系统:长连接架构与实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Workerman+ThinkPHP5实现在线客服系统:长连接架构与实战解析

简介:面向需要部署在线客服系统的 PHP 开发者与运维人员,这是 Workerman 在线客服系统安装部署资源包,围绕 Nginx 1.21.4 + PHP 7.2 + MySQL 5.7.40 环境展开,以课程资源加软件插件形式整理,重点解决源码上传、解压、数据库连接配置等实际部署问题,也适合希望了解 Workerman 与 FastAdmin 后台结构的学员。整个压缩包共 2000 个文件,大小约 25.95 MB,其中 1184 个 JS、106 个 CSS、196 个 HTML 构成前台与后台界面,22 个 PHP、4 个 SQL 支撑业务逻辑和数据库结构,大量 JSON、MD、TXT 用来存放配置项与说明文档,另有 Vue、YAML 等文件,类型覆盖较全。FastAdmin 相关 CSS 文件也包含在内,便于二次开发时定位后台页面样式。压缩包内附详细安装教程,包括环境版本、上传解压、修改 application/database.php 中的数据库名、用户名和密码等关键提示,能帮助按步骤完成部署并排查常见问题。目前已有 408 人学习/下载,适合 PHP 项目部署和在线客服系统二次开发的入门参考。 手头正在做客服系统的朋友,大概率都遇到过这种情况:轮询接口撑不住并发,长连接又不知道从哪下手,后端写起来总觉得别扭。我之前接了个在线客服需求,后端是ThinkPHP 5,要求支持访客和客服实时聊天、未读消息推送、会话记录入库,当时就选了Workerman来做长连接层。这套方案跑下来,整体效果很稳,今天把完整实现过程拆开讲一遍,包括架构选型、核心代码、踩坑记录和优化思路,给打算入坑的朋友一份可以直接参考的实操笔记。

Workerman本身是一个PHP常驻内存的事件驱动框架,基于非阻塞IO实现,特点是能扛住高并发长连接,同时写起来又不像Swoole那样需要扩展编译,纯PHP就能跑。配合ThinkPHP 5这种传统Web框架,正好互补:TP5负责业务逻辑、后台管理、接口输出,Workerman负责长连接、实时推送、在线状态维护。这个“Web框架处理业务,Workerman处理长连接”的组合,是我认为最稳妥、也最容易上手的架构方式。

1. 项目定位与架构思路

1.1 为什么选Workerman而不是其他方案

做在线客服,最核心的技术问题就是实时消息通道。最初我也考虑过用前端轮询,但客服场景里消息频率高、并发集中,轮询的空请求会白白吃掉大量服务器资源,而且消息延迟不可控,访客体验很差。用Swoole也能做,但我当时的环境要装扩展、改PHP配置,而且团队对Swoole的协程模型不熟,出了问题排查成本高。Workerman的优势很明确:纯PHP实现,不需要额外编译扩展,Windows和Linux都能跑(生产环境建议Linux),文档细致、社区活跃,网上能搜到的问题基本都有解决方案。

还有一点很关键,Workerman自带的GatewayWorker框架,把“客户端连接管理”这件事做了高度封装。在线客服系统本质上就是一个多客户端连接、分组通信的场景——访客要发给客服,客服要回复访客,可能还要支持客服之间互相转接。GatewayWorker天然支持客户端分组、跨进程通讯、心跳检测、断线重连,几乎就是为这类IM场景设计的。我不用自己处理底层的连接池和消息路由,只需要关注业务消息怎么解析、怎么分发、怎么落库,开发效率提升非常明显。

1.2 整体架构:GatewayWorker + ThinkPHP 5结合

这套系统的架构,我分成了三层来看。

第一层是客户端,包括浏览器端的访客聊天窗口(Web页面里通过WebSocket连接)和客服端工作台(也是浏览器页面,同样走WebSocket)。两个端本质上是同一种连接类型,只是登录身份不同、数据权限不同。

第二层是GatewayWorker服务,也就是长连接网关层。它负责维护所有客户端的WebSocket连接,接收客户端发来的消息,解码后转发给Event处理逻辑,再把结果推送给目标客户端。

第三层是业务层和存储层。Workerman进程运行期间,通过PHP代码直接操作MySQL、Redis。登录校验、历史消息查询这类请求,由TP5的控制器处理,通过HTTP接口完成;Workerman则通过Redis订阅等方式和TP5通信。

实际开发中,我并没有强行让Workerman去加载TP5的整个框架,因为常驻内存进程和Web请求的生命周期完全不同,直接加载框架容易出现单例模式数据错乱、数据库连接时间长了断开等问题。我的做法是:Workerman独立运行,通过think\Db查询构造器或者原生PDO操作数据库,同时封装一个独立的工具类来处理加解密、数据格式化等逻辑。这样既复用了TP5的数据库配置和连接池能力,又不会因为框架过度耦合导致长连接进程不稳定。

2. 环境准备与基础服务搭建

2.1 安装Workerman与GatewayWorker

我的环境是CentOS 7 + PHP 7.2 + Nginx + MySQL 5.7,这个组合在中小项目里非常常见。安装Workerman有两种方式,我推荐用Composer,方便后续维护依赖版本。

composer require workerman/workerman composer require workerman/gatewayworker

如果你还没安装Composer,先装Composer再执行上面两条命令。安装完成后,vendor/目录下会出现workermangatewayworker两个目录。我习惯在项目根目录下建一个workerman目录,专门放客服系统的服务端代码,和TP5的application目录分开,结构更清晰。

如果你是手动下载安装包的方式,那需要特别注意PHP版本。Workerman 4.x要求PHP 7.0以上,我最初用Workerman 3.x跑PHP 5.6也正常,但后来为了用PHP 7.2的标量类型声明和语法糖,统一升级到了4.x。还有一点,生产环境一定要确保PHP安装了posixpcntl扩展,Workerman的多进程管理依赖这两个扩展,没装的话服务根本起不来。

2.2 启动GatewayWorker服务

GatewayWorker自带了一套完整的启动脚本,放在Applications/目录下。启动前需要修改Applications/your_app/start_gateway.phpstart_business.php这两个配置文件。

// start_gateway.php // 设置Gateway监听的协议、IP和端口 $gateway = new Gateway("websocket://0.0.0.0:8282"); // 设置进程数,建议和CPU核数相同 $gateway->count = 4; // 设置LAN IP,也就是当前服务器的内网IP $gateway->lanIp = '127.0.0.1'; // 内部通讯起始端口 $gateaway->startPort = 4000; // 服务名称,方便日志区分 $gateway->name = 'KefuGateway';
// start_business.php // BusinessWorker进程,专门处理业务逻辑 $worker = new BusinessWorker(); // worker名称 $worker->name = 'KefuBusinessWorker'; // 业务处理类,后面要实现的 $worker->eventHandler = 'KefuEvent'; // 进程数,根据业务压力调整 $worker->count = 2;

启动命令很简单:

php start.php start

看到进程起来后,用php start.php status可以查看各个进程的运行状态。这里有个关键点要提醒:websocket://0.0.0.0:8282是给浏览器端WebSocket连接的端口,而lanIpstartPort是GatewayWorker内部进程通信用的,这两个不能冲突。如果服务器上有防火墙,记得放行8282端口和4000起始的一段内部端口。

3. 核心功能实现

3.1 服务端消息处理逻辑

所有业务消息都集中在事件处理类中,也就是配置文件里指定的KefuEvent类。当有客户端连接、断开、发消息时,GatewayWorker会自动触发对应的方法。我实现了三个最核心的方法:onConnect(连接建立)、onMessage(收到消息)、onClose(连接断开)。

class KefuEvent { public static function onConnect($client_id) { // 连接建立,可以先不处理,等客户端发送登录认证消息 } public static function onMessage($client_id, $message) { $data = json_decode($message, true); switch ($data['type']) { case 'login': // 登录验证,绑定client_id和用户信息 break; case 'chat': // 聊天消息处理 break; case 'ping': // 心跳响应 break; } } public static function onClose($client_id) { // 清理连接绑定的用户信息,发送下线通知 } }

这里重点说下login的实现。客户端建立WebSocket连接后,第一件事不是发聊天消息,而是发一条login消息,带上访客或客服的标识。我在Redis里维护一个映射关系:user_id -> client_id,同时用GatewayWorker的Gateway::bindUid方法把client_id和用户ID绑定。绑定后,我就能用Gateway::sendToUid($uid, $message)给指定用户推送消息,这是最核心的API。

chat消息的处理要做两件事:一是通过Gateway::sendToUid推送给目标用户,二是把消息内容写入MySQL,方便后续历史记录查询。这里有一条经验:不要在onMessage里直接做耗时的业务操作,比如发HTTP请求调用第三方接口,否则会阻塞当前事件循环,影响同进程其他用户的消息。如果确实需要,用异步任务或者把消息丢到Redis队列里,由另外的脚本去消费。

3.2 客服端与访客端JS对接

前端需要建立WebSocket连接并处理收发消息。我用原生JS写了一个简单的封装,方便在访客聊天窗口和客服工作台复用。

function KefuSocket(options) { this.ws = null; this.userId = options.userId; this.userType = options.userType; // visitor 或 agent this.url = options.url; } KefuSocket.prototype.connect = function() { var that = this; this.ws = new WebSocket(this.url); this.ws.onopen = function() { // 发送登录认证 that.ws.send(JSON.stringify({ type: 'login', userId: that.userId, userType: that.userType })); }; this.ws.onmessage = function(event) { var data = JSON.parse(event.data); that.handleMessage(data); }; this.ws.onclose = function() { // 断线自动重连 setTimeout(function() { that.connect(); }, 3000); }; };

这个封装里,我保留了三个钩子函数:onMessageonOpenonClose,在实际业务页面里覆盖它们就行。比如说访客端页面里,收到客服回复后就渲染到聊天窗口里;客服工作台里,收到新访客发来的消息后就弹出提醒并刷新会话列表。

这里有个细节:WebSocket的URL是ws://域名:8282,如果你用了HTTPS,浏览器会强制要求使用wss://协议,否则会报安全错误。我当时在Nginx里做了SSL终结,把/ws路径代理到后端的8282端口,前端连接wss://域名/ws,这样既解决了加密问题,也避免了端口直接暴露。

3.3 后台历史消息与未读计数

在线客服除了实时聊天,还有一个非常重要的功能:历史消息记录。访客刷新页面后,要能拉取之前的聊天记录;客服也要能看到用户的历史咨询记录。这部分功能,我放在TP5的控制器里实现,走HTTP接口,不走WebSocket。

// application/api/controller/Chat.php public function history() { $userId = input('get.user_id'); $page = input('get.page', 1); $list = Db::name('chat_message') ->where('user_id', $userId) ->order('id desc') ->page($page, 20) ->select(); return json(['code' => 0, 'data' => $list]); }

未读消息数量,我用Redis的INCR计数来实现。消息推送给离线用户时(通过Gateway::isUidOnline判断),给该用户的未读计数加一。用户打开聊天窗口时调一个HTTP接口,把未读清零。这个方案实现简单,成本低,在中小规模下完全够用。

4. 数据落地与业务集成

4.1 消息记录入库的设计方案

消息入库这块,我踩过一次坑。最开始我在每收到一条消息时立即写库,高峰期数据库连接频繁创建,压力很大。后来我优化成“批量入库 + 定时刷新”的策略。

具体做法是:在Workerman里增加一个BusinessWorker进程,专门从Redis的message_queue队列里取消息,攒够50条或者每3秒批量插入一次MySQL。这样数据库写入次数从每秒几十次降到了每秒几次,效果非常明显。

// 批量插入示例 $messages = $redis->lRange('message_queue', 0, 49); $redis->lTrim('message_queue', 50, -1); $data = []; foreach ($messages as $msg) { $data[] = json_decode($msg, true); } Db::name('chat_message')->insertAll($data);

消息表结构我设计得比较简单,核心字段包括:消息ID、会话ID、发送者ID、接收者ID、消息类型(文本、图片、系统消息)、消息内容、发送时间。会话ID用来关联同一个访客和同一个客服之间的多次沟通记录。后端查询时按会话ID筛选即可。

4.2 与ThinkPHP 5业务系统的整合

GatewayWorker运行在常驻内存里,它要操作数据库,就需要读取TP5的数据库配置。我的做法是写一个单独的工具类KefuDb,在启动Worker时初始化一次数据库连接配置,后续所有查询都复用这个连接。

// KefuDb.php class KefuDb { public static function init() { $config = require_once __DIR__ . '/../config/think5_database.php'; // 这里用TP5的Db类初始化,或者用原生PDO连接 Db::setConfig($config); } }

还有一个场景要注意:访客在网页上先提交表单,然后接入客服,这中间涉及TP5和Workerman的数据同步。我是用TP5的控制器写入一条“接入记录”到MySQL,同时通过Redispublish一条消息,Workerman里订阅了这个频道,收到消息后就向对应客服推送“有新访客接入”的提醒。这个通过Redis发布订阅实现跨进程通信的方案,比直接让TP5调用Workerman的接口要简单稳定得多。

5. 常见问题与排查技巧实录

5.1 端口占用与进程冲突

这是最常遇到的问题。Workerman启动时报错address already in use,通常是8282端口被之前的进程占用了。排查方法:

netstat -lnp | grep 8282

找到占用进程的PID,用kill -9 PID强制结束,再重新启动。另一个隐蔽问题是开发阶段在Windows上调试,Workerman 3.x在Windows下只能单进程运行,start.php start后要注意看窗口里的提示,如果显示“只能运行一个进程”,是正常现象,部署到Linux就好。

5.2 WebSocket连接不稳定,频繁掉线

如果连接刚建立就断开,或者隔一段时间就掉线,大概率是网络层的问题。首先要确认客户端和服务端是否正常完成了WebSocket握手。可以在Nginx日志里查看/ws接口返回的状态码,如果是200说明握手成功。

其次是心跳机制。GatewayWorker默认有心跳检测时间,如果客户端长时间没有数据往来,服务端会主动断开连接。我的做法是前端每隔30秒发送一个ping消息,服务端收到后返回pong,这样就能保持连接不被误杀。

setInterval(function() { this.ws.send(JSON.stringify({type: 'ping'})); }, 30000);

服务端onMessage里收到typeping时,直接返回一个空消息即可,不要走业务逻辑。这里需要注意,心跳消息也要包含在onMessage里做判断,否则意外发到业务处理里会报错。

5.3 Nginx反向代理WebSocket的配置

如果WebSocket前端直连8282端口,通常没问题。但为了安全、复用80/443端口,很多人会选择用Nginx做反向代理。Nginx需要额外配置才能正确转发WebSocket的升级请求,核心配置如下:

location /ws { proxy_pass http://127.0.0.1:8282; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }

proxy_read_timeoutproxy_send_timeout这两项特别重要,默认60秒超时,如果客户端长时间不发送数据,Nginx会自动断开连接。我之前就是漏了这两个配置,导致每过60秒客服端就掉线一次,排查了整整一个下午。

5.4 BusinessWorker进程内存持续增长

Workerman常驻内存,如果业务代码里有变量没有释放,内存会缓慢增长,最终导致进程崩溃。我遇到过的问题是:在循环处理消息时,把日志数据累加到一个静态变量里,没有及时清空,运行几天后内存直接打满。

解决方法是:每处理完一条消息后,检查一遍代码里是否有$GLOBALS、静态数组、单例对象等在持续积累数据。排查内存泄漏,可以用php start.php status查看进程内存占用,连续观察几个小时,如果内存只升不降,基本可以断定有泄漏。

6. 性能调优与扩展思路

6.1 进程数配置与系统限制调整

GatewayWorker的进程数不是越多越好。Gateway进程数建议和CPU核数一致,BusinessWorker进程数可以稍稍多配一点,但也不要超过CPU核数的两倍。我这里有个经验值:4核8G的服务器,Gateway配4个进程,BusinessWorker配2个进程,实测能稳定支撑3000+并发连接,消息延迟在毫秒级。

系统层还有一个限制要调整:单进程能打开的文件描述符数量。默认是1024,当连接数超过这个值,新连接会被拒绝。需要修改/etc/security/limits.conf,把nofile调大:

* soft nofile 65535 * hard nofile 65535

修改后需要重新登录服务器生效。用ulimit -n验证是否生效。

6.2 平滑重启与多客服路由分发

上线后要改代码,最怕的就是重启服务导致在线用户掉线。GatewayWorker支持平滑重启,执行php start.php reload,它会逐个重启BusinessWorker进程,不影响已建立的连接。但注意,修改启动配置(比如端口、进程数)时,必须用php start.php restart完整重启,reload不会生效。

业务层面,如果以后要做多客服同时在线、自动分配访客的功能,可以在login时把这个客服标记为“在线”,访客发起咨询时,通过Redis里的客服状态列表,自动选一个空闲客服绑定会话。这里不细说实现代码,但思路就是:把“访客与客服的会话绑定关系”维护在Redis里,消息路由时先查绑定关系,再转发。这个设计可以大幅提升系统的扩展能力。

另外,如果要做聊天记录的全文搜索、多维度统计报表,可以在消息入库时同步把数据推送到Elasticsearch或者专门的统计系统,Workerman这边只需要保证消息推送和入库的稳定性,业务分析放到另外的系统去做,互不干扰。

这套系统从开发到上线,我最大的感受是:Workerman把长连接这块硬骨头啃下来之后,剩下的工作其实就是在它上面搭积木。如果你之前只用TP5做过传统的Web请求,第一次接触Worker时可能会不习惯“常驻内存、事件回调”的写法,但只要理解了连接、事件、消息分发这几个核心概念,写起来并不难。尤其是遇到问题的时候,多看看GatewayWorker自带的示例代码,里面的聊天室demo其实就是一个简化的客服系统,值得好好研究。照着这个demo扩展,比自己从零摸索要快得多。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 19:46:22

RAG投毒致注意力崩溃,文档级注意力是关键检测信号

RAG 投毒这套攻击思路,凡是做过知识库问答的人应该都不陌生。但我第一次真正意识到“投毒能打崩注意力”时,并不是因为模型输出了明显有害的话,而是因为一次很普通的问答:用户问某个政策条款,检索系统返回了三篇文档&a…

作者头像 李华
网站建设 2026/9/3 19:46:18

地平线扭亏为盈背后:自动驾驶芯片量产与工具链的胜利

一份顶着“扭亏为盈”字样的财报,放在芯片行业里往往比“某某车型销量破万”更值得细看。地平线机器人 2026 年上半年归母净利润 37.84 亿元、同比扭亏为盈,这个数字出现在新闻标题里是一行资本信号,但放到自动驾驶产业链里,它背后…

作者头像 李华
网站建设 2026/9/3 19:45:43

从37.84亿净利润拆解地平线智驾芯片业务的盈利质量

地平线机器人 2026 年上半年归母净利润 37.84 亿元,同比扭亏为盈。看到这个数字,我第一反应不是去猜股价,而是想先把一个更实际的问题搞清楚:这家做智能驾驶计算方案的公司,靠的是主营业务做大,还是靠一次性…

作者头像 李华
网站建设 2026/9/3 19:43:08

山景DSP烧录全流程:从下载工具到启动配置排查

简介:这是针对上海山景 DSP(常见如 bp1048 芯片)开发的独立固件下载工具包,面向需要绕过 IDE 插件、直接对 bin 格式固件进行烧录的嵌入式开发者。工具内置 64 位与 32 位两套可执行程序,配合动态链接库、批处理脚本与…

作者头像 李华
网站建设 2026/9/3 19:41:27

基于YOLO的人脸表情识别:从数据准备到模型部署全流程实战

简介:本资源是一个基于YOLO架构的人脸表情识别系统实现,面向计算机视觉初学者与AI应用开发者,解决实时人脸检测与七类基础表情(如高兴、愤怒、悲伤等)分类的实际问题,适用于人机交互、智能安防及情绪分析等…

作者头像 李华
网站建设 2026/9/3 19:40:55

CPU笔记本YOLO环境配置实战:从零跑通YOLOv8/11/26预测

拿一台没有独立显卡的普通笔记本,装好 Python 3.11,然后想把 YOLO 跑起来,这是很多零基础同学接触目标检测时遇到的第一道坎。网上教程不少,但大多默认你有 NVIDIA 显卡、默认你熟悉 CUDA、默认你知道 virtualenv 是什么。你照着敲…

作者头像 李华