
简介本资源是一套基于Java-WebSocket框架实现的Android端高可用即时通讯解决方案面向中高级Android开发者解决移动端长连接不稳定、后台存活难、消息实时性差等生产级痛点。项目完整实现了WebSocket长连接建立、双向消息收发、Service与Activity通信机制、锁屏状态下的消息通知、心跳保活与智能重连策略以及Service多层级保活方案聊天界面功能完备已在真实生产环境稳定运行。压缩包共1591个文件约12.82MB包含37个核心Java源码文件、204个编译后class、206个dex、124个配置与数据json、82个布局xml及配套资源文件结构清晰模块职责分明。目前已有3029人学习下载提供可直接集成的工程结构、关键逻辑注释详尽、后台存活与通知适配覆盖主流厂商ROM是学习Android网络通信与后台服务优化的优质实践范例。 做Android端的即时通讯很多人第一反应就是上WebSocket。这个方向本身没错但我见过太多项目卡在“能连上”这个阶段就以为完事了直到线上出现大量断线、消息丢失、连接假死才回头补课。我早期做一个客服系统时也走过同样的弯路Demo跑得飞起一上真机就各种诡异问题——Wi-Fi切流量连接断掉、App进后台再回来发不出消息、服务端说没收到心跳、客户端却显示在线。这篇文章我把从协议原理到工程落地的完整思路整理出来重点放在心跳、重连、消息补偿和线上排错上希望能帮你少踩几个坑。1. 为什么移动端做实时通讯绕不开WebSocket这一层1.1 HTTP轮询的问题不只是费电先看一个最简单的问题客户端要让服务端主动发消息过来HTTP做得到吗做不到HTTP是严格的请求-响应模型服务端不能主动往客户端推数据。最常见的妥协方案是轮询客户端每隔几秒发一次请求问服务端“有没有新消息”。假设3秒轮询一次24小时就是28800次请求。哪怕服务端每次都返回空数据光是HTTP头部开销、DNS解析、TCP握手HTTP/1.1这些固定成本就够受的。更重要的是轮询的实时性取决于轮询频率频率高了服务端和电量扛不住频率低了消息延迟大得没法用。这就像你反复打电话问快递员“到了吗”而不是让快递员到了直接通知你。1.2 WebSocket的握手和帧决定了它天生适合长连接WebSocket走的还是TCP但它通过一次HTTP Upgrade握手把连接升级成一条全双工通道。握手阶段客户端发一个带Upgrade: websocket头的HTTP请求服务端返回101 Switching Protocols之后双方就可以随时互相发数据。握手关键部分长这样GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务端返回HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo一旦握手成功这条连接就是持久的不需要每次通信都重新建连。数据以帧的形式传输帧结构里有几个点做客户端的人必须知道opcode区分数据类型0x1是文本帧、0x2是二进制帧、0x8是关闭帧、0x9是Ping、0xA是Pong。客户端发给服务端的帧必须做掩码masking服务端发给客户端的不需要。这是协议层面的强制要求库都帮你处理了但排查问题时明白了这一点就不容易被绕晕。关闭连接有正常的握手流程一方发Close帧另一方回Close帧然后TCP才断开。如果连接在没有完成这个握手的情况下直接断掉就会出现后面要重点讲的1006状态码。1.3 移动端的网络环境决定了不能照搬浏览器思路浏览器里跑WebSocket相对简单页面关了就断网络变了浏览器自己处理页面刷新重新连。但Android端是完全不同的场景App进后台可能被系统杀死、Wi-Fi和蜂窝网络切换时TCP连接会静默断开、运营商对空闲连接有NAT映射老化策略、弱网环境下TCP半开连接迟迟探测不出来。这些因素叠加在一起结论很明确移动端WebSocket的核心工作不是“连接”而是“连接的管理”——心跳维持、断线重连、网络切换感知、消息补偿。这部分才是真正体现工程水平的地方。2. 选型与通讯层架构从跑通Demo到能上线的距离2.1 OkHttp、Java-WebSocket、Netty怎么选Android上实现WebSocket主流方案就三种OkHttp自带的WebSocket支持、Java-WebSocket库、Netty。我列个表直观对比一下方案优点缺点适用场景OkHttp WebSocket和项目里的HTTP网络层统一依赖已存在API回调简单功能相对基础高级控制力有限绝大多数Android项目我优先推荐Java-WebSocket纯Java实现轻量依赖小和HTTP客户端无关生命周期管理要自己做线程模型需要关注项目没有OkHttp或者要做独立SDKNetty性能天花板协议控制力最强适合自定义协议太重引入成本高Android上需要处理大量线程模型细节需要自研协议或极端性能场景不建议普通业务直接用我在实际项目里推荐OkHttp原因很朴素大部分Android工程本来就依赖OkHttpWebSocket复用同一套网络栈、TLS配置、拦截器体系不需要为即时通讯单独引入一套网络库。Java-WebSocket也有它的场景比如你在做一个嵌入式设备端或纯Java SDK它对HTTP部分零依赖部署更轻。但要注意Java-WebSocket底层是自管理的Socket线程App层要处理好生命周期否则容易出现线程泄漏。自研一个WebSocket客户端我没这么干过也劝你别干。帧解析、分片消息、掩码处理、握手校验、关闭握手的边界情况非常多一个细节处理不当就是线上事故。现成库足够成熟把精力花在连接管理上才是正事。2.2 通讯层不要和业务层搅在一起一个常见的错误是在Activity或Fragment里直接new WebSocket收到消息就地处理。短期能跑一旦多个页面都要收发消息就乱套了。我的做法是单独抽一个通讯模块核心由三个部分组成WsManager单例统一管理连接状态和生命周期业务层只和它交互。ConnectionState连接状态枚举至少包括DISCONNECTED、CONNECTING、CONNECTED、RECONNECTING。MessageCenter负责消息的序列化、分发、ACK匹配。状态流转大致是这样初始DISCONNECTED调用connect()进入CONNECTINGonOpen成功后进入CONNECTED异常断开进入RECONNECTING重连失败继续重试直到放弃或用户主动关闭回到DISCONNECTED。为什么强调状态因为回调是异步的业务层某次点击“发送”时连接可能正处于重连中。如果没有一个统一的状态管理就会出现“明明connect()了但是发消息时连接还没建立”的竞态问题。2.3 裸WebSocket还是上STOMPWebSocket只是传输通道它不关心你业务消息长什么样。你可以直接在上面传自定义JSON也可以选择STOMP这样的消息协议。STOMP本质是一个基于文本的简单协议定义了CONNECT、SUBSCRIBE、SEND等命令帧服务端可以按destination做消息路由和订阅管理。我的判断标准很简单如果你只是做一个客服会话、单频道推送、双人聊天直接用自定义JSON就够了别把STOMP引进来增加复杂度如果你的消息要路由到多个主题、有订阅/发布模型、多个频道动态增删那STOMP能帮你省掉一堆协议设计工作。举个具体场景做一个股票行情App用户订阅的股票列表是动态变化的每个股票代码对应一个实时行情流。用自定义JSON你需要自己设计订阅/取消订阅的消息格式还要处理服务端推送时“这条行情属于哪个股票”的路由逻辑。用STOMP的话每个股票代码就是一个destinationSUBSCRIBE命令天然支持这种模型服务端也容易做权限控制。反过来如果是客服系统一条会话消息直接发给服务端服务端转发给对应的客服坐席一个消息通道从头走到尾就没必要再套一层STOMP。3. 连接管理三件套心跳、重连与消息补偿3.1 心跳机制TCP keepalive为什么救不了你先说结论不要依赖TCP keepalive来维持移动端长连接。系统默认的TCP keepalive探测间隔以小时计而且它只能探测到TCP层通不通探测不了应用层是否还正常。很多时候TCP连接看着还在半开连接实际上服务端已经把这个连接判定为超时清掉了。移动端要维护一条WebSocket连接存活必须靠应用层心跳两种常见做法用WebSocket协议自带的Ping/Pong帧客户端定时发Ping服务端回Pong开销极小。自定义一条业务心跳消息比如发送{type:ping}服务端回{type:pong}同时可以携带时间戳做链路延迟统计。我推荐优先用Ping/Pong帧因为它是协议层面的机制很多服务端框架会自动响应Pong省去业务层处理。但如果你们服务端要基于心跳做更细的链路检测比如统计客户端到服务端的网络延迟、定位是哪一段链路出问题就用自定义业务心跳。心跳间隔怎么定这是一个需要前后端对齐的参数。我在线上用的基线是客户端30秒发一次服务端5分钟内没收到心跳就判定离线。为什么客户端间隔是30秒而不是10秒或60秒因为移动网络的NAT映射老化时间大多在30秒到120秒这个区间太慢了连接容易被中间设备回收太快了白白消耗电量和流量。服务端的5分钟超时阈值要比客户端间隔大一个数量级因为要容忍偶尔一次心跳丢包以及网络抖动导致的延迟。关键的一点客户端也要检测Pong。如果你发了多个Ping都不见Pong回来就不能再傻等下去了应当主动关闭当前连接并触发重连。这里有个经验值连续2次没有收到Pong就判定连接不可用。阈值不宜设得太小否则弱网环境容易误判——弱网下Pong延迟可能到几十秒你5秒没收到就断开会导致频繁重连反而更耗电、更容易雪崩。3.2 断线重连二进制退避加上随机抖动重连策略最怕两件事一是断线后立刻重连服务端还没来得及释放旧连接资源瞬间又涌入大量连接请求二是大量客户端同时断线同时重连把服务端打崩。行业里成熟的方案是二进制退避重连间隔随失败次数指数增长并加上随机抖动。核心思路用代码表达是这样private fun scheduleReconnect() { // 指数退避但封顶 val baseDelay minOf( maxReconnectDelay, // 比如30秒 initialDelay * (1L shl reconnectCount) // 初始1秒2秒4秒…… ) // 随机抖动 0~3000ms val jitter Random.nextLong(0, 3000) handler.postDelayed({ tryConnect() }, baseDelay jitter) }随机抖动不是玄学它的价值在“防惊群”。试想一个故障导致服务端重启所有客户端几乎同时掉线如果不加抖动它们也会几乎同时重新发起连接服务端刚起来就被打趴。加了0到3秒的随机偏移就能把重连请求在时间轴上摊开。还有两个必须处理的重连触发点网络切换Wi-Fi切4G/5G或者反过来这时候旧连接几乎必然失效应该主动close并立刻重连不要等心跳超时后被动发现。App从后台切回前台系统可能已经冻结了App的网络活动回到前台时先检查连接状态如果发现不是CONNECTED立即触发重连。重连过程中还要防止重入如果当前状态已经处于CONNECTING或RECONNECTING就不能再发起一次connect()否则WebSocket底层会抛出IllegalStateException。这就是前面强调状态管理的原因。3.3 消息可靠性与ACKWebSocket本身不保证消息必达把WebSocket当作永不丢失的通道是很多即时通讯事故的根源。WebSocket只能保证“连接建立后数据帧按序传输”但它不保证你的消息被业务层正确处理。连接断了、服务端内部异常、消息到达了但处理失败这些情况WebSocket协议一概不管。正确的做法是在业务层设计一套确认机制发送消息时先落入本地pending队列给消息分配一个唯一msgId。数据帧发出去之后等待服务端返回业务ACKACK里带上这条消息的msgId。收到ACK才把消息从pending队列移除。超时未收到ACK按策略重发或标记发送失败。这套逻辑和TCP的ACK确认是类似的思想只是上移到业务层。你不在业务层做线上丢消息就是早晚的事。除了单条消息的ACK还要处理断线期间的离线消息。服务端通常会在客户端重连成功后根据客户端上报的最后已读消息idlastReadMsgId把中间漏掉的消息补推过来。客户端在登录成功后也要主动拉取一次离线消息而不是傻等服务端推。补拉和补推双管齐下消息丢失的概率才能降到可接受范围。4. 排查实战1006与那些“连不上”的诡异问题4.1 1006状态码到底在说什么从WebSocket返回的close code里最让人头疼的就是1006。用OkHttp的连接回调你会看到类似这样的日志onFailure: WebSocket closed with status 1006 (no reason)要理解1006得先知道它不是什么。1006不是一个由关闭帧携带的正常关闭码它表示的是连接在没有完成正常关闭握手的情况下直接断开了。也就是说TCP连接在双方没有协商的情况下被切断——可能是客户端这边进程被杀、网络被切断也可能是服务端侧主动重置了连接但客户端没有收到正常的Close帧。很多人上来就觉得是服务端的问题其实链路里的每一层都可能是元凶。我之前排查过一个线上案例服务端部署在Nginx后面配置了推荐的反向代理但没注意proxy_read_timeout参数默认60秒。而我们应用层心跳间隔是45秒也就是说如果45秒内没有业务消息Nginx会在60秒时把这60秒内没有任何数据转发的连接切掉。客户端在60秒时收到1006但并不知道是代理层干的。这就是热词里“增加websocket nginx代理配置”的典型场景——Nginx反代WebSocket时proxy_read_timeout必须大于客户端心跳间隔否则中间层会替你先断掉连接。完整的排查链路应该是这样的客户端日志收集close code和触发场景。是进后台被杀了还是网络切换了还是没有任何操作就断了这些线索能帮你快速缩小范围。翻服务端日志看对应的连接是否还在服务端有没有主动关闭连接的动作。在服务端抓包tcpdump或Wireshark看断开瞬间的TCP包是收到FIN正常关闭还是收到RST强制重置或是包根本没到服务端。把客户端心跳时间线和抓包结果对齐看TCP断开发生在最后一次心跳前后哪个位置。这四步走下来绝大部分1006都能定位到具体环节。怕的是不排查直接把所有1006都归结为“网络问题”。4.2 半开连接为什么显示在线却发不出消息另一种常见假象是客户端状态还是CONNECTED但消息发不出去服务端也收不到。这就是典型的半开连接——TCP连接已经不在了但两端都没发现。半开连接的成因很典型Wi-Fi切到4G/5G旧网络断开了系统不会告诉你“你那条TCP连接已经死了”。NAT映射被运营商回收后中间设备直接丢弃后续包客户端发的数据全部石沉大海。处理半开连接有两个层面网络切换时主动处理。注册ConnectivityManager.NetworkCallback监听网络可用和丢失事件一旦发生切换立即主动close旧连接并重连不要等心跳超时。val callback object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { wsManager.onNetworkChanged() } override fun onLost(network: Network) { wsManager.onNetworkChanged() } }应用层心跳兜底。就算网络切换事件没触发只要心跳Ping/Pong机制正常工作最多30秒就能发现问题主动断开重连。这也是为什么心跳间隔不能太长——你总不希望消息发不出去5分钟之后才发现。4.3 弱网环境下的三个隐藏坑弱网是最难排查的因为表象非常迷惑人。第一个坑Ping/Pong超时阈值设太短导致频繁误判。在弱网下一个Pong延迟到几十秒是正常的如果你2秒没收到Pong就判定网络挂掉客户端就会陷入“重连-刚连上又超时-再重连”的死循环。我建议超时阈值至少是心跳间隔的2倍再加一点余量比如心跳30秒、超时判定70到90秒。第二个坑Socket写入成功不等于对方收到。OkHttp的WebSocket回调里send()只是把消息写入底层Socket缓冲区真正发送成功要靠回调确认。但在弱网下就算回调返回成功数据也可能卡在某一层没真正到达服务端。所以业务ACK必须存在不能只看发送端的回调。第三个坑重连期间的状态一致性。弱网下重连可能失败很多次这时候UI上要明确提示“连接中”或“重连中”避免用户以为消息发出去了。消息要落在pending队列里等重连成功后补发。如果重连长时间失败还要给出手动重试和切换到备用通道的入口。5. 上线前必须与后端核对的三件事5.1 心跳期限与多端策略对齐客户端心跳间隔、服务端超时判定、Nginx代理超时时间这三者必须形成一个协调的时间链。我见过太多项目客户端配30秒心跳服务端却按15秒判定超时结果正常网络下也频繁掉线。反过来服务端超时设太长又会出现“死人占着茅坑”的问题连接资源被无效占用。下面给一个可用的参考配置组合角色参数建议值客户端心跳发送间隔30秒客户端Pong超时判定连续2次未收到Pong约60-90秒服务端心跳超时判定180秒3倍客户端间隔Nginxproxy_read_timeout大于心跳间隔建议75秒以上如果你是做IM类产品的还要和后端确认多端登录策略。用户在一台手机上登录在另一台设备上登录旧设备要不要被踢下线如果是服务端会主动下发一个关闭指令或者直接断掉旧连接客户端要能识别这种情况并提示用户“账号在其他设备登录”。5.2 埋点与可观测性没有监控的WebSocket就是裸奔线上问题最怕偶现偶现问题最怕没有日志。WebSocket的连接状态变化一定要埋点否则出了问题你连从哪看起都不知道。最小埋点集合我建议至少包含连接状态变化CONNECTING、CONNECTED、CLOSED等每次切换都上报。重连次数一次会话内的重连次数重连间隔重连是否成功。异常断开close code、发生时的网络类型Wi-Fi/蜂窝、是否在后台。心跳延迟Ping发出到收到Pong的耗时这个指标能提前暴露网络质量问题。消息收发量单位时间发送、接收的消息数以及重发次数。客户端日志建议打全但上报可以做采样或聚合避免给服务端带来压力。至少要做到线上能按用户维度查最近一次连接断开的原因排查效率会高一大截。5.3 备用通道万一WebSocket挂了还有兜底方案WebSocket再稳定也必须承认一个现实有时网络就是不允许一条长连接存活。飞行模式、地铁隧道、运营商故障这些场景下你没有任何办法维护一条实时通道。所以一个合格的即时通讯方案必须在WebSocket之外至少留一条备用通道。最典型的是厂商推送通道如果只做国内市场或APNs/FCM有海外需求时。WebSocket断线期间关键消息通过推送通道下发给用户App点击推送后再拉起WebSocket补拉完整内容。有一条重要的设计原则关键业务的结果确认不能只依赖WebSocket。比如支付结果、订单状态变更这类消息就算WebSocket没有收到也必须有另外的途径让用户看到最终状态否则用户的信任感就毁了。6. 一点个人体会长连接是一种需要持续照顾的资源和WebSocket打了这么多年交道我最大的体会是建立连接很容易维护连接才是真正的技术活。很多团队把即时通讯的复杂度低估了以为引入一个WebSocket库、调通一条消息就叫完成。实际上连接是有生命周期的对象它会被系统杀死、会被网络切换打断、会被中间设备静默回收、会在弱网下假死。你要做的不是祈祷它不断而是围绕“一定会断”去设计心跳维持、断线重连、消息补偿、降级通道每一环都不可少。做方案时也建议想清楚不是所有业务都需要永久长连接。有些场景退一步用轮询或用推送通道反而更简单更稳。判断标准只有一个——业务对消息延迟的容忍度是多少。如果容忍分钟级用推送够了如果容忍秒级WebSocket是合理选择如果要毫秒级那还要深挖链路瓶颈。搞清楚这个前提再决定投入多少精力在长连接维护上才是务实的工程判断。本文还有配套的精品资源点击获取