首頁 > 軟體

Android即時通訊設計(騰訊IM接入和WebSocket接入)

2022-04-06 19:03:21

一、前言

之前專案的群聊是用資料庫直接操作的,體驗很差,訊息很難即時反饋,所以最後考慮到了使用騰訊的IM完成群聊的接入,不過中途還是有點小坎坷的,接入完成之後發現體驗版一個群聊只有20人,當時看到體驗版支援100個使用者也就忍了,現在一個群聊只能20使用者,忍不了了,所以暫時找到了WebSocket作為臨時的解決方案(等有錢了再換),同時支援50個使用者線上聊天,也算還行,勉強夠用,下面就介紹兩種實現方案的接入

二、騰訊IM接入

騰訊雲IM的官網,這裡的接入將其中群聊相關的api抽取出來,更多請看檔案(如果有時間的話,完全可以實現一個類似QQ的簡單聊天平臺)

1.準備工作

需求分析

需要實現一個類似QQ中群聊的功能,只需要開發簡單的接收訊息,傳送訊息,獲取歷史記錄這三個簡單的功能即可

建立應用

這部分就不演示了,很簡單,建立好大概是下圖的樣子

體驗版可以支援100個使用者和一個群聊20個使用者,提供免費的雲端儲存7天,同時可以建立多個IM範例,如果是學習使用的話體驗版足夠了,商業化考慮專業版和旗艦版

依賴整合

使用gradle整合,也可以使用sdk整合,這裡採用新版的sdk進行整合

api 'com.tencent.imsdk:imsdk-plus:6.1.2155'

2.初始化工作

初始化IM

建立範例

引數中有一個回撥,這裡的object相當於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", "連線騰訊雲伺服器失敗")
        }
    })

生成登入憑據

這部分官方提供使用者端快速生成的程式碼和伺服器端程式碼,具體可以到官網找找,一開始測試的時候可以考慮使用者端程式碼後面正式的專案最好部署到伺服器端進行處理,這部分就提個醒,伺服器端有兩個檔案,當時沒看清楚,找了好久的函數,最後發現是某個java檔案忘記看了,還是同一級目錄下,應該是其他api也複用了Base64URL這個類

同時官方還提供生成和校驗憑據的工具

使用者登入

這部分只需要傳入引數即可

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 即使用者的id
  • sig 即使用者的登入憑據
  • V2TIMCallback 回撥的一個類

3.群聊相關

建立群聊

建立群聊的時候需要注意幾個方面的問題

群聊類別(groupType)

需要審批還是不需要,最大的容納使用者數,未支不支援未入群檢視群聊訊息,詳見下圖

其中社群其實挺符合我的需求的,但有個問題,社群需要付費才能開通(還挺貴),所以最後選擇了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主要解決的痛點就是伺服器端不能主動推播訊息,代替之前輪詢的實現方案

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的佈局進行設計的,這裡底色部分沒有做調整

可以優化的更好的部分就是時間,可以對列表時間進行判斷,然後實現類似昨天,前天等等的相對時間,這裡使用的是constraintlayoutlinearlayout的巢狀使用,這裡當時遇到一個問題即文字需要自適應列表,如果沒有另外巢狀一個佈局就會導致wrap_content的填充方式可能會超出介面,出現半個字的情況,猜測wrap_content最大的寬度是根佈局的寬度導致的,所以最後巢狀了一個佈局解決了,下面是設計的框架圖

五、專案使用的介面和地址

web專案比較複雜,是在之前的基礎上開發的,獨立抽離出來有點困難,所以這裡就不放web端的程式碼,這裡提供使用者端的程式碼,只需要替換自己的sdkId和伺服器端相關的url即可執行,同時這裡涉及一些與伺服器端有關的互動,這裡簡單介紹一下伺服器端需要開發的介面

獲取歷史資料的介面

這裡兩個引數,一個確定拉取訊息數目,一個確定拉取起始時間點

//獲取聊天記錄
@GET("chat/refreshes/{time}/{number}")
fun getChat(@Path("time")time:String, @Path("number")count:Int): Call<MessageResponse>

獲取騰訊IM的user簽名

//生成應用憑據
@GET("imSig/{userId}/{expire}")
fun getSig(@Path("userId")userId:String,@Path("expire")expire:Long):Call<String>

還有兩個推播使用的介面,在前面已經敘述過了

專案地址:https://github.com/xyh-fu/ImTest.git

六、總結

這次IM即時通訊的設計收穫滿滿,get到一個新的知識點也算還行(主要是貧窮限制的),後期可以考慮全部換成騰訊的IM,畢竟自己實現的只是小規模測試和商業產品還是有很大的區別。伺服器端涉及的稍微多一點點,使用者端是比較簡單,比較麻煩的就是訊息處理機制,考慮到設計的介面各異,還有伺服器端的資料庫等等,難以統一,故不一一展開敘述。

到此這篇關於Android即時通訊設計(騰訊IM接入和WebSocket接入)的文章就介紹到這了,更多相關Android即時通訊設計內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com