代驾系统长连接实战(二):心跳、断线重连与消息补偿的完整实现
上一篇聊了代驾系统的整体架构和三级派单这篇往下钻一层司机端和乘客端挂在 Netty 长连接上手机进电梯、切后台、弱网闪断是家常便饭连接断了之后怎么发现、怎么重连、断线期间的消息怎么不丢这套机制才是派单链路真正能用起来的地基。全部代码来自我们开源的代驾系统仓库地址放在文末。一、协议设计两个枚举定生死长连接第一件事不是写代码是定协议。我们把客户端要干什么和服务端回了什么拆成两个枚举。客户端操作类型NettyHandleEnumsCONNECT(1)第一次连接或重连时的初始化KEEPALIVE(2)心跳保活COORDINATE_DRIVER(3)司机坐标上报服务端返回码NettyCodeEnums截取几个关键的KEEPALIVE(1,心跳正常),KEEPALIVE_ERROR(-1,心跳异常),CONNECT(2,第一次连接正常),CONNECT_ERROR(-2,第一次连接异常),DRIVER_UNFINISHED_ORDER 和 ORDER_MOVE 这两个码——它们存在的意义就是**重连恢复**后面会讲到。 ## 二、服务端 pipeline10秒空闲就踢Netty的 pipeline 配置在 DataAcceptInitializer 里 javaChannelPipelinepipelinesocketChannel.pipeline();pipeline.addLast(newHttpServerCodec());pipeline.addLast(newChunkedWriteHandler());pipeline.addLast(newHttpObjectAggregator(1024*64));//增加心跳支持/** * 针对客户端如果在1分钟时间内没有向服务端发送读写心跳ALL则主动断开连接 */pipeline.addLast(newIdleStateHandler(0,0,10,TimeUnit.SECONDS));pipeline.addLast(dataAcceptHandler);pipeline.addLast(newWebSocketServerProtocolHandler(/ws));IdleStateHandler(0, 0, 10)意思是读空闲不查、写空闲不查但读写加起来 10 秒没有任何动静就触发 ALL_IDLE 事件。客户端心跳间隔必须小于这个值实际 App 端是几秒一次。触发超时后的处理在userEventTriggeredpublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { IdleStateEvent event (IdleStateEvent) evt; if (event.state() IdleState.ALL_IDLE) { log.info(客户端(ALL_IDLE 总超时 重新连接)); } } ctx.channel().writeAndFlush(new MsgVo(NettyCodeEnums.SOCKET_TIME_OUT).toJsonStringbuf()); channelInactive(ctx);//清除netty通道 }先给客户端推一个SOCKET_TIME_OUT(101)万一它还活着知道自己被踢了、立刻重连然后进channelInactive清理通道。三、CONNECT重连不只是重连是状态恢复连接管理用的是个单机 HashMapkey 带角色前缀public class UserChanelRel { private static HashMapString, Channel manage new HashMap(); // key 前缀区分角色driver:{id} / user:{id} public static void put(String senderId, Channel channel){ manage.put(senderId, channel); } }CONNECT 懆支的核心逻辑DataAcceptHandlercase CONNECT: //客户端连接--第一次或者重连处理 if (chanelContext.equals(CONNECT)) { //每次连接或者重连时把连接存入连接池中 UserChanelRel.put(didStr, ctx.channel()); channel.writeAndFlush(new MsgVo(NettyCodeEnums.CONNECT).toJsonStringbuf()); if (cal.isEquals(channelIdentity, StaticUtils.STATUS_YES)){ //校验司机是否处于服务状态 OrderMain orderMain orderMainService.selectOrderByDriver(did); if (orderMain ! null) { //反馈司机有未完成订单 MsgVo msgVo new MsgVo(NettyCodeEnums.DRIVER_UNFINISHED_ORDER); msgVo.setOrderCode(orderMain.getOrderCode()); channel.writeAndFlush(msgVo.toJsonStringbuf()); } }这里的重点是重连成功不等于完事。司机端重连后服务端要查他名下有没有未完成订单有就推DRIVER_UNFINISHED_ORDERApp 收到后把订单界面恢复出来。用户端更细——如果重连时带着 orderCode且订单正在行驶中服务端会把里程、等待时长、费用等完整状态通过ORDER_MOVE推回去用户端直接接着断线前的界面渲染。这是踩过坑才明白的只恢复连接不恢复状态用户看到的就是一个回到首页的 App订单凭空消失投诉就是这么来的。四、KEEPALIVE心跳顺手干三件正事心跳包不只是保活我们的心跳分支顺手做了三件事caseKEEPALIVE://1. 心跳包超过350字节判定为非法直接断开if(length350){ctx.channel().writeAndFlush(newMsgVo(NettyCodeEnums.SOCKET_ERROR).toJsonStringbuf());channelInactive(ctx);UserChanelRel.delChannel(didStr);}//2. 没走过CONNECT的心跳请求移除Channelchannel1UserChanelRel.get(didStr);if(channel1null){ctx.writeAndFlush(newMsgVo(NettyCodeEnums.SOCKET_ERROR).toJsonStringbuf());channelInactive(ctx);}先做两道安全校验包太大350 字节的是恶意流量踢没经过 CONNECT 注册就发心跳的也是非法连接踢。然后分角色处理。司机端心跳里带着经纬度所以心跳 位置上报一举两得if(cal.isEquals(channelIdentity,StaticUtils.STATUS_YES)){//更新司机位置DDriverdriverdDriverService.getById(did);driver.setLonVal(requestVo.getLongitude());driver.setLatVal(requestVo.getLatitude());driver.setAddress(requestVo.getAddress());//异常状态的司机名下没单则恢复为可接单Integerstatedriver.getState();if(cal.isEquals(state,StaticUtils.STATUS_SUCCESS)||cal.isEquals(state,StaticUtils.STATUS_FAIL)){OrderMainorderMainorderMainService.selectOrderByDriver(did);if(orderMainnull){driver.setState(StaticUtils.STATUS_YES);}}dDriverService.updateById(driver);//回推最新司机状态让App刷新接单开关MsgVomsgVonewMsgVo(NettyCodeEnums.DRIVER_STATUS_RESET);msgVo.setDriverStatus(driver.getState());ctx.channel().writeAndFlush(msgVo.toJsonStringbuf());}用户端心跳则承担消息补偿——断线期间推不出去的消息暂存在 Redis心跳一上来就补发}else{StringkeyredisUtilsService.getKey(didStr);if(StringUtils.isNotEmpty(key)){MsgVomsgVoJSONObject.parseObjedt(key,MsgVo.class);ctx.channel().writeAndFlush(msgVo.toJsonStringbuf());redisUtilsService.delete(didStr);}}五、坦白几个不足这套东西能跑但放在更大规模下有几个明确的坑10 秒的 ALL_IDLE 太短。弱网环境下 TCP 重传都不止 10 秒容易误杀。注释里能看到最早的版本是 120 秒后来调小了这个值应该按客户端心跳间隔的 3 倍以上来配。UserChanelRel 是单机 HashMap。多实例部署时A 机器上的连接B 机器查不到派单推送会丢。正确做法是连接关系存 Redis推送走消息队列广播。channelInactive 里更新司机离线状态的代码被注释掉了。现在的逻辑是司机掉线后状态不立刻变等下次心跳或派单时才发现。这是个妥协立刻置离线会误伤闪断的司机不置又会给已掉线司机派单我们在派单侧做了二次校验来兜底。写在最后长连接这块的完整代码NettyServer、DataAcceptInitializer、DataAcceptHandler、UserChanelRel、协议枚举都在开源仓库里配上一篇的派单和计价主链路是闭环的。Gitee 地址https://gitee.com/zhoujian6666/biaoma-ride-car-service协议以仓库 LICENSE 为准。下一篇准备聊聊抢单池的并发处理有兴趣的可以先 Star。上一篇聊了代驾系统的整体架构和三级派单这篇往下钻一层司机端和乘客端挂在 Netty 长连接上手机进电梯、切后台、弱网闪断是家常便饭连接断了之后怎么发现、怎么重连、断线期间的消息怎么不丢这套机制才是派单链路真正能用起来的地基。全部代码来自我们开源的代驾系统仓库地址放在文末。## 一、协议设计两个枚举定生死长连接第一件事不是写代码是定协议。我们把客户端要干什么和服务端回了什么拆成两个枚举。客户端操作类型NettyHandleEnums- CONNECT(1)第一次连接或重连时的初始化- KEEPALIVE(2)心跳保活- COORDINATE_DRIVER(3)司机坐标上报服务端返回码NettyCodeEnums截取几个关键的javaKEEPALIVE(1,心跳正常),KEEPALIVE_ERROR(-1,心跳异常),CONNECT(2,第一次连接正常),CONNECT_ERROR(-2,第一次连接异常),DRIVER_UNFINISHED_ORDER(29,司机有未完成订单),DRIVER_STATUS_RESET(30,刷新司机状态),ORDER_MOVE(100,订单行驶中),SOCKET_TIME_OUT(101,读写超时),LOGIN_ERROR(997,账号登录失效)注意 DRIVER_UNFINISHED_ORDER 和 ORDER_MOVE 这两个码——它们存在的意义就是重连恢复后面会讲到。## 二、服务端 pipeline10 秒空闲就踢Netty 的 pipeline 配置在 DataAcceptInitializer 里javaChannelPipeline pipeline socketChannel.pipeline();pipeline.addLast(new HttpServerCodec());pipeline.addLast(new ChunkedWriteHandler());pipeline.addLast(new HttpObjectAggregator(1024*64));//增加心跳支持/** * 针对客户端如果在1分钟时间内没有向服务端发送读写心跳ALL则主动断开连接 */pipeline.addLast(new IdleStateHandler(0, 0, 10, TimeUnit.SECONDS));pipeline.addLast(dataAcceptHandler);pipeline.addLast(new WebSocketServerProtocolHandler(/ws));IdleStateHandler(0, 0, 10) 意思是读空闲不查、写空闲不查但读写加起来 10 秒没有任何动静就触发 ALL_IDLE 事件。客户端心跳间隔必须小于这个值实际 App 端是几秒一次。触发超时后的处理在 userEventTriggeredjavapublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof IdleStateEvent) { IdleStateEvent event (IdleStateEvent) evt; if (event.state() IdleState.ALL_IDLE) { log.info(客户端(ALL_IDLE 总超时 重新连接)); } } ctx.channel().writeAndFlush(new MsgVo(NettyCodeEnums.SOCKET_TIME_OUT).toJsonStringbuf()); channelInactive(ctx);//清除netty通道}先给客户端推一个 SOCKET_TIME_OUT(101)万一它还活着知道自己被踢了、立刻重连然后进 channelInactive 清理通道。

