Netty初級應用之通訊框架分析

程序員的成長之路

互聯網/程序員/技術/資料共享 

關注

閱讀本文大概需要 14 分鐘。

原文:www.cnblogs.com/scy251147/p/10498008.html

作者:程序詩人

1. 寫作緣起

幾年前,我在一家農業物聯網公司,負責解決其物聯網產品線。

我們當時基於.net平臺打造了一套實時數據採集系統,可以把數以百萬級的傳感器傳送回來的數據採集入庫並根據這些數據進行建模。

在搭建這套實時數據採集系統的時候,高併發高可用被首次提出,同時要求系統不會有太大的時延。一旦有時延,也就意味着損失。

比如一個有3000頭豬的豬舍,假設空氣溫度達到了比較高的水平,但是採集探頭採集的數據上傳到服務器管道中,由於被積壓了5分鐘後才被處理,那麼主動預警系統打開風機的時候,也許已經晚了,這五分鐘的時間裏,上百頭小豬仔因爲溫度過高的緣故死於非命。

當然,魚塘,蔬菜大棚等也有類似的場景。

當時在打造此係統的時候,我們用的還是.net,翻閱了很多源碼,查閱了很多資料,最後我們基於SocketAsyncEventArgs來打造一個自己的物聯網服務端。

當時在.net裏面,還沒有一款能夠匹敵netty的開源組件出來,這就導致我們不僅要處理心跳,而且還要處理粘包,甚至緩衝區都需要自己來處理,一旦消息沒被及時拿出來,那麼後到的數據會將之前的數據一股腦兒的覆蓋。

從底層來實現這些功能的好處是讓我們對服務端的編寫有了非常清楚的認知,但是也由於思慮不全帶來非常多的坑。可以說那幾年是踩着TCP的坑走過來的。最後我們基於SocketAsyncEventArgs封裝了我們自己的物聯網通訊框架:TinySocket。

在那個時候,彼時的聯想佳沃藍莓基地依舊用數據庫輪詢的方式來支持物聯網設備,和他們對接的時候,發現經常會因爲遇到網絡層面的問題而愁眉不展,而彼時的我們卻因爲我們可以在任何設備上自動/手動控制我們的設備而高興不已。因爲她的可靠度極高。

後來,離開了那裏,但是懷着要打造一個能支撐巨流量的物聯網高併發和高可用架構的夢想,而選擇了互聯網公司來進行深造。

也是在這個時候,我從.net平臺轉到了java平臺,也正是在這個時候,我有緣認識了netty,一個彷彿是爲了解決我當年的各種問題而生的框架,雖和她只有一面之緣,但是那一刻,我決定將她納入麾下,情定終生也許用在此刻再合適不過了。

因爲她有成熟的架構,普適的解決方式,優雅的接入方式,良好的社區支持,成熟的商業產品。這些特性,讓我們無法拒絕使用。

由於對netty的執迷,導致我說起了過往,止不住的文字流淌,接下來我們就轉入正題吧。

在數據傳輸過程中,由於網絡的不確定性,每個數據包都有可能遭遇形式各樣的問題,諸如掉線,網絡變差等,所以到達的時候,這些數據包有可能亂序,也有可能丟失。

所以爲了應對這些異常狀況,TCP協議在其內部通過序列號來保證數據包亂序的問題,同時通過確認號來保證數據包丟失的問題。所以基於TCP協議實現的上層應用,都認爲TCP傳輸是可靠的。

但是通過一些網絡抓包工具,可以窺見其具體實現數據包有序和防丟失的過程,感興趣的可以自己去試試。

那麼上面提到序列號和確認號,究竟是什麼呢?我們來看一下:

  •   Sequence Number:順序號,意即數據包的序號,主要用來解決數據包亂序問題。

  •   Acknowledgement Number:確認號,意即數據包用來進行雙端消息確認的號碼,主要用來解決網絡傳輸過程中,數據丟包的問題。

在TCP進行數據傳輸的過程中,主機A傳輸數據給主機B,假設第一次A傳輸512字節的數據給B,那麼seq=1;當B收到這512字節的時候,會將seq進行累加來避免亂序,在這裏,B會將seq重新設置爲512+1,然後回傳給A,A收到B傳回來的seq=513的時候,就知道第一個數據包已經傳給了B。

