STOMP 协议
-
STOMP(Simple Text Oriented Messaging Protocol)是一种简单的文本协议,用于通过 WebSocket 进行消息传递。
-
STOMP 的优点包括跨平台支持、易用性和可扩展性。它允许通过插件和扩展来定制消息处理逻辑。
STOMP(Simple Text Oriented Messaging Protocol)是一种简单、基于文本的消息传递协议,它提供了一种可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议的设计灵感来源于HTTP的简单性,它非常容易实现客户端,且与多种语言和平台兼容。
TOMP协议的工作流程
STOMP协议的工作流程大致如下:
- 连接:客户端首先发送
CONNECT
帧,服务器收到后响应CONNECTED
帧,表示连接建立成功。 - 订阅:客户端发送
SUBSCRIBE
帧,订阅某个消息主题(topic)或队列(queue)。服务器会将该主题/队列中的消息推送到客户端。 - 发送消息:客户端可以发送
SEND
帧,将消息发送到服务器的指定目标(如/topic/news
)。 - 接收消息:服务器会将来自该目标的消息以
MESSAGE
帧的形式推送给所有订阅了这个目标的客户端。 - 断开连接:客户端在会话结束时发送
DISCONNECT
帧,通知服务器关闭连接。
STOMP协议的帧结构
STOMP协议的帧结构十分简单,类似于HTTP请求,命令和头部字段都是纯文本,方便解析和调试。一个典型的STOMP帧包括以下几个部分:
- 命令:表示帧的类型,如
CONNECT
、SEND
、SUBSCRIBE
等。 - 头部字段:类似于HTTP头部,包含键值对,用于传递额外的信息,如目标地址、消息的内容类型等。
- 消息体:可选的消息内容,可以是任何文本或二进制数据\
okhttps 框架
class StompUtils {
companion object {
private var stomp: Stomp? = null
val instance: StompUtils by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { StompUtils() }
}
var logCallback: (String) -> Unit = {}
var reConnect: Boolean = true
fun init(
userId: String,
workMobile: (WorkMsgBean) -> Boolean,
messageNotify: (StompMsgNotificationBean) -> Unit,
) {
if (stomp == null) {
val token: String = GlobalApp.instance.companyInfoBean?.access_token
?: ""//SharedPreferencesUtil.getInfo(SharedPreferencesUtil.TOKEN)
val headers: MutableList<Header> = ArrayList()
headers.add(Header(DZ_AUTH, "$BEARER $token"))
LogUtils.d("stomp headers:$headers")
Stomp.over(
OkHttps.webSocket(WSS_DOMAIN)
.heatbeat(20, 20)
)
.setOnConnected { stomp: Stomp? ->
LogUtils.d("stomp->connected")
StompUtils.stomp = stomp?.apply {
dynamicProxyLog(this) {
logCallback(it)
}
subscribe("/user/$userId/queue/message", null) { msg: Message ->
JsonHelper.parserJson2Object(
msg.payload,
StompMsgNotificationBean::class.java
)?.let(messageNotify)
}
subscribe("/user/$userId/queue/workMobile", null) { msg: Message ->
JsonHelper.parserJson2Object(
msg.payload,
WorkMsgBean::class.java
)?.let {
if (workMobile(it)) {
sendTo(
"/ws/receive-work-mobile",
JsonHelper.parserObject2Json(
StompMsgWorkCallbackBean(
it.messageId,
userId
)
)
)
}
}
}
}
}
.setOnDisconnected { close: com.ejlchina.okhttps.WebSocket.Close? ->
LogUtils.d("stomp->close : ${close?.toString()} ")
if (reConnect)
if (stomp?.isConnected != true) {
stomp = null
init(userId, workMobile, messageNotify)// 尝试重新连接
}
}
.setOnError { msg: Message? -> LogUtils.d("stomp->msg:${msg.toString()}") }
.setOnException { e: Throwable? -> LogUtils.d("stomp->e:${e?.message}") }
.connect(headers)
}
}
private fun dynamicProxyLog(stomp: Stomp, logCallback: (String) -> Unit) {
val field = stomp.javaClass.getDeclaredField("msgCodec")
field.isAccessible = true
val msgCodecInstance = field.get(stomp)
msgCodecInstance?.let {
val handler = CodecInvocationHandler(msgCodecInstance, logCallback)
val proxyInstance: MsgCodec = Proxy.newProxyInstance(
stomp.javaClass.getClassLoader(), arrayOf<Class<*>>(MsgCodec::class.java), handler
) as MsgCodec
field.set(stomp, proxyInstance)
}
}
fun disconnect() {
if (stomp?.isConnected == true)
stomp?.disconnect()
}
fun receiveConnected(): Boolean {
return stomp?.isConnected ?: false
}
/**
* @author : LiuJP
* @desc
* @since 2024/10/16 17:26
*
* websocket stomp 动态代理打印日志
*/
inner class CodecInvocationHandler(
private val target: Any,
private val logCallback: (String) -> Unit
) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any>): Any {
val result = method.invoke(target, *args) ?: Object()
if ("decode" == method.name) {
val obj0 = args[0] as String
val logStr = "stomp->${method.name}\n:$obj0"
Logger.d(logStr)
logCallback(obj0)
} else if ("encode" == method.name) {
val obj0 = result as String
val logStr = "stomp->${method.name}\n:$obj0"
Logger.d(logStr)
}
return result
}
}
}