1. 寫做緣起html
幾年前,我在一家農業物聯網公司,負責解決其物聯網產品線。咱們當時基於.net平臺打造了一套實時數據採集系統,能夠把數以百萬級的傳感器傳送回來的數據採集入庫並根據這些數據進行建模。在搭建這套實時數據採集系統的時候,高併發高可用被首次提出,同時要求系統不會有太大的時延。一旦有時延,也就意味着損失。好比一個有3000頭豬的豬舍,假設空氣溫度達到了比較高的水平,可是採集探頭採集的數據上傳到服務器管道中,因爲被積壓了5分鐘後才被處理,那麼主動預警系統打開風機的時候,也許已經晚了,這五分鐘的時間裏,上百頭小豬仔由於溫度太高的緣故死於非命。固然,魚塘,蔬菜大棚等也有相似的場景。java
當時在打造此係統的時候,咱們用的仍是.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方法進行服務端鏈接操做。其實和服務端差異不大。因爲其設計也涉及到鑑權認證,編碼解碼,粘包拆包等,因此編碼是有些相似的:
好了,到了這裏,咱們就已經可以打造出來一個通用的通信框架了,此框架雖然簡單,可是勝在囊括了各類必須的設計元素。能夠做爲指導框架進行業務邏輯的耦合設計,避免出現設計過程當中由於缺少指導思想致使設計出來的東西不符合業務需求,好比高可用需求。
上面就是Netty初級應用,咱們介紹了在設計一個簡單通信框架過程當中所涉及到的比較重要的特性,接下來的篇章,咱們將會講解如何設計分佈式服務框架等一些中級內容,但願您可以繼續駐足品嚐。