news 2026/9/15 15:30:38

Workerman在线客服系统实战:PHP常驻进程与WebSocket实时通信解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Workerman在线客服系统实战:PHP常驻进程与WebSocket实时通信解析

简介:一份完整的商业版Workerman在线客服系统源码及搭建教程,面向需要快速部署在线客服能力的企业开发者与PHP工程师。系统采用常驻内存架构并基于模块化设计,具备一键生成、响应式布局、灵活子级权限分配等特性,同时整合会员与API统一验证体系,并提供可在线安装卸载的应用市场,适合二次开发或直接投入生产环境。资源共2000个文件,以JavaScript脚本、HTML页面与CSS样式等前端代码为主,辅以JSON配置、Markdown文档、PHP服务端源码及SQL数据库脚本,压缩包约19MB,内容结构非常清晰,目录划分明确,便于按需检索与二次开发。已有204人学习下载。配套搭建教程覆盖Nginx 1.21.4、PHP 7.2与MySQL 5.7.40环境配置,可帮助读者快速跑通系统,并根据业务场景扩展云存储、云短信、富文本编辑器等功能模块。

1. Workerman 在线客服系统:为什么 PHP 常驻进程才是实时客服的地基

第一次把商业版 Workerman 客服系统跑起来的人,通常都会发现它和传统 LNMP 项目的启动方式完全不同:PHP 不在 Nginx 里通过 php-fpm 处理请求后退出,而是在 CLI 模式常驻内存,自己监听 WebSocket 端口,把会话推送、在线状态、消息路由全部接管。这个源码包的后台搭建在 FastAdmin 模块化结构上,权限、会员、API、应用市场这些企业客服高频需求都能直接在后台配置,实时层则交给 Workerman 系列组件处理。对想私有化部署客服平台、或者在现有 PHP 项目里补长连接能力的团队来说,这套源码的价值在于把「后台管理」和「长连接服务」做进了同一个工程,搭建时不需要跨多个语言栈维护。

2. 搭建环境:Workerman 客服系统对 Nginx、PHP CLI、MySQL 的边界要求

源码里的数据库脚本和配置基准是按 Nginx 1.21.4、PHP 7.2、MySQL 5.7.40 写入的。如果把 PHP 直接换到 8.x,Workerman 本身不会有太大问题,但 FastAdmin 后台里很多旧插件会出现 deprecated 提示,应用市场组件也未必兼容。所以先按这个组合把环境复现出来,跑通后再考虑升级。

2.1 PHP 扩展:pcntl、posix、redis 决定常驻进程能不能启动

Workerman 的进程管理依赖pcntl_fork,信号处理依赖posix,这两个扩展在 LNMP 默认 php-fpm 安装里经常被砍掉。拿到源码后第一时间检查php -m,缺少扩展时按当前 PHP 版本安装:

# CentOS 7 + Remi 源示例 yum install -y php72-php-cli php72-php-pdo php72-php-mysqlnd \ php72-php-mbstring php72-php-bcmath php72-php-pcntl \ php72-php-posix php72-php-redis php72-php-json

如果不是通过包管理器安装的 PHP,可以进入源码目录重新编译,在./configure时加上--enable-pcntl --enable-posix。注意 Workerman 必须在 CLI 模式下运行,所以php-cli必须存在;php-redis不是 Workerman 的硬依赖,但这个客服源码的会话缓存、在线状态、消息队列都会读 Redis,建议一并装上,否则启动后大量 DB 查询会把 MySQL 拖垮。

PHP 的常驻进程没有请求生命周期,不能沿用 Web 的max_execution_time。调整php.ini里以下参数:

memory_limit = 512M max_execution_time = 0 max_input_time = 0

max_execution_time = 0表示脚本不因超时退出,这是长连接服务的基本要求。还有一个小细节:opcache.enable_cli如果为 0,常驻进程每次 reload 都要重新编译 PHP 文件,启动速度会明显变慢,至少在本地环境打开它。

2.2 Nginx 反代:WebSocket 升级必须保留 Upgrade 头

客户端在前台页面通过ws://连接,你的服务器如果只暴露 80/443 端口,Nginx 需要把 WebSocket 流量反代到 Workerman 监听的 TCP 端口。常见配置是把/socket.io/或者/ws/路径转发到127.0.0.1:7272

