Workerman在线客服系统实战:从轮询到WebSocket长连接
简介这是一套基于Workerman的在线客服系统源码及配套安装教程面向需要快速搭建网页端客服功能的PHP开发者与运维人员尤其适合熟悉Nginx、PHP、MySQL基础环境、希望自建即时通讯客服模块的中级技术人员。资源包共约2000个文件压缩后25.95MB以1184个js脚本、196个html页面、163个json配置、106个css样式及22个php后端文件为主另含sql建表脚本、sh部署脚本与md说明文档覆盖前端界面、后端逻辑与数据库结构。安装教程明确了Nginx 1.21.4、PHP 7.2、MySQL 5.7.40的运行环境并给出/application/database.php中数据库名、用户名、密码的配置位置便于按步骤完成部署。目前已有415人学习下载读者可借此获得一套可直接运行的客服系统源码理解Workerman常驻进程与Web端通信的整合方式并参考目录结构进行二次开发或功能裁剪。1. 从轮询到长连接Workerman 在线客服系统到底解决了什么很多做 PHP 在线客服的团队都经历过同一个尴尬前端用setInterval每 3 秒拉一次消息接口十个客服同时在线MySQL 的SELECT就开始喘访客一多消息延迟肉眼可见客服抱怨“客户发了我半天才看到”。这套 Workerman 在线客服系统本质上是把“客户端反复问服务器有没有新消息”换成“服务器主动把消息推给客户端”用常驻内存的 WebSocket 进程替掉短连接轮询。它适合已经会 PHP、能配 Nginx 和 MySQL但没正经写过常驻进程的开发者——你不需要先啃完 Swoole 全家桶只要理解 Workerman 的进程模型就能把一套能跑、能改、能上线的客服系统搭起来。课程资源里给的是完整源码包不是片段下面按“先跑通、再改透、最后避坑”的顺序拆。2. 环境搭建与 Workerman 进程模型为什么不是普通 PHP-FPM2.1 先看清 Workerman 和 PHP-FPM 的根本差别普通 PHP 项目跑在 PHP-FPM 下每个请求进来PHP 重新加载一遍框架、重新连一次 MySQL请求结束进程就回收。这套模式做网页没问题做长连接就是灾难——客服要保持连接不断开PHP-FPM 根本撑不住。Workerman 的做法是启动一个常驻内存的 PHP 进程自己监听端口、自己管理连接进程不退出代码只加载一次。这带来两个直接后果。第一全局变量和静态变量在多个请求之间是共享的你不能再像写 FPM 那样随手static $cache []当请求级缓存用它会跨连接累积。第二任何未捕获的异常都可能让整个进程挂掉所有在线客服瞬间掉线。所以 Workerman 项目里try/catch不是可选项是保命符。常见做法是用 Workerman 起两个监听一个 WebSocket 端口给浏览器和客服端一个内部 Text 端口给后端业务系统推事件。课程资源里的start.php就是这个结构下面拆开看。2.2 启动文件与四个核心回调Workerman 的入口文件通常长这样我按客服系统的实际需要补了注释?php use Workerman\Worker; require_once __DIR__ . /vendor/autoload.php; // 创建 WebSocket 服务监听 0.0.0.0:8282 $ws new Worker(websocket://0.0.0.0:8282); // 进程数。客服系统连接数不大4 个进程足够 // 注意进程数 1 时连接会分散到不同进程跨进程通信必须靠 Gateway 或外部存储 $ws-count 4; // 进程启动时触发每个进程各触发一次 $ws-onWorkerStart function ($worker) { // 这里一般用来初始化数据库连接池或 Redis 连接 // 千万不要在这里做只应该执行一次的全局初始化比如建表 }; // 有客户端连接进来 $ws-onConnect function ($connection) { // 可以在这里做 token 校验但注意此时还没收到业务数据 }; // 收到客户端消息 $ws-onMessage function ($connection, $data) { // $data 是原始字符串客服系统里通常是 JSON $msg json_decode($data, true); if (!$msg || !isset($msg[type])) { $connection-send(json_encode([code 400, msg 格式错误])); return; } // 按 type 分发登录、发消息、心跳、已读回执 }; // 连接断开 $ws-onClose function ($connection) { // 必须在这里清理该连接对应的客服在线状态否则会残留“幽灵客服” }; Worker::runAll();逻辑说明onWorkerStart每个进程执行一次所以初始化数据库连接池放这里合适但建表、写种子数据这种全局只该做一次的事不能放。onMessage是业务主入口客服系统所有动作都从这里分发。onClose最容易被忽略但它是清理在线状态唯一可靠的时机。参数说明count设多少要看并发连接数一般单进程撑几千连接没问题客服系统几百个连接用 2 到 4 个进程足够。端口8282要和后面 Nginx 反代配置一致。websocket://协议决定了浏览器端必须用ws://或wss://连接。2.3 Nginx 反向代理 WebSocket 的关键三行Workerman 直接对外暴露端口不安全也不方便和现有域名共用 80/443。标准做法是 Nginx 反代。这里有个血泪经验WebSocket 升级握手必须显式转发Upgrade和Connection头少一行就连不上浏览器控制台只会报一个含糊的 400。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 Host $host; proxy_read_timeout 3600s; }逻辑说明proxy_http_version 1.1是 WebSocket 升级的前提HTTP/1.0 不支持。Upgrade和Connection两行负责把浏览器的升级请求透传给 Workerman。proxy_read_timeout默认 60 秒长连接会被 Nginx 主动掐断必须调大否则客服每隔一分钟掉一次线还找不到原因。参数说明proxy_read_timeout 3600s配合前端心跳间隔一般 30 到 55 秒使用心跳间隔必须小于这个超时否则连接照样断。2.4 数据库与消息表的最小结构客服系统的消息最终要落库否则客服刷新页面记录就没了。课程资源里用的是 MySQL最小可用结构如下CREATE TABLE chat_message ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, session_id VARCHAR(64) NOT NULL COMMENT 会话标识, from_type TINYINT NOT NULL COMMENT 1访客 2客服, from_id INT NOT NULL, content TEXT NOT NULL, is_read TINYINT DEFAULT 0, created_at INT NOT NULL, KEY idx_session (session_id, id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明session_id把一次访客和客服的对话串起来索引建在(session_id, id)上因为拉历史记录永远是“按会话查、按时间排序”。created_at用 INT 存时间戳而不是 DATETIME写入更快前端转换也方便。参数说明content用 TEXT 而不是 VARCHAR客服可能发长文本。is_read用于已读回执别小看这个字段没有它客服不知道客户看没看到回复体验差一截。3. 消息收发与在线状态把“谁在线、发给谁”讲透3.1 连接与用户身份的绑定WebSocket 连接建立时服务器只知道有个连接进来了不知道这是哪个客服、哪个访客。所以第一件事是让客户端在连接成功后立刻发一条登录消息把connection和业务身份绑定。常见做法是用一个内存映射表存起来// 全局连接表uid connection // 注意count 1 时这张表只在单个进程内有效跨进程要用 Gateway 或 Redis global $uidConnMap; $uidConnMap []; $ws-onMessage function ($connection, $data) use ($ws) { global $uidConnMap; $msg json_decode($data, true); switch ($msg[type]) { case login: // 绑定身份uid 由前端从登录态带过来 $uid (int)$msg[uid]; $connection-uid $uid; $uidConnMap[$uid] $connection; // 回执告诉前端绑定成功 $connection-send(json_encode([type login_ok])); break; case chat: // 找到目标用户的连接推送 $toUid (int)$msg[to_uid]; if (isset($uidConnMap[$toUid])) { $uidConnMap[$toUid]-send(json_encode([ type chat, from $connection-uid, content $msg[content], ])); } // 无论对方在不在线都先落库 saveMessage($connection-uid, $toUid, $msg[content]); break; } };逻辑说明$connection-uid $uid把身份挂在连接对象上onClose时可以直接读出来清理映射表。消息先推送再落库还是先落库再推送取决于业务对可靠性的要求——客服系统一般先落库避免推送成功但入库失败导致记录丢失。参数说明uid必须由服务端从可信登录态推导不能直接信前端传的值否则任何人都能冒充客服。课程资源里前端传 uid 只是演示真实项目要在login时校验 token。3.2 单进程映射表的局限与 Gateway 方案上面那张$uidConnMap有个致命问题count 4时四个进程各有一张表客服 A 连在进程 1访客 B 连在进程 3进程 1 的表里根本没有 B 的连接消息发不过去。这就是新手最容易翻车的地方——本地测试count 1一切正常上线改成 4 就时好时坏。解决办法有两个。简单的是把count设成 1单进程扛几百连接没问题但没法利用多核也没法平滑重启。正规做法是引入 GatewayWorker它把连接管理和业务逻辑拆成两组进程连接层负责维护所有连接业务层通过内部通信向连接层发指令跨进程推送由框架处理。课程资源如果给的是纯 Workerman 版本通常会附带一个count 1的说明如果给的是 GatewayWorker 版本目录里会有Gateway.php和BusinessWorker.php两个启动文件。判断方法很简单看有没有GatewayWorker\Gateway这个类。3.3 心跳机制为什么连接会“莫名其妙”断掉长连接不是永久的。中间任何一层——浏览器、Nginx、防火墙、运营商——都可能因为长时间无数据而回收连接。表现就是客服挂着页面去泡了杯咖啡回来发现收不到消息了刷新才好。这不是 bug是长连接的常态必须靠心跳维持。// 前端心跳每 30 秒发一次 let heartTimer null; function startHeartbeat(ws) { heartTimer setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping, t: Date.now() })); } }, 30000); } // 收到 pong 说明链路正常可以顺带做延迟统计逻辑说明心跳间隔要小于所有中间层的空闲超时。Nginx 的proxy_read_timeout设了 3600 秒但运营商网关可能 60 秒就回收所以 30 秒是保险值。服务端收到ping回一个pong即可不需要落库。参数说明30 秒是经验值移动网络下可以缩到 20 秒。心跳太频繁会增加无效流量太稀疏则断线发现不及时。前端还要监听onclose触发自动重连重连成功后重新走login绑定身份。3.4 已读回执与消息顺序客服系统里“已读”状态直接影响服务质量。实现方式是在消息表加is_read客服端打开会话时把该会话访客发来的消息批量置为已读并通过 WebSocket 通知访客“客服已读”。这里有个坑批量更新要用WHERE session_id ? AND from_type 1 AND is_read 0别全表更新。消息顺序则依赖id自增前端按id排序渲染。不要用created_at排序同一秒内多条消息时间戳相同顺序会乱。这是很多客服系统消息“跳来跳去”的根因。4. 避坑与排查上线后最容易炸的五个点4.1 现象本地正常上线后消息时有时无原因count大于 1连接分散在多个进程单进程内的$uidConnMap找不到目标连接。 解决要么把count改成 1要么换 GatewayWorker 方案要么用 Redis 存uid 进程ID的映射通过内部端口转发。别指望用文件或数据库存连接对象连接对象没法序列化。4.2 现象客服每隔一分钟掉线一次原因Nginxproxy_read_timeout默认 60 秒长连接空闲被掐断。 解决在location里显式设置proxy_read_timeout 3600s同时前端心跳间隔设为 30 秒双保险。改完记得nginx -s reload别只改配置不重载。4.3 现象进程跑一段时间就挂日志报内存超限原因常驻进程里全局数组只增不减比如$uidConnMap在onClose时没清理断开的连接对象一直被引用内存只涨不降。 解决onClose里必须unset($uidConnMap[$connection-uid])。另外任何缓存都要设上限或过期Workerman 进程不会自动回收全局变量。4.4 现象中文消息乱码或截断原因WebSocket 传输的是 UTF-8 字节流但数据库连接没设utf8mb4或者content字段长度不够。 解决PDO 连接串加charsetutf8mb4表字段用 TEXT 或更大。注意 emoji 是 4 字节utf8存不下必须utf8mb4。4.5 现象客服收到消息但访客没收到回复原因推送逻辑只处理了“对方在线”的情况对方离线时消息没落库或者落库了但重连后没拉历史。 解决消息一律先落库推送只是“锦上添花”。访客重连后主动拉一次该会话的历史消息保证不丢。别把推送成功当成消息送达的唯一凭据。5. 进阶把客服系统做成能平滑重启、能查延迟的样子跑通之后真正决定这套系统能不能长期用的是两件事平滑重启和可观测性。平滑重启靠 Workerman 的reload能力。改完业务代码执行php start.php reload框架会启动新进程、等旧进程处理完当前请求再退出客服连接不会断。但有个前提你的代码里不能有阻塞很久的操作比如同步调用一个慢接口。我一般会把耗时操作丢给独立的任务进程WebSocket 进程只做收发和落库保持轻量。可观测性方面至少加三个统计当前在线连接数、消息平均延迟、每分钟消息量。在线连接数可以直接在onConnect和onClose里增减一个计数器定时打到日志。消息延迟则在前端发消息时带一个客户端时间戳服务端回执时带回前端算差值。下面这个片段是我习惯加的延迟统计// 服务端回执时带上服务端时间前端用 (now - clientTime) 估算往返延迟 $connection-send(json_encode([ type ack, client_t $msg[client_t], // 前端发消息时的时间戳 server_t microtime(true) * 1000, ]));逻辑说明前端拿到ack后用当前时间减去client_t得到往返延迟超过 500 毫秒就该查网络或数据库了。这个数字比任何“感觉卡”都靠谱。另外客服系统的会话分配也值得做一层。简单轮询会把访客随机丢给客服忙闲不均。常见做法是维护一个“客服当前会话数”的计数新访客进来分配给会话数最少的在线客服。这个计数存在 Redis 里每次分配和会话结束各更新一次。别用数据库做这个高频读写扛不住。最后说个我自己的习惯每次改完onMessage里的分发逻辑我都会先用wscat或浏览器控制台手动发几条畸形消息——缺字段的、类型不对的、超长的——确认服务端不崩、能返回错误码再交给前端联调。常驻进程最怕的就是一条脏消息把整个进程带走而这类问题在正常测试里根本暴露不出来。从那以后我每次上线前都强制走一遍畸形消息测试希望帮到你。本文还有配套的精品资源点击获取

