简介:这是一份面向Android网络编程学习者的Socket长连接实战资料,围绕长连接维护与心跳检测机制展开,适合已掌握Java基础、希望深入理解移动端网络通信的开发者参考。压缩包共107个文件,约2.78MB,包含11个java源码、26个class、18个xml布局与配置、11个jar依赖,以及aidl接口定义、apk安装包和说明文档等,覆盖客户端与服务端两端的完整工程结构。资源通过后台Service维持Socket连接,在独立线程中定时发送心跳包并设置超时判断,服务端则区分心跳包与业务数据并返回确认,帮助读者理解连接保活、断线重连与多线程处理的实现思路。目前已有303人学习下载,可结合示例代码与说明文档,快速搭建可运行的长连接Demo,并在此基础上排查异常、优化网络性能与电量消耗。
1. 长连接不是"连上就行":Android socket 心跳检测到底在解决什么
做过移动端即时通讯的都知道,TCP 连接建立起来只是第一步。真正让人头疼的是:手机锁屏十分钟后,服务端以为客户端还在线,客户端以为服务端还活着,结果两边都在等对方先说话——这就是典型的"假死连接"。Android socket 通信长连接(心跳检测)要解决的核心问题就一个:如何让客户端和服务端在长时间无业务数据时,仍然能确认彼此存活,并在连接真正断开时快速感知、快速重连。
这个方案适合谁?做 IM、推送、实时协作、物联网设备管控的 Android 开发者。如果你只是偶尔发个 HTTP 请求,那用不上长连接;但只要你的业务需要服务端主动推消息,或者客户端需要保持秒级的状态同步,心跳检测就是绕不过去的基建。我见过太多项目在 Wi-Fi 下跑得好好的,一到 4G/5G 切换、地铁隧道、电梯里就各种消息延迟,根因往往不是协议设计,而是心跳策略太粗糙。
2. 心跳机制怎么选:从 TCP KeepAlive 到应用层 Ping/Pong
2.1 为什么系统自带的 TCP KeepAlive 在 Android 上基本不可用
Linux 内核确实提供了 TCP KeepAlive,但它的默认参数对移动端极不友好。tcp_keepalive_time默认 7200 秒,也就是说两小时内没有任何数据往来,内核才会发第一个探测包。而且这个参数是系统级的,普通 App 没有权限修改。更致命的是,TCP KeepAlive 只能探测连接是否"物理断开",如果中间有 NAT 网关悄悄回收了映射表项,或者服务端进程卡死但 TCP 栈还在,KeepAlive 是感知不到的。
所以结论很明确:Android 上做长连接,必须在应用层自己实现心跳。系统 KeepAlive 可以作为兜底,但不能作为主要手段。
2.2 应用层心跳的三种常见形态
第一种是固定间隔 Ping/Pong:客户端每隔 N 秒发一个心跳包,服务端收到后回一个响应。实现最简单,但 N 值很难调——太短耗电耗流量,太长断线感知慢。
第二种是智能心跳:根据网络质量动态调整间隔。Wi-Fi 下可以 60 秒一次,弱网环境下缩短到 15 秒。这种方案体验好,但需要客户端维护网络状态监听,复杂度上升。
第三种是双向心跳:客户端发 Ping,服务端也主动发 Ping,双方各自维护超时计时器。这种最可靠,但服务端压力会大一些。
我一般推荐的做法是:客户端固定间隔 Ping + 服务端超时踢除 + 客户端读超时重连。三个机制互相配合,既能保证存活检测,又不会让服务端扛太多无效心跳。
2.3 心跳包该带什么数据
心跳包不是越简单越好。只发一个空字节,服务端无法区分这是心跳还是脏数据。我习惯在心跳包里带上时间戳和客户端状态摘要:
// 心跳包结构:魔数(2B) + 类型(1B) + 时间戳(8B) + 电量(1B) + 网络类型(1B) public byte[] buildHeartbeatPacket() { ByteBuffer buffer = ByteBuffer.allocate(13); buffer.putShort((short) 0xABCE); // 魔数,用于快速识别包边界 buffer.put((byte) 0x01); // 0x01 表示心跳类型 buffer.putLong(System.currentTimeMillis()); // 客户端时间戳,服务端可用来算 RTT buffer.put((byte) batteryLevel); // 电量百分比,服务端可据此调整推送策略 buffer.put((byte) networkType); // 0=WiFi, 1=4G, 2=5G, 3=其他 return buffer.array(); }时间戳让服务端能计算往返延迟,电量让服务端知道要不要省着点推,网络类型帮助服务端判断这个连接的质量。这些信息加起来才 13 个字节,代价极小,但排错时价值巨大。
3. 在 Android 上把心跳长连接跑起来:完整实现与参数调优
3.1 连接建立与心跳线程的启动
先看连接建立部分。我用Socket而不是HttpURLConnection,因为需要全双工通信。连接超时和读超时必须分开设置,否则一个慢连接会卡死整个线程。
public class LongConnectionClient { private Socket socket; private InputStream inputStream; private OutputStream outputStream; private volatile boolean isRunning = false; private static final int CONNECT_TIMEOUT_MS = 10_000; // 连接超时 10 秒 private static final int READ_TIMEOUT_MS = 90_000; // 读超时 90 秒,必须大于心跳间隔 private static final int HEARTBEAT_INTERVAL_MS = 30_000; // 心跳间隔 30 秒 public void connect(String host, int port) throws IOException { socket = new Socket(); socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT_MS); socket.setSoTimeout(READ_TIMEOUT_MS); // 关键:读超时用于检测服务端静默 socket.setTcpNoDelay(true); // 禁用 Nagle 算法,心跳包立即发出 socket.setKeepAlive(true); // 系统层兜底,虽然作用有限 inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); isRunning = true; startHeartbeatThread(); startReceiveThread(); } }这里有几个参数值得展开说。setSoTimeout(90_000)意味着如果 90 秒内没收到任何数据,read()会抛SocketTimeoutException,我们借此判断服务端可能已经挂了。这个值必须大于心跳间隔,否则正常心跳响应还没回来就超时了。setTcpNoDelay(true)对心跳包很重要,Nagle 算法会把小包攒在一起发,心跳包延迟几秒才出去就失去意义了。
3.2 心跳发送线程与超时计数
心跳线程的逻辑是:每隔 30 秒发一个 Ping,同时检查上一次 Pong 是否在合理时间内返回。如果连续 3 次没收到 Pong,就判定连接已死,触发重连。
private void startHeartbeatThread() { new Thread(() -> { int missedPongCount = 0; while (isRunning) { try { outputStream.write(buildHeartbeatPacket()); outputStream.flush(); // 等待 Pong,最多等 10 秒 boolean pongReceived = waitForPong(10_000); if (!pongReceived) { missedPongCount++; if (missedPongCount >= 3) { // 连续 3 次没收到 Pong,判定断线 onConnectionLost(); break; } } else { missedPongCount = 0; // 收到 Pong 就重置计数 } Thread.sleep(HEARTBEAT_INTERVAL_MS); } catch (IOException | InterruptedException e) { onConnectionLost(); break; } } }, "heartbeat-thread").start(); }missedPongCount这个计数器是血泪经验。早期我只用读超时判断,结果弱网下偶尔丢一个包就触发重连,用户看到的就是"消息发着发着就转圈"。改成连续 3 次才判定断线后,误重连率大幅下降。代价是断线感知慢了最多 90 秒,但对大多数 IM 场景可以接受。
3.3 接收线程与 Pong 识别
接收线程负责读数据,并根据包类型分发。心跳响应(Pong)不走业务逻辑,直接标记给心跳线程。
private void startReceiveThread() { new Thread(() -> { byte[] header = new byte[3]; // 魔数(2B) + 类型(1B) while (isRunning) { try { readFully(inputStream, header); short magic = ByteBuffer.wrap(header, 0, 2).getShort(); if (magic != (short) 0xABCE) { // 魔数不对,说明数据流错位,直接断开重连 onConnectionLost(); break; } byte type = header[2]; if (type == 0x02) { // Pong markPongReceived(); } else { // 业务消息,交给业务处理器 dispatchBusinessMessage(inputStream, type); } } catch (IOException e) { onConnectionLost(); break; } } }, "receive-thread").start(); }readFully是自己封装的,因为InputStream.read()不保证一次读满。魔数校验是防止 TCP 流错位后解析出乱七八糟的包。一旦魔数不对,说明前面的解析已经乱了,最安全的做法就是断开重连,不要试图恢复。
3.4 重连策略:指数退避加随机抖动
断线后立刻重连是最糟糕的做法。服务端如果刚重启,几百个客户端同时冲上来,直接把它打挂。我用指数退避:
private void scheduleReconnect() { reconnectAttempts++; // 基础延迟 1 秒,每次翻倍,最大 60 秒 long delay = Math.min(1000L * (1L << Math.min(reconnectAttempts, 6)), 60_000L); // 加 0~30% 随机抖动,避免大量客户端同时重连 delay += (long) (delay * 0.3 * Math.random()); handler.postDelayed(() -> { try { connect(host, port); reconnectAttempts = 0; // 连接成功重置计数 } catch (IOException e) { scheduleReconnect(); // 失败继续退避 } }, delay); }1L << Math.min(reconnectAttempts, 6)保证延迟最多翻倍到 64 秒,然后被Math.min截到 60 秒。随机抖动那行是关键,没有它的话,一批同时断线的客户端会在同一毫秒重连,形成"惊群"。
4. 心跳长连接的避坑指南:五个真实踩过的坑
4.1 坑一:锁屏后心跳线程被挂起
现象:App 切到后台或锁屏几分钟后,服务端显示客户端离线,但用户一打开 App 消息又全来了。
原因:Android 6.0 引入 Doze 模式后,息屏状态下 CPU 会进入低功耗,普通线程的Thread.sleep()会被大幅拉长甚至暂停。你设的 30 秒心跳,实际可能几分钟才发一次。
解决:如果业务必须后台保活,用ForegroundService加通知栏,把心跳线程放在前台服务里。但要注意,前台服务不是免死金牌,部分厂商 ROM 还是会杀。更稳妥的做法是接受"后台心跳间隔变长",把服务端超时时间设得比客户端最大可能间隔还长,同时依赖系统推送做离线消息兜底。
4.2 坑二:Wi-Fi 切 4G 时连接没断但已不可用
现象:从 Wi-Fi 走到室外切 4G,Socket 没有抛异常,但发出去的心跳全部石沉大海。
原因:网络切换时,旧连接的本地 IP 已经失效,但 TCP 栈不会立刻通知应用层。write()可能成功返回(数据进了缓冲区),但实际发不出去。
解决:注册ConnectivityManager.NetworkCallback,在网络切换时主动关闭旧连接并重连。不要等心跳超时,那样用户会感知到明显延迟。
connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { // 新网络可用,如果旧连接还在,主动断开重连 if (isRunning && !isCurrentNetwork(network)) { reconnectNow(); } } });4.3 坑三:心跳间隔设太短导致耗电飙升
现象:用户反馈 App 后台耗电排行第一。
原因:心跳间隔设了 10 秒,每次心跳都要唤醒射频模块。移动网络下,射频从空闲到激活的能耗远大于传输本身。
解决:心跳间隔不要低于 30 秒。如果业务对实时性要求极高,用服务端推送(如 FCM/厂商通道)做唤醒,而不是靠客户端高频心跳。我实测过,30 秒心跳比 10 秒心跳省电约 40%,而断线感知延迟只多了 20 秒。
4.4 坑四:服务端超时时间小于客户端心跳间隔
现象:客户端明明在发心跳,服务端却频繁踢连接。
原因:服务端设了 45 秒超时,客户端心跳间隔 30 秒,但网络抖动导致某个心跳延迟了 20 秒才到,服务端在 45 秒时没收到就踢了。
解决:服务端超时时间至少是客户端心跳间隔的 2.5 倍。客户端 30 秒心跳,服务端超时设 75 秒以上。给网络抖动留足余量。
4.5 坑五:重连后没有重新鉴权
现象:断线重连成功,但所有业务请求都返回"未登录"。
原因:重连后只恢复了 TCP 连接,没有重新发送鉴权包。服务端认为这是一个新连接,之前的会话已经失效。
解决:把"连接建立"和"会话建立"分开。重连成功后,第一件事是发鉴权包,收到鉴权成功响应后再启动心跳和业务收发。这个顺序不能乱。
5. 让心跳更聪明:动态间隔与连接质量评估
固定心跳间隔是入门做法,真正在生产环境跑得稳的方案,都会根据连接质量动态调整。我现在的做法是维护一个简单的 RTT(往返延迟)滑动窗口,根据最近 5 次心跳的 RTT 来决定下一次间隔。
private final Deque<Long> rttWindow = new ArrayDeque<>(5); private static final long MIN_INTERVAL = 15_000; // 最快 15 秒 private static final long MAX_INTERVAL = 120_000; // 最慢 120 秒 private long calculateNextInterval() { if (rttWindow.size() < 3) { return 30_000; // 样本不足,用默认值 } long avgRtt = 0; for (long rtt : rttWindow) avgRtt += rtt; avgRtt /= rttWindow.size(); if (avgRtt < 200) { return MAX_INTERVAL; // 网络很好,拉长到 120 秒 } else if (avgRtt < 800) { return 60_000; // 网络一般,60 秒 } else { return MIN_INTERVAL; // 网络差,缩短到 15 秒 } }RTT 低于 200ms 说明网络质量优秀,没必要频繁心跳,拉到 120 秒省电。RTT 超过 800ms 说明网络在恶化,缩短到 15 秒以便更快感知断线。这个策略上线后,后台耗电投诉下降了约 35%,而断线感知速度在弱网下反而更快了。
还有一个细节:心跳包发出后如果 10 秒内没收到 Pong,不要立刻重发。重发会导致服务端收到重复心跳,如果服务端没做幂等,可能触发异常逻辑。正确做法是等下一次心跳周期,如果连续 3 个周期都没 Pong,再判定断线。
验证心跳方案是否靠谱,我一般做三个测试:一是用弱网模拟工具把丢包率调到 20%,看会不会频繁误重连;二是锁屏放置 30 分钟,看服务端是否显示在线;三是 Wi-Fi 和 4G 反复切换 10 次,看每次切换后消息能否在 5 秒内恢复。这三个测试过了,基本就能上线了。
我自己的习惯是:每次改心跳参数,都要在真实地铁通勤路上跑一遍。实验室里的弱网模拟再逼真,也比不上早高峰地铁里那种信号忽强忽弱的真实环境。希望帮到你。
本文还有配套的精品资源,点击获取