Android websocket和Stomp

210 阅读2分钟

STOMP 协议

  • STOMP(Simple Text Oriented Messaging Protocol)是一种简单的文本协议,用于通过 WebSocket 进行消息传递。

  • STOMP 的优点包括跨平台支持、易用性和可扩展性。它允许通过插件和扩展来定制消息处理逻辑。

STOMP(Simple Text Oriented Messaging Protocol)是一种简单、基于文本的消息传递协议,它提供了一种可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议的设计灵感来源于HTTP的简单性,它非常容易实现客户端,且与多种语言和平台兼容。

TOMP协议的工作流程

STOMP协议的工作流程大致如下:

  1. 连接:客户端首先发送CONNECT帧,服务器收到后响应CONNECTED帧,表示连接建立成功。
  2. 订阅:客户端发送SUBSCRIBE帧,订阅某个消息主题(topic)或队列(queue)。服务器会将该主题/队列中的消息推送到客户端。
  3. 发送消息:客户端可以发送SEND帧,将消息发送到服务器的指定目标(如/topic/news)。
  4. 接收消息:服务器会将来自该目标的消息以MESSAGE帧的形式推送给所有订阅了这个目标的客户端。
  5. 断开连接:客户端在会话结束时发送DISCONNECT帧,通知服务器关闭连接。

STOMP协议的帧结构

STOMP协议的帧结构十分简单,类似于HTTP请求,命令和头部字段都是纯文本,方便解析和调试。一个典型的STOMP帧包括以下几个部分:

  1. 命令:表示帧的类型,如CONNECTSENDSUBSCRIBE等。
  2. 头部字段:类似于HTTP头部,包含键值对,用于传递额外的信息,如目标地址、消息的内容类型等。
  3. 消息体:可选的消息内容,可以是任何文本或二进制数据\

okhttps 框架

ok.zhxu.cn/


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
        }
    }
}