相关新闻

变频器与直流调速器维修实战:多品牌覆盖与就近服务的核心价值

变频器与直流调速器维修实战:多品牌覆盖与就近服务的核心价值

干这行十几年,变频器和直流调速器维修的活儿接了不少,从最初的“能修就行”到现在的“多品牌覆盖加就近响应”,中间的坑和门道确实不少。很多客户第一次打电话来,开口就是“我这台设备趴窝了,你们能不能马上来人”&…

2026/10/13 22:03:56 阅读更多 →
C++ YOLO ONNX 部署:OpenCV dnn 推理与后处理实战

C++ YOLO ONNX 部署:OpenCV dnn 推理与后处理实战

简介:这份资源面向具备一定C基础、希望将YOLO目标检测模型落地到实际工程中的开发者,聚焦于使用OpenCV的dnn模块加载并运行ONNX格式的YOLO模型,解决从模型文件到可执行推理代码的部署问题。压缩包共2个文件,包含1个cpp源文件与1个…

2026/10/12 19:30:13 阅读更多 →
A股情绪指标构建全链路:从评论到可回测时间序列

A股情绪指标构建全链路:从评论到可回测时间序列

简介:本资源是一套基于Python实现的股市市场情绪分析完整项目,面向金融数据分析初学者、量化投资爱好者及NLP实践者,旨在通过挖掘股评文本情绪辅助投资决策,在A股非有效市场中构建可解释的情绪指标。压缩包共15个文件,…

