Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 499, comments - 13, trackbacks - 0, articles - 1

          導航

          公告


            ① 即時通訊開發社區
            地址: 52im.net
            專業的資料、社區

            ② 關注我的公眾號:

            讓技術不再封閉

            ③ 我的Github
            地址: 點此進入
            好代碼,與大家分享
          <2019年7月>
          30123456
          78910111213
          14151617181920
          21222324252627
          28293031123
          45678910

          常用鏈接

          留言簿(288)

          隨筆檔案

          文章檔案

          搜索

          •  

          最新評論

          閱讀排行榜

          評論排行榜

          60天內閱讀排行

          本文由作者FreddyChen原創分享,為了更好的體現文章價值,引用時有少許改動,感謝原作者。

          1、寫在前面

          一直想寫一篇關于im即時通訊分享的文章,無奈工作太忙,很難抽出時間。今天終于從公司離職了,打算好好休息幾天再重新找工作,趁時間空閑,決定靜下心來寫一篇文章,畢竟從前輩那里學到了很多東西。

          工作了五年半,這三四年來一直在做社交相關的項目,有直播、即時通訊、短視頻分享、社區論壇等產品,深知即時通訊技術在一個項目中的重要性,本著開源分享的精神,也趁這機會總結一下,所以寫下了這篇文章。

          * 重要提示:本文不是一篇即時通訊理論文章,文章內容全部由實戰代碼組織而成,如果你對即時通訊(IM)技術理論了解的太少,建議先詳細閱讀:《新手入門一篇就夠:從零開發移動端IM》。

          本文實踐內容將涉及以下即時通訊技術內容:

          1)Protobuf序列化;

          2)TCP拆包與粘包;

          3)長連接握手認證;

          4)心跳機制;

          5)重連機制;

          6)消息重發機制;

          7)讀寫超時機制;

          8)離線消息;

          9)線程池。

          不想看文章的同學,可以直接到Github下載本文源碼:

          1)原始地址:https://github.com/FreddyChen/NettyChat

          2)備用地址:https://github.com/52im/NettyChat

          接下來,讓我們進入正題。

          (本文同步發布于:http://www.52im.net/thread-2671-1-1.html

          2、本文閱讀對象

          本文適合沒有任何即時通訊(IM)開發經驗的小白開發者閱讀,文章將教你從零開始,圍繞一個典型即時通訊(IM)系統的方方面面,手把手為你展示如何基于Netty+TCP+Protobuf來開發出這樣的系統。非常適合從零入門的Android開發者。

          本文不適合沒有編程的準開發者閱讀,因為即時通訊(IM)系統屬于特定的業務領域,如果你連一般的邏輯代碼都很難編寫出來,不建議閱讀本文。本文顯然不是一個編程語言入門教程。

          3、關于作者

          本文原文內容由FreddyChen原創分享,作者現從事Android程序開發,他的技術博客地址:https://juejin.im/user/5bd7affbe51d4547f763fe72 

          4、為什么使用TCP?

          這里需要簡單解釋一下,TCP/UDP的區別,簡單地總結一下。

          優點:

          1)TCP:優點體現在穩定、可靠上,在傳輸數據之前,會有三次握手來建立連接,而且在數據傳遞時,有確認、窗口、重傳、擁塞控制機制,在數據傳完之后,還會斷開連接用來節約系統資源。

          2)UDP:優點體現在快,比TCP稍安全,UDP沒有TCP擁有的各種機制,是一個無狀態的傳輸協議,所以傳遞數據非常快,沒有TCP的這些機制,被攻擊利用的機制就少一些,但是也無法避免被攻擊。

          缺點:

          1)TCP:缺點就是慢,效率低,占用系統資源高,易被攻擊,TCP在傳遞數據之前要先建立連接,這會消耗時間,而且在數據傳遞時,確認機制、重傳機制、擁塞機制等都會消耗大量時間,而且要在每臺設備上維護所有的傳輸連接。

          2)UDP:缺點就是不可靠,不穩定,因為沒有TCP的那些機制,UDP在傳輸數據時,如果網絡質量不好,就會很容易丟包,造成數據的缺失。

          適用場景:

          1)TCP:當對網絡通訊質量有要求時,比如HTTP、HTTPS、FTP等傳輸文件的協議, POP、SMTP等郵件傳輸的協議。

          2)UDP:對網絡通訊質量要求不高時,要求網絡通訊速度要快的場景。

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

          關于TCP和UDP的對比和選型的詳細文章,請見:

          簡述傳輸層協議TCP和UDP的區別

          為什么QQ用的是UDP協議而不是TCP協議?

          移動端即時通訊協議選擇:UDP還是TCP?

          網絡編程懶人入門(四):快速理解TCP和UDP的差異

          網絡編程懶人入門(五):快速理解為什么說UDP有時比TCP更有優勢

          Android程序員必知必會的網絡通信傳輸層協議——UDP和TCP

          或者,如果你對TCP、UDP協議了解的太少,可以閱讀一下文章:

          TCP/IP詳解 - 第11章·UDP:用戶數據報協議

          TCP/IP詳解 - 第17章·TCP:傳輸控制協議

          TCP/IP詳解 - 第18章·TCP連接的建立與終止

          TCP/IP詳解 - 第21章·TCP的超時與重傳

          腦殘式網絡編程入門(一):跟著動畫來學TCP三次握手和四次揮手

          技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)

          通俗易懂-深入理解TCP協議(上):理論基礎

          網絡編程懶人入門(三):快速理解TCP協議一篇就夠

          邁向高階:優秀Android程序員必知必會的網絡基礎

          5、為什么使用Protobuf?

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

          PS:如果你不了解protobuf是什么,建議詳細閱讀:《Protobuf通信協議詳解:代碼演示、詳細原理介紹等》。

          優點:

          1)json:優點就是較XML格式更加小巧,傳輸效率較xml提高了很多,可讀性還不錯。

          2)xml:優點就是可讀性強,解析方便。

          3)protobuf:優點就是傳輸效率快(據說在數據量大的時候,傳輸效率比xml和json快10-20倍),序列化后體積相比Json和XML很小,支持跨平臺多語言,消息格式升級和兼容性還不錯,序列化反序列化速度很快。

          缺點:

          1)json:缺點就是傳輸效率也不是特別高(比xml快,但比protobuf要慢很多)。

          2)xml:缺點就是效率不高,資源消耗過大。

          3)protobuf:缺點就是使用不太方便。

          在一個需要大量的數據傳輸的場景中,如果數據量很大,那么選擇protobuf可以明顯的減少數據量,減少網絡IO,從而減少網絡傳輸所消耗的時間。考慮到作為一個主打社交的產品,消息數據量會非常大,同時為了節約流量,所以采用protobuf是一個不錯的選擇。

          更多有關IM相關的協議格式選型方面的文章,可進一步閱讀:

          如何選擇即時通訊應用的數據傳輸格式

          強列建議將Protobuf作為你的即時通訊應用數據傳輸格式

          全方位評測:Protobuf性能到底有沒有比JSON快5倍?

          移動端IM開發需要面對的技術問題(含通信協議選擇)

          簡述移動端IM開發的那些坑:架構設計、通信協議和客戶端

          理論聯系實際:一套典型的IM通信協議設計詳解

          58到家實時消息系統的協議設計等技術實踐分享

          詳解如何在NodeJS中使用Google的Protobuf

          技術掃盲:新一代基于UDP的低延時網絡傳輸層協議——QUIC詳解

          金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(原理篇)

          金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(實戰篇)

          >> 更多同類文章 ……

          6、為什么使用Netty?

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

          PS:如果你對Java的經典IO、NIO或者Netty框架不了解,請閱讀以下文章:

          史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

          少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的區別

          寫給初學者:Java高性能NIO框架Netty的學習方法和進階策略

          NIO框架詳解:Netty的高性能之道

          為什么不用Java BIO?

          1)一連接一線程:由于線程數是有限的,所以這樣非常消耗資源,最終也導致它不能承受高并發連接的需求。

          2)性能低:因為頻繁的進行上下文切換,導致CUP利用率低。

          3)可靠性差:由于所有的IO操作都是同步的,即使是業務線程也如此,所以業務線程的IO操作也有可能被阻塞,這將導致系統過分依賴網絡的實時情況和外部組件的處理能力,可靠性大大降低。

          為什么不用Java NIO?

          1)NIO的類庫和API相當復雜,使用它來開發,需要非常熟練地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等。

          2)需要很多額外的編程技能來輔助使用NIO,例如,因為NIO涉及了Reactor線程模型,所以必須必須對多線程和網絡編程非常熟悉才能寫出高質量的NIO程序。

          3)想要有高可靠性,工作量和難度都非常的大,因為服務端需要面臨客戶端頻繁的接入和斷開、網絡閃斷、半包讀寫、失敗緩存、網絡阻塞的問題,這些將嚴重影響我們的可靠性,而使用原生NIO解決它們的難度相當大。

          4)JDK NIO中著名的BUG--epoll空輪詢,當select返回0時,會導致Selector空輪詢而導致CUP100%,官方表示JDK1.6之后修復了這個問題,其實只是發生的概率降低了,沒有根本上解決。

          為什么用Netty?

          1)API使用簡單,更容易上手,開發門檻低;

          2)功能強大,預置了多種編解碼功能,支持多種主流協議;

          3)定制能力高,可以通過ChannelHandler對通信框架進行靈活地拓展;

          4)高性能,與目前多種NIO主流框架相比,Netty綜合性能最高;

          5)高穩定性,解決了JDK NIO的BUG;

          6)經歷了大規模的商業應用考驗,質量和可靠性都有很好的驗證。

          為什么不用第三方SDK,如:融云、環信、騰訊TIM?

          這個就見仁見智了,有的時候,是因為公司的技術選型問題,因為用第三方的SDK,意味著消息數據需要存儲到第三方的服務器上,再者,可擴展性、靈活性肯定沒有自己開發的要好,還有一個小問題,就是收費。比如,融云免費版只支持100個注冊用戶,超過100就要收費,群聊支持人數有限制等等...

          ▲ 以上截圖內容來自某云IM官網

          Mina其實跟Netty很像,大部分API都相同,因為是同一個作者開發的。但感覺Mina沒有Netty成熟,在使用Netty的過程中,出了問題很輕易地可以找到解決方案,所以,Netty是一個不錯的選擇。

          PS:有關MINA和Netty框架的關系和對比,詳見以下文章:

          有關“為何選擇Netty”的11個疑問及解答

          開源NIO框架八卦——到底是先有MINA還是先有Netty?

          選Netty還是Mina:深入研究與對比(一)

          選Netty還是Mina:深入研究與對比(二)

          好了,廢話不多說,直接開始吧。

          7、準備工作

          首先,我們新建一個Project,在Project里面再新建一個Android Library,Module名稱暫且叫做im_lib,如圖所示:

          然后,分析一下我們的消息結構,每條消息應該會有一個消息唯一id,發送者id,接收者id,消息類型,發送時間等,經過分析,整理出一個通用的消息類型,如下:

          msgId:消息id

          fromId:發送者id

          toId:接收者id

          msgType:消息類型

          msgContentType:消息內容類型

          timestamp:消息時間戳

          statusReport:狀態報告

          extend:擴展字段

          根據上述所示,我整理了一個思維導圖,方便大家參考:

          這是基礎部分,當然,大家也可以根據自己需要自定義比較適合自己的消息結構。

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

          syntax = "proto3";// 指定protobuf版本

          option java_package = "com.freddy.im.protobuf";// 指定包名

          option java_outer_classname = "MessageProtobuf";// 指定生成的類名


          message Msg {

              Head head = 1;// 消息頭

              string body = 2;// 消息體

          }


          message Head {

              string msgId = 1;// 消息id

              int32 msgType = 2;// 消息類型

              int32 msgContentType = 3;// 消息內容類型

              string fromId = 4;// 消息發送者id

              string toId = 5;// 消息接收者id

              int64 timestamp = 6;// 消息時間戳

              int32 statusReport = 7;// 狀態報告

              string extend = 8;// 擴展字段,以key/value形式存放的json

          }

          然后執行命令(我用的mac,windows命令應該也差不多):

          然后就會看到,在和proto文件同級目錄下,會生成一個java類,這個就是我們需要用到的東東:

          我們打開瞄一眼:

          東西比較多,不用去管,這是google為我們生成的protobuf類,直接用就行,怎么用呢?

          直接用這個類文件,拷到我們開始指定的項目包路徑下就可以啦:

          添加依賴后,可以看到,MessageProtobuf類文件已經沒有報錯了,順便把netty的jar包也導進來一下,還有fastjson的:

          建議用netty-all-x.x.xx.Final的jar包,后續熟悉了,可以用精簡的jar包。

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

          8、代碼封裝

          為什么需要封裝呢?說白了,就是為了解耦,為了方便日后切換到不同框架實現,而無需到處修改調用的地方。

          舉個栗子,比如Android早期比較流行的圖片加載框架是Universal ImageLoader,后期因為某些原因,原作者停止了維護該項目,目前比較流行的圖片加載框架是Picasso或Glide,因為圖片加載功能可能調用的地方非常多,如果不作一些封裝,早期使用了Universal ImageLoader的話,現在需要切換到Glide,那改動量將非常非常大,而且還很有可能會有遺漏,風險度非常高。

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

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

          我們分析一下,ims(IM Service,下文簡稱ims)應該是有初始化、建立連接、重連、關閉連接、釋放資源、判斷長連接是否關閉、發送消息等功能。

          基于上述分析,我們可以進行一個接口抽象:

          OnEventListener是與應用層交互的listener:

          IMConnectStatusCallback是ims連接狀態回調監聽器:

          然后寫一個Netty tcp實現類:

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

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

          9、初始化

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

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

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

          10、連接及重連

          resetConnect()方法作為連接的起點,首次連接以及重連邏輯,都是在resetConnect()方法進行邏輯處理。

          我們來瞄一眼:

          可以看到,非首次進行連接,也就是連接一個周期失敗后,進行重連時,會先讓線程休眠一段時間,因為這個時候也許網絡狀況不太好,接著,判斷ims是否已關閉或者是否正在進行重連操作,由于重連操作是在子線程執行,為了避免重復重連,需要進行一些并發處理。

          開始重連任務后,分四個步驟執行:

          1)改變重連狀態標識;

          2)回調連接狀態到應用層;

          3)關閉之前打開的連接channel;

          4)利用線程池執行一個新的重連任務。

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

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

          initBootstrap()是初始化Netty Bootstrap:

          注:NioEventLoopGroup線程數設置為4,可以滿足QPS是一百多萬的情況了,至于應用如果需要承受上千萬上億流量的,需要另外調整線程數。(參考自:《netty實戰之百萬級流量NioEventLoopGroup線程數配置》)

          接著,我們來看看TCPChannelInitializerHanlder

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

          11、TCP的拆包與粘包

          什么是TCP拆包?為什么會出現TCP拆包?

          簡單地說,我們都知道TCP是以“流”的形式進行數據傳輸的,而且TCP為提高性能,發送端會將需要發送的數據刷入緩沖區,等待緩沖區滿了之后,再將緩沖區中的數據發送給接收方,同理,接收方也會有緩沖區這樣的機制,來接收數據。拆包就是在socket讀取時,沒有完整地讀取一個數據包,只讀取一部分。

          什么是TCP粘包?為什么會出現TCP粘包?

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

          引用一張圖片來解釋一下在TCP出現拆包、粘包以及正常狀態下的三種情況:

          了解了TCP出現拆包/粘包的原因,那么,如何解決呢?

          通常來說,有以下四種解決方式:

          1)消息定長;

          2)用回車換行符作為消息結束標志;

          3)用特殊分隔符作為消息結束標志,如\t、\n等,回車換行符其實就是特殊分隔符的一種;

          4)將消息分為消息頭和消息體,在消息頭中用字段標識消息總長度。

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

          1)FixedLengthFrameDecoder,定長消息解碼器;

          2)LineBasedFrameDecoder,回車換行符消息解碼器;

          3)DelimiterBasedFrameDecoder,特殊分隔符消息解碼器;

          4)LengthFieldBasedFrameDecoder,自定義長度消息解碼器。

          我們用到的就是LengthFieldBasedFrameDecoder自定義長度消息解碼器,同時配合LengthFieldPrepender編碼器使用,關于參數配置,建議參考《netty--最通用TCP黏包解決方案:LengthFieldBasedFrameDecoder和LengthFieldPrepender》這篇文章,講解得比較細致。

          我們配置的是消息頭長度為2個字節,所以消息包的最大長度需要小于65536個字節,netty會把消息內容長度存放消息頭的字段里,接收方可以根據消息頭的字段拿到此條消息總長度,當然,netty提供的LengthFieldBasedFrameDecoder已經封裝好了處理邏輯,我們只需要配置lengthFieldOffset、lengthFieldLength、lengthAdjustment、initialBytesToStrip即可,這樣就可以解決TCP的拆包與粘包,這也就是netty相較于原生nio的便捷性,原生nio需要自己處理拆包/粘包等問題。

          12、長連接握手認證

          接著,我們來看看LoginAuthHandler和HeartbeatRespHandler。

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

          比如:

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

          HeartbeatRespHandler:是當客戶端接收到服務端登錄成功的消息后,主動向服務端發送一條心跳消息,心跳消息可以是一個空包,消息包體越小越好,服務端收到客戶端的心跳包后,原樣返回給客戶端,這里,就是收到服務端返回的心跳消息響應的處理handler。

          比如:

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

          13、心跳機制及讀寫超時機制

          心跳包是定期發送,也可以自己定義一個周期,比如:《移動端IM實踐:實現Android版微信的智能心跳機制》,為了簡單,此處規定應用在前臺時,8秒發送一個心跳包,切換到后臺時,30秒發送一次,根據自己的實際情況修改一下即可。心跳包用于維持長連接以及檢測長連接是否斷開等。

          PS:更多心跳保活方面的文章請見:

          Android端消息推送總結:實現原理、心跳保活、遇到的問題等

          為何基于TCP協議的移動端IM仍然需要心跳保活機制?

          微信團隊原創分享:Android版微信后臺保活實戰分享(網絡保活篇)

          移動端IM實踐:WhatsApp、Line、微信的心跳策略分析

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

          可以看到,利用userEventTriggered()方法回調,通過IdleState類型,可以判斷讀超時/寫超時/讀寫超時,這個在添加IdleStateHandler時可以配置,下面會貼上代碼。

          首先我們可以在READER_IDLE事件里,檢測是否在規定時間內沒有收到服務端心跳包響應,如果是,那就觸發重連操作。在WRITER_IDEL事件可以檢測客戶端是否在規定時間內沒有向服務端發送心跳包,如果是,那就主動發送一個心跳包。發送心跳包是在子線程中執行,我們可以利用之前寫的work線程池進行線程管理。

          addHeartbeatHandler()代碼如下:

          從圖上可看到,在IdleStateHandler里,配置的讀超時為心跳間隔時長的3倍,也就是3次心跳沒有響應時,則認為長連接已斷開,觸發重連操作。寫超時則為心跳間隔時長,意味著每隔heartbeatInterval會發送一個心跳包。讀寫超時沒用到,所以配置為0。

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

          連接成功后,立即發送一條握手消息,再次梳理一下整體流程:

          1)客戶端根據服務端返回的host及port,進行第一次連接;

          2)連接成功后,客戶端向服務端發送一條握手認證消息(1001);

          3)服務端在收到客戶端的握手認證消息后,從擴展字段里取出用戶token,到本地數據庫校驗合法性;

          4)校驗完成后,服務端把校驗結果通過1001消息返回給客戶端,也就是握手消息響應;

          5)客戶端收到服務端的握手消息響應后,從擴展字段取出校驗結果。若校驗成功,客戶端向服務端發送一條心跳消息(1002),然后進入心跳發送周期,定期間隔向服務端發送心跳消息,維持長連接以及實時檢測鏈路可用性,若發現鏈路不可用,等待一段時間觸發重連操作,重連成功后,重新開始握手/心跳的邏輯。

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

          可以看到,在channelInactive()exceptionCaught()方法都觸發了重連,channelInactive()方法在當鏈路斷開時會調用,exceptionCaught()方法在當出現異常時會觸發,另外,還有諸如channelUnregistered()channelReadComplete()等方法可以重寫,在這里就不貼了,相信聰明的你一眼就能看出方法的作用。

          我們仔細看一下channelRead()方法的邏輯,在if判斷里,先判斷消息類型,如果是服務端返回的消息發送狀態報告類型,則判斷消息是否發送成功,如果發送成功,從超時管理器中移除,這個超時管理器是干嘛的呢?

          下面講到消息重發機制的時候會詳細地講。在else里,收到其他消息后,會立馬給服務端返回一個消息接收狀態報告,告訴服務端,這條消息我已經收到了,這個動作,對于后續需要做的離線消息會有作用。如果不需要支持離線消息功能,這一步可以省略。最后,調用消息轉發器,把接收到的消息轉發到應用層即可。

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

          發送消息:

          關閉ims:

          ims默認配置:

          還有,應用層實現的ims client啟動器:

          由于代碼有點多,不太方便全部貼上,如果有興趣可以下載本文的完整demo進行體驗。

          額,對了,還有一個簡易的服務端代碼,如下:

          14、運行調試

          我們先來看看連接及重連部分(由于錄制gif比較麻煩,體積較大,所以我先把重連間隔調小成3秒,方便看效果)。

          啟動服務端:

          啟動客戶端:

          可以看到,正常的情況下已經連接成功了,接下來,我們來試一下異常情況。

          比如服務端沒啟動,看看客戶端的重連情況:

          這次我們先啟動的是客戶端,可以看到連接失敗后一直在進行重連,由于錄制gif比較麻煩,在第三次連接失敗后,我啟動了服務端,這個時候客戶端就會重連成功。

          然后,我們再來調試一下握手認證消息即心跳消息:

          可以看到,長連接建立成功后,客戶端會給服務端發送一條握手認證消息(1001),服務端收到握手認證消息會,給客戶端返回了一條握手認證狀態消息,客戶端收到握手認證狀態消息后,即啟動心跳機制。gif不太好演示,下載demo就可以直觀地看到。

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

          15、消息重發機制

          消息重發,顧名思義,即使對發送失敗的消息進行重發。考慮到網絡環境的不穩定性、多變性(比如從進入電梯、進入地鐵、移動網絡切換到wifi等),在消息發送的時候,發送失敗的概率其實不小,這時消息重發機制就很有必要了。

          有關即時通訊(IM)應用中的消息送達保證機制,可以詳細閱讀以下文章:

          IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞

          IM群聊消息如此復雜,如何保證不丟不重?

          完全自已開發的IM該如何設計“失敗重試”機制?

          我們先來看看實現的代碼邏輯。

          MsgTimeoutTimer:

          MsgTimeoutTimerManager:

          然后,我們看看收消息的TCPReadHandler的改造:

          最后,看看發送消息的改造:

          說一下邏輯吧:發送消息時,除了心跳消息、握手消息、狀態報告消息外,消息都加入消息發送超時管理器,立馬開啟一個定時器,比如每隔5秒執行一次,共執行3次,在這個周期內,如果消息沒有發送成功,會進行3次重發,達到3次重發后如果還是沒有發送成功,那就放棄重發,移除該消息,同時通過消息轉發器通知應用層,由應用層決定是否再次重發。如果消息發送成功,服務端會返回一個消息發送狀態報告,客戶端收到該狀態報告后,從消息發送超時管理器移除該消息,同時停止該消息對應的定時器即可。

          另外,在用戶握手認證成功時,應該檢查消息發送超時管理器里是否有發送超時的消息,如果有,則全部重發:

          16、離線消息

          由于離線消息機制,需要服務端數據庫及緩存上的配合,代碼就不貼了,太多太多。

          我簡單說一下實現思路吧:客戶端A發送消息到客戶端B,消息會先到服務端,由服務端進行中轉。

          這個時候,客戶端B存在兩種情況:

          1)長連接正常,就是客戶端網絡環境良好,手機有電,應用處在打開的情況;

          2)廢話,那肯定就是長連接不正常咯。這種情況有很多種原因,比如wifi不可用、用戶進入了地鐵或電梯等網絡不好的場所、應用沒打開或已退出登錄等,總的來說,就是沒有辦法正常接收消息。

          如果是長連接正常,那沒什么可說的,服務端直接轉發即可。

          如果長連接不正常,需要這樣處理:

          服務端接收到客戶端A發送給客戶端B的消息后,先給客戶端A回復一條狀態報告,告訴客戶端A,我已經收到消息,這個時候,客戶端A就不用管了,消息只要到達服務端即可。然后,服務端先嘗試把消息轉發到客戶端B,如果這個時候客戶端B收到服務端轉發過來的消息,需要立馬給服務端回一條狀態報告,告訴服務端,我已經收到消息,服務端在收到客戶端B返回的消息接收狀態報告后,即認為此消息已經正常發送,不需要再存庫。

          如果客戶端B不在線,服務端在做轉發的時候,并沒有收到客戶端B返回的消息接收狀態報告,那么,這條消息就應該存到數據庫,直到客戶端B上線后,也就是長連接建立成功后,客戶端B主動向服務端發送一條離線消息詢問,服務端在收到離線消息詢問后,到數據庫或緩存去查客戶端B的所有離線消息,并分批次返回,客戶端B在收到服務端的離線消息返回后,取出消息id(若有多條就取id集合),通過離線消息應答把消息id返回到服務端,服務端收到后,根據消息id從數據庫把對應的消息刪除即可。

          以上是單聊離線消息處理的情況,群聊有點不同,群聊的話,是需要服務端確認群組內所有用戶都收到此消息后,才能從數據庫刪除消息,就說這么多,如果需要細節的話,可以私信我。

          更多有關離線消息處理思路的文章,可以詳細閱讀:

          IM消息送達保證機制實現(二):保證離線消息的可靠投遞

          IM群聊消息如此復雜,如何保證不丟不重?

          淺談移動端IM的多點登陸和消息漫游原理

          不知不覺,NettyTcpClient中定義了很多變量,為了防止大家不明白變量的定義,還是貼上代碼吧:

          18、最終運行

          運行一下,看看效果吧:

          運行步驟是:

          1)首先,啟動服務端。

          2)然后,修改客戶端連接的ip地址為192.168.0.105(這是我本機的ip地址),端口號為8855,fromId,也就是userId,定義成100001,toId為100002,啟動客戶端A。

          3)再然后,fromId,也就是userId,定義成100002,toId為100001,啟動客戶端B。

          4)客戶端A給客戶端B發送消息,可以看到在客戶端B的下面,已經接收到了消息。

          5)用客戶端B給客戶端A發送消息,也可以看到在客戶端A的下面,也已經接收到了消息。

          至于,消息收發測試成功。至于群聊或重連等功能,就不一一演示了,還是那句話,下載demo體驗一下吧:https://github.com/52im/NettyChat

          由于gif錄制體積較大,所以只能簡單演示一下消息收發,具體下載demo體驗吧。如果有需要應用層UI實現(就是聊天頁及會話頁的封裝)的話,我再分享出來吧。

          19、寫在最后

          終于寫完了,這篇文章大概寫了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(已經上傳到github工程了),目前自測沒有問題,如果發現bug,請告訴我,謝謝。

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

          代碼已更新到Github:

          https://github.com/52im/NettyChat

          附錄:更多網絡編程/即時通訊/消息推送的實戰入門文章

          手把手教你用Netty實現網絡通信程序的心跳機制、斷線重連機制
          NIO框架入門(一):服務端基于Netty4的UDP雙向通信Demo演示
          NIO框架入門(二):服務端基于MINA2的UDP雙向通信Demo演示
          NIO框架入門(三):iOS與MINA2、Netty4的跨平臺UDP雙向通信實戰
          NIO框架入門(四):Android與MINA2、Netty4的跨平臺UDP雙向通信實戰
          微信小程序中如何使用WebSocket實現長連接(含完整源碼)
          Web端即時通訊安全:跨站點WebSocket劫持漏洞詳解(含示例代碼)
          解決MINA數據傳輸中TCP的粘包、缺包問題(有源碼)
          開源IM工程“蘑菇街TeamTalk”2015年5月前未刪減版完整代碼 [附件下載]
          用于IM中圖片壓縮的Android工具類源碼,效果可媲美微信 [附件下載]
          高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]
          一個WebSocket實時聊天室Demo:基于node.js+socket.io [附件下載]
          Android聊天界面源碼:實現了聊天氣泡、表情圖標(可翻頁) [附件下載]
          高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]
          開源libco庫:單機千萬連接、支撐微信8億用戶的后臺框架基石 [源碼下載]
          分享java AMR音頻文件合并源碼,全網最全
          微信團隊原創Android資源混淆工具:AndResGuard [有源碼]
          一個基于MQTT通信協議的完整Android推送Demo [附件下載]
          Android版高仿微信聊天界面源碼 [附件下載]
          高仿手機QQ的Android版鎖屏聊天消息提醒功能 [附件下載]
          高仿iOS版手機QQ錄音及振幅動畫完整實現 [源碼下載]
          Android端社交應用中的評論和回復功能實戰分享[圖文+源碼]
          Android端IM應用中的@人功能實現:仿微博、QQ、微信,零入侵、高可擴展[圖文+源碼]
          仿微信的IM聊天時間顯示格式(含iOS/Android/Web實現)[圖文+源碼]

          (本文同步發布于:http://www.52im.net/thread-2671-1-1.html



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 托克逊县| 水富县| 阿拉善盟| 城口县| 武宁县| 黄冈市| 墨江| 精河县| 宝兴县| 景洪市| 遂溪县| 桃园市| 辽源市| 南华县| 阿克陶县| 清涧县| 大足县| 仁寿县| 云梦县| 上杭县| 朝阳县| 文水县| 萨嘎县| 乐业县| 佳木斯市| 灵璧县| 玉树县| 万载县| 石河子市| 彭山县| 海原县| 灌南县| 海南省| 磴口县| 清涧县| 北川| 临城县| 巢湖市| 景德镇市| 宿州市| 溧阳市|