One, foreword

Project group chat is to use the database directly before operation, the experience is very poor, the message is hard to get immediate feedback, so finally considered use tencent IM complete access group chat, but the way I still have a little bumpy, access to the complete version found experience after a group of about 20 people, saw experience version support 100 users also endure, Now a group chat can only 20 users, can not bear, so temporarily found WebSocket as a temporary solution (such as money to change), at the same time support 50 users online chat, is also ok, barely enough, the following two implementation of the access, the text is about to start ~~

2. Tencent IM access

The official website of Tencent Cloud IM, the access here extracts the API related to group chat, please refer to the documents for more information (if you have time, you can completely achieve a simple chat platform similar to QQ).

https://cloud.tencent.com/document/product/269/42440
Copy the code

1. Preparation

  • Demand analysis

    Need to achieve a similar QQ group chat function, only need to develop simple message receiving, sending messages, access to the history of these three simple functions

  • Create an

    I’m not going to show you this part, but it’s very simple, and it looks something like this

Experience version can support 100 users and a group chat 20 users, provide free cloud storage for 7 days, can create multiple IM instances at the same time, if it is to learn to use the experience version is enough, consider the professional version and flagship version for commercialization

  • Relying on the integration

    Integrate with Gradle, or you can integrate with the SDK, which is a new version of the SDK

    api 'com. Tencent. Imsdk: imsdk - plus: 6.1.2155'
    Copy the code

2. Initialization

Initialize the IM

  • Create an instance

    There is a callback in the argument, where Object is equivalent to an anonymous Java class

    val config = V2TIMSDKConfig()
    V2TIMManager.getInstance()
        .initSDK(this, sdkId, config, object : V2TIMSDKListener() {
            override fun onConnecting(a) {
                // Connecting to Tencent cloud server
                Log.e("im"."Connecting to Tencent Cloud server")}override fun onConnectSuccess(a) {
                // Tencent cloud server has been successfully connected
                Log.e("im"."Successfully connected to Tencent Cloud server")}override fun onConnectFailed(code: Int, error: String) {
                // Failed to connect to Tencent cloud server
                Log.e("im"."Failed to connect to Tencent cloud server")}})Copy the code
  • Generate login credentials

    This part of the official offer clients quickly generate the code and the server code, specific can go to the website to find, the start when the test can be considered the client code behind the formal project best deployed to the server for processing, this part of the reminder, the service side, there are two files, then never see clear, find the function for a long time, The Base64URL class is also used by other apis

Tools for generating and verifying credentials are also provided

The user login

This part just needs to pass in the parameters

V2TIMManager.getInstance().login(currentUser,sig, object : V2TIMCallback {
    override fun onSuccess(a) {
        Log.e("im"."${currentUser}Login successful")}override fun onError(code: Int, desc: String?). {
        Log.e("im"."${currentUser}Login failed. The error code is:${code}, specific errors:${desc}")}})Copy the code
  • CurrentUser is the USER ID
  • Sig is the user’s login credentials
  • V2TIMCallback is a class for the callback

3. Group chat is relevant

Create a group chat

There are several things to be aware of when creating a group chat

  • groupType

    Whether to approve or not, maximum number of users, whether to view group chat messages without joining the group, see the following figure

The community actually meets my needs, but there is a problem that I need to pay to open the community (which is quite expensive), so I finally choose the group of Meeting type

  • Group chat profile Settings

    GroupID (groupID) is not alphanumeric and special symbols (of course not Chinese) are ok, groupName (groupName), introduction (introduction), etc., there is also a set of initial members, can be added to the master administrator (here is a little confused is to create a group chat, No default creator added.

  • Create listener callbacks for group chats

    The parameters passed in here are groupInfo and memberInfoList, which are used to initialize the group chat and then have a callback to listen for the creation result

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) {
            // Failed to create
            Log.e("im"."Create failed"${code}Details:${desc}")}override fun onSuccess(groupID: String?). {
            // Created successfully
            Log.e("im"."The group id is${groupID}")}})Copy the code

Join the group chat

This part only requires a callback listener, and the reason there is no login user is that the default is to group with the current login ID, so a very important prerequisite is login

V2TIMManager.getInstance().joinGroup("Group chat ID"."Validate message".object :V2TIMCallback{
    override fun onSuccess(a) {
        Log.e("im"."Add group successfully")}override fun onError(p0: Int, p1: String?). {
        Log.e("im"."Group failure")}})Copy the code