2026/10/13 22:03:54 阅读更多 →

最新新闻

Python Plotly交互式数据可视化:从快速出图到精细定制

Python Plotly交互式数据可视化:从快速出图到精细定制

做数据分析的同学都有一个共同感受:数据算完之后,最头疼的不是处理逻辑,而是怎么把结果变成一张别人一眼能看懂的图。这两年我用得最顺手的工具就是 Plotly,一个典型的 Python 交互式绘图库。用 Plotly 创建交互式图表&#xff0c…

2026/10/13 22:03:47 阅读更多 →
基于Spring Boot和Vue的电影订票管理系统全栈项目

基于Spring Boot和Vue的电影订票管理系统全栈项目

每年做课程设计或者毕业设计的季节,总有人问我"有没有一个既能体现技术栈又不算太复杂、最好还带完整界面的项目"。老实说,这类需求里出镜率最高的就是各种"管理系统",而其中电影订票管理系统又是一直没凉过的经典选题。…

2026/10/13 22:03:47 阅读更多 →
机器学习集成学习实战:三大流派与工程应用

机器学习集成学习实战:三大流派与工程应用

集成学习这几年在机器学习面试和实际工程里都是绕不开的东西。不管你是刚开始学模型,还是已经在跑实验,迟早会遇到一个问题:单个模型的效果到了瓶颈,怎么再往上提一点?很多人第一反应是换更复杂的模型,但更…

