
简介StompProtocolAndroid 是一套面向 Android 开发者的开源库用于在 WebSocket 连接上实现 STOMP 实时消息通信。它封装了客户端连接、心跳与消息收发等逻辑可直接接入 Node.js、Spring Boot 等支持 STOMP 的后端服务适合聊天、通知、行情推送等实时场景。资源包共包含 65 个文件以 Java 源码、XML 配置、Gradle/Groovy 构建脚本等为主压缩包整体约 131KB体量不大但目录结构完整。已有 924 人学习/下载。通过该资源可获得完整客户端源码、example-client 示例及基于 Spring Boot 的 WebSocket 后端配置同时附带 Gradle 构建与 ProGuard 规则能够帮助开发者快速理解 STOMP over WebSocket 的接入流程并灵活集成到实际项目中。 做Android端的实时通信很多人第一反应是WebSocket再直接点就是用OkHttp的WebSocketListener自己维护连接。但如果服务端用的是ActiveMQ、RabbitMQ这类消息中间件前端页面也走STOMP协议那Android端还在自己拆帧、解析消息体、维护订阅关系就完全是重复造轮子。STOMPSimple Text Oriented Messaging Protocol是一种跑在WebSocket之上的文本消息协议命令语义清晰客户端按CONNECT、SUBSCRIBE、SEND、DISCONNECT这几个动作就能跟Broker顺畅沟通。这篇文章把我实际项目里接入StompProtocolAndroid的过程拆开讲连接、订阅、收发消息、心跳重连、TLS、内存泄漏这些环节逐个说过给准备在Android里接STOMP的同学一份能直接参考的实战笔记。1. 为什么Android端要接STOMP而不是裸写WebSocket1.1 STOMP带来的是什么先说结论STOMP不是用来替代WebSocket的它是跑在WebSocket之上的一套消息语义规范。WebSocket只保证你有一条全双工通道但数据发出去之后服务端怎么区分这是一条订阅请求还是一条业务消息消息该发到哪个主题服务端要如何应答这些问题WebSocket本身都不管。STOMP的办法是定义了一组固定命令比如CONNECT、CONNECTED、SUBSCRIBE、SEND、ACK、DISCONNECT消息体统一用文本帧由headers和body组成一眼就能看懂。举个直观例子一次订阅动作发到服务端是这样一个帧SUBSCRIBE destination:/topic/notifications id:sub-1 ack:auto如果你自己用WebSocket做这些语义全都得自己定义前后端靠默契维持。今天约定type1是订阅明天服务端说要加个reqId客户端没跟上就乱套。而STOMP把这一切标准化了服务端、前端、Android端只要各自按协议实现就能无缝对接不需要三端各写一套私有报文。这个价值在团队协作场景下比代码本身重要得多。1.2 三套方案怎么选STOMP、WebSocket、MQTT我在项目选型时一般把常见实时通信方案拉出来做对比方案协议层级特点适合场景裸WebSocket传输层自由但无语义需要自定报文两端都是自己掌控交互简单STOMP消息层文本协议、命令语义清晰、支持订阅/事务/应答服务端有ActiveMQ/RabbitMQ或使用Spring WebSocketMQTT消息层轻量、QoS分级、二进制、IoT生态好弱网、低功耗、设备间通信这里的核心判断点是服务端用没用消息中间件。如果后端已经是ActiveMQ、RabbitMQ或者在Spring框架里通过MessageMapping做消息推送那STOMP几乎是不用纠结的答案。它跟这些中间件天然契合客户端一句topic(/topic/xxx)就能订阅到对应目的地。如果只是给自家App做简单的双人聊天WebSocket裸写反而更直接。MQTT则适合硬件设备和低带宽网络但服务端要额外部署MQTT Broker成本并不低。而且Spring WebSocket官方就支持STOMPAndroid端用STOMP跟Spring Boot服务端对接时服务端不需要写任何协议解析代码直接按注解方法处理消息即可。这也是我最终定下STOMP的直接原因。2. 工程准备依赖导入与StompClient初始化要点2.1 依赖和联网权限别踩坑我用的库是GitHub上NaikSoftware维护的StompProtocolAndroid当前稳定版本是1.6.6。这个库底层依赖OkHttp消息流用RxJava2做响应式处理所以你的Gradle里除了库本身一般还要带上RxJava和RxAndroid。repositories { maven { url https://jitpack.io } } dependencies { implementation com.github.NaikSoftware:StompProtocolAndroid:1.6.6 implementation io.reactivex.rxjava2:rxjava:2.2.21 implementation io.reactivex.rxjava2:rxandroid:2.1.1 }权限方面只要一个联网权限AndroidManifest.xml里加上uses-permission android:nameandroid.permission.INTERNET /很多同学卡在连不上服务端十有八九是漏了权限或者服务端地址写成了http://而不是ws://。注意STOMP走的是WebSocket通道地址必须以ws://或wss://开头这个顺序不能反。我见过一个案例服务端文档给的是http://ip:61614/stomp结果客户端怎么都握手失败换成ws://后几秒钟就通了。2.2 初始化StompClient的两种姿势StompProtocolAndroid提供了两种方式来创建客户端。如果只需要一个简洁的客户端直接用StompClient.overMapString, String headers new HashMap(); headers.put(Authorization, Bearer your-token); StompClient stompClient StompClient.over( WebSocketConnection.class, wss://your-host:61614/stomp, headers );如果希望在初始化阶段就把心跳、重连、连接提供器都配好可以用ReactiveStomp的builder方式ReactiveStomp reactiveStomp ReactiveStomp.builder() .connectionProvider(ConnectionProvider.builder(stomp) .uri(wss://your-host:61614/stomp) .build()) .build(); StompClient stompClient reactiveStomp;这里特别说一下headers参数。很多生产环境的消息中间件需要鉴权比如ActiveMQ需要用户名密码Spring WebSocket的STOMP端点也经常会校验Token。如果服务端要求特殊握手headerStompClient.over的第三个参数就是干这个用的。不需要鉴权时传null即可但要注意如果服务端在握手阶段返回401这个异常在Android端往往表现为WebSocket握手失败而不是一个清晰的HTTP状态码排查时容易绕弯路。2.3 心跳参数到底该怎么配STOMP协议用heart-beat这个header来协商客户端和服务端的心跳周期。StompProtocolAndroid提供了setHeartbeat方法stompClient.setHeartbeat(10000, 10000);两个参数分别表示客户端发送心跳的间隔毫秒和期望服务端发送心跳的间隔。服务端在CONNECTED帧里会返回最终协商值通常是两边取较大值。如果你的网络环境不太稳定建议设置在8000到15000毫秒之间。太短会增加无谓流量太长又会让服务端认为客户端已经死掉主动断开连接。这里说我踩过的一个坑有一段时间我把心跳设成了0心想省点电结果服务端那边的NIO检测超时策略直接把我这个连接判定为失效推送自然收不到。后来把心跳恢复到10秒问题立刻消失。弱网环境下不建议禁用心跳这是一个基本结论。3. 核心链路连接、订阅、发送与自动重连3.1 建立连接从Connect到CONNECTED代码上建立连接很简单一行stompClient.connect()就够了但这个方法返回的是Completable需要在subscribe里监听生命周期事件来决定后续逻辑是否执行。更实用的做法是观察lifecycle()Disposable disposable stompClient.lifecycle() .observeOn(AndroidSchedulers.mainThread()) .subscribe(lifecycleEvent - { switch (lifecycleEvent.getType()) { case OPENED: // 这里才表示CONNECTED帧已经收到 break; case CLOSED: // 连接被关闭 break; case ERROR: // 出现错误 break; } });这里有一个细节connect()发起的是WebSocket握手握手成功不代表STOMP连接就绪。只有StompProtocolAndroid收到服务端的CONNECTED帧后生命周期事件才会切到OPENED。所以在订阅消息之前一定要先等OPENED事件。如果一上来就topic(...).subscribe()订阅消息很可能早于CONNECTED到达导致订阅失败。我最初写Demo时没注意这个时序服务端日志里频繁出现subscription not found查了很久才发现是连接还没就绪。3.2 订阅Topic和Queue消息订阅是STOMP里最高频的操作。StompProtocolAndroid中topic()用于订阅多播目的地queue()用于订阅点对点队列业务上也能混用只不过底层对应服务端的不同交换类型。stompClient.topic(/topic/notifications) .observeOn(AndroidSchedulers.mainThread()) .subscribe(topicMessage - { String payload topicMessage.getPayload(); NotificationBean bean new Gson().fromJson(payload, NotificationBean.class); showNotification(bean); }, throwable - Log.e(STOMP, 订阅异常, throwable));注意topicMessage.getPayload()拿到的是字符串如果服务端下发的是JSON这里需要自己解析。StompProtocolAndroid不会帮你反序列化对象。消息里的getHeaders()也值得关注尤其message-id、subscription这些header在做消息去重和问题排查时非常有用。我在生产环境里就遇到过一次服务端重复投递客户端靠message-id做了窗口去重才避免用户看到两条一模一样的通知。3.3 发送消息与业务确认发送消息用send方法String json {\from\:\android\,\content\:\hello\}; stompClient.send(/app/chat, json) .subscribe();如果这是一条重要消息比如订单状态变更业务上希望确认服务端已经处理我的做法是让服务端在处理完成后主动向某个应答目的地回发一条消息客户端订阅那个目的地即可。STOMP协议本身支持receipt机制但StompProtocolAndroid对receipt的支持目前还比较弱在项目里我更多依赖业务层面的确认消息代码反而更简单直观也更容易在日志里追踪。3.4 断线自动重连不要用简单的delaySTOMP长连接在移动端一定会断网络切换、App进后台、服务端重启。断线后自动重连是刚需。最容易想到的写法是错误后delay(5秒)再接一次但网络抖动时容易导致重连风暴服务端还没恢复几十台设备同时打过来直接把网关打崩。我推荐使用指数退避stompClient.connect() .retryWhen(errors - errors.zipWith(Flowable.range(1, Integer.MAX_VALUE), (error, retryCount) - retryCount) .flatMap(retryCount - Flowable.timer( Math.min(retryCount * 2, 30), TimeUnit.SECONDS)) ) .subscribe();也就是第1次失败等2秒第2次等4秒依次翻倍最大30秒。同时连接成功后要把重连计数清零。如果你在lifecycle的CLOSED事件里也写了重连逻辑注意别和retryWhen同时触发否则会出现两个并发连接消息订阅会变得非常诡异。我的做法是只保留retryWhen这一处重连入口CLOSED事件只用来更新UI状态。4. 常见坑位TLS、心跳、内存泄漏排查实录4.1 自签名证书连不上怎么办生产环境和测试环境的wss很可能是自签名证书。StompProtocolAndroid底层是OkHttp所以处理方式就是标准的OkHttp自定义Client。我建议在构造连接时传入一个自定义ConnectionProvider把配置好的OkHttpClient放进去OkHttpClient okHttpClient new OkHttpClient.Builder() .sslSocketFactory(sslContext.getSocketFactory(), trustManager) .hostnameVerifier((hostname, session) - true) .build();如果服务端是权威CA签发的证书这一步可以完全忽略。只有测试机用自签名证书才建议临时加。特别提醒把hostnameVerifier直接返回true只适合联调环境正式包绝对不要这么写否则等于把TLS校验整体关掉相当于脱了铠甲在公网裸奔。如果团队有协作规范最好用BuildConfig字段区分测试包和正式包。4.2 心跳、超时与服务端主动断开的假死我遇到过一个线上问题App锁屏一晚后第二天打开所有推送都收不到。排查下来发现系统在深度Doze模式下禁用了网络STOMP连接实际上已经被服务端断开但客户端完全没有感知。锁屏期间心跳发不出去服务端等待若干心跳周期后判定客户端死亡主动关闭了连接。恢复前台后WebSocket并没有立刻触发CLOSED导致重连逻辑也没跑起来。解决办法是两层配合连接层用lifecycle()的ERROR和CLOSED事件做一个保底延迟重连应用层监听ProcessLifecycleOwnerApp回到前台时主动检查连接状态断了就立即重连。这两层都做了才能覆盖类似的连接假死场景。只做一层理论上都有漏洞。4.3 订阅泄漏导致的内存问题STOMP库基于RxJava2topic()返回的Flowable在subscribe之后如果没有保管好Disposable在Activity或Fragment销毁时忘记释放轻则导致消息回调空指针重则整个Activity被泄漏。项目里务必用CompositeDisposable统一管理CompositeDisposable disposables new CompositeDisposable(); disposables.add(stompClient.topic(/topic/notifications) .observeOn(AndroidSchedulers.mainThread()) .subscribe(msg - handle(msg))); disposables.add(stompClient.lifecycle().subscribe(event - handleLifecycle(event))); Override protected void onDestroy() { disposables.clear(); super.onDestroy(); }这里还有一个隐藏问题如果你在subscribe的onNext里直接更新UI没有做isDestroyed判断Fragment销毁后消息依然会回调可能触发空指针或状态异常。所以释放Disposable之前回调里也要对宿主状态做校验。这是RxJava异步在Android开发里的经典坑接STOMP之后会频繁遇到。4.4 常见问题速查表现象可能原因解决方法connect一直没回调服务端地址协议写错改成ws://或wss://OPENED后马上CLOSED心跳设为0被服务端判定超时setHeartbeat设为8000~15000订阅后收不到消息订阅动作早于CONNECTED等OPENED事件后再订阅锁屏后推送失效Doze模式下连接断开且没感知增加Resume感知重连自签名wss握手失败证书不受信任自定义OkHttpClient信任证书消息重复弹通知服务端重复投递用message-id做去重5. 生产落地封装一个能长期用的StompManager5.1 单例封装全局只留一个连接入口业务代码直接持有StompClient的缺点很明显你在十个界面各自topic订阅哪天服务端地址变了要改十个地方某次connect()重试失败日志里看不出是谁发起的。我的做法是封装一个StompManager全局只保留一个StompClient实例。public final class StompManager { private static volatile StompManager instance; private StompClient client; private PublishSubjectString notifySubject PublishSubject.create(); private StompManager() { client StompClient.over(WebSocketConnection.class, BuildConfig.STOMP_URL, null); client.setHeartbeat(10000, 10000); connectWithRetry(); client.lifecycle() .subscribe(event - { if (event.getType() LifecycleEvent.Type.OPENED) { client.topic(/topic/notifications) .subscribe(msg - notifySubject.onNext(msg.getPayload())); } }); } public static StompManager get() { ... } public PublishSubjectString notifications() { return notifySubject; } }所有界面想收通知直接StompManager.get().notifications().subscribe(...)连接状态和重连策略全部收敛在Manager里。业务层不感知STOMP细节测试时也方便替换成Mock数据源。这个封装看起来简单但实际价值很大尤其是当项目从1个界面使用STOMP增长到10个界面、20个订阅点时它帮你把复杂度彻底控制在一个类里。5.2 消息可靠性与幂等策略STOMP的QoS能力比MQTT弱很多默认就是尽力而为消息不保证不重复、不保证有序。如果业务对消息丢失敏感我的建议是两条腿走路客户端发送重要消息后服务端在业务处理完成后主动回发确认客户端如果在超时时间内没收到确认就重发并携带messageId。服务端根据messageId做幂等判断避免重复下单、重复通知。Android端的发送逻辑不要直接在UI线程构造报文send本身是异步的但构造消息体、序列化JSON这些操作也要放到后台线程。实测在低端机上频繁在主线程组装字符串会有微小卡顿长连接场景下累计起来体感明显。用Gson统一处理对象和JSON的转换能少写很多样板代码。5.3 我在用的埋点监控手段最后分享一个非常实际的技巧在StompManager里把lifecycle()的每次事件都埋点上报到日志平台。长连接问题很难复现往往隔几天才出现一次。有了事件埋点就算用户不主动反馈后台也能看到某台设备在某个时间点发生了CLOSED、ERROR再配合心跳记录基本就能还原断线现场。我在这套方案落地后处理过的连接类工单数量几乎降为零因为大部分问题都能靠日志精确定位不用再让用户反复操作复现。以我自己的经验StompProtocolAndroid这个库本身不算复杂真正的复杂度都在连接治理上。如果能在一开始就把连接、订阅、重连、监控统一收口后面的业务迭代会顺利得多。接入之前也建议先厘清服务端的鉴权方式、心跳策略和消息投递模型再动手写代码可以少走不少弯路。本文还有配套的精品资源点击获取