如果A收到B的回覆,發現B沒有收到數據包的話,那麼將會進行重發操作,這樣來防止丟包。

下面來說下TCP的標誌位,一共有6種:

  •      SYN(synchronous建立聯機)

  •       ACK(acknowledgement 確認)

  •       PSH(push傳送)

  •       FIN(finish結束)

  •       RST(reset重置)

  •       URG(urgent緊急)

第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;     

第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;   

第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。

完成三次握手,客戶端與服務器開始傳送數據.

更多詳細的信息,推薦閱讀斯坦福大學的Transmission Control Protocol (TCP)的這篇短小精悍的文章。

大略講解了下TCP的基礎,我們接下來開始我們的netty之旅吧。由於JDK內置的NIO操作類庫並非我們的講解要點,所以這裏我不會過多的進行講解,直接從netty講起吧。

2. 網絡通訊基礎,包含(粘包拆包,編解碼,鑑權認證,心跳檢測,斷線重連)

在設計網絡通訊框架的時候,有些設計點是必須被考慮進去的,這些設計點可以說是不可或缺的。接下來我們就一一梳理並進行講解。

>>粘包拆包

粘包拆包,顧名思義,粘包,就是指數據包黏在一塊了;拆包,則是指完整的數據包被拆開了。由於TCP通訊過程中,會將數據包進行合併後再發出去,所以會有這兩種情況發生,但是UDP通訊則不會。

下面我們以兩個數據包A,B來講解具體的粘包拆包過程:

第一種情況,A數據包和B數據包被分別接收且都是整包狀態,無粘包拆包情況發生,此種情況最佳。

第二種情況,A數據包和B數據包在一塊兒且一起被接收,此種情況,即發生了粘包現象,需要進行數據包拆分處理。

第三種情況,A數據包和B數據包的一部分先被接收,然後收到B數據包的剩餘部分,此種情況,即發生了拆包現象,即B數據包被拆分。

第四種情況,A數據包的一部分先被接收,然後收到A數據包的剩餘部分和B數據包的完整部分,此種情況,即發生了拆包現象,即A數據包被拆分。

第五種情況,也是最複雜的一種,先收到A數據包的部分,然後收到A數據包剩餘部分和B數據包的一部分,最後收到B數據包的剩餘部分,此種情況也發生了拆包現象。

上面五種粘包拆包現象的發生,其實歸根到底,原因有三:

  (1) 應用程序write寫入的字節大小大於套接口發送緩衝區大小。

  (2) 進行MSS大小的TCP分段。

  (3) 以太網幀的payload大於MTU進行IP分片。

我們來詳細講解一下。

對於(1)中的內容,我們可以認定爲應用程序內部自身的緩衝區,此緩衝區因爲大小不同會導致連續寫入的數據太長被截斷,從而導致一個完整的業務消息體被分爲兩段發送出去。

對於(2)中的內容,其實是TCP協議裏面的MSS大小,此大小會決定發送的數據包的長度。屬於協議層面的緩衝區。

對於(3)中的內容,則屬於網卡自身的緩衝區大小,屬於硬件層面。

既然瞭解了粘包拆包發生的原因了,那麼有什麼辦法來應對呢?由於不同業務有不同的實現方式,所以一般情況下都會採用如下的解決方式來進行處理:

(1)  數據消息固定長度,比如說1024字節,接收方接收到數據,以1024字節爲單位進行截取即可。如若當前接收到的數據不夠1024字節,可以等後續的數據到達後,以1024爲單位進行截取。適用於數據結構固定長度的場合。

(2)  數據消息採用分隔符,比如用換行符或者使用豎線分隔等,依據具體的業務來進行。在進行數據處理的時候,可以根據這些分隔符來截取數據。適用於數據結構長度不固定的場合。前面提到的物聯網採集端通訊協議就是採用的此種做法。

(3)  數據消息包含數據頭和數據體,數據頭中包含數據長度,此種做法可以讓數據定義更爲靈活多變,但是會讓數據結構變得臃腫,非常適合於自定義通訊協議的場合中。

(4)  其他根據具體業務而衍生出來的處理方式。比如Dubbo通訊協議等。

