一、为什么服务端要主动关闭WebSocket连接
很多开发者刚接触WebSocket时,总觉得连接开了就不用管了——反正客户端断线会自动重连。但实际项目里,服务端主动关闭连接是刚需:比如你做了个外卖订单实时推送,用户完成支付后,订单状态的实时连接就没必要再占着服务器带宽;又或者在线做题系统,用户提交答案后,再继续保持连接就是浪费资源;更常见的是,用户退出账号或长时间离线(比如半个钟头没任何操作),对应的WebSocket连接如果不主动关,就会像没人打扫的房间,占着内存和CPU,慢慢把服务器拖卡。简单说,服务端主动关连接,就是帮服务器“扔垃圾”,也是给用户发“我要断了”的通知,提升体验。
二、服务端主动关闭的正确姿势
2.1 先清理连接关联的资源
关闭连接前,必须把这个连接绑定的所有“附属品”清干净——比如你给每个用户开的心跳定时器(用来检测是否在线)、存在内存里的用户聊天缓存、或者房间列表里的用户在线标识。举个例子:你给用户A的WebSocket开了个定时器,每30秒发一次心跳,要是直接关连接没清定时器,那个定时器还会每隔30秒跑一次,既浪费CPU,还会因为连接已经断了,报一堆错误。
2.2 发送符合WebSocket标准的关闭帧
WebSocket有自己的“礼貌道别”规则,不能直接把连接砍断(比如用 terminate 方法),得给客户端发个“我要关了”的信号,这就是关闭帧。关闭帧得用标准的关闭码,比如RFC规定的1000代表“正常关闭”,1001代表“服务器维护/离开”,别自己随便编个数字当关闭码,不然客户端可能识别不了,当成错误处理。比如你要断用户的连接,可以发个1000码,附带消息“长时间没操作,连接已断开,请重新登录”,用户能清晰看到原因。
2.3 等待客户端确认(可选但推荐)
你发了关闭帧后,客户端会回一个确认的关闭帧,服务端最好等这个确认收到后,再彻底把连接从自己的连接池里删掉。要是提前删,客户端可能还没收到关闭通知,还以为自己在线,继续发消息,结果消息没地方放,既浪费网络,又容易让用户困惑。
三、常见陷阱及避坑指南
3.1 不清理资源就用粗暴断开
很多开发者图省事,直接用 ws.terminate() 关连接,这个方法太“粗鲁”——它不会发任何关闭帧,客户端根本不知道连接断了,还会一直尝试发消息;而且之前绑定的定时器、缓存也不会清,连接的引用还留在内存里,慢慢就成了“僵尸连接”。举个反例:如果把后面示例里的 ws.close() 换成 ws.terminate(),用户明明已经被断开了,客户端状态栏还显示在线,他以为还能发消息,结果操作半天没反应,体验极差。
3.2 自定义关闭码太随意
有些开发者会用自己编的数字当关闭码,比如4000,以为是业务用的自定义码。但按照WebSocket的规范,1000-3999是官方标准码,4000及以上才是自定义业务码,而且必须和客户端提前约定好。要是随便用个非标准码,浏览器或App可能把这个关闭当成异常,弹个错误提示给用户,明明是正常关连接,却给用户添麻烦。
3.3 忽略关闭时的异常处理
关闭连接的时候,很容易出异常:比如连接已经因为网络问题挂了,你还往里面发关闭帧;又或者服务器内存不够,删连接的时候出错。这时候必须监听WebSocket的 error 和 close 事件,哪怕关连接时出错,也要把剩下的资源清掉,别让僵尸连接一直占着服务器资源。
四、正确关闭姿势的优缺点和注意事项
4.1 优缺点
正确姿势的优点很明显:用户能收到明确的关闭通知,不会误以为连接还在线;服务器资源不会浪费,不会出现僵尸连接;符合WebSocket规范,和客户端(浏览器、App)的兼容性更好。缺点就是比粗暴断开多了几步操作,代码稍微复杂一点,但这点复杂度换来的是更稳定的服务和更好的用户体验,完全值得。
4.2 注意事项
第一,先清资源再关连接:绝对不能先关连接再清定时器或缓存,不然定时器还在跑,必然报错;第二,用标准关闭码:别自己编关闭码,哪怕用1000这种简单的标准码,也比自定义码靠谱;第三,给客户端留反应时间:发了关闭帧后,别马上删连接,等1秒或者等客户端的确认帧,避免用户没收到通知;第四,必须监听事件:close 和 error 事件一定要加,哪怕没出错,也要确保资源清干净。
五、完整示例(Node.js + ws模块)
这个示例用最常用的Node.js ws库,模拟服务端主动关闭连接的完整流程,每个步骤都加了注释,方便理解:
// 技术栈:Node.js + ws(WebSocket官方推荐的Node.js库)
const WebSocket = require('ws');
// 启动WebSocket服务,端口用8080,方便测试
const wss = new WebSocket.Server({ port: 8080 });
// 用Map存储所有活跃连接,方便后续主动查找和关闭
const activeConnections = new Map();
// 监听新的WebSocket连接
wss.on('connection', (ws) => {
// 给每个连接分配唯一的用户ID,方便关联资源,这里用时间+随机数生成
const userId = `user_${Date.now()}_${Math.random().toString(36).slice(2)}`;
// 把新连接存到Map里
activeConnections.set(userId, ws);
// 模拟心跳定时器:每30分钟没收到客户端消息,就主动关闭连接
const heartBeatTimer = setTimeout(() => {
// 步骤1:先清理定时器,避免后续还在跑
clearTimeout(heartBeatTimer);
try {
// 步骤2:发送标准关闭帧,1000代表正常关闭,消息给用户看
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, '长时间无操作,连接已自动断开,请重新登录');
}
} catch (err) {
console.error(`关闭用户${userId}连接出错:`, err);
} finally {
// 步骤3:从连接池里删掉该用户
activeConnections.delete(userId);
console.log(`用户${userId}因无操作,连接已主动关闭`);
}
}, 30 * 60 * 1000); // 30分钟,测试的时候可以改短比如30秒
// 监听关闭事件:确认客户端已收到关闭帧,再彻底清理资源
ws.on('close', (code, reason) => {
// 清理心跳定时器,避免残留
clearTimeout(heartBeatTimer);
// 从连接池删除
activeConnections.delete(userId);
console.log(`用户${userId}连接彻底关闭,关闭码:${code},原因:${reason.toString()}`);
});
// 监听错误事件:哪怕连接出错,也要清理资源
ws.on('error', (err) => {
console.error(`用户${userId}连接出错:`, err);
clearTimeout(heartBeatTimer);
activeConnections.delete(userId);
});
});
// 模拟服务端维护场景:1小时后主动断开所有连接
setTimeout(() => {
console.log('服务端即将维护,主动断开所有连接');
for (const [userId, ws] of activeConnections) {
try {
if (ws.readyState === WebSocket.OPEN) {
// 发维护通知,再关闭连接
ws.close(1001, '服务器正在维护,请稍后再试');
}
} catch (err) {
console.error(`关闭用户${userId}连接出错:`, err);
}
}
// 关闭WebSocket服务
wss.close();
}, 60 * 60 * 1000);
评论
围绕“服务端主动关闭WebSocket连接的正确姿势与常见陷阱”参与讨论