Diseño de mensajería instantánea de Android (acceso a Tencent IM y acceso a WebSocket)

I. Introducción

El chat grupal del proyecto anterior funcionaba directamente con la base de datos, la experiencia era muy mala y era difícil proporcionar comentarios instantáneos de los mensajes, así que al final, consideré usar la mensajería instantánea de Tencent para completar el acceso al chat grupal, pero hubo un poco de baches en el medio, y descubrí después de que se completó el acceso. Solo hay 20 personas en un chat grupal en la versión de prueba. En ese momento, vi que la versión de prueba admite 100 usuarios. Ahora solo puedo tener 20 usuarios en un chat de grupo . No es suficiente admitir 50 usuarios de chat en línea al mismo tiempo, apenas es suficiente, lo siguiente presentará el acceso de los dos esquemas de implementación, el texto comenzará pronto ~~

2. Acceso a mensajería instantánea Tencent

El sitio web oficial de Tencent Cloud IM, el acceso aquí extrae las API relacionadas con el chat grupal, consulte el documento para obtener más información (si tiene tiempo, se puede realizar una plataforma de chat simple similar a QQ)

https://cloud.tencent.com/document/product/269/42440
复制代码

1. Preparativos

  • análisis de la demanda

    Debe implementar una función similar al chat grupal en QQ, solo necesita desarrollar tres funciones simples de recibir mensajes, enviar mensajes y obtener registros de historial.

  • crear una aplicación

    Esta parte no se demostrará. Es muy simple. Después de crearla, se verá como la imagen de abajo.

1.jpg

La versión de prueba puede admitir 100 usuarios y 20 usuarios en un chat grupal, proporciona almacenamiento gratuito en la nube durante 7 días y puede crear múltiples instancias de mensajería instantánea al mismo tiempo. Si está aprendiendo a usar, la versión de prueba es suficiente y la comercialización considera la versión profesional y la versión definitiva

  • Integración de dependencia

    Al usar la integración de gradle, también puede usar la integración de sdk, aquí se usa la nueva versión de sdk para la integración

    api 'com.tencent.imsdk:imsdk-plus:6.1.2155'
    复制代码

2. Trabajo de inicialización

Inicializar MI

  • Crear instancia

    Hay una devolución de llamada en el parámetro, el objeto aquí es equivalente a la clase anónima en Java

    val config = V2TIMSDKConfig()
    V2TIMManager.getInstance()
        .initSDK(this, sdkId, config, object : V2TIMSDKListener() {
            override fun onConnecting() {
                // 正在连接到腾讯云服务器
                Log.e("im", "正在连接到腾讯云服务器")
            }
    
            override fun onConnectSuccess() {
                // 已经成功连接到腾讯云服务器
                Log.e("im", "已经成功连接到腾讯云服务器")
            }
    
            override fun onConnectFailed(code: Int, error: String) {
                // 连接腾讯云服务器失败
                Log.e("im", "连接腾讯云服务器失败")
            }
        })
    复制代码
  • Generar credenciales de inicio de sesión

    Esta parte proporciona oficialmente el código generado por el cliente y el código del lado del servidor rápidamente. Puede encontrarlo en el sitio web oficial. Al realizar la prueba, puede considerar que el proyecto oficial detrás del código del lado del cliente se implementa mejor en el servidor para procesamiento. Esta parte se menciona aquí. Despierta, hay dos archivos en el lado del servidor. No lo vi claramente en ese momento. Después de buscar la función durante mucho tiempo, finalmente descubrí que era un archivo java. que se me olvidó leer, o estaba en el directorio del mismo nivel.Debería ser que otras APIs también reutilicen la clase Base64URL.

2.jpg

A su vez, el funcionario también brinda herramientas para generar y verificar credenciales

3.jpg

Inicio de sesión de usuario

Esta parte solo necesita pasar los parámetros.

V2TIMManager.getInstance().login(currentUser,sig, object : V2TIMCallback {
    override fun onSuccess() {
        Log.e("im", "${currentUser}登录成功")
    }
    override fun onError(code: Int, desc: String?) {
        Log.e("im", "${currentUser}登录失败,错误码为:${code},具体错误:${desc}")
    }
})
复制代码
  • currentUser es la identificación del usuario
  • sig son las credenciales de inicio de sesión del usuario
  • Una clase para devoluciones de llamada V2TIMCallback

3. Chat de grupo relacionado

Crear un chat de grupo