4. Related to sending and receiving messages

Send a message

The advanced interface is used to send messages. Various message types are sent and customized message types are supported. Therefore, the advanced message sending and receiving interface is used

You create a message first, in this case a custom message, and any other message

val myMessage = "A piece of custom JSON data"

// Since the custom message is received as an argument of type byteArray, a conversion is performed
val messageCus= V2TIMManager.getMessageManager().createCustomMessage(myMessage.toByteArray())
Copy the code

To send a message, you need to set some parameters

“MessageCus” is the converted byte data, “toUserId” is the receiver, “groupId” is the ID of the group chat, “groupId” is the ID of the group chat, “weight” is the weight of your message to be received (it is not guaranteed that all messages will be received, Set onlineUserOnly to false, offlinePushInfo to null, and then send a callback

V2TIMManager.getMessageManager().sendMessage(messageCus,toUserId,groupId,weight,onlineUserOnly, offlinePushInfo,object:V2TIMSendCallback<V2TIMMessage>{
    override fun onSuccess(message: V2TIMMessage?). {
       	Log.e("im"."Sent successfully with the following content:${message? .customElem}")
        // We also need to parse the message ourselves, which needs to be converted to String data
        val data= String(message? .customElem? .data)... }override fun onError(p0: Int, p1: String?). {
        Log.e("im"."Error code:${p0}, specific errors:${p1}")}override fun onProgress(p0: Int) {
        Log.e("im"."Processing progress:${p0}")}})Copy the code

Get historical messages

  • GroupId is the group chat ID
  • PullNumber Indicates the number of pull messages
  • LastMessage is the lastMessage and is used to get the location of more messages
  • V2TIMValueCallback is message callback

This parameter can be set to a global variable, then initially set to null, and then set the lastMessage in the retrieved message list to 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"."There is no more news.")
               "There is no more news.".showToast()
           }else {
               // Record the last message
               lastMessage = p0[p0.size - 1]
               for (msgIndex in p0.indices) {
                   // Parse various messages
                   when(p0[msgIndex].elemType){
                       V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{
                           ...
                       }
                       V2TIMMessage.V2TIM_ELEM_TYPE_TEXT -> {}
                          ...
                       }
                       else -> {
                         ...
                       }
                   }  							
               }
           }
       }
    }
    override fun onError(p0: Int, p1: String?).{... }})Copy the code

Listening for new messages

This is mainly used for receiving and listening to new messages, while requiring their own parsing and related processing of various messages

V2TIMManager.getMessageManager().addAdvancedMsgListener(object:V2TIMAdvancedMsgListener(){
    override fun onRecvNewMessage(msg: V2TIMMessage?). {
        Log.e("im"."The new message${msg? .customElem}")

        // There are different ways to handle different message types
        when(msg? .elemType){ V2TIMMessage.V2TIM_ELEM_TYPE_CUSTOM ->{valmessage = msg.customElem? .data. } V2TIMMessage.V2TIM_ELEM_TYPE_TEXT ->{val message = msg.textElem.text
                ...
            }
            else- > {"Reception of this message is not supported at this time".showToast()
                Log.e("im"."${msg? .elemType}")}}}})Copy the code

Now that the access section is complete, this is just a brief introduction to access, and more details can be viewed in the project source code

3. WebSocket access

This requirement is the same as above, and it also provides API with similar functions to Tencent IM above. This part involves network related API (not very professional), mainly describes some ideas, and the specific code is not very difficult

1. Introduce the WebSocket

WebSocket can realize long connection, can be used as a tool for instant processing of message receiving, using WS or WSS protocol (SSL) for communication, Tencent IM version also launched a webSocket implementation scheme, webSocket mainly solves the pain point is that the server can not actively push messages, Replace the previous polling implementation

2. The server is related

