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

发布时间:2026/9/3 19:46:56
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/目录下会出现workerman和gatewayworker两个目录。我习惯在项目根目录下建一个workerman目录专门放客服系统的服务端代码和TP5的application目录分开结构更清晰。如果你是手动下载安装包的方式那需要特别注意PHP版本。Workerman 4.x要求PHP 7.0以上我最初用Workerman 3.x跑PHP 5.6也正常但后来为了用PHP 7.2的标量类型声明和语法糖统一升级到了4.x。还有一点生产环境一定要确保PHP安装了posix和pcntl扩展Workerman的多进程管理依赖这两个扩展没装的话服务根本起不来。2.2 启动GatewayWorker服务GatewayWorker自带了一套完整的启动脚本放在Applications/目录下。启动前需要修改Applications/your_app/start_gateway.php和start_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连接的端口而lanIp和startPort是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); }; };这个封装里我保留了三个钩子函数onMessage、onOpen、onClose在实际业务页面里覆盖它们就行。比如说访客端页面里收到客服回复后就渲染到聊天窗口里客服工作台里收到新访客发来的消息后就弹出提醒并刷新会话列表。这里有个细节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里收到type为ping时直接返回一个空消息即可不要走业务逻辑。这里需要注意心跳消息也要包含在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_timeout和proxy_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扩展比自己从零摸索要快得多。本文还有配套的精品资源点击获取