>>編解碼

當我們將數據從本機發到遠端的時候,我們需要將數據轉換爲二進制放到緩衝區,然後發送出去,這叫做編碼。

當我們接收遠端數據到本機的時候,我們需要將緩衝區的二進制數據還原爲對象,這叫做解碼。

由於目前能夠進行這種編解碼的組件非常的多,比如ProtoBuffer,ProtoStuff,Marshalling,MessagePack等,由於這些組件有性能上的差別和使用簡便性方面的差別,所以需要自己通過Benchmark來選擇最適合自己業務的。

由於ProtoStuff是對ProtoBuffer的封裝,省去了我們手寫協議文件的煩惱,且性能上的損耗在可以接收範圍內,所以我們接下來的講解均以此組件來進行。

>>鑑權認證

雙端的機器在進行通訊的時候,必須要進行身份認證後才能進行連接,此舉可以防止非法用戶通過構造數據包來非法訪問服務數據的作用。

此鑑權認證發生在雙方機器第一次進行連接通訊的時候,客戶端必須先發送鑑權認證的數據包給服務端,服務端對此客戶端進行鑑權認證,如果鑑權認證不通過(比如客戶端ip在黑名單中或者客戶端的請求token無效等),則拒絕連接。

其實這種鑑權認證就類似咱們訪問網頁時候,需要先進行用戶登錄的情況一樣。雖然此種做法無法百分之百的保證非法用戶的訪問,但是可以在極大程度上提升服務端的安全性能。

>>心跳檢測

雙端的機器在進行通訊的時候,由於鏈路保持在活躍狀態,所以不會導致鏈路中斷。

但是一旦當一方機器(比如說客戶端)由於網絡變差,網絡閃斷,機器掛掉等原因導致掉線,那麼此種情況下,服務端是感知不到客戶端掉線的。

所以這裏需要利用心跳包來檢測客戶端的這種行爲。心跳包的實現方式有多種,但是無外乎如下幾種情況:

(1)  服務端發送心跳包給客戶端,客戶端接收到後計數清零,當客戶端在規定的時間間隔內(比如1分鐘)沒有接收到服務端發送的心跳包,則計數器遞增一次,累積遞增三次,則視爲服務端掉線。此種方式主要檢測服務端存活。比如物聯網採集模塊中,就需要客戶端實時檢測服務端的存活。

(2)  客戶端發送心跳給服務端,服務端接收到後計數清零,當服務端在規定的時間間隔內(比如1分鐘)沒有接收到客戶端發送的心跳包,則計數器遞增一次,累積遞增三次,則視爲客戶端掉線。此種方式主要檢測客戶端存活。比如IM通訊軟件中,通過此方法可以檢測哪個用戶掉線,然後將此掉線用戶廣播給其他用戶告知掉線信息。

(3)  客戶端發送心跳給服務端,服務端接收後計數清零,同時服務端給客戶端發送一個心跳包,客戶端接收後計數清零。當雙端任何一方未能及時收到心跳包,則計數器進行遞增,累積遞增三次,則視爲對方掉線。此種方式可以同時檢測服務端和客戶端的存活。

當然,上面是我經常用到的三種心跳包設計模式,如果有更好的設計方式,還請指教。

>>斷線重連

客戶端由於種種原因,導致和服務端的連接中斷,此種情況下,需要考慮到重連。此種機制可最大程度的保證整體服務的穩定性和可用性。所以其重要性毋庸置疑。

上面就是在設計通訊組件的時候,必須要考慮的諸多細節,由於不同的業務對這些細節的依賴度有高有低,所以在實際設計的時候,可以依據業務來進行詳細定製或者粗粒度實現,由此出發,打造一套自己的通訊組件,不是什麼難事兒了。

上面都是一些理論點,如何將這些理論點變成實踐,則是接下來要講的內容了。Netty,終於要出場了。

3. 自定義協議棧。

封裝一個通用的通訊組件所具備的一些要點,已經講解的比較全面和清楚了,但是隻是理論知識,本着實踐出真知的態度,我們決定利用上面的知識點來打造一款自己的通訊協議,這個通訊協議會在基於CS模型(Client-Server)的通訊組件上進行信息傳輸。