创建群聊的时候需要注意几个方面的问题

  • 群聊类别(groupType)

    需要审批还是不需要,最大的容纳用户数,未支不支持未入群查看群聊消息,详见下图

4.jpg

其中社群其实挺符合我的需求的,但有个问题,社群需要付费才能开通(还挺贵),所以最后选择了Meeting类型的群组

  • 群聊资料设置

    群聊id(groupID)是没有字母数字和特殊符号(当然不能中文)都是可以的,群聊名字(groupName),群聊介绍(introduction)等等,还有就是设置初始的成员,可以将主管理员加入(这里稍微有点疑惑的就是创建群聊,居然没有默认添加创建人)

  • 创建群聊的监听回调

    这里传入的参数就是上述的groupInfo和memberInfoList,主要用于初始化群聊,然后有一个回调的参数监听创建结果

val group = V2TIMGroupInfo()
group.groupName = "test"
group.groupType = "Meeting"
group.introduction = "more to show"
group.groupID = "test"
val memberInfoList: MutableList<V2TIMCreateGroupMemberInfo> = ArrayList()
val memberA = V2TIMCreateGroupMemberInfo()
memberA.setUserID("master")
memberInfoList.add(memberA)
V2TIMManager.getGroupManager().createGroup(
    group, memberInfoList, object : V2TIMValueCallback<String?> {
        override fun onError(code: Int, desc: String) {
            // 创建失败
            Log.e("im","创建失败${code},详情:${desc}")
        }

        override fun onSuccess(groupID: String?) {
            // 创建成功
            Log.e("im","创建成功,群号为${groupID}")
        }
    })
复制代码

加入群聊

这部分只需要一个回调监听即可,这里没有login的用户的原因是,默认使用当前登录的id加群,所以一个很重要的前提是登录

V2TIMManager.getInstance().joinGroup("群聊ID","验证消息",object :V2TIMCallback{
    override fun onSuccess() {
        Log.e("im","加群成功")
    }
    override fun onError(p0: Int, p1: String?) {
        Log.e("im","加群失败")
    }
})
复制代码

4.消息收发相关

发送消息

这里发送消息是采用高级接口,发送的消息类型比较丰富,并且支持自定义消息类型,所以这里采用了高级消息收发接口

首先创建消息,这里是创建自定义消息,其他消息同理

val myMessage = "一段自定义的json数据"

//由于这里自定义消息接收的参数为byteArray类型的,所以进行一个转换
val messageCus= V2TIMManager.getMessageManager().createCustomMessage(myMessage.toByteArray())
复制代码

发送消息,这里需要设置一些参数

messageCus即转换过后的byte类型的数据,toUserId即接收方,这里为群聊的话,用空字符串置空即可,groupId即群聊的ID,如果是单聊的话,这里同样置空字符串即可,weight即你的消息被接收到的权重(不保证全部都能收到,这里设置权重确定优先级),onlineUserOnly即是否只有在线的用户可以收到,这个的话设置false即可,offlinePushInfo这个只有旗舰版才有推送消息的功能,所以这里设置null即可,然后就是一个发送消息的回调

V2TIMManager.getMessageManager().sendMessage(messageCus,toUserId,groupId,weight,onlineUserOnly, offlinePushInfo,object:V2TIMSendCallback<V2TIMMessage>{
    override fun onSuccess(message: V2TIMMessage?) {
       	Log.e("im","发送成功,内容为:${message?.customElem}")
        //这里同时需要自己进行解析消息,需要转换成String类型的数据
        val data = String(message?.customElem?.data)
       	...
    }

    override fun onError(p0: Int, p1: String?) {
        Log.e("im","错误码为:${p0},具体错误:${p1}")
    }

    override fun onProgress(p0: Int) {
        Log.e("im","处理进度:${p0}")
    }
})
复制代码

获取历史消息

  • groupId即群聊ID
  • pullNumber即拉取消息数量
  • lastMessage即上一次的消息,用于获取更多消息的定位
  • V2TIMValueCallback即消息回调

这里关于lastMessage进行解释说明,这个参数可以设置成全局变量,然后一开始设置为null,然后获取到的消息列表的最后一条设置成lastMessage即可

