自已開發IM有那麼難嗎?手把手教你自擼一個Andriod版簡易IM (有源碼)

本文由作者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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章