The server uses SpringBoot for development and kotlin for programming

  • WebSoket relies on integration

    Here is gradle dependency integration

    implementation "org.springframework.boot:spring-boot-starter-websocket"
    Copy the code
  • The WebSocketConfig configuration is related

    @Configuration
    class WebSocketConfig {
        @Bean
        fun serverEndpointExporter(a): ServerEndpointExporter {
            return ServerEndpointExporter()
        }
    }
    Copy the code
  • WebSocketServer related

    This section of code is the key code, which overrides the four methods of webSocket, and then configates static variables and methods for global communication. A framework is given below

    @ServerEndpoint("/imserver/{userId}")
    @Component
    class WebSocketServer {
        @OnOpen
        fun onOpen(session: Session? .@PathParam("userId") userId: String){... }@OnClose
        fun onClose(a){... }@OnMessage
        fun onMessage(message: String, session: Session?).{... }@OnError
        fun onError(session: Session? , error:Throwable){... }// Solve the problem of @Component and @resource conflict and fail to automatically initialize
        @Resource
        fun setMapper(chatMapper: chatMapper){
            WebSocketServer.chatMapper = chatMapper
        }
        
        // This is the function used to send messages
        @Throws(IOException::class)
        fun sendMessage(message: String?).{ session!! .basicRemote.sendText(message) }// Static variables and methods
        companion object{... }}Copy the code

    companion object

    A key variable here is the webSocketMap that stores the user’s webSocket object, which will be used later to implement full and partial push of messages

    companion object {
        // Count the number of people online
        private var onlineCount: Int = 0
        
        // Store the webSocket object for each user
        val webSocketMap = ConcurrentHashMap<String, WebSocketServer>()
    
        // Delay initialization of the mapper object that operates on the database
        lateinit var chatMapper:chatMapper
        
        // An open method for the server to actively push messages
        @Throws(IOException::class)
        fun sendInfo(message: String.@PathParam("userId") userId: String) {
            if(userId.isNotBlank() && webSocketMap.containsKey(userId)) { webSocketMap[userId]? .sendMessage(message) }else {
                println("User$userId, not online!")}}// Online statistics
        @Synchronized
        fun addOnlineCount(a) {
            onlineCount++
        }
    
        // Offline statistics
        @Synchronized
        fun subOnlineCount(a) {
            onlineCount--
        }
    }
    Copy the code

    @OnOpen

    This method is executed when WebSocket is opened and performs some initialization and statistics

    @OnOpen
    fun onOpen(session: Session? .@PathParam("userId") userId: String) {
        this.session = session
        this.userId = userId
        if (webSocketMap.containsKey(userId)) {
            // If this id is included, a webSocket channel is opened elsewhere
            webSocketMap.remove(userId)
            webSocketMap[userId] = this
        } else {
            webSocketMap[userId] = this
            addOnlineCount()
        }
        println("User Connection:$userId, the current number of online users is:$onlineCount")}Copy the code

    @OnClose

    This method is called at the end of the webSocket channel and performs the logoff logic and related statistics

    @OnClose
    fun onClose(a) {
        if (webSocketMap.containsKey(userId)) {
            webSocketMap.remove(userId)
            subOnlineCount()
        }
        println("User logout:$userId, the current number of online users is:$onlineCount")}Copy the code

    @OnMessage

    This method is used to deal with message distribution, here generally need to do some processing of the message, specific processing refer to the processing of custom message, here is the design of group chat scheme, so adopt

    @OnMessage
    fun onMessage(message: String, session: Session?). {
        if (message.isNotBlank()) {
            // Parse the sent packet
            val newMessage = ...
            
            // Insert a piece of data to persist the message, that is, users who are not online can also see the message
            chatMapper.insert(newMessage)
            
            // Iterate over all messages
            webSocketMap.forEach { 
                it.value.sendMessage(sendMessage.toMyJson())
            }
        }
    }
    Copy the code

    @OnError

    The method that was incorrectly called

    @OnError
    fun onError(session: Session? , error:Throwable) {
        println("User error:$userIdThe reason:${error.message}")
        error.printStackTrace()
    }
    Copy the code

    sendMessage

    This method is called when messages are distributed to individual clients

    fun sendMessage(message: String?).{ session!! .basicRemote.sendText(message) }Copy the code
  • WebSocketController

    This part is mainly to achieve the server directly push message design, similar to the system message setting

    @PostMapping("/sendAll/{message}")
    fun sendAll(@PathVariable message: String):String{
        // Message processing
        val newMessage = ... 
        
        // Whether to store system messages depends on specific requirements
        WebSocketServer.webSocketMap.forEach { 
            WebSocketServer.sendInfo(sendMessage.toMyJson(), it.key)
        }
        
        return "ok"
    }
    
    @PostMapping("/sendC2C/{userId}/{message}")
    fun sendC2C(@PathVariable userId:String.@PathVariable message:String):String{
        // Message processing
        val newMessage = ... 
        
        WebSocketServer.sendInfo(newMessage, userId)
        return  "ok"
    }
    Copy the code

    This is the end of the server side of the explanation, let’s look at our Android client implementation

