一文聊聊基于OkHttp封装STOMP实践

一、STOMP 协议介绍

1、STOMP与WebSocket的关系

首先STOMP 与 WebSocket不是一个层级的协议。STOMP是一种简单的文本协议,在WebSocket协议基础之上运行。STOMP是应用层协议,当与WebSocket协议共同使用时,WebSocket 负责底层的连接和数据传输,而 STOMP 负责消息的格式化和路由。

2、STOMP协议

STOMP的帧格式是由一个命令、若干头部、和一个可选的消息体组成的。

命令

每个帧都以一个命令开始。命令是一个文本字符串,定义了帧的类型。 在我们的项目中我们封装的命令如下:

CONNECT
  • 客户端向服务端发起连接,用于建立与服务器的新连接。
  • 重要头部:
    • accept-version: 表示客户端支持的 STOMP 协议版本(如 1.0,1.1,1.2)。
    • host: 指定客户端想要连接的虚拟主机。
CONNECTED
  • 服务器对客户端的 CONNECT 请求的响应,确认连接成功并协商协议版本。
  • 重要头部:
    • version: 服务器选择的 STOMP 协议版本。
DISCONNECT
  • 客户端请求断开连接
  • 重要头部:
    • receipt: 请求确认断开连接成功。
SEND
  • 用于发送消息到指定的目标(如队列或主题),客户端将消息发送到服务器。
  • 重要头部:
    • destination: 消息的目标地址(如 /queue/a)。
    • content-type: 消息体的 MIME 类型。
MESSAGE
  • 收到消息,服务器向客户端发送的消息。
  • 重要头部:
    • destination: 消息来自的目标。
    • message-id: 消息的唯一标识符。
    • subscription: 消息对应的订阅 ID。
SUBSCRIBE
  • 发起订阅,客户端请求订阅某个目标的消息,注册客户端以接收来自指定目标的消息。
  • 重要头部:
    • destination: 要订阅的目标。
RECEIPT
  • 收到凭据,服务器确认收到客户端的请求
  • 重要头部:
    • receipt-id: 对应请求的标识符。
UNSUBSCRIBE
  • 发起退订,客户端请求取消先前的订阅。停止接收从某个目标发送的消息。
  • 重要头部:
    • id: 要取消的订阅的唯一标识符。
ACK
  • 确认信息,客户端确认已收到并成功处理消息
  • 重要头部:
    • id: 要确认的消息的标识符。
NACK
  • 描述:客户端通知服务器消息未被成功处理,告知服务器消息处理失败,可能需要重新发送。
  • 重要头部:
    • id: 未被确认的消息的标识符。
ERROR
  • 收到错误,服务器向客户端发送的错误信息。
  • 重要头部:
    • message: 错误描述信息。

二、封装消息

由于STOMP协议比较简单,所以针对STOMP封装主要是针对Command、Header、以及整体的Message

1、Commands

用于定义所有的命令,代码这里就不贴了。

2、Header

用于封装用到的header key

  • id
  • accept-version
  • heart-beat
  • destination
  • message-id
  • receipt
  • receipt-id
  • subscription
  • ack

以上key的含义在上面介绍的Command中基本都有罗列,这里就不再罗列了。

示例代码:

kotlin 复制代码
data class Header(val key: String, val value: String) {
    override fun toString(): String {
        return "$key:$value"
    }
}

3、Message

我们将一帧封装成为一条消息,其包含command、headers、content

示例代码:

kotlin 复制代码
data class Message(
    val command: String,
    val headers: List<Header>,
    val payload: String? = null
) {
    fun getHeaders(): List<Header> {
        return headers
    }

    fun getPayload(): String? {
        return payload
    }

    fun getCommand(): String {
        return command
    }

    fun headerValue(key: String): String? {
        val header = header(key)
        return header?.value
    }

    fun header(key: String): Header? {
        for (header in headers) {
            if (header.key == key) return header
        }
        return null
    }

    override fun toString(): String {
        return "Message {command='$command', headers=$headers, payload='$payload'}"
    }
}

这样我们针对STOMP的协议就封装好了。下面就处理编码、解码。

三、编码 & 解码

1、编码

编码接口:

kotlin 复制代码
interface IEncode {
    /**
     * 编码
     */
    fun encode(input: Message): String
}

编码主要是将我们封装的Message转化为String,然后交给WebSocket对象发送出去。

所以最终的String 就是将命令、Header、消息内容按照STOMP协议的格式构建起来。

具体实现示例:

scss 复制代码
fun encode(input: Message): String {
    val sb = StringBuilder()
    sb.append(input.command).append("\n")
    val headers = msgHeaders(input)
    val hCnt = headers.size
    for ((index, header) in headers.withIndex()) {
        sb.append(header.key)
            .append(":")
            .append(header.value)
        if (index < hCnt - 1) {
            sb.append("\n")
        }
    }
    sb.append("\n\n")
    val payload = input.payload
    if (payload != null) {
        sb.append(payload)
    }
    sb.append("\u0000")
    return sb.toString()
}

2、解码

解码接口

kotlin 复制代码
interface IDecode {
    /**
     * 解码
     */
    fun decode(content: String): Message
}

解码就是将我们收到的String转化为我们封装的Message对象。解码时主要是注意一下几点:

  • 判断收到的content是否为空
  • 注意存在拆包的情况,例如接收到消息,但是结束符所在位置为-1,那么说明发生了拆包,继续等待
  • 注意校验command、header,例如收到的command是否是合法的