V2TIMManager.getMessageManager().getGroupHistoryMessageList(
    groupId,pullNumber,lastMessage,object:V2TIMValueCallback<List<V2TIMMessage>>{
    override fun onSuccess(p0: List<V2TIMMessage>?) {
       if (p0 != null) {
           if (p0.isEmpty()){
               Log.e("im","没有更多消息了")
               "没有更多消息了".showToast()
           }else {
               //记录最后一条消息
               lastMessage = p0[p0.size - 1]
               for (msgIndex in p0.indices) {
                   //解析各种消息
                   when(p0[msgIndex].elemType){
                       V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{
                           ...
                       }
                       V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {}
                          ...
                       }
                       else -> {
                         ...
                       }
                   }  							
               }
           }
       }
    }
    override fun onError(p0: Int, p1: String?) {
        ....
    }
})
复制代码

新消息的监听

这个主要用于新消息的接收和监听,同时需要自己对于各种消息的解析和相关处理

V2TIMManager.getMessageManager().addAdvancedMsgListener(object:V2TIMAdvancedMsgListener(){
    override fun onRecvNewMessage(msg: V2TIMMessage?) {
        Log.e("im","新消息${msg?.customElem}")

        //这里针对多种消息类型有不同的处理方法
        when(msg?.elemType){
            V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{
                val message = msg.customElem?.data
                ...
            }
            V2TIMMessage.V2TIM_ELEM_TYPE_TEXT ->{
                val message = msg.textElem.text
                ...
            }
            else -> {
                "暂时不支持此消息的接收".showToast()
                Log.e("im","${msg?.elemType}")
            }
        }
    }
})
复制代码

至此接入部分就已经完成了,这里只是简单的介绍接入,还有更多的细节可以查看项目源码

三、WebSocket接入

这个需求和上面的是一样的,同时提供和上面腾讯IM类似功能的api,这部分涉及网络相关的api(不是非常专业),主要描述一些思路上的,具体代码不是很困难

1.WebSocket介绍

webSocket可以实现长连接,可以作为消息接收的即时处理的一个工具,采用ws协议或者wss协议(SSL)进行通信,腾讯IM的版本也推出了webSocket实现方案,webSocket主要解决的痛点就是服务端不能主动推送消息,代替之前轮询的实现方案

5.jpg

2.服务端相关