3. Client related

  • Relying on the integration

    Integrated Java language webSocket(rounded to Kotlin’s version)

    implementation 'org. Java - websocket: Java - websocket: 1.5.2'
    Copy the code
  • implementation

    This part of the rewrite method is similar to the server side, but without service-related processing, the code is much less. One thing to be reminded here is that these methods are run in child threads and do not allow direct writing of UI-related operations, so we need to use handle or runOnUIThread for processing

    val userSocket = object :WebSocketClient(URI("WSS :// server address: port number /imserver/${userId}")) {override fun onOpen(handshakedata: ServerHandshake?). {
            // open the initialization operation
        }
    
        override fun onMessage(message: String?).{...// Here do recyclerView update
        }
    
        override fun onClose(code: Int, reason: String? , remote:Boolean) {
           // Perform a notification operation here. }override fun onError(ex: Exception?).{... } } userSocket.connect()// Reconnect to reconnect
    // It is important to note that this operation cannot be performed in overriding methods
    userSocket.reconnect()
    Copy the code

    There are too many details can not be shown one by one, but in general is to imitate the Tencent IM implementation, specific can see the project address

Some details of the list design

Here’s a brief description of some of the details of list design, which can be quite tedious

1. The use of the handle

The update time and timing of the list depend on the specific network acquisition situation, so a global Handle is needed to process the messages in the list. Meanwhile, the sliding behavior of the list is different. A small problem that needs to be paid attention to is that it is better to send messages one by one, otherwise there may be the risk of memory leakage

  • Pull down refresh, and the refreshed list must be at the first item or it would be a little weird
  • The first time a history message is fetched, the scene should be the last item in the list
  • Get the new message, which is the last 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 -> {
                "Refresh failed please check network".showToast()
                viewBinding.swipeRefresh.isRefreshing = false}}}}Copy the code

2. Obtain messages and refresh RecycleView

The message part is designed from new to old, and Tencent IM is also in this order, so this part needs to be added at the top of the list

viewModel.chatList.add(0,msg)
adapter.notifyItemInserted(0)
Copy the code

Use the notifyItemInserted method that responds to the Adapter to refresh the list of reminders, although using the most generic notifyDataSetChanged will do the same thing, but the experience is not as good. If there is a large amount of data, there may be a significant delay

3. Design details about message item

This item is specifically designed to imitate the layout of QQ, and the background color is not adjusted here

The better part to optimize is the time. You can judge the time of the list and implement the relative time of yesterday, the day before yesterday, etc. The nested use of constraintLayout and linearLayout is used here. If you do not nest a different layout, the wrap_content will be filled out of the interface and will appear half a word. It is assumed that the maximum width of wrap_content is caused by the width of the root layout

V. Interface and address used by the project

The Web project is complicated and developed on the basis of the previous one, so it is a little difficult to separate it independently. Therefore, the code of the Web side is not put here, but the code of the client side is provided here, and it can run only by replacing its sdkId and the URL related to the server side. Meanwhile, there are some interactions related to the server side. Here is a brief description of the interfaces that need to be developed on the server side

  • Interface to get historical data

    There are two parameters, one to determine the number of pull messages, and one to determine the start time of the pull

    // Get chat records
    @GET("chat/refreshes/{time}/{number}")
    fun getChat(@Path("time")time:String.@Path("number")count:Int): Call<MessageResponse>
    Copy the code
  • Obtain Tencent IM user signature

    // Generate application credentials
    @GET("imSig/{userId}/{expire}")
    fun getSig(@Path("userId")userId:String.@Path("expire")expire:Long):Call<String>
    Copy the code

    There are also two interfaces that push uses, described earlier

  • The project address

    https://github.com/xyh-fu/ImTest.git
    Copy the code
  • Demo Application Address

    There are only two ids available in the app so you can test them face to face if you have friends

    https://res.dreamstudio.online/apk/imtest.apk
    Copy the code

Six, summarized

This time IM instant messaging design harvest is full, get a new knowledge point is also ok (mainly limited by poverty), later can consider all Tencent IM, after all, I realized only a small test and commercial products are still very different. The server side involves a little more, the client side is relatively simple, more troublesome is the message processing mechanism, considering the design of different interfaces, as well as the server side of the database and so on, it is difficult to unify, so not a description.