相关新闻

喝水检测数据集实战:995张VOC+YOLO双格式训练与避坑指南

喝水检测数据集实战:995张VOC+YOLO双格式训练与避坑指南

简介:这份喝水检测数据集面向计算机视觉学习者与目标检测开发者,用于训练和验证「喝水行为」相关模型,可应用于课堂行为分析、驾驶疲劳监测或健康习惯识别等场景。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg原图及一一对…

2026/10/11 23:18:18 阅读更多 →
松鼠目标检测数据集:528张实拍图+YOLO/VOC双格式标签

松鼠目标检测数据集:528张实拍图+YOLO/VOC双格式标签

简介:本资源是面向计算机视觉初学者与YOLO系列算法实践者的松鼠目标检测专用数据集,适用于YOLOv5/v7/v8/v9/v10/v11等主流版本的模型训练、验证与测试,可直接用于课程实验、课程设计或小型科研项目中的动物目标识别任务。压缩包共1585个文件&…

2026/10/11 23:18:18 阅读更多 →
YOLOv8安全帽与工作服双目标检测实战指南

YOLOv8安全帽与工作服双目标检测实战指南

简介:本资源是基于YOLOv8目标检测框架实现的安全帽与工作服双类别识别项目,面向计算机、电子信息、人工智能等专业学生及初学者,适用于课程设计、期末大作业与毕业设计参考。项目提供完整可运行的Python源码及配套模型权重,涵盖图…