location /socket.io/ { proxy_pass http://127.0.0.1:7272; 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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 86400; }

关键在proxy_set_header UpgradeConnection "upgrade"。浏览器发起 WebSocket 握手时会带Upgrade: websocket,Nginx 只有把这两个头原样转给后端,Workerman 才能完成 101 协议切换;缺少其中任意一个,前端会一直停在“连接中”,或者握手成功后几秒内断开。proxy_read_timeout 86400是防止 Nginx 按默认 60 秒杀掉空闲长连接。这里有一个容易忽略的对应关系:Nginx 的超时时间必须大于 Gateway 的pingInterval + pingNotResponseLimit之和,也就是至少两个心跳周期,否则服务端还在正常踢空闲连接,Nginx 先动手断开。

需要 WSS 时,把证书配置放在刚才的 server 块中,在listen 443 ssl;下加同样的 location。证书路径、HTTP/2 这些不是客服系统特有的,不展开。

2.3 MySQL 初始化:utf8mb4 和长连接超时

进入源码包先找到 SQL 文件,一般叫install.sqlkefu.sql。MySQL 端准备数据库和账号:

CREATE DATABASE `kefu` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'kefu'@'localhost' IDENTIFIED BY 'kefu_password'; GRANT ALL PRIVILEGES ON kefu.* TO 'kefu'@'localhost'; FLUSH PRIVILEGES;

导入时看报错。如果出现 1366 字符集错误,说明 SQL 文件本身不是 utf8mb4 编码,或者 MySQL 服务端默认字符集不是 utf8mb4,在my.cnf[mysqld]段加上character_set_server=utf8mb4。导入命令:

mysql -h127.0.0.1 -ukefu -pkefu_password kefu < kefu.sql

长连接场景下,MySQL 的wait_timeout默认 8 小时,Workerman 进程跨天运行后连接会被 MySQL 服务端回收,触发SQLSTATE[HY000] General error: 2006 MySQL server has gone away。可以调大超时,但更建议用框架的断线重连机制。配置参考:

[mysqld] max_connections = 512 wait_timeout = 31536000 interactive_timeout = 31536000

wait_timeout调成一年后,空闲连接不会被服务端回收,但要注意连接数:每个 BusinessWorker 进程默认持有自己的 MySQL 连接,按count=4计算,连接数压力不大。如果应用市场里装了很多组件,有些组件会自己创建连接池,这时候max_connections适当调高。

下面把环境基准整理成一张表,方便后续排错对照:

参数配置位置推荐值作用
max_execution_timephp.ini0防止常驻 PHP 进程超时退出
proxy_read_timeoutNginx location86400等待后端响应超时,必须大于心跳周期
pingIntervalstart_gateway.php55 秒Gateway 发送心跳的间隔
pingNotResponseLimitstart_gateway.php2连续 N 次无响应则断开客户端
wait_timeoutmy.cnf31536000防止 MySQL 主动回收空闲连接

3. 源码拆解:权限、会员/API、一键生成在客服系统里的实际落点

这一层对应的是后台管理部分。FastAdmin 的模块化思路是把每个业务域拆成控制器、模型、视图三层,权限和会员是公共基础,一键生成负责把表结构快速变成可维护的后台页面。

3.1 多级权限与同账号多组:auth_group 的表关系

“一个管理员同时属于多个组别”这个需求,在源码里由三张表完成:fa_admin存管理员账号,fa_auth_group存组别和规则,fa_auth_group_access做多对多关联。

CREATE TABLE `fa_auth_group` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `pid` int(10) unsigned NOT NULL DEFAULT '0', `name` varchar(100) NOT NULL, `rules` text NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `fa_auth_group_access` ( `admin_id` int(10) unsigned NOT NULL, `group_id` int(10) unsigned NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

pid让组别形成树形结构,子级组别自动继承父级的rulesrules字段存的是权限规则 ID 的逗号分隔字符串,规则 ID 来自fa_auth_rule表。后台分配子级权限时,直接修改rules并清理缓存,不需要动表结构。

读取一个管理员的权限时,先查他所属的所有组别,再合并组别里的规则 ID,最后查规则表:

$adminId = 1; $groupIds = Db::name('auth_group_access') ->where('admin_id', $adminId) ->column('group_id'); $ruleLines = Db::name('auth_group') ->whereIn('id', $groupIds) ->column('rules'); $ruleSet = []; foreach ($ruleLines as $line) { $ruleSet = array_merge($ruleSet, explode(',', $line)); } $ruleSet = array_unique($ruleSet); $menuList = Db::name('auth_rule') ->whereIn('id', $ruleSet) ->where('status', 1) ->order('weigh desc') ->select();

fa_auth_group_access不设主键,靠admin_idgroup_id联合唯一索引保证不重复。这个设计在源码里被封装成Auth类,里面有缓存逻辑,实际生产环境不建议每次请求都直接查三张表。另外注意:编辑组别时不要删掉当前管理员所属组的rules,否则那个管理员的后台菜单会瞬间消失,这种权限事故很隐蔽。

3.2 会员与 API 共用账号:轻量 Token 验证

前端会员中心会话验证和 API 接口验证共用同一套账号体系,后台通过type区分:type=admin走管理员表,type=user走会员表。登录成功后会生成一个 token,Web 端写入 cookie,API 端要求请求头带Authorization: Bearer <token>

简易实现可以这样写:

public function makeToken($uid) { $header = base64_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); $payload = base64_encode(json_encode([ 'uid' => $uid, 'iat' => time(), 'exp' => time() + 24 * 3600, ])); $key = config('site.kefu_token_key'); $signature = hash_hmac('sha256', "$header.$payload", $key); return "$header.$payload.$signature"; }

这里不是完整 JWT,但思路一致:签名用 HMAC-SHA256,密钥放在站点配置项里,exp控制过期时间。验证端解析 payload,判断exp是否在当前时间之前。注意 base64 生成的字符串可能包含+/,放进 URL 时要进行 urlencode,否则 API 网关可能把请求路径截断。

这个客服系统把会员中心和客服坐席放在同一个user类型下,区别在profile里加了一个role字段:customeragent。这样做的好处是客服和访客在 WebSocket 层使用同一套 UID 映射,不需要单独维护客服身份表。

3.3 一键生成:将客服话术表变成后台 CRUD

一键生成基于 FastAdmin 的crud命令,它的能力是把 MySQL 表结构反向生成控制器、模型、视图、JS 和菜单权限节点。客服系统里最常见的话术管理、常见问题、工单分类,都可以先用这个命令铺底。

cd /www/wwwroot/kefu php think crud -t kefu_word -c KefuWord -i id,title,content,category_id,sort,status -u 1

-t指定表名,-c是控制器名,-i是参与生成的字段列表,-u 1表示生成后自动写入菜单和权限节点。执行完,application/admin/controller/KefuWord.phpview/kefu_word/以及public/assets/js/backend/kefuword.js都会被创建,后台菜单栏直接出现“话术管理”。

常用参数含义:

参数作用本场景建议
-t数据库表名kefu_word
-c控制器名称KefuWord
-i参与生成的字段只保留需要维护的字段
-u是否生成权限节点和菜单1 表示是
-d软删除字段名deletetime时传入

生成后要二次调整的是数据权限。默认index返回全表数据,客服坐席应该只能看到自己的话术,需要在控制器 index 方法里追加过滤条件:

$where['admin_id'] = $this->auth->id;

同时把admin_id字段加到表结构里。这个改动必须放在生成之后做,否则再次执行 crud 命令会把修改覆盖掉。一键生成解决的是重复性 CRUD,业务过滤、消息推送、状态流转这些还是要手写。

4. 实时会话实现:Workerman 事件回调、uid 绑定与消息落库

客服系统的实时层是 Workerman,源码里通常包含 GatewayWorker 的启动文件:start_gateway.phpstart_business.phpstart_register.php。这里按 GatewayWorker 的事件回调模型拆解消息链路。它的核心是三个事件:onConnect建立连接、onMessage处理业务消息、onClose释放连接。

4.1 事件回调边界:onConnect、onMessage、onClose 各管什么

use GatewayWorker\Gateway; class Events { public static function onConnect($client_id) { // 连接建立,此时还没有用户身份 Gateway::sendToClient($client_id, json_encode([ 'type' => 'welcome', 'msg' => 'please auth' ])); } public static function onMessage($client_id, $message) { $data = json_decode($message, true); if (json_last_error() !== JSON_ERROR_NONE) { return; } switch ($data['type']) { case 'auth': Gateway::bindUid($client_id, $data['uid']); break; case 'chat': self::chat($client_id, $data); break; } } public static function onClose($client_id) { $session = Gateway::getSession($client_id); if (!empty($session['uid'])) { Gateway::sendToUid($session['uid'], json_encode(['type' => 'offline'])); } } }

onConnect只做握手成功后的欢迎消息,不能在这里直接写业务逻辑,因为这时候连接还没有绑定任何用户身份。onMessage是业务入口,按type字段分发到不同处理函数。auth事件里调用的Gateway::bindUid是关键:它会把这个$client_id和用户 ID 绑定,之后服务端就能用sendToUid($uid, ...)给指定用户推送。onClose里先读 session 再广播离线,不要把$_SESSION当成所有场景都可靠,调用Gateway::getSession更稳。

4.2 bindUid 与会话绑定:把连接映射到客服和用户

bindUid之后的 session 数据存在 Gateway 进程内存中,重启便消失,所以只适合存放会话临时态,例如当前连接的用户 ID、登录时间。把在线状态和未读消息放到 Redis 里,避免服务重启后丢失。

private static function bindUser($client_id, $uid) { Gateway::bindUid($client_id, $uid); Gateway::setSession($client_id, [ 'uid' => $uid, 'client_id' => $client_id, 'type' => 'user', 'logined' => time(), ]); // 广播在线人数,频率要控制 $onlineCount = count(Gateway::getAllUidList()); Gateway::sendToAll(json_encode([ 'type' => 'online_count', 'count' => $onlineCount ])); }

这里注意setSession的第三个参数是 session 数组,会覆盖之前写入的数据;如果要保留历史字段,先getSession再合并。同一个 UID 从新设备登录时,GatewayWorker 默认不会踢掉旧连接,需要业务层处理,否则一个客服账号在两台电脑登录,消息会推送到旧连接上,用户感知不到。

一个实用的做法是维护uid => client_id的映射,当同一个 UID 再次 bind 时,主动Gateway::closeClient旧连接。这个逻辑建议放在auth事件之前。

4.3 消息落库与推送顺序:先写库还是先推送

客服消息必须能重放,所以消息要先落库,再推送。客户端收到消息后如果发现msg_id已经存在,自动去重,这样即使服务端重试推了两次,界面也不会出现重复气泡。

private static function chat($client_id, $data) { $msgId = Db::name('kefu_message')->insertGetId([ 'conversation_id' => $data['conversation_id'], 'from_uid' => $data['from_uid'], 'to_uid' => $data['to_uid'], 'content' => $data['content'], 'createtime' => time(), ]); $data['msg_id'] = $msgId; Gateway::sendToUid($data['to_uid'], json_encode($data)); }

先写数据库再推送的缺点是多一次数据库事务开销,在客服场景下完全可接受。高并发下可以把插入操作丢给订阅的异步队列,但从零搭建时建议保持简单。conversation_id的生成规则一般是:用户 ID 和客服 ID 先排序,再用min(uid)_max(uid)拼接,确保同一对会话的conversation_id稳定。

事件对整个连接生命周期的影响可以归纳成下面这张表:

事件触发时机常用动作注意点
onConnectTCP/WebSocket 握手完成发送欢迎消息不要在这里查询 DB
onMessage收到客户端消息业务分发、消息落库校验 JSON 格式与字段完整性
onClose客户端断开或超时清理 session、广播离线不保证一定收到
onWorkerStartBusinessWorker 进程启动初始化 DB/Redis 连接配合worker->id判断是否重复初始化

4.4 Gateway 进程参数:count、pingInterval 与 startPort 的配合

start_gateway.php里的参数决定了实际吞吐能力。拿到源码后建议先做一次本机压测,再调整进程数。

use Workerman\Worker; use GatewayWorker\Gateway; use GatewayWorker\Register; $register = new Register('text://0.0.0.0:1238'); $gateway = new Gateway("websocket://0.0.0.0:7272"); $gateway->name = 'KefuGateway'; $gateway->count = 4; $gateway->lanIp = '127.0.0.1'; $gateway->startPort = 2900; $gateway->pingInterval = 55; $gateway->pingNotResponseLimit = 2; $gateway->registerAddress = '127.0.0.1:1238';

count是 Gateway 进程数,不是连接数上限;startPort是给内部通信用的端口区间起始值,每增加一个 Gateway 进程,内部通信会占用相邻一个端口,所以count=4startPort=2900会占 2900、2901、2902、2903。lanIp如果是单机部署就用127.0.0.1,多机部署要换成内网 IP,否则 BusinessWorker 和 Gateway 无法互相发现。pingInterval=55配合pingNotResponseLimit=2,意味着 Gateway 在 110 秒内没有收到客户端的任何数据包,就主动断开该连接。

客户端 JS 侧的心跳也要对得上,我一般让浏览器每隔 50 秒发送一次ping帧,服务端收到后回pong。Nginx 的proxy_read_timeout设成 86400 秒,不会干扰这个节奏。

5. 进阶排错:心跳、连接数、应用市场扩展的现场调优

5.1 心跳参数与超时链路的一致性检查

遇到“连接几秒后自动断开”的问题,先按 心跳包 → Gateway → Nginx → 浏览器 这条链路排查。Gateway 心跳是服务端主动发 ping,客户端如果实现了onmessage但没有回 pong,Gateway 的onClose会在两个周期后被触发。这时日志里会出现类似client close in 110s的记录。

故障现象常见原因检查方式
握手成功,60 秒左右断开Nginxproxy_read_timeout过短查看 Nginx error.log
110–120 秒断开客户端没有回 pong在浏览器 Network 面板观察 WebSocket 帧
偶发连接 ID 冲突同一个 UID 从多个设备登录auth事件中主动关闭旧连接
消息推送到错误目标Gateway 与 BusinessWorker 网络不通检查lanIp是否为内网地址
数据库连接丢失MySQLwait_timeout过短查看 error.log 里的MySQL server has gone away

修改参数后必须重启 Workerman,不要用reload,因为start_gateway.php里的进程模型变更需要重新 fork:

php think workerman restart

5.2 应用市场扩展安装后的缓存清理

源码自带的应用市场把云存储、云短信、富文本编辑器这类组件做成了可安装插件。安装过程会写数据库配置,并往composer.json里追加 autoload 依赖。安装完成后如果后台功能没有按预期生效,先清掉 runtime 缓存,再重启 Workerman:

php think clear php think workerman restart

如果某个插件和 GatewayWorker 的自动加载冲突,优先检查插件是否依赖 ThinkPHP 5.0 的老命名空间。这种情况我会先手动删除runtime目录下的类映射文件,再观察php think workerman start的启动日志,插件扩展一般不直接写在 events 回调里,所以在不开启调试模式时,问题会被框架缓存掩盖。

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

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

Spring Boot 图书管理系统实战:从零搭建生产级应用

简介&#xff1a;本资源是一套基于SpringBoot开发的轻量级图书管理系统&#xff0c;面向Java Web初学者与高校课程设计学生&#xff0c;解决图书馆场景下学生借阅与管理员多维度管理的实际需求。系统采用前后端分离架构&#xff0c;包含学生端&#xff08;借阅统计、图书浏览、…

作者头像 李华
网站建设 2026/9/15 15:29:07

PAM360特权访问管理评测:从密码保险库到零信任落地的完整实践

1. 为什么2026年企业开始认真考虑PAM3601.1 特权账号是攻防双方的“必争之地”在讲PAM360之前&#xff0c;先聊一个我最近经常被问到的场景&#xff1a;某公司运维部为了图省事&#xff0c;把数据库root密码贴在内部笔记里&#xff0c;整个部门十几个人都知道&#xff0c;离职了…

作者头像 李华
网站建设 2026/9/15 15:28:24

国密算法SM2/SM3/SM4的C#实现与工程实战解析

简介&#xff1a;国密算法是我国自主设计的商用密码标准&#xff0c;其中SM2为非对称加密算法&#xff0c;SM3为密码杂凑算法&#xff0c;SM4为分组加密算法。这份C#工程源码完整实现了三大国密算法&#xff0c;面向需要在.NET平台下集成国产密码能力的开发者。工程基于Visual …

作者头像 李华
网站建设 2026/9/15 15:27:10

Wazuh安装实战:从部署到Agent接入的避坑指南

Wazuh这套东西&#xff0c;说好装也好装&#xff0c;一条官方脚本跑完就能看到仪表板&#xff1b;说难装也难装&#xff0c;生产环境里拆成三个组件部署时&#xff0c;每个环节都能把你卡上半天。群里天天有人问“为什么dashboard打不开”“为什么agent一直显示Disconnected”&…

作者头像 李华
网站建设 2026/9/15 15:26:37

STM32与ESP8266串口通信:USART2 DMA接收+IDLE中断实战解析

简介&#xff1a;面向STM32嵌入式开发者与物联网初学者的STM32ESP8266驱动集成资源包&#xff0c;聚焦两者通过UART/SPI通信、AT指令集控制、TCP/IP协议栈对接等关键环节&#xff0c;可帮助读者快速搭建Wi-Fi联网应用。压缩包共85个文件&#xff0c;以37个C源文件与36个H头文件…

作者头像 李华