本次我們將採用Netty作爲通訊組件的底層,ProtoStuff作爲編解碼的工具。接下來就開始吧。

>>編解碼

在Netty中,編碼是指將數據轉換爲緩衝區中的二進制數據,對應的編碼類是MessageToByteEncoder,此類中的write方法可以將消息對象進行編碼,然後寫入到發送管道中。

由於在此類中,encode編碼方法是abstract的,所以需要用戶來自己實現,我們就以ProtoStuff來書寫一下。

而解碼則是指將緩衝區中的二進制數據轉換爲數據對象,對應的解碼類是ByteToMessageDecoder,類似的,我們需要自己實現decode的編碼方法,因爲它也是abstract的。

首先我們需要封裝一個SerializeUtil通用類出來,此類只包含基於ProtoStuff實現的serialize(Object object)和deserialize(byte[] data, Class<T> clazz)出來,具體封裝如下:

由於Netty提供了MessageToByteEncoder和ByteToMessageDecoder這兩個類供我們進行編碼解碼,所以我們需要分別繼承這兩個類來實現我們的編碼器,解碼器。

首先來看看編碼器,主要是將二進制數據放入管道中。

然後來看看解碼器,主要是將二進制數據提取出來並轉換爲消息對象。

注意這裏我們並非直接繼承自ByteToMessageDecoder來實現,是因爲單純的繼承自這個類,需要我們自己手動處理粘包拆包的情況,比較麻煩。

所以我們繼承自LengthFieldBasedFrameDecoder這個用來處理粘包拆包的類,此類正是繼承自ByteToMessageDecoder,所以大大簡化了我們的工作。粘包拆包的具體實現,後面我們會詳細講解。

從上面的代碼中,我們就可以看到在Netty中,實現自己的編碼解碼器是多麼的簡單和方便。

需要注意的是,在解碼的時候,由於ByteBuf本身的readerIndex和writeIndex機制,在讀取的時候需要用readBytes來使得readerIndex索引後移,不可以用getBytes來操作,否則會導致readerIndex不能向後移動,從而導致netty did not read anything but decoded a message的錯誤,這個錯誤的意思就是你當前讀取的數據是空的,無法轉化爲消息對象,原因是因爲我們之前已經讀過此數據了,由於readerIndex未更新,導致我們讀取的是空數據。

關於readerIndex和writIndex更多詳細內容,可以翻閱此文,我在這裏做了更加詳細的講解。

>>粘包拆包

在Netty中,已經提供好了粘包拆包的公共類庫,他們是:LineBasedFrameDecoder,StringDecoder,LengthFieldBasedFrameDecoder,DelimiterBasedFrameDecoder,FixedLengthFrameDecoder。

其中StringDecoder擴展自MessageToMessageDecoder類,其他的幾個均擴展自ByteToMessageDecoder類。

爲什麼擴展自ByteToMessageDecoder類呢?

因爲粘包拆包發生在從緩衝區中將二進制數據讀取出來的過程中,而ByteToMessageDecoder類,是將二進制數據轉換爲具體的消息對象的類,所以這些類庫繼承自這個類也是理所當然的事情了。

接下來我們對這些粘包拆包工具進行一一講解和實踐。

LineBasedFrameDecoder:遍歷ByteBuf中的可讀字節,然後看是否有\n或者\r\n,如果存在,就認爲當前尋找的消息體已經找尋完畢。同時此類也支持最大長度的數據匹配,當讀取的數據長度已達到最大長度但是仍舊沒有找到\n或者\r\n換行結束符的時候,將會拋出異常,同時忽略掉之前讀取的異常碼流。

StringDecoder:將接收到的內容轉換爲String串。

將LineBasedFrameDecoder+StringDecoder組合起來,就可以形成按行進行切分的文本解碼器,使用這種組合來進行粘包拆包處理,非常可靠易用。由於此組合只支持數據消息含有結束換行符的,所以只適合簡單的純文本場合。

LengthFieldBasedFrameDecoder:此解碼器主要是通過消息頭部附帶的消息體的長度來進行粘包拆包操作的。由於其配置參數過多(maxFrameLength,lengthFieldOffset,lengthFieldLength,lengthAdjustment,initialBytesToStrip等),所以可以最大程度的保證能用消息體長度字段來進行消息的解碼操作。這些不同的配置參數可以組合出不同的粘包拆包處理效果。