服务端采用springboot进行开发,同时也是使用kotlin进行编程

  • webSoket 依赖集成

    下面是gradle的依赖集成

    implementation "org.springframework.boot:spring-boot-starter-websocket"
    复制代码
  • WebSocketConfig配置相关

    @Configuration
    class WebSocketConfig {
        @Bean
        fun serverEndpointExporter(): ServerEndpointExporter {
            return ServerEndpointExporter()
        }
    }
    复制代码
  • WebSocketServer相关

    这部分代码是关键代码,里面重写了webSocket的四个方法,然后配置静态的变量和方法用于全局通信,下面给出一个框架

    @ServerEndpoint("/imserver/{userId}")
    @Component
    class WebSocketServer {
        @OnOpen
        fun onOpen(session: Session?, @PathParam("userId") userId: String) {
            ...
        }
    
        @OnClose
        fun onClose() {
            ...
        }
    
        @OnMessage
        fun onMessage(message: String, session: Session?) {
          ...
        }
        
        @OnError
        fun onError(session: Session?, error: Throwable) {
           ...
        }
    
        //主要解决@Component和@Resource冲突导致未能自动初始化的问题
        @Resource
        fun setMapper(chatMapper: chatMapper){
            WebSocketServer.chatMapper = chatMapper
        }
        
        //这是发送消息用到的函数
        @Throws(IOException::class)
        fun sendMessage(message: String?) {
            session!!.basicRemote.sendText(message)
        }
    
        //静态变量和方法
        companion object {
    		...
        }
    }
    复制代码

    companion object

    这里一个比较关键的变量就是webSocketMap存储用户的webSocket对象,后面将利用这个实现消息全员推送和部分推送

    companion object {
        //统计在线人数
        private var onlineCount: Int = 0
        
        //用于存放每个用户对应的webSocket对象
        val webSocketMap = ConcurrentHashMap<String, WebSocketServer>()
    
        //操作数据库的mapper对象的延迟初始化
        lateinit var chatMapper:chatMapper
        
        //服务端主动推送消息的对外开放的方法
        @Throws(IOException::class)
        fun sendInfo(message: String, @PathParam("userId") userId: String) {
            if (userId.isNotBlank() && webSocketMap.containsKey(userId)) {
                webSocketMap[userId]?.sendMessage(message)
            } else {
                println("用户$userId,不在线!")
            }
        }
    
        //在线统计
        @Synchronized
        fun addOnlineCount() {
            onlineCount++
        }
    
        //离线统计
        @Synchronized
        fun subOnlineCount() {
            onlineCount--
        }
    }
    复制代码

    @OnOpen

    这个方法在websocket打开时执行,主要执行一些初始化和统计工作

    @OnOpen
    fun onOpen(session: Session?, @PathParam("userId") userId: String) {
        this.session = session
        this.userId = userId
        if (webSocketMap.containsKey(userId)) {
            //包含此id说明此时其他地方开启了一个webSocket通道,直接kick下线重新连接
            webSocketMap.remove(userId)
            webSocketMap[userId] = this
        } else {
            webSocketMap[userId] = this
            addOnlineCount()
        }
        println("用户连接:$userId,当前在线人数为:$onlineCount")
    }
    复制代码

    @OnClose

    这个方法在webSocket通道结束时调用,执行下线逻辑和相关的统计工作

    @OnClose
    fun onClose() {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId)
            subOnlineCount()
        }
        println("用户退出:$userId,当前在线人数为:$onlineCount")
    }
    复制代码

    @OnMessage

    这个方法用于处理消息分发,这里一般需要对消息进行一些处理,具体处理参考自定义消息的处理,这里是设计成群聊的方案,所以采用

    @OnMessage
    fun onMessage(message: String, session: Session?) {
        if (message.isNotBlank()) {
            //解析发送的报文
            val newMessage = ...
            
            //这里需要进行插入一条数据,做持久化处理,即未在线的用户也同样可以看到这条消息
            chatMapper.insert(newMessage)
            
            //遍历所有的消息
            webSocketMap.forEach { 
                it.value.sendMessage(sendMessage.toMyJson())
            }
        }
    }
    复制代码

    @OnError

    发生错误调用的方法

    @OnError
    fun onError(session: Session?, error: Throwable) {
        println("用户错误:$userId 原因: ${error.message}")
        error.printStackTrace()
    }
    复制代码

    sendMessage

    此方法用于消息分发给各个客户端时调用的

    fun sendMessage(message: String?) {
        session!!.basicRemote.sendText(message)
    }
    复制代码
  • WebSocketController

    这部分主要是实现服务端直接推送消息设计的,类似系统消息的设定

    @PostMapping("/sendAll/{message}")
    fun sendAll(@PathVariable message: String):String{
        //消息的处理
        val newMessage = ... 
        
        //需不要存储系统消息就看具体需求了
        WebSocketServer.webSocketMap.forEach { 
            WebSocketServer.sendInfo(sendMessage.toMyJson(), it.key)
        }
        
        return "ok"
    }
    
    @PostMapping("/sendC2C/{userId}/{message}")
    fun sendC2C(@PathVariable userId:String,@PathVariable message:String):String{
        //消息的处理
        val newMessage = ... 
        
        WebSocketServer.sendInfo(newMessage, userId)
        return  "ok"
    }
    复制代码

    至此服务端的讲解就结束了,下面就看看我们安卓客户端的实现了

3.客户端相关

  • 依赖集成

    集成java语言的webSocket(四舍五入就是Kotlin版本的)

    implementation 'org.java-websocket:Java-WebSocket:1.5.2'
    复制代码
  • 实现部分

    这部分的重写的方法和服务端差不多,但少了服务相关的处理,代码少了很多,这里需要提醒的一点就是,重写的这些方法都是子线程中运行的,不允许直接写入UI相关的操作,所以这里需要使用handle进行处理或者使用runOnUIThread

    val userSocket = object :WebSocketClient(URI("wss://服务端地址:端口号/imserver/${userId}")){
        override fun onOpen(handshakedata: ServerHandshake?) {
            //打开进行初始化的操作
        }
    
        override fun onMessage(message: String?) {
           ...
            //这里做recyclerView的更新
        }
    
        override fun onClose(code: Int, reason: String?, remote: Boolean) {
           //这里执行一个通知操作即可
            ...
        }
    
        override fun onError(ex: Exception?) {
           ...
        }
    
    }
    userSocket.connect()
    
    //断开连接的话使用自带的reconnect重新连接即可
    //需要注意的一点就是不能在重写方法里面执行这个操作
    userSocket.reconnect()
    复制代码

    这里还有太多很多细节不能一一展示,但就总体而言是模仿上述腾讯IM实现的,具体的可以看项目地址

四、列表设计的一些细节

这里简单叙述一下列表设计的一些细节,这部分设计还是挺繁琐的

1.handle的使用

