ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Workerman在线客服系统实战:WebSocket长连接与MySQL消息落库

Workerman在线客服系统实战:WebSocket长连接与MySQL消息落库 简介这是一套基于Workerman的在线客服系统源码面向需要搭建实时客服功能的PHP开发者与运维人员尤其适合已掌握Nginx、PHP、MySQL基础、希望快速部署一套可用客服后台的中级学习者。资源包共约2000个文件压缩后25.95MB以1184个js脚本、196个html页面、106个css样式及163个json配置为主另含22个php核心逻辑、4个sql建表脚本和若干md、txt说明文档前端资源与后端逻辑分层清晰便于二次开发与模块定位。安装教程覆盖Nginx 1.21.4、PHP 7.2、MySQL 5.7.40环境并给出/application/database.php中数据库名、用户名、密码的配置示例读者可据此完成源码上传解压与数据库连接。目前已有415人学习下载适合作为在线客服、即时通讯类项目的部署参考与功能改造起点。1. 从轮询到长连接Workerman 在线客服系统到底解决了什么如果你做过传统 PHP 在线客服大概率经历过这种场景前端setInterval每 3 秒拉一次getNewMessage.phpMySQL 里messages表被SELECT打到冒烟客服回复延迟肉眼可见用户发完消息盯着屏幕等半天没反应。这套「轮询 短连接」的组合在并发过百之后基本就是玄学——你永远不知道下一条消息是 1 秒到还是 10 秒到。Workerman 在线客服系统换了个思路用 PHP 常驻内存的 Socket 服务替代 HTTP 轮询客户端和客服端通过 WebSocket 长连接保持会话消息到达即推送。它适合两类人一是手里有 LNMP 环境、想给现有业务加一套实时客服的中小团队二是想学「PHP 也能做长连接服务」这个反直觉命题的开发者。这套资源把 Nginx、PHP、MySQL 和 Workerman 串成了一条完整链路不是玩具 Demo是能跑起来的工程骨架。2. 环境搭建与 Workerman 常驻进程从零把服务拉起来2.1 为什么是 Workerman 而不是 Swoole选型这件事得先说清楚不然装到一半你会怀疑人生。Swoole 是 C 扩展性能确实猛但它要求你改php.ini加载.so很多虚拟主机和共享环境根本不给这个权限Workerman 是纯 PHP 写的composer require workerman/workerman就能跑对部署环境几乎零侵入。在线客服这种场景单机几千连接完全够用Workerman 的性能天花板远没到瓶颈而它的可移植性优势是实打实的。另一个现实原因是调试成本。Swoole 的协程模型对不熟悉的人是个黑匣子出问题只能看扩展日志Workerman 的代码你能直接var_dump进程模型是「一个 Master 多个 Worker」ps aux | grep worker就能看到进程状态排查起来心里有底。2.2 LNMP 基础环境与依赖安装假设你用的是 Ubuntu 20.04 或 CentOS 7先把 Nginx、PHP、MySQL 装好。PHP 版本建议 7.4 以上Workerman 4.x 对 PHP 8 也兼容但生产环境我一般锁 7.4稳。# 安装 PHP 及必要扩展Ubuntu 示例 sudo apt update sudo apt install -y php7.4-cli php7.4-fpm php7.4-mysql php7.4-mbstring php7.4-curl php7.4-gd # 安装 Composer curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer # 验证 php -v composer -V这里php7.4-cli是关键Workerman 跑在 CLI 模式下不是 FPM。很多人装完发现php start.php报错一查是 CLI 版本和 FPM 版本不一致php -v和php-fpm -v对不上号。装完顺手php -m看一眼pcntl和posix扩展在不在Workerman 的多进程依赖这两个缺了会直接启动失败。2.3 拉取项目与 Composer 依赖把源码包解压到/www/wwwroot/chat这类目录然后进目录装依赖。cd /www/wwwroot/chat composer install --no-dev --optimize-autoloader--no-dev跳过开发依赖--optimize-autoloader生成类映射表生产环境加载更快。装完你会看到vendor/workerman/workerman目录这就是核心。2.4 启动 Workerman 服务与进程守护Workerman 的入口通常是start.php启动命令有几种模式# 前台启动调试用能看到实时输出 php start.php start # 守护进程模式后台跑 php start.php start -d # 查看状态 php start.php status # 平滑重启改代码后用 php start.php reload # 停止 php start.php stop-d模式是生产标配但第一次跑千万别直接-d前台启动能看到报错。常见的一个坑是端口被占用start.php里默认可能写 8282 或 1234如果被其他服务占了启动会报Address already in use改配置里的端口或者lsof -i:8282杀掉占用进程。进程守护方面Workerman 自带reload和stop但服务器重启后不会自动拉起。常见做法是写 systemd 服务单元或者用 supervisor 托管。我一般用 systemd因为不额外装东西# /etc/systemd/system/workerman-chat.service [Unit] DescriptionWorkerman Chat Service Afternetwork.target [Service] Typeforking Userwww WorkingDirectory/www/wwwroot/chat ExecStart/usr/bin/php start.php start -d ExecReload/usr/bin/php start.php reload ExecStop/usr/bin/php start.php stop Restartalways [Install] WantedBymulti-user.targetTypeforking是因为-d模式会 fork 到后台systemd 需要知道主进程退出了。Userwww要和你 Nginx 跑的用户一致不然日志文件权限会打架。写完systemctl daemon-reload systemctl enable workerman-chat就能开机自启。3. WebSocket 通信与 MySQL 消息落库把消息链路打通3.1 WebSocket 握手与 Nginx 反向代理配置Workerman 原生支持 WebSocket 协议客户端new WebSocket(ws://yourdomain.com:8282)就能连。但生产环境不会让用户直连 8282 端口得用 Nginx 做反向代理走 443 的 wss。# /etc/nginx/conf.d/chat.conf server { listen 443 ssl http2; server_name chat.yourdomain.com; ssl_certificate /etc/nginx/ssl/chat.crt; ssl_certificate_key /etc/nginx/ssl/chat.key; # 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 Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; } # 普通 HTTP 接口 location / { root /www/wwwroot/chat/public; index index.php index.html; try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; } }proxy_http_version 1.1和那两个Upgrade/Connection头是 WebSocket 代理的命门少一个握手就失败浏览器控制台报WebSocket connection failed。proxy_read_timeout默认 60 秒长连接会被 Nginx 掐断必须调大3600 秒是常见值。3.2 消息收发核心逻辑与代码结构Workerman 的onMessage回调是消息处理中枢。一个典型的客服消息流转是这样的客户端发{type:chat, to:客服ID, content:你好}服务端解析后先落库再查目标连接是否在线在线就推不在线就存离线消息。// application/ChatGateway.php 核心片段 use Workerman\Connection\TcpConnection; use Workerman\Worker; $worker new Worker(websocket://0.0.0.0:8282); $worker-count 4; // 4 个 Worker 进程 // 维护 uid 到 connection 的映射 $uidConnMap []; $worker-onConnect function(TcpConnection $conn) { echo 新连接: {$conn-id}\n; }; $worker-onMessage function(TcpConnection $conn, $data) use ($uidConnMap) { $msg json_decode($data, true); if (!$msg || !isset($msg[type])) { $conn-send(json_encode([code 400, msg 格式错误])); return; } switch ($msg[type]) { case login: // 绑定 uid 与连接 $uidConnMap[$msg[uid]] $conn; $conn-uid $msg[uid]; $conn-send(json_encode([code 0, msg 登录成功])); break; case chat: // 1. 落库 $insertId Db::table(messages)-insertGetId([ from_uid $conn-uid, to_uid $msg[to], content $msg[content], created_at date(Y-m-d H:i:s), is_read 0, ]); // 2. 推送 $target $uidConnMap[$msg[to]] ?? null; if ($target) { $target-send(json_encode([ type chat, from $conn-uid, content $msg[content], msg_id $insertId, ])); } // 3. 回执给发送方 $conn-send(json_encode([code 0, msg_id $insertId])); break; } }; $worker-onClose function(TcpConnection $conn) use ($uidConnMap) { if (isset($conn-uid)) { unset($uidConnMap[$conn-uid]); } }; Worker::runAll();$uidConnMap是内存里的映射表count 4意味着有 4 个独立进程每个进程有自己的$uidConnMap用户 A 连到进程 1、用户 B 连到进程 2进程 1 里根本找不到 B 的连接。这是新手最容易翻车的地方——单进程测试一切正常开了多进程消息就丢。解决办法是用 Workerman 的Channel组件做进程间通信或者把在线状态存 Redis推送时通过 Redis 发布订阅转发。3.3 MySQL 消息表设计与索引优化消息表设计直接影响查询性能尤其是历史消息拉取。CREATE TABLE messages ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, from_uid INT UNSIGNED NOT NULL, to_uid INT UNSIGNED NOT NULL, content TEXT NOT NULL, is_read TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_from_to_time (from_uid, to_uid, created_at), KEY idx_to_read (to_uid, is_read) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;idx_from_to_time覆盖「查某两人之间的聊天记录」这个高频查询idx_to_read用于「查我有多少未读」。注意content用TEXT而不是VARCHAR(255)客服场景经常发长文本和图片链接255 不够用。字符集必须utf8mb4不然 emoji 存进去变问号用户发个表情你这边显示乱码这种问题排查起来很费时间。4. 避坑与排查那些让我加班到凌晨的坑4.1 现象消息偶发丢失单机测试正常原因Worker 进程数大于 1 时$uidConnMap不共享跨进程推送找不到连接。这是 Workerman 多进程模型的固有特性不是 Bug。解决引入 Redis 做在线状态存储和消息中转。用户登录时SADD online_uids {uid}并SET uid:{uid}:worker {worker_id}推送时先查 Redis 判断目标在哪个 Worker通过 Workerman 的Channel\Client::publish发到对应进程。或者简单点把count设为 1但这就放弃了多核利用只适合小规模。4.2 现象Nginx 报 502Workerman 进程还在原因Nginx 的proxy_pass指向127.0.0.1:8282但 Workerman 监听的是0.0.0.0:8282或反过来协议对不上。另一个常见原因是 SELinux 拦截了 Nginx 到本地端口的连接。解决netstat -tlnp | grep 8282确认监听地址Nginx 配置里proxy_pass的地址要和 Workerman 监听的一致。SELinux 用setsebool -P httpd_can_network_connect 1放行。4.3 现象连接几分钟后自动断开原因Nginxproxy_read_timeout默认 60 秒或者客户端所在网络有 NAT 超时。Workerman 自身也有$worker-onWorkerStart里可以设置心跳检测。解决Nginx 调大proxy_read_timeout同时在 Workerman 里加心跳// 每 30 秒检查一次55 秒没收到心跳就断开 $worker-onWorkerStart function($worker) { \Workerman\Timer::add(30, function() use ($worker) { $now time(); foreach ($worker-connections as $conn) { if ($now - $conn-lastMessageTime 55) { $conn-close(); } } }); };客户端配合每 25 秒发一个{type:ping}服务端收到更新lastMessageTime。4.4 现象MySQL 连接数暴涨报 Too many connections原因每个 Worker 进程在onWorkerStart里建了数据库连接但 Workerman 是常驻进程连接不会自动释放。如果代码里每次消息都new PDO连接数会迅速打满。解决用单例或连接池。Workerman 生态里有workerman/mysql组件它内部维护长连接Db::instance()全局一个连接。注意在onWorkerStart里初始化不要在onMessage里反复建。4.5 现象reload 后部分用户掉线且不重连原因php start.php reload是平滑重启会等当前请求处理完再重启 Worker但 WebSocket 长连接不会自动迁移客户端需要重连逻辑。解决前端加onclose重连带指数退避let retryDelay 1000; function connect() { const ws new WebSocket(wss://chat.yourdomain.com/ws); ws.onclose () { setTimeout(connect, retryDelay); retryDelay Math.min(retryDelay * 2, 30000); }; ws.onopen () { retryDelay 1000; }; }5. 进阶技巧用 Redis 打通多进程与消息可靠性验证多进程消息推送这件事绕不开 Redis。我现在的做法是每个 Worker 启动时订阅一个专属频道worker:{id}用户登录时把uid - worker_id写进 Redis Hash。推送消息时先HGET online_uids {to_uid}拿到目标 Worker ID然后PUBLISH worker:{worker_id} {消息JSON}对应 Worker 收到后从自己的$uidConnMap里找连接推送。// onWorkerStart 里订阅 $worker-onWorkerStart function($worker) { $redis new \Redis(); $redis-connect(127.0.0.1, 6379); $redis-subscribe([worker:{$worker-id}], function($redis, $channel, $msg) use ($worker) { $data json_decode($msg, true); $target $uidConnMap[$data[to_uid]] ?? null; if ($target) { $target-send(json_encode($data)); } }); };这里有个细节subscribe是阻塞的会占住当前进程所以不能直接在 Worker 主进程里调得单独开一个进程或者用Timer异步。我一般把订阅逻辑放在一个独立的Worker里count 1专门做消息中转。验证消息可靠性我习惯用一个小脚本模拟并发# 用 websocat 模拟 100 个客户端连接 for i in $(seq 1 100); do websocat -n1 ws://127.0.0.1:8282 {type:login,uid:$i} done wait然后观察php start.php status里的连接数以及 MySQLmessages表的写入是否完整。如果发现连接数对不上大概率是某个 Worker 进程挂了看workerman.log里的报错。还有一个血泪经验reload之后一定要手动验证一遍消息收发因为平滑重启期间新连接可能落到正在重启的 Worker 上导致握手成功但消息发不出去。从那以后我每次改完代码reload都强制走一遍「登录 → 发消息 → 收消息 → 查库」四步验证确认无误才敢下班。希望帮到你。本文还有配套的精品资源点击获取
返回列表