四、整个链路

整个链路由于我们是依赖OkHttp中的WebSocket,所以关于如何建联OkHttp已经为我们封装好了,我们在上层封装时只需要将WebSocket 链路逻辑与STOMP 协议串联到一起就行了。

1、建立连接

建立连接主要分为三步:

  • 构建WebSocket对象,建立连接
  • 发送CONNECT命令
  • 接收到服务端的CONNECTED,确认已连接

构建WebSocket示例代码

ini 复制代码
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder()
        .url(authUrl)
        .build();
//建立连接
webSocket = client.newWebSocket(request, this);

在 WebSocketListener 的onOpen回调中,发送CONNECT命令

示例代码:

ini 复制代码
List<Header> headers = new ArrayList<>();
headers.add(new Header(Header.VERSION, SUPPORTED_VERSIONS));
send(new Message(Commands.CONNECT, headers));

其中send方法就是调用WebSocket.send()方法,入参是String,需要先使用编码接口将Message转为String。

示例代码:

ini 复制代码
webSocket.send(encodeImpl.encode(message));

服务端在接收到客户端的CONNECT命令后,如果确认可以进行连接,则会发送CONNECTED命令给到客户端。

客户端中的代码可以集中在 WebSocketListener 的onMessage回调中处理接收到的消息。接收到消息之后优先调用 decodeImpl.decode() 将String转为Message。如果Command为CONNECTED则确认已经连接成功,可以回调给上层。后面就可以正常收发消息了。

2、订阅 & 退订

在与服务端建立连接之后,通常我们会直接发送 SUBSCRIBE 命令,Header中添加destination(订阅地址)、id(订阅ID) 订阅某一个地址的消息。

后续收到服务端的Message命令消息中,其包含 subscription (订阅ID),当其与我们订阅消息中的id相同时,则就是这个订阅地址需要的消息。

值得注意的是,客户端可以同时订阅多个地址。例如在我们的业务中,业务地址与心跳地址就不同。

取消订阅也比较简单,发送 UNSUBSCRIBE 命令,携带订阅的id就可以了。

3、断开连接

断开连接有两种路径:

  • 第一种是直接调用调用WebSocket提供的close方法。
  • 第二种先发送 DISCONNECT
直接断开

第一种是直接调用调用WebSocket提供的close方法。webSocket.close();

这种方案不需要管STOMP协议,直接断开连接,这种方式简单粗暴,使用这种方式直接忽略了STOMP协议,需要提前与服务端沟通好。

通过STOMP协议
  • 先发送 DISCONNECT 命令,并在Header中携带receipt 凭证
  • 收到 RECEIPT 命令后,确认凭证正确后,再调用webSocket.close();

五、断线重连

上一节中主要聊了整个链路的封装逻辑,这一节介绍一下上层的断线重连逻辑。我这里简单的将断线重连总结为三板斧:

  • 心跳
  • 网络变化重连
  • 链接探测

1、心跳

在我们的项目中,STOMP的心跳有独立的订阅地址,所以在我们封装的STOMP库中,我们是独立封装的心跳逻辑。心跳地址、心跳间隔都支持外部设置。

示例代码:

ini 复制代码
List<Header> headers = new ArrayList<>();
String id = UUID.randomUUID().toString();
headers.add(new Header(Header.ID, id));
headers.add(new Header(Header.DESTINATION, destination));
headers.add(new Header(Header.RECEIPT_ID, String.valueOf(1)));
Message message = new Message(Commands.SEND, headers, data);
send(message);

2、网络变化

在上层我们会监听网络的变化,针对网络的断链及时的进行断线重连,确保网络发生变化后,长连接可以尽快回复。网络监听的代码比较简单这里我就不贴出来了。

3、链接探测

为了保证App在前后台等场景可以快速感知到长连接是否真正的连接,我们在STOMP SDK中添加了基于心跳的探测逻辑。这个逻辑我另外一篇文章中有详细介绍过,这里我贴一下地址就不重复写了。如何设计IM类App(协议选择、断线重连、链接探测等)

六、总结

本文介绍了利用OkHttp封装STOMP的主体逻辑,可以看到STOMP本身算是一个比较简单的协议,封装下来也比较简单,希望读者看过此篇后能有一定的收获。这篇应该是农历新年前的最后一篇了,在这里也祝大家新年快乐!

相关推荐
m0_674031433 小时前
业务架构、数据架构、应用架构和技术架构
架构
安的列斯凯奇5 小时前
分布式事务介绍 Seata架构与原理+部署TC服务 示例:黑马商城
分布式·架构
zhangjiaofa6 小时前
Android中的LoadedApk:使用指南与核心代码解析
android
m0_748252238 小时前
万字详解 MySQL MGR 高可用集群搭建
android·mysql·adb
SoulKuyan8 小时前
Android系统默认开启adb root模式
android·adb
0wioiw011 小时前
逆向安卓抓包
android·linux·运维
zhangjiaofa11 小时前
深入理解 Android 中的 KeyguardManager
android
-代号952711 小时前
云计算中的可用性SLA
android·java·云计算
HsuYang12 小时前
Vite源码学习(三)——Vite内置插件
前端·javascript·架构
重生之Java开发工程师12 小时前
⭐MySQL的底层原理与架构
数据库·mysql·面试·架构