2026/10/11 23:18:16 阅读更多 →

最新新闻

基于Solidworks的土豆去皮机三维设计流程与避坑指南

基于Solidworks的土豆去皮机三维设计流程与避坑指南

简介:基于Solidworks的土豆去皮机三维设计文档,面向机械设计及自动化专业学生、毕业设计或课程设计人员,以及餐饮设备研发人员。文档围绕中小型饭店、宾馆等餐饮场所的土豆预处理需求,完成了一款经济实用型去皮机的整机设计&#…

2026/10/12 0:06:02 阅读更多 →
上海大学答辩通用PPT模板:从母版版式到配色字体的参数化设计

上海大学答辩通用PPT模板:从母版版式到配色字体的参数化设计

简介:专为上海大学学生设计的通用答辩PPT模板,覆盖毕业答辩、学术汇报、开题答辩与周会汇报等场景。模板内置清晰的目录结构、标题页、多种内容版式与图表展示模块,标题页预留汇报人、指导老师、时间等基本信息位;章节标题可参考模…

2026/10/12 0:06:02 阅读更多 →
CAD-VBA二次开发实战:从ActiveX对象模型到批量自动化

CAD-VBA二次开发实战:从ActiveX对象模型到批量自动化

简介:《CAD-VBA开发人员手册》是面向AutoCAD二次开发初学者与进阶VBA开发者编写的实用技术指南,作者解祥成。全书共十章,从VBA入门、嵌入与全局工程管理、宏处理讲起,逐步深入ActiveX自动操作基础、AutoCAD对象模型与集合对象操作…

2026/10/12 0:06:01 阅读更多 →
绝缘子缺陷检测数据集清洗与工业级训练实战指南

绝缘子缺陷检测数据集清洗与工业级训练实战指南

简介:本资源是面向电力AI研发人员、工业视觉工程师及智能巡检系统开发者的绝缘子缺陷检测专用YOLO格式数据集,解决无人机航拍场景下绝缘子破损、污闪、积雪等9类典型缺陷的精准识别与定位难题。数据集共2139张真实巡检图像(含训练/验证/测试集…

2026/10/12 0:05:01 阅读更多 →
牙科影像龋齿四级像素级分割数据集与临床落地实践

牙科影像龋齿四级像素级分割数据集与临床落地实践

简介:本资源是一套面向医学影像AI研究者与口腔临床算法开发者的专业蛀牙分割数据集,专为U-Net、DeepLab等分割模型训练设计,解决真实场景下多类别蛀牙区域精细识别与程度量化评估难题。数据集含400张高精度口腔内窥镜及X光影像(对…

2026/10/12 0:05:01 阅读更多 →
条形码目标检测数据集实战:从YOLOv8训练到部署

条形码目标检测数据集实战:从YOLOv8训练到部署

简介:这是一份面向目标检测与计算机视觉学习者的条形码识别数据集,涵盖零售、物流、制造等场景下的真实商品条码图像,适合用于训练YOLO系列模型或开展算法实验。数据集共684张图片,按训练集624张、验证集60张划分,采用…

2026/10/12 0:04:01 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →