消息已读还是没读?job-uniapp 的 Socket.IO 会话,断线重连与未读计数

发布时间:2026/10/14 18:00:49
消息已读还是没读?job-uniapp 的 Socket.IO 会话,断线重连与未读计数 消息已读还是没读job-uniapp 的 Socket.IO 会话断线重连与未读计数导读招聘者和求职者聊起来了最烦的不是消息发不出去而是对方到底看没看。job-uniapp 的 IM 走 Node.js Socket.IO 长连接未读数由后端 Redis 维护。这篇把连接建立、断线重连、未读计数三个环节讲透顺便复盘一次消息发出去对方收不到的事故。IM 场景和普通接口不一样普通接口是你问我答IM 是我随时可能找你。HTTP 轮询能实现但延迟高、浪费资源。Socket.IO 的长连接方案服务端可以主动推消息延迟能压到 100ms 内。代价是连接状态的维护——断线、重连、消息补偿这些才是 IM 工程的真正工作量。连接建立认证 加入自己的房间Socket.IO 用auth带 token 做连接认证连上后让客户端 join 到以自己 userId 命名的房间服务端推送就有了精确目标// Node.js 服务端IM serverconst{Server}require(socket.io);constjwtrequire(jsonwebtoken);constionewServer(server,{cors:{origin:*}});io.use((socket,next){// 连接时校验 JWT没 token 直接拒绝consttokensocket.handshake.auth.token;if(!token)returnnext(newError(unauthorized));try{constpayloadjwt.verify(token,process.env.JWT_SECRET);socket.userIdpayload.sub;socket.rolepayload.role;next();}catch(e){next(newError(invalid token));}});io.on(connection,(socket){// 加入个人房间后续推送只需 io.to(userId).emitsocket.join(String(socket.userId));console.log(connected:,socket.userId);socket.on(private_msg,async(data){constmsg{from:socket.userId,to:data.to,content:data.content,ts:Date.now()};awaitsaveMsg(msg);// 落库io.to(String(data.to)).emit(private_msg,msg);// 推给接收方// 未读数 1RedisawaitredisClient.incr(unread:${data.to}:${socket.userId});});socket.on(disconnect,(){console.log(disconnected:,socket.userId);});});未读数按unread:{接收方}:{发送方}粒度存每个会话一个 key——会话列表里每个会话的未读数独立显示这是小红点计数的基础。客户端uniapp 里管理连接生命周期uniapp 端用hyoga/uni-socket.io这类适配器封装成一个全局连接对象。核心是断线自动重连 心跳保活// utils/im.jsimportiofrom/uni_modules/uni-socket.io;letsocketnull;letconnectedfalse;letpending[];// 断线期间的消息暂存exportfunctionconnectIM(token,userId){socketio(wss://im.qinkonglan.cn,{auth:{token},transports:[websocket],reconnection:true,reconnectionAttempts:10,reconnectionDelay:2000});socket.on(connect,(){connectedtrue;// 重连成功补偿断线期间的消息pending.forEach((m)sendMsg(m));pending[];});socket.on(disconnect,(reason){connectedfalse;console.log(im disconnected:,reason);});socket.on(private_msg,(msg){getApp().globalData.msgHandlergetApp().globalData.msgHandler(msg);});}exportfunctionsendMsg(msg){if(!connected){pending.push(msg);// 没连上先缓存重连后补发return;}socket.emit(private_msg,msg);}断线补偿是这套设计的关键消息发出时如果连接断了先放 pending 队列重连成功后自动补发。不然用户会碰到消息显示已发送对方却没收到的诡异情况。未读数已读要联动会话打开和消息落库未读数清掉的时机要谨慎用户打开会话页时清空该会话的未读数而不是收到消息就清。实现上前端打开会话页调一个markRead接口// Spring Boot 端标记已读PostMapping(/im/mark-read)publicRmarkRead(RequestParamLongtargetUserId){LongmyIdCurrentUser.id();// 删掉对方发给我的未读数redis.delete(unread:myId:targetUserId);// 同时把该会话的已读回执推给发送方让他那边的消息变成已读imNotifyService.readReceipt(targetUserId,myId);returnR.ok();}会话列表接口聚合未读数// 会话列表每条会话带未读数publicListlistConversations(LonguserId){// 按会话 key 批量取未读数ListkeysconversationKeys(userId);// unread:{userId}:{otherId}Listunreadsredis.multiGet(keys);// 用 pipeline别循环 get// 组装 VO...}Redis 批量取未读数一定要用 pipeline/multiGet——会话多了以后循环get一次会话一次 RTT20 个会话就是 20 次网络往返接口直接慢一倍。踩坑实录消息发出去对方 5 分钟后才收到现象线上反馈求职者给招聘者发消息招聘者那边 5 分钟后才弹出来有时干脆收不到。但消息列表里双方都能看到这条消息已落库。排查过程先看消息表落库时间正常再看 Socket.IO 连接日志发现招聘者端的连接经常断开重连——disconnect reasontransport close、reconnection日志高频出现。再查服务端io.on(connection)里能看到同一个 userId 反复 join。定位思路消息其实推了但推的时候接收方的连接是断的服务端io.to(userId).emit落到空房间消息丢了。客户端 5 分钟后重连成功但重连逻辑里只补偿了自己发送的 pending 消息没补偿断线期间别人发给我的消息——服务端没有做离线消息补推。最终解决双向补偿服务端负责离线消息服务端private_msg落库后除了实时推还要判断接收方是否在线——不在线就把消息 id 记入该用户的离线队列Redis list用户重连 join 房间后服务端检测到上线自动补推离线消息socket.on(connect_ok,async(){// 客户端连上后主动拉取离线消息constofflineawaitredisClient.lrange(offline:${socket.userId},0,-1);for(constidofoffline){constmsgawaitgetMsgById(id);socket.emit(private_msg,msg);}awaitredisClient.del(offline:${socket.userId});});// 发送时接收方不在线则入离线队列constisOnlineawaitredisClient.sismember(online_users,String(data.to));if(!isOnline){awaitredisClient.rpush(offline:${data.to},msg.id);}客户端重连成功后再主动调一次会话列表刷新未读数保证 UI 与后端一致。改完压测断开接收方连接 → 发送方连发 5 条 → 接收方重连 → 5 条消息 未读数 5 全部到齐。可以直接抄的结论Socket.IO 连接用auth带 JWT 认证连上后 join 个人房间做精确推送未读数用unread:{接收方}:{发送方}粒度存 Redis会话列表 pipeline 批量取断线重连要双向补偿客户端补发自己没发出去的服务端补推别人发来的离线消息打开会话页才清未读数别收到消息就清消息先落库再推送丢消息可追溯接收方不在线走 Redis 离线队列长连接必须真机测试弱网、切后台、锁屏重连——这些场景才是 IM 事故的高发区。IM 的工程难点从来不在发一条消息而在消息在各种断连场景下都不丢。把连接生命周期和消息补偿两条线管住未读数自然就准了。项目源码https://gitee.com/gzqkl/job-uniapp

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询