2026/10/13 22:03:47 阅读更多 →
WEKA数据挖掘入门:从ARFF格式到J48决策树与交叉验证实战

WEKA数据挖掘入门:从ARFF格式到J48决策树与交叉验证实战

简介:一份面向数据挖掘初学者与课程学习者的WEKA中文详细教程PPT,围绕怀卡托智能分析环境(WEKA)的开源特性、获得ACM SIGKDD奖项的里程碑地位及常见应用场景展开,适合作为高校数据挖掘、机器学习课程的配套讲义或自学入…

2026/10/13 22:03:47 阅读更多 →
支付系统设计与落地:状态机、幂等、对账与避坑指南

支付系统设计与落地:状态机、幂等、对账与避坑指南

线下业务上线支付系统的那段时间,几乎每天都要和数据、异常单、渠道对账单较劲。当时踩过的坑、重构过的状态机、跟渠道商务来回确认的限额规则,后来都沉淀成了这套经验。今天不聊架构图,也不念文档,就实打实拆一下“支付系统”从…

2026/10/13 22:03:47 阅读更多 →
同花顺日线文件解析:从二进制乱码到可回测的OHLCV表

同花顺日线文件解析:从二进制乱码到可回测的OHLCV表

简介:这份资源面向金融数据分析初学者与量化爱好者,聚焦同花顺日线二进制文件(.day)的解析问题。压缩包仅4KB,内含2个文件:一个以600000为代码的日线样本文件,以及一份约20行的Python解析脚本&a…

2026/10/13 22:02:46 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →