開源一個自用的Android IM庫,基於Netty+TCP+Protobuf實現。

歡迎轉載,轉載請註明出處:juejin.im/post/5c97ae…html

寫在前面

一直想寫一篇關於im即時通信分享的文章,無奈工做太忙,很難抽出時間。今天終於從公司離職了,打算好好休息幾天再從新找工做,趁時間空閒,決定靜下心來寫一篇文章,畢竟從前輩那裏學到了不少東西。工做了五年半,這三四年來一直在作社交相關的項目,有 直播即時通信短視頻分享社區論壇 等產品,深知即時通信技術在一個項目中的重要性,本着開源分享的精神,也趁這機會總結一下,因此寫下這篇文章,文中有不對之處歡迎批評與指正。java

本文將介紹:git

  • Protobuf序列化
  • TCP拆包與粘包
  • 長鏈接握手認證
  • 心跳機制
  • 重連機制
  • 消息重發機制
  • 讀寫超時機制
  • 離線消息
  • 線程池
  • AIDL跨進程通訊

本想花一部分時間介紹一下利用AIDL實現多進程通訊,提高應用保活率,無奈這種方法在目前大部分Android新版本上已失效,並且也比較複雜,因此考慮再三,把AIDL這一部分去掉,須要瞭解的童鞋能夠私信我。github

先來看看效果: 數據庫

最終運行效果

不想看文章的同窗能夠直接移步到Github fork源碼:github地址編程

接下來,讓咱們進入正題。json


爲何使用TCP?

這裏須要簡單解釋一下,TCP/UDP/WebSocket的區別。 這裏就很好地解釋了TCP/UDP的優缺點和區別,以及適用場景,簡單地總結一下:windows

  • 優勢:設計模式

    • TCP的優勢體如今穩定可靠上,在傳輸數據以前,會有三次握手來創建鏈接,並且在數據傳遞時,有確認、窗口、重傳、擁塞控制機制,在數據傳完以後,還會斷開鏈接用來節約系統資源。
    • UDP的優勢體如今比TCP稍安全,UDP沒有TCP擁有的各類機制,是一個無狀態的傳輸協議,因此傳遞數據很是快,沒有TCP的這些機制,被攻擊利用的機制就少一些,可是也沒法避免被攻擊。
  • 缺點:緩存

    • TCP缺點就是效率低佔用系統資源高易被攻擊,TCP在傳遞數據以前要先創建鏈接,這會消耗時間,並且在數據傳遞時,確認機制、重傳機制、擁塞機制等都會消耗大量時間,並且要在每臺設備上維護全部的傳輸鏈接。
    • UDP缺點就是不可靠不穩定,由於沒有TCP的那些機制,UDP在傳輸數據時,若是網絡質量很差,就會很容易丟包,形成數據的缺失。
  • 適用場景:

    • TCP:當對網絡通信質量有要求時,好比HTTP、HTTPS、FTP等傳輸文件的協議, POP、SMTP等郵件傳輸的協議。
    • UDP:對網絡通信質量要求不高時,要求網絡通信速度要快的場景。

至於WebSocket,後續可能會專門寫一篇文章來介紹。 綜上所述,決定採用TCP協議。


爲何使用Protobuf?

對於App網絡傳輸協議,咱們比較常見的、可選的,有三種,分別是json/xml/protobuf,老規矩,咱們先分別來看看這三種格式的優缺點:

  • 優勢:

    • json優勢就是較XML格式更加小巧,傳輸效率較xml提升了不少,可讀性還不錯。
    • xml優勢就是可讀性強,解析方便。
    • protobuf優勢就是傳輸效率快(聽說在數據量大的時候,傳輸效率比xml和json快10-20倍),序列化後體積相比Json和XML很小,支持跨平臺多語言,消息格式升級和兼容性還不錯,序列化反序列化速度很快。
  • 缺點:

    • json缺點就是傳輸效率也不是特別高(比xml快,但比protobuf要慢不少)。
    • xml缺點就是效率不高,資源消耗過大。
    • protobuf缺點就是使用不太方便。

在一個須要大量的數據傳輸的場景中,若是數據量很大,那麼選擇protobuf能夠明顯的減小數據量,減小網絡IO,從而減小網絡傳輸所消耗的時間。考慮到做爲一個主打社交的產品,消息數據量會很是大,同時爲了節約流量,因此採用protobuf是一個不錯的選擇。


爲何使用Netty?

首先,咱們來了解一下,Netty究竟是個什麼東西。網絡上找到的介紹:Netty是由JBOSS提供的基於Java NIO的開源框架,Netty提供異步非阻塞、事件驅動、高性能、高可靠、高可定製性的網絡應用程序和工具,可用於開發服務端和客戶端。

  • 爲何不用Java BIO?

    • 一鏈接一線程,因爲線程數是有限的,因此這樣很是消耗資源,最終也致使它不能承受高併發鏈接的需求。
    • 性能低,由於頻繁的進行上下文切換,致使CUP利用率低。
    • 可靠性差,因爲全部的IO操做都是同步的,即便是業務線程也如此,因此業務線程的IO操做也有可能被阻塞,這將致使系統過度依賴網絡的實時狀況和外部組件的處理能力,可靠性大大下降。
  • 爲何不用Java NIO?

    • NIO的類庫和API至關複雜,使用它來開發,須要很是熟練地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等。
    • 須要不少額外的編程技能來輔助使用NIO,例如,由於NIO涉及了Reactor線程模型,因此必須必須對多線程和網絡編程很是熟悉才能寫出高質量的NIO程序。
    • 想要有高可靠性,工做量和難度都很是的大,由於服務端須要面臨客戶端頻繁的接入和斷開、網絡閃斷、半包讀寫、失敗緩存、網絡阻塞的問題,這些將嚴重影響咱們的可靠性,而使用原生NIO解決它們的難度至關大。
    • JDK NIO中著名的BUG--epoll空輪詢,當select返回0時,會致使Selector空輪詢而致使CUP100%,官方表示JDK1.6以後修復了這個問題,其實只是發生的機率下降了,沒有根本上解決。
  • 爲何用Netty?

    • API使用簡單,更容易上手,開發門檻低
    • 功能強大,預置了多種編解碼功能,支持多種主流協議
    • 定製能力高,能夠經過ChannelHandler對通訊框架進行靈活地拓展
    • 高性能,與目前多種NIO主流框架相比,Netty綜合性能最高
    • 高穩定性,解決了JDK NIO的BUG
    • 經歷了大規模的商業應用考驗,質量和可靠性都有很好的驗證。

以上摘自:爲何要用Netty開發

  • 爲何不用第三方SDK,如:融雲、環信、騰訊TIM?
    這個就見仁見智了,有的時候,是由於公司的技術選型問題,由於用第三方的SDK,意味着消息數據須要存儲到第三方的服務器上,再者,可擴展性、靈活性確定沒有本身開發的要好,還有一個小問題,就是收費。好比,融雲免費版只支持100個註冊用戶,超過100就要收費,羣聊支持人數有限制等等...
    融雲收費

Mina其實跟Netty很像,大部分API都相同,由於是同一個做者開發的。但感受Mina沒有Netty成熟,在使用Netty的過程當中,出了問題很輕易地能夠找到解決方案,因此,Netty是一個不錯的選擇。

好了,廢話很少說,直接開始吧。


準備工做

  • 首先,咱們新建一個Project,在Project裏面再新建一個Android Library,Module名稱暫且叫作im_lib,如圖所示:

    新建項目

  • 而後,分析一下咱們的消息結構,每條消息應該會有一個消息惟一id,發送者id,接收者id,消息類型,發送時間等,通過分析,整理出一個通用的消息類型,以下:

    • msgId 消息id
    • fromId 發送者id
    • toId 接收者id
    • msgType 消息類型
    • msgContentType 消息內容類型
    • timestamp 消息時間戳
    • statusReport 狀態報告
    • extend 擴展字段

    根據上述所示,我整理了一個思惟導圖,方便你們參考:

    消息結構

    這是基礎部分,固然,你們也能夠根據本身須要自定義比較適合本身的消息結構。

    咱們根據自定義的消息類型來編寫proto文件。

    編寫proto文件
    而後執行命令(我用的mac,windows命令應該也差很少):
    執行protoc命令
    而後就會看到,在和proto文件同級目錄下,會生成一個java類,這個就是咱們須要用到的東東:
    生成的protobuf java類文件
    咱們打開瞄一眼:
    打開的protobuf java類文件
    東西比較多,不用去管,這是google爲咱們生成的protobuf類,直接用就行,怎麼用呢?直接用這個類文件,拷到咱們開始指定的項目包路徑下就能夠啦:
    導入protobuf java類文件到項目中
    添加依賴後,能夠看到,MessageProtobuf類文件已經沒有報錯了,順便把netty的jar包也導進來一下,還有fastjson的:
    導入protobuf以及netty的依賴
    建議用netty-all-x.x.xx.Final的jar包,後續熟悉了,能夠用精簡的jar包。

    至此,準備工做已結束,下面,咱們來編寫java代碼,實現即時通信的功能。


封裝

爲何須要封裝呢?說白了,就是爲了解耦,爲了方便往後切換到不一樣框架實現,而無需處處修改調用的地方。舉個栗子,好比Android早期比較流行的圖片加載框架是Universal ImageLoader,後期由於某些緣由,原做者中止了維護該項目,目前比較流行的圖片加載框架是Picasso或Glide,由於圖片加載功能可能調用的地方很是多,若是不做一些封裝,早期使用了Universal ImageLoader的話,如今須要切換到Glide,那改動量將很是很是大,並且還頗有可能會有遺漏,風險度很是高。

那麼,有什麼解決方案呢?

很簡單,咱們能夠用工廠設計模式進行一些封裝,工廠模式有三種:簡單工廠模式、抽象工廠模式、工廠方法模式。在這裏,我採用工廠方法模式進行封裝,具體區別,能夠參見:通俗講講我對簡單工廠、工廠方法、抽象工廠三種設計模式的理解

咱們分析一下,ims(IM Service,下文簡稱ims)應該是有初始化創建鏈接重連關閉鏈接釋放資源判斷長鏈接是否關閉發送消息等功能,基於上述分析,咱們能夠進行一個接口抽象:

抽象的ims接口1
抽象的ims接口2
OnEventListener是與應用層交互的listener:
OnEventListener
IMConnectStatusCallback是ims鏈接狀態回調監聽器:
IMConnectStatusCallback

而後寫一個Netty tcp實現類:

Netty tcp ims1
Netty tcp ims2

接下來,寫一個工廠方法:

ims實例工廠方法

封裝部分到此結束,接下來,就是實現了。


初始化

咱們先實現init(Vector serverUrlList, OnEventListener listener, IMSConnectStatusCallback callback)方法,初始化一些參數,以及進行第一次鏈接等:

初始化參數

其中,MsgDispatcher是消息轉發器,負責將接收到的消息轉發到應用層:

MsgDispatcher

ExecutorServiceFactory是線程池工廠,負責調度重連及心跳線程:

ExecutorServiceFactory1
ExecutorServiceFactory2
ExecutorServiceFactory3


鏈接及重連

resetConnect()方法做爲鏈接的起點,首次鏈接以及重連邏輯,都是在resetConnect()方法進行邏輯處理,咱們來瞄一眼:

resetConnect
能夠看到,非首次進行鏈接,也就是鏈接一個週期失敗後,進行重連時,會先讓線程休眠一段時間,由於這個時候也許網絡情況不太好,接着,判斷ims是否已關閉或者是否正在進行重連操做,因爲重連操做是在子線程執行,爲了不重複重連,須要進行一些併發處理。開始重連任務後,分四個步驟執行:

  • 改變重連狀態標識
  • 回調鏈接狀態到應用層
  • 關閉以前打開的鏈接channel
  • 利用線程池執行一個新的重連任務

ResetConnectRunnable是重連任務,核心的重連邏輯都放到這裏執行:

ResetConnectRunnable1
ResetConnectRunnable2
ResetConnectRunnable3

toServer()是真正鏈接服務器的地方:

toServer

initBootstrap()是初始化Netty Bootstrap:

initBootstrap
注:NioEventLoopGroup線程數設置爲4,能夠知足QPS是一百多萬的狀況了,至於應用若是須要承受上千萬上億流量的,須要另外調整線程數。參考自: netty實戰之百萬級流量NioEventLoopGroup線程數配置

接着,咱們來看看TCPChannelInitializerHanlder

TCPChannelInitializerHandler
其中, ProtobufEncoderProtobufDecoder是添加對protobuf的支持, LoginAuthRespHandler是接收到服務端握手認證消息響應的處理handler, HeartbeatRespHandler是接收到服務端心跳消息響應的處理handler, TCPReadHandler是接收到服務端其它消息後的處理handler,先不去管,咱們重點來分析下 LengthFieldPrependerLengthFieldBasedFrameDecoder,這就須要引伸到TCP的拆包與粘包啦。


TCP的拆包與粘包

  • 什麼是TCP拆包?爲何會出現TCP拆包?

    簡單地說,咱們都知道TCP是以「流」的形式進行數據傳輸的,並且TCP爲提升性能,發送端會將須要發送的數據刷入緩衝區,等待緩衝區滿了以後,再將緩衝區中的數據發送給接收方,同理,接收方也會有緩衝區這樣的機制,來接收數據。
    拆包就是在socket讀取時,沒有完整地讀取一個數據包,只讀取一部分。

  • 什麼是TCP粘包?爲何會出現TCP粘包?

    同上。
    粘包就是在socket讀取時,讀到了實際意義上的兩個或多個數據包的內容,同時將其做爲一個數據包進行處理。

引用網上一張圖片來解釋一下在TCP出現拆包、粘包以及正常狀態下的三種狀況,如侵請聯繫我刪除:

TCP拆包、粘包、正常狀態
瞭解了TCP出現拆包/粘包的緣由,那麼,如何解決呢?一般來講,有如下四種解決方式:

  • 消息定長
  • 用回車換行符做爲消息結束標誌
  • 用特殊分隔符做爲消息結束標誌,如\t、\n等,回車換行符其實就是特殊分隔符的一種。
  • 將消息分爲消息頭和消息體,在消息頭中用字段標識消息總長度。

netty針對以上四種場景,給咱們封裝瞭如下四種對應的解碼器:

  • FixedLengthFrameDecoder,定長消息解碼器
  • LineBasedFrameDecoder,回車換行符消息解碼器
  • DelimiterBasedFrameDecoder,特殊分隔符消息解碼器
  • LengthFieldBasedFrameDecoder,自定義長度消息解碼器。

咱們用到的就是LengthFieldBasedFrameDecoder自定義長度消息解碼器,同時配合LengthFieldPrepender編碼器使用,關於參數配置,建議參考netty--最通用TCP黏包解決方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender這篇文章,講解得比較細緻。咱們配置的是消息頭長度爲2個字節,因此消息包的最大長度須要小於65536個字節,netty會把消息內容長度存放消息頭的字段裏,接收方能夠根據消息頭的字段拿到此條消息總長度,固然,netty提供的LengthFieldBasedFrameDecoder已經封裝好了處理邏輯,咱們只須要配置lengthFieldOffset、lengthFieldLength、lengthAdjustment、initialBytesToStrip便可,這樣就能夠解決TCP的拆包與粘包,這也就是netty相較於原生nio的便捷性,原生nio須要本身處理拆包/粘包等問題。


長鏈接握手認證

接着,咱們來看看LoginAuthHandlerHeartbeatRespHandler

  • LoginAuthRespHandler是當客戶端與服務端長鏈接創建成功後,客戶端主動向服務端發送一條登陸認證消息,帶入與當前用戶相關的參數,好比token,服務端收到此消息後,到數據庫查詢該用戶信息,若是是合法有效的用戶,則返回一條登陸成功消息給該客戶端,反之,返回一條登陸失敗消息給該客戶端,這裏,就是在接收到服務端返回的登陸狀態後的處理handler,好比:

    LoginAuthRespHandler
    能夠看到,當接收到服務端握手消息響應後,會從擴展字段取出status,若是status=1,則表明握手成功,這個時候就先主動向服務端發送一條心跳消息,而後利用Netty的IdleStateHandler讀寫超時機制,按期向服務端發送心跳消息,維持長鏈接,以及檢測長鏈接是否還存在等。

  • HeartbeatRespHandler是當客戶端接收到服務端登陸成功的消息後,主動向服務端發送一條心跳消息,心跳消息能夠是一個空包,消息包體越小越好,服務端收到客戶端的心跳包後,原樣返回給客戶端,這裏,就是收到服務端返回的心跳消息響應的處理handler,好比:

    HeartbeatRespHandler
    這個就比較簡單,收到心跳消息響應,無需任務處理,直接打印一下方便咱們分析便可。


心跳機制及讀寫超時機制

心跳包是按期發送,也能夠本身定義一個週期,好比Android微信智能心跳方案,爲了簡單,此處規定應用在前臺時,8秒發送一個心跳包,切換到後臺時,30秒發送一次,根據本身的實際狀況修改一下便可。心跳包用於維持長鏈接以及檢測長鏈接是否斷開等。

接着,咱們利用Netty的讀寫超時機制,來實現一個心跳消息管理handler:

HeartbeatHandler
能夠看到,利用 userEventTriggered()方法回調,經過IdleState類型,能夠判斷讀超時/寫超時/讀寫超時,這個在添加 IdleStateHandler時能夠配置,下面會貼上代碼。首先咱們能夠在READER_IDLE事件裏,檢測是否在規定時間內沒有收到服務端心跳包響應,若是是,那就觸發重連操做。在WRITER_IDEL事件能夠檢測客戶端是否在規定時間內沒有向服務端發送心跳包,若是是,那就主動發送一個心跳包。發送心跳包是在子線程中執行,咱們能夠利用以前寫的work線程池進行線程管理。
addHeartbeatHandler()代碼以下:
addHeartbeatHandler
從圖上可看到,在 IdleStateHandler裏,配置的讀超時爲心跳間隔時長的3倍,也就是3次心跳沒有響應時,則認爲長鏈接已斷開,觸發重連操做。寫超時則爲心跳間隔時長,意味着每隔heartbeatInterval會發送一個心跳包。讀寫超時沒用到,因此配置爲0。

onConnectStatusCallback(int connectStatus)爲鏈接狀態回調,以及一些公共邏輯處理:

onConnectStatusCallback
鏈接成功後,當即發送一條握手消息,再次梳理一下總體流程:

  • 客戶端根據服務端返回的host及port,進行第一次鏈接。
  • 鏈接成功後,客戶端向服務端發送一條握手認證消息(1001)
  • 服務端在收到客戶端的握手認證消息後,從擴展字段裏取出用戶token,到本地數據庫校驗合法性。
  • 校驗完成後,服務端把校驗結果經過1001消息返回給客戶端,也就是握手消息響應。
  • 客戶端收到服務端的握手消息響應後,從擴展字段取出校驗結果。若校驗成功,客戶端向服務端發送一條心跳消息(1002),而後進入心跳發送週期,按期間隔向服務端發送心跳消息,維持長鏈接以及實時檢測鏈路可用性,若發現鏈路不可用,等待一段時間觸發重連操做,重連成功後,從新開始握手/心跳的邏輯。

看看TCPReadHandler收到消息是怎麼處理的:

TCPReadHandler1
TCPReadHandler2
能夠看到,在 channelInactive()及 exceptionCaught()方法都觸發了重連, channelInactive()方法在當鏈路斷開時會調用, exceptionCaught()方法在當出現異常時會觸發,另外,還有諸如 channelUnregistered()、 channelReadComplete()等方法能夠重寫,在這裏就不貼了,相信聰明的你一眼就能看出方法的做用。
咱們仔細看一下channelRead()方法的邏輯,在if判斷裏,先判斷消息類型,若是是服務端返回的消息發送狀態報告類型,則判斷消息是否發送成功,若是發送成功,從超時管理器中移除,這個超時管理器是幹嗎的呢?下面講到消息重發機制的時候會詳細地講。在else裏,收到其餘消息後,會立馬給服務端返回一個消息接收狀態報告,告訴服務端,這條消息我已經收到了,這個動做,對於後續須要作的離線消息會有做用。若是不須要支持離線消息功能,這一步能夠省略。最後,調用消息轉發器,把接收到的消息轉發到應用層便可。

代碼寫了這麼多,咱們先來看看運行後的效果,先貼上缺失的消息發送代碼及ims關閉代碼以及一些默認配置項的代碼。
發送消息:

發送消息
關閉ims:
關閉ims
ims默認配置:
ims默認配置
還有,應用層實現的ims client啓動器:
IMSClientBootstrap
因爲代碼有點多,不太方便所有貼上,若是有興趣能夠下載demo體驗。 額,對了,還有一個簡易的服務端代碼,以下:
NettyServerDemo1
NettyServerDemo2
NettyServerDemo3


調試

咱們先來看看鏈接及重連部分(因爲錄製gif比較麻煩,體積較大,因此我先把重連間隔調小成3秒,方便看效果)。

  • 啓動服務端:
    啓動服務端
  • 啓動客戶端:
    啓動客戶端
    能夠看到,正常的狀況下已經鏈接成功了,接下來,咱們來試一下異常狀況,好比服務端沒啓動,看看客戶端的重連狀況:
    調試重連
    此次咱們先啓動的是客戶端,能夠看到鏈接失敗後一直在進行重連,因爲錄製gif比較麻煩,在第三次鏈接失敗後,我啓動了服務端,這個時候客戶端就會重連成功。

而後,咱們再來調試一下握手認證消息即心跳消息:

握手消息及心跳消息測試
能夠看到,長鏈接創建成功後,客戶端會給服務端發送一條握手認證消息(1001),服務端收到握手認證消息會,給客戶端返回了一條握手認證狀態消息,客戶端收到握手認證狀態消息後,即啓動心跳機制。gif不太好演示,下載demo就能夠直觀地看到。

接下來,在講完消息重發機制及離線消息後,我會在應用層作一些簡單的封裝,以及在模擬器上運行,這樣就能夠很直觀地看到運行效果。


消息重發機制

消息重發,顧名思義,即便對發送失敗的消息進行重發。考慮到網絡環境的不穩定性、多變性(好比從進入電梯、進入地鐵、移動網絡切換到wifi等),在消息發送的時候,發送失敗的機率其實不小,這時消息重發機制就頗有必要了。
咱們先來看看實現的代碼邏輯。 MsgTimeoutTimer:

MsgTimeoutTimer1
MsgTimeoutTimer2
MsgTimeoutTimerManager:
MsgTimeoutTimerManager1
MsgTimeoutTimerManager2

而後,咱們看看收消息的TCPReadHandler的改造:
加入消息重發機制的TCPReadHandler
最後,看看發送消息的改造:
加入消息重發機制的發送消息

說一下邏輯吧:發送消息時,除了心跳消息、握手消息、狀態報告消息外,消息都加入消息發送超時管理器,立馬開啓一個定時器,好比每隔5秒執行一次,共執行3次,在這個週期內,若是消息沒有發送成功,會進行3次重發,達到3次重發後若是仍是沒有發送成功,那就放棄重發,移除該消息,同時經過消息轉發器通知應用層,由應用層決定是否再次重發。若是消息發送成功,服務端會返回一個消息發送狀態報告,客戶端收到該狀態報告後,從消息發送超時管理器移除該消息,同時中止該消息對應的定時器便可。
另外,在用戶握手認證成功時,應該檢查消息發送超時管理器裏是否有發送超時的消息,若是有,則所有重發:

握手認證成功檢查是否有發送超時的消息


離線消息

因爲離線消息機制,須要服務端數據庫及緩存上的配合,代碼就不貼了,太多太多,我簡單說一下實現思路吧: 客戶端A發送消息到客戶端B,消息會先到服務端,由服務端進行中轉。這個時候,客戶端B存在兩種狀況:

  • 1.長鏈接正常,就是客戶端網絡環境良好,手機有電,應用處在打開的狀況。
  • 2.廢話,那確定就是長鏈接不正常咯。這種狀況有不少種緣由,好比wifi不可用、用戶進入了地鐵或電梯等網絡很差的場所、應用沒打開或已退出登陸等,總的來講,就是沒有辦法正常接收消息。

若是是長鏈接正常,那沒什麼可說的,服務端直接轉發便可。
若是長鏈接不正常,須要這樣處理:服務端接收到客戶端A發送給客戶端B的消息後,先給客戶端A回覆一條狀態報告,告訴客戶端A,我已經收到消息,這個時候,客戶端A就不用管了,消息只要到達服務端便可。而後,服務端先嚐試把消息轉發到客戶端B,若是這個時候客戶端B收到服務端轉發過來的消息,須要立馬給服務端回一條狀態報告,告訴服務端,我已經收到消息,服務端在收到客戶端B返回的消息接收狀態報告後,即認爲此消息已經正常發送,不須要再存庫。若是客戶端B不在線,服務端在作轉發的時候,並無收到客戶端B返回的消息接收狀態報告,那麼,這條消息就應該存到數據庫,直到客戶端B上線後,也就是長鏈接創建成功後,客戶端B主動向服務端發送一條離線消息詢問,服務端在收到離線消息詢問後,到數據庫或緩存去查客戶端B的全部離線消息,並分批次返回,客戶端B在收到服務端的離線消息返回後,取出消息id(如有多條就取id集合),經過離線消息應答把消息id返回到服務端,服務端收到後,根據消息id從數據庫把對應的消息刪除便可。
以上是單聊離線消息處理的狀況,羣聊有點不一樣,羣聊的話,是須要服務端確認羣組內全部用戶都收到此消息後,才能從數據庫刪除消息,就說這麼多,若是須要細節的話,能夠私信我。


不知不覺,NettyTcpClient中定義了不少變量,爲了防止你們不明白變量的定義,仍是貼上代碼吧:

定義了不少變量的NettyTcpClient

應用層封裝

這個就見仁見智啦,每一個人代碼風格不一樣,我把本身簡單封裝的代碼貼上來吧:
MessageProcessor消息處理器:

MessageProcessor1
MessageProcessor2
IMSEventListener與ims交互的listener:
IMSEventListener1
IMSEventListener2
IMSEventListener3
MessageBuilder消息轉換器:
MessageBuilder1
MessageBuilder2
MessageBuilder3
AbstractMessageHandler抽象的消息處理handler,每一個消息類型對應不一樣的messageHandler:
AbstractMessageHandler
SingleChatMessageHandler單聊消息處理handler:
SingleChatMessageHandler
GroupChatMessageHandler羣聊消息處理handler:
GroupChatMessageHandler
MessageHandlerFactory消息handler工廠:
MessageHandlerFactory
MessageType消息類型枚舉:
MessageType
IMSConnectStatusListenerIMS鏈接狀態監聽器:
IMSConnectStatusListener
因爲每一個人代碼風格不一樣,封裝代碼都有本身的思路,因此,在此就不過多講解,只是把本身簡單封裝的代碼所有貼上來,做一個參考便可。只須要知道,接收到消息時,會回調OnEventListener的dispatchMsg(MessageProtobuf.Msg msg)方法:
應用層接收ims消息入口
發送消息須要調用imsClient的sendMsg(MessageProtobuf.Msg msg)方法:
應用層調用ims發送消息入口
便可,至於怎樣去封裝得更好,你們自由發揮吧。


最後,爲了測試消息收發是否正常,咱們須要改動一下服務端:

改動後的服務端1
改動後的服務端2
改動後的服務端3
改動後的服務端4
改動後的服務端5
能夠看到,當有用戶握手成功後,會保存該用戶對應的channel到容器裏,給用戶發送消息時,根據用戶id從容器裏取出對應的channel,利用該channel發送消息。當用戶斷開鏈接後,會把該用戶對應的channel從容器裏移除掉。

運行一下,看看效果吧:

最終運行效果

  • 首先,啓動服務端。
  • 而後,修改客戶端鏈接的ip地址爲192.168.0.105(這是我本機的ip地址),端口號爲8855,fromId,也就是userId,定義成100001,toId爲100002,啓動客戶端A。
  • 再而後,fromId,也就是userId,定義成100002,toId爲100001,啓動客戶端B。
  • 客戶端A給客戶端B發送消息,能夠看到在客戶端B的下面,已經接收到了消息。
  • 用客戶端B給客戶端A發送消息,也能夠看到在客戶端A的下面,也已經接收到了消息。 至於,消息收發測試成功。至於羣聊或重連等功能,就不一一演示了,仍是那句話,下載demo體驗一下吧。。。

因爲gif錄製體積較大,因此只能簡單演示一下消息收發,具體下載demo體驗吧。。。

若是有須要應用層UI實現(就是聊天頁及會話頁的封裝)的話,我再分享出來吧。

github地址




發現的bug

  1. MsgTimeoutTimer
    MsgTimeoutTimer bug1
    這個bug是本身在檢查代碼時發現的,多是連續熬幾天夜寫文章魔怔了。。。 修改以下:
    MsgTimeoutTimer bug1 fix

一我的精力有限,你們在使用過程當中,若是發現其它bug,煩請告訴我,反正我是會虛心接受,堅定不改,呸,必定改,必定改。另外,歡迎fork,期待你們與我一塊兒完善。。。




寫在最後

終於寫完了,這篇文章大概寫了10天左右,有很大部分的緣由是本身有拖延症,每次寫完一小段,總靜不下心來寫下去,致使一直拖到如今,之後得改改。第一次寫技術分享文章,有不少地方也許邏輯不太清晰,因爲篇幅有限,也只是貼了部分代碼,建議你們把源碼下載下來看看。一直想寫這篇文章,之前在網上也嘗試過找過不少im方面的文章,都找不到一篇比較完善的,本文談不上完善,但包含的模塊不少,但願起到一個拋磚引玉的做用,也期待着你們跟我一塊兒發現更多的問題並完善,最後,若是這篇文章對你有用,但願在github上給我一個star哈。。。

應你們要求,精簡了netty-all-4.1.33.Final.jar包。原netty-all-4.1.33.Final.jar包大小爲3.9M,經測試發現目前im_lib庫只須要用到如下jar包:

  • netty-buffer-4.1.33.Final.jar
  • netty-codec-4.1.33.Final.jar
  • netty-common-4.1.33.Final.jar
  • netty-handler-4.1.33.Final.jar
  • netty-resolver-4.1.33.Final.jar
  • netty-transport-4.1.33.Final.jar

因此,抽取以上jar包,從新打成了netty-tcp-4.1.33-1.0.jar,目前自測沒有問題,若是發現bug,請告訴我,謝謝。

附上原jar及裁剪後jar包的大小對比:

原netty-all-4.1.33.Final.jar大小
裁剪後netty-tcp-4.1.33-1.0.jar大小
代碼已更新到Github.


接下來,會抽時間把下圖想寫的文章都寫了,沒有前後順序,想到哪就寫到哪吧。。。

想寫的文章

另外,建立了一個Android即時通信技術交流QQ羣:1015178804,有須要的同窗能夠加進來,不懂的問題,我會盡可能解答,一塊兒學習,一塊兒成長。

The end.

相關文章
相關標籤/搜索