列表的更新时间和时机是取决于具体网络获取情况的,故需要一个全局的handle用于处理其中的消息,同时列表滑动行为不一样,这里需要注意的一个小问题,就是message最好是用一个发一个,不然可能出现内存泄漏的风险

  • 下拉刷新,此时刷新完毕列表肯定就是在第一个item的位置不然就有点奇怪
  • 首次获取历史消息,此时的场景应该是列表最后一个item
  • 获取新消息,也是最后一个item
private val up = 1
private val down = 2
private val fail = 0
private val handler = object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: android.os.Message) {
        when (msg.what) {
            up -> {
                viewBinding.chatRecyclerview.scrollToPosition(0)
                viewBinding.swipeRefresh.isRefreshing = false
            }
            down ->{
                viewBinding.chatRecyclerview.scrollToPosition(viewModel.chatList.size-1)
            }
            fail -> {
                "刷新失败请检查网络".showToast()
                viewBinding.swipeRefresh.isRefreshing = false
            }
        }
    }
}
复制代码

2.消息的获取和RecycleView的刷新

消息部分设计成从新到老的设计,上述腾讯IM也是这个顺序,所以这部分添加列表时需要加在最前面

viewModel.chatList.add(0,msg)
adapter.notifyItemInserted(0)
复制代码

同时需要注意的就是刷新位置,这部分是插入故使用adapter中响应的notifyItemInserted方法进行提醒列表刷新,虽然直接使用最通用的notifyDataSetChanged也是可以达到相同的目的,但体验效果就不那么好了,如果是大量的数据,可能会产生比较大的延迟

3.关于消息item的设计细节

这个item具体是模仿QQ的布局进行设计的,这里底色部分没有做调整

6.jpg

La mejor parte que se puede optimizar es el tiempo. Puede juzgar el tiempo de la lista y luego darse cuenta del tiempo relativo como ayer, anteayer, etc. Aquí se usa el uso anidado de diseño de diseño linealyrestricciones wrap_content puede exceder la interfaz y habrá media palabra. Se supone que el ancho máximo de wrap_content es causado por el ancho del diseño raíz, por lo que finalmente un diseño está anidado Resuelto, el siguiente es el diagrama de marco del diseño

7.jpg

5. Interfaces y direcciones utilizadas por el proyecto

El proyecto web es relativamente complejo y se desarrolló sobre la base del anterior, es un poco difícil separarlo de forma independiente, por lo que el código del lado web no se pone aquí, el código del lado del cliente se proporciona aquí. Solo necesita reemplazar su propio sdkId y la URL relacionada con el lado del servidor. Al mismo tiempo, hay algunas interacciones relacionadas con el lado del servidor. Aquí hay una breve introducción a las interfaces que el lado del servidor necesita desarrollar.

  • Interfaz para la obtención de datos históricos

    Aquí hay dos parámetros, uno determina la cantidad de mensajes de extracción y el otro determina el punto de tiempo de inicio de extracción.

    //获取聊天记录
    @GET("chat/refreshes/{time}/{number}")
    fun getChat(@Path("time")time:String, @Path("number")count:Int): Call<MessageResponse>
    复制代码
  • Obtener la firma de usuario de Tencent IM

    //生成应用凭据
    @GET("imSig/{userId}/{expire}")
    fun getSig(@Path("userId")userId:String,@Path("expire")expire:Long):Call<String>
    复制代码

    También hay dos interfaces utilizadas por push, que se han descrito anteriormente

  • dirección del proyecto

    https://github.com/xyh-fu/ImTest.git
    复制代码
  • Dirección de la aplicación de demostración

    La aplicación aquí actualmente solo está abierta para dos identificaciones, si tiene amigos, puede probarla cara a cara

    https://res.dreamstudio.online/apk/imtest.apk
    复制代码

6. Resumen

Esta vez, el diseño de la mensajería instantánea de mensajería instantánea está lleno de cosecha, y no está mal obtener un nuevo punto de conocimiento (principalmente limitado por la pobreza). En la etapa posterior, puede considerar reemplazarlo con la mensajería instantánea de Tencent. Después de todo, ¿qué te das cuenta de que son solo pruebas y negocios a pequeña escala. Los productos siguen siendo muy diferentes. El lado del servidor implica un poco más, el lado del cliente es relativamente simple y el mecanismo de procesamiento de mensajes es más problemático. Teniendo en cuenta las diferentes interfaces diseñadas, así como la base de datos del lado del servidor, etc., es difícil de unificar. así que no los describiré uno por uno.

Supongo que te gusta

Origin juejin.im/post/7083017803388682270
Recomendado
Clasificación