天天看點

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

原文: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來講解具體的粘包拆包過程:

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

第一種情況,A資料包和B資料包被分别接收且都是整包狀态,無粘包拆包情況發生,此種情況最佳。

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

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

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

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

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

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

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

第五種情況,也是最複雜的一種,先收到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初級應用之通訊架構分析

由于Netty提供了MessageToByteEncoder和ByteToMessageDecoder這兩個類供我們進行編碼解碼,是以我們需要分别繼承這兩個類來實作我們的編碼器,解碼器。

首先來看看編碼器,主要是将二進制資料放入管道中。

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

然後來看看解碼器,主要是将二進制資料提取出來并轉換為消息對象。

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

注意這裡我們并非直接繼承自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端開始發送鑒權申請。

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

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

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

當Server端鑒權成功之後,會将鑒權成功的資訊發送給Client端,Client端接收到鑒權成功的資訊後,列印出鑒權成功資訊:

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

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

>>心跳檢測

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

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

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

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

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

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

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

>>斷線重連

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

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

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

>>服務端編寫

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

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

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

此時Selector會一直進行輪詢操作,一旦發現注冊的Channel處于Ready狀态,則執行Handler鍊調用。

由于以上所有的元件都準備齊全,是以我們這裡可以很友善的進行服務端編碼了:

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

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

>>用戶端編寫

首先來看看Netty建立用戶端的時序圖:

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

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

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

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

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

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

<END>