DelimiterBasedFrameDecoder:此解碼器主要通過設定分隔符來進行消息的粘包拆包處理。

FixedLengthFrameDecoder:此解碼器主要是通過設置固定數據長度來進行消息的粘包拆包處理。

>>鑑權認證

此包爲Client連接Server的時候,需要發送的第一個數據包,Server端接收到此包的內容後,通過業務解析,來對當前請求登錄的Client進行鑑權操作。如果操作成功,則允許登錄,否則拒絕登錄。

由於業務解析這塊不屬於我們重點講解的內容,在示例代碼中,我們以簡單的鑑權操作來進行延時講解:

首先,Client端連接到Server端,當鏈路Active的時候,Client端開始發送鑑權申請。

然後,Server端接收到Client的鑑權申請,進行鑑權操作:

當Server端鑑權成功之後,會將鑑權成功的信息發送給Client端,Client端接收到鑑權成功的信息後,打印出鑑權成功信息:

這樣,一個鑑權認證的基本流程就出來了,從Client端到Server端,然後再到Client端。由於鑑權的具體方式和業務關聯性比較高,所以可以利用具體鑑權業務進行替換即可。

>>心跳檢測

當鑑權通過之後,Client端和Server端的正常通訊建立。可以進行業務消息的交流。但是由於網絡原因等會造成Client和Server的交流中斷,而且此種中斷是無法被感知的,所以Client端的心跳檢測設計如下:

從代碼可以看出,我們的HeartBeatTask會以固定5秒的頻率向Server端發送一次心跳信息,如果收到Server端的心跳回復,則打印出來。

然後來看看Server端的心跳檢測代碼:

從代碼可以看出,Server端收到Client端的心跳包後,會打印出來,然後構建另一個心跳包回覆給Client端,也就是向Client端報告我還活着。

這樣,通過一來一去的心跳包檢測機制,就可以對Server端和Client端進行探活操作,避免業務上的不可用問題。

>>斷線重連

爲了提高高可用性,可以對Client端加上此項特性保證服務的可用率。Client端示例代碼如下:

由於Client關閉後,會跑到finally代碼塊中,所以在這裏可以進行重連操作。

>>服務端編寫

首先來看看Netty創建服務端的時序圖:

從圖示可以看出,ServerBootstrap實例是出發點;然後綁定EventLoopGroup線程池;之後設置並綁定服務端Channel,綁定各種Handler;最後就綁定到本機進行監聽。

此時Selector會一直進行輪詢操作,一旦發現註冊的Channel處於Ready狀態,則執行Handler鏈調用。

由於以上所有的組件都準備齊全,所以我們這裏可以很方便的進行服務端編碼了:

從代碼中我們可以看到,之前講過的鑑權認證,編碼解碼,粘包拆包等都體現在了服務端Handler中,所以非常的簡介明瞭。

>>客戶端編寫

首先來看看Netty創建客戶端的時序圖:

從圖示可以看出,BootStrap是出發點;然後設置EventLoopGroup線程池;之後設置並綁定客戶端Channel和各種Handler;最後通過Connect方法進行服務端連接操作。其實和服務端差別不大。

由於其設計也涉及到鑑權認證,編碼解碼,粘包拆包等,所以編碼是有些類似的:

好了,到了這裏,我們就已經能夠打造出來一個通用的通訊框架了,此框架雖然簡單,但是勝在囊括了各種必須的設計元素。

可以作爲指導框架進行業務邏輯的耦合設計,避免出現設計過程中因爲缺乏指導思想導致設計出來的東西不符合業務需求,比如高可用需求。

<END>

推薦閱讀:

Java中 volatile 關鍵字的最全總結,趕快給自己查缺補漏吧!

用 Java 寫一個植物大戰殭屍簡易版!

5T技術資源大放送!包括但不限於:C/C++,Linux,Python,Java,PHP,人工智能,單片機,樹莓派,等等。在公衆號內回覆「2048」,即可免費獲取!!

微信掃描二維碼,關注我的公衆號

寫留言

朕已閱 

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