本文由做者FreddyChen原創分享,爲了更好的體現文章價值,引用時有少量改動,感謝原做者。php
一直想寫一篇關於im即時通信分享的文章,無奈工做太忙,很難抽出時間。今天終於從公司離職了,打算好好休息幾天再從新找工做,趁時間空閒,決定靜下心來寫一篇文章,畢竟從前輩那裏學到了不少東西。html
工做了五年半,這三四年來一直在作社交相關的項目,有直播、即時通信、短視頻分享、社區論壇等產品,深知即時通信技術在一個項目中的重要性,本着開源分享的精神,也趁這機會總結一下,因此寫下了這篇文章。java
* 重要提示:本文不是一篇即時通信理論文章,文章內容所有由實戰代碼組織而成,若是你對即時通信(IM)技術理論瞭解的太少,建議先詳細閱讀:《新手入門一篇就夠:從零開發移動端IM》。node
本文實踐內容將涉及如下即時通信技術內容:git
1)Protobuf序列化;程序員
2)TCP拆包與粘包;github
3)長鏈接握手認證;web
4)心跳機制;數據庫
5)重連機制;編程
6)消息重發機制;
7)讀寫超時機制;
8)離線消息;
9)線程池。
不想看文章的同窗,能夠直接到Github下載本文源碼:
接下來,讓咱們進入正題。
(本文同步發佈於:http://www.52im.net/thread-2671-1-1.html)
本文適合沒有任何即時通信(IM)開發經驗的小白開發者閱讀,文章將教你從零開始,圍繞一個典型即時通信(IM)系統的方方面面,手把手爲你展現如何基於Netty+TCP+Protobuf來開發出這樣的系統。很是適合從零入門的Android開發者。
本文不適合沒有編程的準開發者閱讀,由於即時通信(IM)系統屬於特定的業務領域,若是你連通常的邏輯代碼都很難編寫出來,不建議閱讀本文。本文顯然不是一個編程語言入門教程。
本文原文內容由FreddyChen原創分享,做者現從事Android程序開發,他的技術博客地址:https://juejin.im/user/5bd7affbe51d4547f763fe72
這裏須要簡單解釋一下,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協議瞭解的太少,能夠閱讀一下文章:
《腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手》
對於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倍?》
《詳解如何在NodeJS中使用Google的Protobuf》
《技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解》
《金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(原理篇)》
《金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(實戰篇)》
>> 更多同類文章 ……
首先,咱們來了解一下,Netty究竟是個什麼東西。網絡上找到的介紹:Netty是由JBOSS提供的基於Java NIO的開源框架,Netty提供異步非阻塞、事件驅動、高性能、高可靠、高可定製性的網絡應用程序和工具,可用於開發服務端和客戶端。
PS:若是你對Java的經典IO、NIO或者Netty框架不瞭解,請閱讀如下文章:
《史上最強Java NIO入門:擔憂從入門到放棄的,請讀這篇!》
爲何不用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框架的關係和對比,詳見如下文章:
好了,廢話很少說,直接開始吧。
首先,咱們新建一個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代碼,實現即時通信的功能。
爲何須要封裝呢?說白了,就是爲了解耦,爲了方便往後切換到不一樣框架實現,而無需處處修改調用的地方。
舉個栗子,好比Android早期比較流行的圖片加載框架是Universal ImageLoader,後期由於某些緣由,原做者中止了維護該項目,目前比較流行的圖片加載框架是Picasso或Glide,由於圖片加載功能可能調用的地方很是多,若是不做一些封裝,早期使用了Universal ImageLoader的話,如今須要切換到Glide,那改動量將很是很是大,並且還頗有可能會有遺漏,風險度很是高。
那麼,有什麼解決方案呢?
很簡單,咱們能夠用工廠設計模式進行一些封裝,工廠模式有三種:簡單工廠模式、抽象工廠模式、工廠方法模式。在這裏,我採用工廠方法模式進行封裝,具體區別,能夠參見:《通俗講講我對簡單工廠、工廠方法、抽象工廠三種設計模式的理解》。
咱們分析一下,ims(IM Service,下文簡稱ims)應該是有初始化、創建鏈接、重連、關閉鏈接、釋放資源、判斷長鏈接是否關閉、發送消息等功能。
基於上述分析,咱們能夠進行一個接口抽象:
OnEventListener是與應用層交互的listener:
IMConnectStatusCallback是ims鏈接狀態回調監聽器:
而後寫一個Netty tcp實現類:
接下來,寫一個工廠方法:
封裝部分到此結束,接下來,就是實現了。
咱們先實現init(Vector serverUrlList, OnEventListener listener, IMSConnectStatusCallback callback)方法,初始化一些參數,以及進行第一次鏈接等:
其中,MsgDispatcher是消息轉發器,負責將接收到的消息轉發到應用層:
ExecutorServiceFactory是線程池工廠,負責調度重連及心跳線程:
resetConnect()方法做爲鏈接的起點,首次鏈接以及重連邏輯,都是在resetConnect()方法進行邏輯處理。
咱們來瞄一眼:
能夠看到,非首次進行鏈接,也就是鏈接一個週期失敗後,進行重連時,會先讓線程休眠一段時間,由於這個時候也許網絡情況不太好,接着,判斷ims是否已關閉或者是否正在進行重連操做,因爲重連操做是在子線程執行,爲了不重複重連,須要進行一些併發處理。
開始重連任務後,分四個步驟執行:
1)改變重連狀態標識;
2)回調鏈接狀態到應用層;
3)關閉以前打開的鏈接channel;
4)利用線程池執行一個新的重連任務。
ResetConnectRunnable是重連任務,核心的重連邏輯都放到這裏執行:
toServer()是真正鏈接服務器的地方:
initBootstrap()是初始化Netty Bootstrap:
注:NioEventLoopGroup線程數設置爲4,能夠知足QPS是一百多萬的狀況了,至於應用若是須要承受上千萬上億流量的,須要另外調整線程數。(參考自:《netty實戰之百萬級流量NioEventLoopGroup線程數配置》)
接着,咱們來看看TCPChannelInitializerHanlder:
其中,ProtobufEncoder和ProtobufDecoder是添加對protobuf的支持,LoginAuthRespHandler是接收到服務端握手認證消息響應的處理handler,HeartbeatRespHandler是接收到服務端心跳消息響應的處理handler,TCPReadHandler是接收到服務端其它消息後的處理handler,先不去管,咱們重點來分析下LengthFieldPrepender和LengthFieldBasedFrameDecoder,這就須要引伸到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須要本身處理拆包/粘包等問題。
接着,咱們來看看LoginAuthHandler和HeartbeatRespHandler。
LoginAuthRespHandler:是當客戶端與服務端長鏈接創建成功後,客戶端主動向服務端發送一條登陸認證消息,帶入與當前用戶相關的參數,好比token,服務端收到此消息後,到數據庫查詢該用戶信息,若是是合法有效的用戶,則返回一條登陸成功消息給該客戶端,反之,返回一條登陸失敗消息給該客戶端,這裏,就是在接收到服務端返回的登陸狀態後的處理handler。
好比:
能夠看到,當接收到服務端握手消息響應後,會從擴展字段取出status,若是status=1,則表明握手成功,這個時候就先主動向服務端發送一條心跳消息,而後利用Netty的IdleStateHandler讀寫超時機制,按期向服務端發送心跳消息,維持長鏈接,以及檢測長鏈接是否還存在等。
HeartbeatRespHandler:是當客戶端接收到服務端登陸成功的消息後,主動向服務端發送一條心跳消息,心跳消息能夠是一個空包,消息包體越小越好,服務端收到客戶端的心跳包後,原樣返回給客戶端,這裏,就是收到服務端返回的心跳消息響應的處理handler。
好比:
這個就比較簡單,收到心跳消息響應,無需任務處理,直接打印一下方便咱們分析便可。
心跳包是按期發送,也能夠本身定義一個週期,好比:《移動端IM實踐:實現Android版微信的智能心跳機制》,爲了簡單,此處規定應用在前臺時,8秒發送一個心跳包,切換到後臺時,30秒發送一次,根據本身的實際狀況修改一下便可。心跳包用於維持長鏈接以及檢測長鏈接是否斷開等。
PS:更多心跳保活方面的文章請見:
《Android端消息推送總結:實現原理、心跳保活、遇到的問題等》
接着,咱們利用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進行體驗。
額,對了,還有一個簡易的服務端代碼,以下:
咱們先來看看鏈接及重連部分(因爲錄製gif比較麻煩,體積較大,因此我先把重連間隔調小成3秒,方便看效果)。
啓動服務端:
啓動客戶端:
能夠看到,正常的狀況下已經鏈接成功了,接下來,咱們來試一下異常狀況。
好比服務端沒啓動,看看客戶端的重連狀況:
此次咱們先啓動的是客戶端,能夠看到鏈接失敗後一直在進行重連,因爲錄製gif比較麻煩,在第三次鏈接失敗後,我啓動了服務端,這個時候客戶端就會重連成功。
而後,咱們再來調試一下握手認證消息即心跳消息:
能夠看到,長鏈接創建成功後,客戶端會給服務端發送一條握手認證消息(1001),服務端收到握手認證消息會,給客戶端返回了一條握手認證狀態消息,客戶端收到握手認證狀態消息後,即啓動心跳機制。gif不太好演示,下載demo就能夠直觀地看到。
接下來,在講完消息重發機制及離線消息後,我會在應用層作一些簡單的封裝,以及在模擬器上運行,這樣就能夠很直觀地看到運行效果。
消息重發,顧名思義,即便對發送失敗的消息進行重發。考慮到網絡環境的不穩定性、多變性(好比從進入電梯、進入地鐵、移動網絡切換到wifi等),在消息發送的時候,發送失敗的機率其實不小,這時消息重發機制就頗有必要了。
有關即時通信(IM)應用中的消息送達保證機制,能夠詳細閱讀如下文章:
咱們先來看看實現的代碼邏輯。
MsgTimeoutTimer:
MsgTimeoutTimerManager:
而後,咱們看看收消息的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中定義了不少變量,爲了防止你們不明白變量的定義,仍是貼上代碼吧:
運行一下,看看效果吧:
運行步驟是:
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實現(就是聊天頁及會話頁的封裝)的話,我再分享出來吧。
終於寫完了,這篇文章大概寫了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與MINA二、Netty4的跨平臺UDP雙向通訊實戰》
《NIO框架入門(四):Android與MINA二、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)