简介:Workerman在线客服系统是一套基于PHP的实时客服部署源码,面向需要快速搭建网页客服功能的开发者、中小企业站运营者及PHP学习者。资源围绕Nginx 1.21.4、PHP-7.2、MySQL 5.7.40组合展开,提供完整的安装与配置说明,重点覆盖上传解压、数据库连接设置等关键环节,并明确标注application/database.php中数据库名、用户名、密码的修改位置;压缩包内另附有详细文字教程,可帮助使用者规避环境版本不一致、数据库连接失败等常见问题,显著降低部署门槛。包内共2000个文件,其中1184个js文件承担前端逻辑与交互,196个html与106个css文件构成客服界面,22个php文件完成服务端接口与Workerman进程管理,4个sql脚本用于初始化数据库表结构,另有163个json、159个md、142个txt等配置与文档类文件,整体约25.95MB,类型覆盖从前端展示、后端服务到数据库初始化的完整链路,目录层级清晰,便于按模块检索和二次开发。目前已有408人学习使用,适合需要独立部署、调试或深入理解Workerman客服系统工作原理的技术人员,也可作为课程设计或企业建站的参考方案。 搞这类“长连接IM”项目,几乎每个做PHP的团队都会在某个阶段碰到一个坎:Web端客服、工单、站内信、实时通知……业务方提出需求时,第一反应是“这不就是WebSocket吗”,但真到了落地上,才发现在PHP的生态里,从零写一套能抗住生产压力的长连接服务并不轻松。
我之前在团队里负责过一版在线客服系统的选型和开发,最终选择了Workerman + ThinkPHP 5这套组合。不是因为别的框架不够好,而是这套方案在“快速交付”和“长期可维护”之间找到了一个很实用的平衡点。这篇博文就把这个项目的完整拆解记录下来,包括架构思路、核心代码、运行部署,以及我在线上环境踩过的一些坑,希望能给准备做同类项目的朋友省点时间。
1. 项目做了什么事:需求场景与结果
1.1 表面需求与本质需求
产品最初给的文档很简单:在官网加一个“在线咨询”入口,访客点开弹窗,可以和客服实时聊天;客服在后台看到会话列表,点开就能回复。听起来就是个聊天室,但拆解下来会发现真实的需求其实分好几层:
- 访客不需要登录,打开页面就能发起咨询,系统要能自动分配一个客服;
- 同一个访客刷新页面、切换设备后,会话记录不能丢;
- 客服后台要能看到访客来源页面、当前访问地址、停留时间等信息,这在客服场景里是刚需;
- 消息要有历史记录,且可以翻查;客服离线时,消息要能落地,避免丢失;
- 实时性要求高,客服发出消息后访客要能秒收,不能像轮询那样有明显滞后。
这些需求叠加在一起,决定了服务端不能只做一个简单的WebSocket转发,还需要有会话管理、用户绑定、消息存储、离线消息补发这些业务逻辑。所以架构上必须拆成“长连接网关”和“业务处理层”两个部分,这正是 Workerman 的 GatewayWorker 方案最擅长的事情。
1.2 技术选型:为什么是 Workerman + ThinkPHP 5
当时团队的技术栈是 ThinkPHP 5,业务代码全部跑在PHP-FPM里,如果为了客服系统单独引入Swoole或者Node.js,意味着要么多维护一套技术栈,要么就得把TP5的整个生命周期强行搬到Swoole常驻内存里,坑会很多。
选择 Workerman 主要有几个现实考量:
第一,Workerman 是纯PHP实现的,部署和维护成本极低。不用装额外的扩展(只有可选的event扩展),只要PHP环境支持pcntl、posix,就能在Linux上跑起来。这对当时“不想为了一个客服系统改服务端架构”的团队来说很关键。
第二,Team 里大家对 PHP 比较熟,用 Workerman 不需要换语言,写业务逻辑时心智负担小。相比 Swoole 需要理解协程、理解常驻内存下变量生命周期的问题,Workerman 的事件驱动模型更像是“你写一个回调函数,框架帮你管连接”,入门门槛低很多。
第三,GatewayWorker 这套现成的分布式通讯框架解决了最麻烦的连接管理问题。在线客服场景天然支持水平扩展——Gateway挂多台机器、BusinessWorker挂多台机器,内部通过Register做服务发现。这个能力用原生Workerman裸写的话,光靠自己和client_id拼消息路由就能写掉三分之一的工作量。
第四,和 ThinkPHP 5 的集成路径很干净。可以把TP5当做一个“业务库”挂在 BusinessWorker 进程里,在事件回调里直接调用TP5的模型层和Service层,事务、ORM、缓存全都能复用。这样在整个项目里,客服系统的业务写法和其他模块保持一致,新同事接手时也不会有割裂感。
2. 整体架构与通信流程设计
2.1 GatewayWorker 三层架构
GatewayWorker 的架构值得展开说一下,因为不搞懂这里,后面代码写起来就是知其然不知其所以然。
整个系统分为三层,分别对应三个进程角色:
Register(注册中心):负责管理 Gateway 和 BusinessWorker 的注册与发现。Gateway 启动后向 Register 注册自己的内网IP和端口;BusinessWorker 也向 Register 上报自己的进程信息。两边通过 Register 互相知道对方的位置,才能形成一条完整的通信链路。
Gateway(网关层):负责与客户端建立WebSocket长连接,维护连接状态,解析并转发消息。一个典型的客服系统可以部署多个 Gateway 进程,每个进程对应一个对外端口。客户端只和 Gateway 直接通信。
BusinessWorker(业务层):真正处理业务逻辑的地方。收到 Gateway 转发过来的用户消息后,在这里落库、写缓存、做客服分配、调用TP5的Service,然后把结果通过 Gateway 推回给指定客户端。
调用关系如果用文字描述就是:客户端连上 Gateway,Gateway 把消息发给 BusinessWorker,BusinessWorker 处理完后再通过 Gateway 推给目标客户端。消息不走 Redis 中转,而是走内网TCP,这也是为什么这套方案性能表现不错的原因之一。
2.2 消息流转的关键路径
以最核心的“访客发消息给客服”为例,完整的数据流是这样的:
- 访客页面通过 WebSocket 连接到 Gateway 的地址(如
ws://chat.xxx.com:8282)。 - 连接建立后,Gateway 生成一个全局唯一的
client_id,触发onConnect事件。 - 访客在页面输入内容并点击发送,前端 JS 通过 WebSocket 发送一条 JSON,比如
{"type":"chat","content":"你好,我想咨询..."}。 - Gateway 接收到消息,转发给任意一台空闲的 BusinessWorker,触发
onMessage事件。 - BusinessWorker 解析 JSON,识别
type=chat,调用ChatService::sendMessage()方法,方法内部做消息落库、更新会话最后时间等操作。 - BusinessWorker 通过
Gateway::sendToUid($toUid, $message)把消息推送给目标客服的客户端。这里的$toUid需要客户端在连接时先完成“登录绑定”(即把client_id和用户ID关联起来)。 - 客服端收到消息后,WebSocket 的
onmessage回调触发,渲染到聊天界面中。
这套设计里最关键的一步是第6步的sendToUid,它依赖 GatewayWorker 的uid绑定机制。在客服端和访客端建立连接后,需要先发送一条login类型的消息,把当前连接绑定到对应的用户ID上。之后无论这个用户连接在哪个 Gateway 进程上,只要传uid就能准确推送到对应连接。
2.3 前后端通信协议设计
由于所有消息都走WebSocket,必须设计一个统一的 JSON 协议格式,否则前后端会越写越乱。我在项目里用的是这样的结构:
{ "type": "chat", "data": { "from_uid": 1001, "to_uid": 2001, "content": "你好,请问这个商品的保修期是多久?", "timestamp": 1714000000 }, "msg_id": "170e1d34-8a2c-4e18-9b9f-81c9a1a5f45c" }type区分消息类型,比如chat(聊天消息)、login(登录绑定)、ping(心跳)、read(已读回执)、close(结束会话)等;data是业务数据;msg_id是一个 UUID,用于客户端做消息去重和发送状态确认。如果客户端在超时后重发同一条消息,服务端可以根据msg_id判断是否已经处理过,避免重复落库。
协议要简单但也要留扩展余地,后面如果想加表情、图片、文件消息,只需要在data里增加msg_type字段即可,不需要改动通信层的逻辑。
3. 核心实现:服务端与业务端
3.1 服务进程启动脚本 start.php
在项目根目录创建一个server/文件夹,里面放 GatewayWorker 的启动入口。如果你的代码仓库已经用 Composer 管理了依赖,直接安装workerman/gateway-worker即可。
启动脚本server/start.php的完整代码如下:
<?php use Workerman\Worker; use Workerman\Autoloader; use GatewayWorker\Gateway; use GatewayWorker\BusinessWorker; use GatewayWorker\Register; // 自动加载 require_once __DIR__ . '/../vendor/autoload.php'; // ---------- 注册中心 ---------- $register = new Register(); $register->listen = '0.0.0.0:1238'; // ---------- 业务进程 ---------- $businessWorker = new BusinessWorker(); $businessWorker->name = 'ChatBusinessWorker'; $businessWorker->count = 4; // 按CPU核数调整,一般设为CPU的1-2倍 $businessWorker->eventHandler = \app\chat\worker\Events::class; $businessWorker->registerAddress = '127.0.0.1:1238'; // ---------- 网关进程 ---------- $gateway = new Gateway("websocket://0.0.0.0:8282"); $gateway->name = 'ChatGateway'; $gateway->count = 4; $gateway->lanIp = '127.0.0.1'; $gateway->startPort = 2900; // 内部通讯起始端口 $gateway->pingInterval = 30; // 心跳检测间隔,单位秒 $gateway->pingNotResponseLimit = 2; // 连续2次未回应则断开 $gateway->pingData = '{"type":"ping"}'; // 服务端主动发送的心跳数据 $gateway->registerAddress = '127.0.0.1:1238'; // 运行 Worker::runAll();这里有几个参数值得单独说明:
$businessWorker->count:业务进程数。因为是CPU密集型任务少、IO密集型任务多的场景(主要是Redis、MySQL操作),多开几个进程能有效提升并发处理能力。我线上用的8核机器,开的是4个业务进程,实测并发在2000连接时CPU占用率依然很低。$gateway->startPort:Gateway 进程启动后,会在lanIp上开启一串监听端口用于内部通信。如果有多个 Gateway 进程,端口会从startPort开始依次递增。这个端口段需要在防火墙里放行,并且不能和其他服务冲突。$gateway->pingInterval和pingNotResponseLimit:这两个参数配合实现心跳检测。服务端每隔30秒向客户端发送一个ping数据包,如果连续2次客户端没有回应,服务端主动断开这个连接。同时客户端也要有对应的心跳机制,收到ping后回一个pong或者收到心跳后重置本地计数器。双端心跳是保证连接不假死的关键,否则一天下来会积累大量死连接。
3.2 业务事件处理 Events.php
这是整个项目的核心,所有业务逻辑都从这里分发。文件位置建议放在app/chat/worker/Events.php,这样可以用 TP5 的自动加载规则直接加载(需要在composer.json里配置对应的 PSR-4 映射)。
<?php namespace app\chat\worker; use GatewayWorker\Lib\Gateway; use think\facade\Db; use app\chat\service\ChatService; class Events { /** * 客户端连接建立时触发 */ public static function onConnect($client_id) { // 连接刚建立时,还没有绑定用户身份 // 这里只做日志记录,不做业务处理 // 如果要做用户在线状态统计,可以在这里给 client_id 对应的初始数据占位 } /** * 收到客户端消息时触发 */ public static function onMessage($client_id, $message) { $data = json_decode($message, true); if (empty($data['type'])) { return; } switch ($data['type']) { case 'login': // 登录绑定:把 client_id 和用户ID绑定 $uid = (int) $data['uid']; Gateway::bindUid($client_id, $uid); // 通知前端绑定成功 Gateway::sendToClient($client_id, json_encode([ 'type' => 'login_success', 'client_id' => $client_id, ])); break; case 'chat': self::handleChat($client_id, $data); break; case 'read': // 已读回执:把会话中对方发的未读消息标记为已读 self::handleRead($client_id, $data); break; case 'close': // 结束会话 self::handleClose($client_id, $data); break; } } /** * 客户端断开连接时触发 */ public static function onClose($client_id) { // 获取该 client_id 绑定的 uid $uid = Gateway::getUidByClientId($client_id); if ($uid) { // 处理离线逻辑:标记客服下线、更新会话状态等 ChatService::handleOffline($uid, $client_id); } } /** * 处理聊天消息 */ protected static function handleChat($client_id, $data) { $fromUid = (int) $data['from_uid']; $toUid = (int) $data['to_uid']; $content = trim(strip_tags((string) $data['content'])); $msgId = $data['msg_id'] ?? ''; if ($content === '' || $toUid <= 0) { return; } // 通过TP5的Service层落库 $messageId = ChatService::saveMessage([ 'from_uid' => $fromUid, 'to_uid' => $toUid, 'content' => $content, 'msg_id' => $msgId, 'create_time' => time(), ]); // 判断对方是否在线 $isOnline = Gateway::isUidOnline($toUid); $message = json_encode([ 'type' => 'chat', 'msg_id' => $msgId, 'message_id' => $messageId, 'from_uid' => $fromUid, 'content' => $content, 'timestamp' => time(), ], JSON_UNESCAPED_UNICODE); if ($isOnline) { // 在线则直接推送 Gateway::sendToUid($toUid, $message); } else { // 离线则走离线消息逻辑,等对方上线后补发 ChatService::pushOfflineMessage($toUid, $message); } } }这段代码需要注意几个细节:
尽量用 TP5 的 Service 层封装业务处理逻辑,Events 只做消息分发和转发,不要把 SQL 写在这里。因为 BusinessWorker 是多进程常驻内存的,所有被调用的类必须保证代码更新后能重新加载(开发时用
php start.php reload重载业务进程),业务逻辑集中管理能减少踩坑。Gateway::getUidByClientId和Gateway::isUidOnline这两个API非常实用。前者可以在断开时排查用户身份,解决一些边界场景,比如用户关浏览器前没来得及发close消息;后者可以用来决定消息是直接推还是走离线队列。注意
strip_tags过滤。在线客服的访客端消息默认是纯文本,但WebSocket消息经常被人拿去注入HTML或脚本。在入口处做一层过滤很必要。
3.3 与 ThinkPHP 5 业务层的数据打通
Events 类里用到了think\facade\Db和app\chat\service\ChatService,这意味着 BusinessWorker 进程需要加载 TP5 的框架内核。具体做法是在start.php中引入vendor/autoload.php,同时把 TP5 的app目录注册进 Composer 的自动加载。
在composer.json中,通常这样配置:
{ "autoload": { "psr-4": { "app\\": "application/" } }, "require": { "workerman/gateway-worker": "^3.0", "topthink/framework": "5.1.*" } }然后在start.php的最前面执行一次 TP5 的容器初始化,让Db、Cache这些门面在 BusinessWorker 进程里可用:
// 在 Worker::runAll() 之前执行 $app = new \think\App(); $http = $app->http; $response = $http->run();不过这个方案要小心:Http->run()会解析路由并生成响应,在 CLI 环境下并不适用。更稳妥的做法是只初始化容器和应用配置,不调用 HTTP 相关的逻辑。实际操作中,我的做法是在start.php里手动初始化App:
$app = new \think\App(); $app->initialize();这样Db、Log、Config等核心服务都可用,又不会进入HTTP请求流程。在Events中直接调用think\facade\Db就不会报“未初始化”的错误了。
这一点是整个集成的关键,很多第一次做 Workerman + TP5 集成的朋友都卡在这里:单独跑 Workerman 一切正常,已进 TP5 模型就报错,本质就是进程没初始化 TP 应用容器。
3.4 前端 WebSocket 对接代码
前端这块我用的是原生 WebSocket,没有引入第三方库,这样依赖最少、排查问题也最直接。核心代码如下:
class ChatClient { constructor(options) { this.wsUrl = options.wsUrl; this.uid = options.uid; this.onMessage = options.onMessage || function() {}; this.ws = null; this.heartbeatTimer = null; this.connect(); } connect() { this.ws = new WebSocket(this.wsUrl); this.ws.onopen = () => { // 连接建立后,先把当前用户身份绑定到服务端 this.send({ type: 'login', uid: this.uid }); // 开启心跳 this.startHeartbeat(); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); // 如果收到ping,重置心跳计数器(也可以回复一个pong) this.onMessage(data); }; this.ws.onclose = () => { clearInterval(this.heartbeatTimer); // 简单重连:3秒后重连,生产环境可以做指数退避 setTimeout(() => { this.connect(); }, 3000); }; this.ws.onerror = (err) => { console.error('WebSocket error:', err); }; } send(data) { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(data)); } } startHeartbeat() { // 前端心跳,30秒发送一次,防止连接被网关静默回收 this.heartbeatTimer = setInterval(() => { this.send({ type: 'ping' }); }, 30000); } close() { clearInterval(this.heartbeatTimer); this.ws.close(); } } // 使用示例 const chat = new ChatClient({ wsUrl: 'ws://chat.example.com:8282', uid: 1001, onMessage: (data) => { if (data.type === 'chat') { // 渲染消息到聊天界面 console.log('收到消息:', data); } } });这里有几个细节要说明:
心跳不仅服务端要做,前端也要做。因为有些网络环境(比如企业防火墙、路由器NAT超时)会静默断开长连接,双端心跳是最稳妥的保活手段。前端这里是30秒一次
ping,服务端配置的pingInterval也是30秒,两边节奏对齐。断线重连时要考虑消息补发。因为断线期间的消息可能已经通过离线消息逻辑写入数据库了,重连后需要调一个 HTTP 接口拉取离线消息,WebSocket本身只负责实时收发,不负责历史消息的完整性。
onclose里的重连逻辑在生产环境建议加随机退避,避免大量客户端同时断线、同时重连造成服务端瞬间压力过大。
4. 运行部署与实施细节
4.1 环境准备与扩展安装
Workerman 在 Linux 下运行需要pcntl和posix两个扩展。用 PHP7.2+ 的默认编译参数通常自带这两个扩展,但还是要确认一下:
php -m | grep pcntl php -m | grep posix如果没有,需要重新编译PHP或者在已有环境上安装扩展。另外建议安装event扩展,它能提升 Workerman 的事件驱动性能,特别是在高并发连接场景下,event扩展可以显著降低 CPU 占用。安装方式可以用 pecl:
pecl install event装好后在php.ini中添加extension=event.so,然后用php -m验证。
对于连接数超过1024的场景,还需要修改系统文件描述符限制:
# 临时生效 ulimit -n 102400 # 永久生效 echo "* soft nofile 102400" >> /etc/security/limits.conf echo "* hard nofile 102400" >> /etc/security/limits.conf这个操作很关键。我第一版部署时没改这个,压测到800个连接就开始报Too many open files,后来才发现是系统默认限制。在线客服系统的并发连接数很容易就上千,提前改好能少踩很多坑。
4.2 启动与守护进程运行
启动脚本写好后,可以用下面命令启动所有进程:
php server/start.php start开发调试时用start会输出实时日志,适合观察报错。线上运行时建议改用start -d进入守护模式,或者使用 systemd 来管理,这样进程崩溃后能自动拉起。
一个简单可用的 systemd 配置示例:
[Unit] Description=Workerman Chat Server After=network.target [Service] Type=simple WorkingDirectory=/data/wwwroot/chat-server ExecStart=/usr/bin/php /data/wwwroot/chat-server/server/start.php start ExecStop=/usr/bin/php /data/wwwroot/chat-server/server/start.php stop Restart=always RestartSec=3 [Install] WantedBy=multi-user.target每次修改Events.php或业务代码后,不用重启所有连接,只需执行 reload 重载业务进程,保持长连接不断:
php server/start.php reload这里要注意,reload 只能重载 BusinessWorker 的代码,Gateway 的配置改动(比如端口、心跳间隔)需要完整重启才能生效。
4.3 Nginx 反向代理 WebSocket
线上环境一般不直接把 8282 端口暴露给用户,而是通过 Nginx 做反向代理,顺便可以加一层 TLS 证书,让客户端走 WSS 协议。
关键配置片段:
map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 443 ssl http2; server_name chat.example.com; ssl_certificate /etc/nginx/ssl/chat.pem; ssl_certificate_key /etc/nginx/ssl/chat.key; location / { proxy_pass http://127.0.0.1:8282; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }proxy_read_timeout和proxy_send_timeout默认值是60秒,如果不改,长连接会在60秒后被Nginx断开。这对于需要长时间在线的客服系统来说是不能接受的,必须要调大,或者比心跳间隔大即可。
前端连接地址也相应变成:
wss://chat.example.comWSS 的好处是把WebSocket流量混在443端口里,不易被某些网络环境拦截,同时也省去了给WebSocket单独开防火墙端口的麻烦。
5. 常见问题与排查经验
5.1 频繁掉线的排查路径
线上遇到最头疼的问题就是用户反馈“聊几句就掉线”。通常我会按以下顺序排查:
- 检查Nginx代理超时时间。没设置
proxy_read_timeout是最常见的原因,长连接超过60秒就被掐断。 - 检查心跳是否正常。用
Gateway::$pingData和客户端的心跳逻辑是否闭合。可以打开 Workerman 的日志,观察是否有大量close事件。 - 检查防火墙/NAT。有些云环境的安全组限制了长连接的空闲时间,客户端在空闲时收不到任何数据包就会被网络设备回收。解决方法是缩短心跳间隔,比如从30秒改为15秒。
- 检查连接数限制。确认
ulimit -n是否足够,以及 Gateway 进程数是否合理。单进程支持的连接数一般没问题,但如果监听端口被占用,会导致新连接全部失败。
5.2 消息延迟与性能瓶颈分析
消息出现延迟时,先别急着加机器,先看瓶颈在哪:
- 如果延迟集中在某些时间段,多半是 MySQL 慢查询。查一下
saveMessage的写入是否命中了索引,事务锁是否造成排队。 - 如果并发高时 CPU 飙升,用
top看是哪个进程占用高。如果是 PHP-FPM 的进程高,说明 HTTP 接口有慢请求,不是 Workerman 的问题;如果是 BusinessWorker 进程高,说明业务代码有死循环或大循环。 - 如果 Redis 用了
BLPOP之类的阻塞操作,也要注意等待超时对消息链路的影响。
我的经验是,在线客服系统的瓶颈几乎不可能在 Workerman 本身,而是在消息落库和会话分配的策略上。后来我把saveMessage改成了先写 Redis 队列,再由单独的消费脚本批量落库,吞吐量立刻上了一个台阶。如果你不需要强实时性,可以考虑这个方案。
5.3 业务数据与长连接状态不同步
一个容易被忽视的坑是:客服在后台被删除或封禁了,但他在 Gateway 里绑定的 uid 还在线,还能照常收发消息。解决办法是在删除操作后显式调用Gateway::closeClientByUid($uid),强制断开他的长连接。
另外,访客在客户端发起会话时,可能会在短时间内重复连接、断开,导致同一个 uid 对应多个 client_id。GatewayWorker 的默认行为是一个 uid 可以绑定多个 client_id,推消息时会推给所有连接。这会带来两个问题:一是消息重复展示,二是离线状态判断不准确。解决方法是在绑定新client_id之前,先用Gateway::getClientIdByUid($uid)获取旧连接,判断是否还在线,如果在线则先断开旧连接再绑定新连接:
$oldClientId = Gateway::getClientIdByUid($uid); if ($oldClientId) { Gateway::closeClient($oldClientId[0]); } Gateway::bindUid($client_id, $uid);这样能最大程度保证“一个用户同一时刻只有一个活跃连接”,避免消息串台。
最后梳理一下整体维护的感受
做完整套客服系统,再回头看当初的选型,我只想说一个结论:Workerman 是 PHP 团队做长连接场景最平滑的过渡方案,没有之一。它没有 Swoole 那种性能极致追求的学习成本,也不需要引入 Node.js 这种跨语言的心智切换,却能把 WebSocket、TCP 长连接、多进程分布式这些硬核概念落地到完全可运维的技术栈上。
如果你正在用 ThinkPHP 5,或者刚接手一个 PHP 的客服/聊天/通知项目,完全可以按照这篇文章的思路,先把 GatewayWorker 跑起来,把Events.php里的四个回调方法理解透,再逐步加业务。这套东西本身的原理并不复杂,真正复杂的是在真实网络环境中,如何把超时、重连、离线、并发这些边界情况都处理到位。
最后分享一个调试小技巧:开发时不要直接调 WebSocket,可以先在 Linux 上用一个简单的命令行工具测试连接,比如websocat或 Node 的wscat,发送一段 JSON 观察服务端返回,能快速定位问题出在前端还是后端。我就是靠着这个工具排查了好几起“前端以为后端没收到、后端以为前端没发”的乌龙事故。
本文还有配套的精品资源,点击获取