一文聊聊基于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本身算是一个比较简单的协议,封装下来也比较简单,希望读者看过此篇后能有一定的收获。这篇应该是农历新年前的最后一篇了,在这里也祝大家新年快乐!

相关推荐
网络安全(king)2 小时前
网络安全知识:网络安全网格架构
安全·web安全·架构
openinstall全渠道统计3 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫3 小时前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫3 小时前
一句话说透Android里面的查找服务
android
双鱼大猫3 小时前
一句话说透Android里面的SystemServer进程的作用
android
双鱼大猫3 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫4 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫4 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫4 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标5 小时前
android 快速定位当前页面
android