Netty是由JBOSS提供的一個java開源框架,是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持,做爲一個異步NIO框架,Netty的全部IO操做都是異步非阻塞html
做爲當前最流行的NIO框架,Netty在互聯網領域、大數據分佈式計算領域、遊戲行業、通訊行業等得到了普遍的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。java
Netty是業界最流行的NIO框架之一,它的健壯性、功能、性能、可定製性和可擴展性在同類框架中都是數一數二的,它已經獲得成百上千的商用項目驗證,例如Hadoop的RPC框架avro使用Netty做爲底層通訊框架;不少其餘業界主流的RPC框架,也使用Netty來構建高性能的異步通訊能力。算法
高併發編程
Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網絡通訊框架,對比於BIO(Blocking I/O,阻塞IO),他的併發性能獲得了很大提升 。segmentfault
傳輸快後端
Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。api
封裝好數組
Netty封裝了NIO操做的不少細節,提供易於使用的API。瀏覽器
JDK 原生也有一套網絡應用程序 API,可是存在一系列問題,主要以下:緩存
1)NIO 的類庫和 API 繁雜,使用麻煩:你須要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
2)須要具有其餘的額外技能作鋪墊:例如熟悉 Java 多線程編程,由於 NIO 編程涉及到 Reactor 模式,你必須對多線程和網路編程很是熟悉,才能編寫出高質量的 NIO 程序。
3)可靠性能力補齊,開發工做量和難度都很是大:例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等。NIO 編程的特色是功能開發相對容易,可是可靠性能力補齊工做量和難度都很是大。
4)JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它會致使 Selector 空輪詢,最終致使 CPU 100%。官方聲稱在 JDK 1.6 版本的 update 18 修復了該問題,可是直到 JDK 1.7 版本該問題仍舊存在,只不過該 Bug 發生機率下降了一些而已,它並無被根本解決。
API使用簡單,開發門檻低;
功能強大,預置了多種編解碼功能,支持多種主流協議;
定製能力強,能夠經過ChannelHandler對通訊框架進行靈活地擴展;
性能高,經過與其餘業界主流的NIO框架對比,Netty的綜合性能最優;
成熟、穩定,Netty修復了已經發現的全部JDK NIO BUG,業務開發人員不須要再爲NIO的BUG而煩惱;
社區活躍,版本迭代週期短,發現的BUG能夠被及時修復,同時,更多的新功能會加入;
經歷了大規模的商業應用考驗,質量獲得驗證。在互聯網、大數據、網絡遊戲、企業應用、電信軟件等衆多行業獲得成功商用,證實了它已經徹底可以知足不一樣行業的商業應用了。
Netty應用中必不可少的組件:
Bootstrap or ServerBootstrap
EventLoop
EventLoopGroup
ChannelPipeline
Channel
Future or ChannelFuture
ChannelInitializer
ChannelHandler
1.Bootstrap
一個Netty應用一般由一個Bootstrap開始,它主要做用是配置整個Netty程序,串聯起各個組件。
Handler,爲了支持各類協議和處理數據的方式,便誕生了Handler組件。Handler主要用來處理各類事件,這裏的事件很普遍,好比能夠是鏈接、數據接收、異常、數據轉換等。
2.ChannelInboundHandler
一個最經常使用的Handler。這個Handler的做用就是處理接收到數據時的事件,也就是說,咱們的業務邏輯通常就是寫在這個Handler裏面的,ChannelInboundHandler就是用來處理咱們的核心業務邏輯。
3.ChannelInitializer
當一個連接創建時,咱們須要知道怎麼進行接收或者發送數據,固然,咱們有各類各樣的Handler實現來處理它,那麼ChannelInitializer即是用來配置這些Handler,它會提供一個ChannelPipeline,並把Handler加入到ChannelPipeline。
4.ChannelPipeline
一個Netty應用基於ChannelPipeline機制,這種機制須要依賴於EventLoop和EventLoopGroup,由於它們三個都和事件或者事件處理相關。
EventLoops的目的是爲Channel處理IO操做,一個EventLoop能夠爲多個Channel服務。
EventLoopGroup會包含多個EventLoop。
5.Channel
表明了一個Socket連接,或者其它和IO操做相關的組件,它和EventLoop一塊兒用來參與IO處理。
6.Future
在Netty中全部的IO操做都是異步的,所以,你不能馬上得知消息是否被正確處理,可是咱們能夠過一會等它執行完成或者直接註冊一個監聽,具體的實現就是經過Future和ChannelFutures,他們能夠註冊一個監聽,當操做執行成功或失敗時監聽會自動觸發。
總之,全部的操做都會返回一個ChannelFuture。
1.互聯網行業
在分佈式系統中,各個節點之間須要遠程服務調用,高性能的RPC框架必不可少,Netty做爲異步高新能的通訊框架,每每做爲基礎通訊組件被這些RPC框架使用。
典型的應用有:阿里分佈式服務框架Dubbo的RPC框架使用Dubbo協議進行節點間通訊,Dubbo協議默認使用Netty做爲基礎通訊組件,用於實現各進程節點之間的內部通訊。
除了 Dubbo 以外,淘寶的消息中間件 RocketMQ 的消息生產者和消息消費者之間,也採用 Netty 進行高性能、異步通訊。
2.遊戲行業
不管是手遊服務端仍是大型的網絡遊戲,Java語言獲得了愈來愈普遍的應用。Netty做爲高性能的基礎通訊組件,它自己提供了TCP/UDP和HTTP協議棧。
很是方便定製和開發私有協議棧,帳號登陸服務器,地圖服務器之間能夠方便的經過Netty進行高性能的通訊
3.大數據領域
經典的Hadoop的高性能通訊和序列化組件Avro的RPC框架,默認採用Netty進行跨界點通訊,它的Netty Service基於Netty框架二次封裝實現。
Netty 採用了比較典型的三層網絡架構進行設計,邏輯架構圖以下所示:
#第一層,Reactor 通訊調度層,它由一系列輔助類完成,包括 NioEventLoop 以及其父類、NioSocketChannel/NioServerSocketChannel 以及其父 類、ByteBuffer 以及由其衍生出來的各類 Buffer、Unsafe 以及其衍生出的各類內部類等。該層的主要職責就是監聽網絡的讀寫和鏈接操做,負責將網絡層的數據讀取到內存緩衝區中,而後觸發各類網絡事件,例如鏈接建立、鏈接激活、讀事 件、寫事件等等,將這些事件觸發到 PipeLine 中,由 PipeLine 充當的職責鏈來進行後續的處理。
#第二層,職責鏈 PipeLine,它負責事件在職責鏈中的有序傳播,同時負責動態的編排職責鏈,職責鏈能夠選擇監聽和處理本身關心的事件,它能夠攔截處理和向後/向前傳播事件,不一樣的應用的 Handler 節點的功能也不一樣,一般狀況下,每每會開發編解碼 Handler 用於消息的編解碼,它能夠將外部的協議消息轉換成內部的 POJO 對象,這樣上層業務側只須要關心處理業務邏輯便可,不須要感知底層的協議差別和線程模型差別,實現了架構層面的分層隔離。
#第三層,業務邏輯處理層。能夠分爲兩類:純粹的業務邏輯處理,例如訂單處理;應用層協議管理,例如 HTTP 協議、FTP 協議等。
傳統同步阻塞I/O模式以下圖所示:
幾種I/O模型的功能和特性對比:
Netty的I/O模型基於非阻塞I/O實現,底層依賴的是JDK NIO框架的Selector。Selector提供選擇已經就緒的任務的能力。簡單來說,Selector會不斷地輪詢註冊在其上的Channel,若是某個Channel上面有新的TCP鏈接接入、讀和寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,而後經過SelectionKey能夠獲取就緒Channel的集合,從而進行後續的I/O操做。
一個多路複用器Selector能夠同時輪詢多個Channel,因爲JDK1.5_update10版本(+)使用了epoll()代替傳統的select實現,因此它並無最大鏈接句柄1024/2048的限制。這也就意味着只須要一個線程負責Selector的輪詢,就能夠接入成千上萬的客戶端,這確實是個很是巨大的技術進步。使用非阻塞I/O模型以後,Netty解決了傳統同步阻塞I/O帶來的性能、吞吐量和可靠性問題。
經常使用的Reactor線程模型有三種,分別以下:
#Reactor單線程模型:Reactor單線程模型,指的是全部的I/O操做都在同一個NIO線程上面完成。對於一些小容量應用場景,可使用單線程模型。
#Reactor多線程模型:Rector多線程模型與單線程模型最大的區別就是由一組NIO線程處理I/O操做。主要用於高併發、大業務量場景。
#主從Reactor多線程模型:主從Reactor線程模型的特色是服務端用於接收客戶端鏈接的再也不是個1個單獨的NIO線程,而是一個獨立的NIO線程池。利用主從NIO線程模型,能夠解決1個服務端監聽線程沒法有效處理全部客戶端鏈接的性能不足問題。
Netty的線程模型
說完了Reactor的三種模型,那麼Netty是哪種呢?其實Netty的線程模型是Reactor模型的變種,那就是去掉線程池的第三種形式的變種,這也是Netty NIO的默認模式。
事實上,Netty的線程模型並不是固定不變,經過在啓動輔助類中建立不一樣的EventLoopGroup實例並經過適當的參數配置,就能夠支持上述三種Reactor線程模型.
在大多數場景下,並行多線程處理能夠提高系統的併發性能。可是,若是對於共享資源的併發訪問處理不當,會帶來嚴重的鎖競爭,這最終會致使性能的降低。爲了儘量的避免鎖競爭帶來的性能損耗,能夠經過串行化設計,即消息的處理儘量在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。
爲了儘量提高性能,Netty採用了串行無鎖化設計,在I/O線程內部進行串行操做,避免多線程競爭致使的性能降低。表面上看,串行化設計彷佛CPU利用率不高,併發程度不夠。可是,經過調整NIO線程池的線程參數,能夠同時啓動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工做線程模型性能更優。
Reactor模型
Java NIO非堵塞技術實際是採起反應器模式,或者說是觀察者(observer)模式爲咱們監察I/O端口,若是有內容進來,會自動通知咱們,這樣,咱們就沒必要開啓多個線程死等,從外界看,實現了流暢的I/O讀寫,不堵塞了。
NIO 有一個主要的類Selector,這個相似一個觀察者,只要咱們把須要探知的socketchannel告訴Selector,咱們接着作別的事情,當有事件發生時,他會通知咱們,傳回一組SelectionKey,咱們讀取這些Key,就會得到咱們剛剛註冊過的socketchannel,而後,咱們從這個Channel中讀取數據,接着咱們能夠處理這些數據。
反應器模式與觀察者模式在某些方面極爲類似:當一個主體發生改變時,全部依屬體都獲得通知。不過,觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯 。
通常模型
EventLoopGroup:對應於Reactor模式中的定時器的角色,不斷地檢索是否有事件可用(I/O線程-BOSS),而後交給分離者將事件分發給對應的事件綁定的handler(WORK線程)。
經驗分享:在客戶端編程中常常容易出如今EVENTLOOP上作定時任務的,若是定時任務耗時很長或者存在阻塞,那麼可能會將I/O操做掛起(由於要等到定時任務作完才能作別的操做)。解決方法:用獨立的EventLoopGroup
序列化方式
影響序列化性能的關鍵因素總結以下:
- 序列化後的碼流大小(網絡帶寬佔用)
- 序列化&反序列化的性能(CPU資源佔用)
- 併發調用的性能表現:穩定性、線性增加、偶現的時延毛刺等
對Java序列化和二進制編碼分別進行性能測試,編碼100萬次,測試結果代表:Java序列化的性能只有二進制編碼的6.17%左右。
Netty默認提供了對Google Protobuf的支持,經過擴展Netty的編解碼接口,用戶能夠實現其它的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。
不一樣的應用場景對序列化框架的需求也不一樣,對於高性能應用場景Netty默認提供了Google的Protobuf二進制序列化框架,若是用戶對其它二進制序列化框架有需求,也能夠基於Netty提供的編解碼框架擴展實現。
Netty面臨的可靠性挑戰:
\1. 做爲RPC框架的基礎網絡通訊框架,一旦故障將致使沒法進行遠程服務(接口)調用。
\2. 做爲應用層協議的基礎通訊框架,一旦故障將致使應用協議棧沒法正常工做。
\3. 網絡環境複雜(例如推送服務的GSM/3G/WIFI網絡),故障不可避免,業務卻不能中斷。
從應用場景看,Netty是基礎的通訊框架,一旦出現Bug,輕則須要重啓應用,重則可能致使整個業務中斷。它的可靠性會影響整個業務集羣的數據通訊和交換,在當今以分佈式爲主的軟件架構體系中,通訊中斷就意味着整個業務中斷,分佈式架構下對通訊的可靠性要求很是高。
從運行環境看,Netty會面臨惡劣的網絡環境,這就要求它自身的可靠性要足夠好,平臺可以解決的可靠性問題須要由Netty自身來解決,不然會致使上層用戶關注過多的底層故障,這將下降Netty的易用性,同時增長用戶的開發和運維成本。
Netty的可靠性是如此重要,它的任何故障均可能會致使業務中斷,蒙受巨大的經濟損失。所以,Netty在版本的迭代中不斷加入新的可靠性特性來知足用戶日益增加的高可靠和健壯性需求。
鏈路有效性檢測
Netty提供的心跳檢測機制分爲三種:
- 讀空閒,鏈路持續時間t沒有讀取到任何消息
- 寫空閒,鏈路持續時間t沒有發送任何消息
- 讀寫空閒,鏈路持續時間t沒有接收或者發送任何消息
當網絡發生單通、鏈接被防火牆攔截住、長時間GC或者通訊線程發生非預期異常時,會致使鏈路不可用且不易被及時發現。特別是異常發生在凌晨業務低谷期間,當早晨業務高峯期到來時,因爲鏈路不可用會致使瞬間的大批量業務失敗或者超時,這將對系統的可靠性產生重大的威脅。
從技術層面看,要解決鏈路的可靠性問題,必須週期性的對鏈路進行有效性檢測。目前最流行和通用的作法就是心跳檢測。
心跳檢測機制分爲三個層面:
\1. TCP層面的心跳檢測,即TCP的Keep-Alive機制,它的做用域是整個TCP協議棧;
\2. 協議層的心跳檢測,主要存在於長鏈接協議中。例如SMPP協議;
\3. 應用層的心跳檢測,它主要由各業務產品經過約定方式定時給對方發送心跳消息實現。
Keep-Alive僅僅是TCP協議層會發送連通性檢測包,但並不表明設置了Keep-Alive就是長鏈接了。
心跳檢測的目的就是確認當前鏈路可用,對方活着而且可以正常接收和發送消息。
作爲高可靠的NIO框架,Netty也提供了基於鏈路空閒的心跳檢測機制:
- 讀空閒,鏈路持續時間t沒有讀取到任何消息
- 寫空閒,鏈路持續時間t沒有發送任何消息
- 讀寫空閒,鏈路持續時間t沒有接收或者發送任何消息(netty自帶心跳處理Handler IdleStateHandler)
客戶端和服務端之間鏈接斷開機制
TCP鏈接的創建須要三個分節(三次握手),終止則須要四個分節。
對於大量短鏈接的狀況下,常常出現卡在FIN_WAIT2和TIMEWAIT狀態的鏈接,等待系統回收,可是操做系統底層回收的時間頻率很長,致使SOCKET被耗盡。
TCP狀態圖
TCP/IP半關閉
從上述講的TCP關閉的四個分節能夠看出,被動關閉執行方,發送FIN分節的前提是TCP套接字對應應用程序調用close產生的。若是服務端有數據發送給客戶端那麼可能存在服務端在接受到FIN以後,須要將數據發送到客戶端才能發送FIN字節。這種處於業務考慮的情形一般稱爲半關閉。
半關閉可能致使大量socket處於CLOSE_WAIT狀態
誰負責關閉鏈接合理
鏈接關閉觸發的條件一般分爲以下幾種:
\1. 數據發送完成(發送到對端而且收到響應),關閉鏈接
\2. 通訊過程當中產生異常
\3. 特殊指令強制要求關閉鏈接
對於第一種,一般關閉時機是,數據發送完成方發起(客戶端觸發居多); 對於第二種,異常產生方觸發(例如殘包、錯誤數據等)發起。可是此種狀況可能也致使壓根沒法發送FIN。對於第三種,一般是用於運維等。由命令發起方產生。
流量整形(Traffic Shaping)是一種主動調整流量輸出速率的措施。
Netty的流量整形有兩個做用:
\1. 防止因爲上下游網元性能不均衡致使下游網元被壓垮,業務流程中斷
\2. 防止因爲通訊模塊接收消息過快,後端業務線程處理不及時致使的"撐死"問題
流量整形的原理示意圖以下:
流量整形(Traffic Shaping)是一種主動調整流量輸出速率的措施。一個典型應用是基於下游網絡結點的TP指標來控制本地流量的輸出。
流量監管TP(Traffic Policing)就是對流量進行控制,經過監督進入網絡的流量速率,對超出部分的流量進行「懲罰」,使進入的流量被限制在一個合理的範圍以內,從而保護網絡資源和用戶的利益。
流量整形與流量監管的主要區別在於,流量整形對流量監管中須要丟棄的報文進行緩存——一般是將它們放入緩衝區或隊列內,也稱流量整形(Traffic Shaping,簡稱TS)。當令牌桶有足夠的令牌時,再均勻的向外發送這些被緩存的報文。流量整形與流量監管的另外一區別是,整形可能會增長延遲,而監管幾乎不引入額外的延遲。
#全局流量整形:全局流量整形的做用範圍是進程級的,不管你建立了多少個Channel,它的做用域針對全部的Channel。用戶能夠經過參數設置:報文的接收速率、報文的發送速率、整形週期。[GlobalChannelTrafficShapingHandler]
#鏈路級流量整形:單鏈路流量整形與全局流量整形的最大區別就是它以單個鏈路爲做用域,能夠對不一樣的鏈路設置不一樣的整形策略。[ChannelTrafficShapingHandler針對於每一個channel]
優雅停機
Netty的優雅停機三部曲: 1. 再也不接收新消息 2. 退出前的預處理操做 3. 資源的釋放操做
Java的優雅停機一般經過註冊JDK的ShutdownHook來實現,當系統接收到退出指令後,首先標記系統處於退出狀態,再也不接收新的消息,而後將積壓的消息處理完,最後調用資源回收接口將資源銷燬,最後各線程退出執行。
一般優雅退出須要有超時控制機制,例如30S,若是到達超時時間仍然沒有完成退出前的資源回收等操做,則由停機腳本直接調用kill -9 pid,強制退出。
在實際項目中,Netty做爲高性能的異步NIO通訊框架,每每用做基礎通訊框架負責各類協議的接入、解析和調度等,例如在RPC和分佈式服務框架中,每每會使用Netty做爲內部私有協議的基礎通訊框架。 當應用進程優雅退出時,做爲通訊框架的Netty也須要優雅退出,主要緣由以下:
\1. 儘快的釋放NIO線程、句柄等資源
\2. 若是使用flush作批量消息發送,須要將積攢在發送隊列中的待發送消息發送完成
\3. 正在write或者read的消息,須要繼續處理
\4. 設置在NioEventLoop線程調度器中的定時任務,須要執行或者清理
Netty架構剖析之安全性
Netty面臨的安全挑戰:
- 對第三方開放
- 做爲應用層協議的基礎通訊框架
安全威脅場景分析:
#對第三方開放的通訊框架:若是使用Netty作RPC框架或者私有協議棧,RPC框架面向非授信的第三方開放,例如將內部的一些能力經過服務對外開放出去,此時就須要進行安全認證,若是開放的是公網IP,對於安全性要求很是高的一些服務,例如在線支付、訂購等,須要經過SSL/TLS進行通訊。
#應用層協議的安全性:做爲高性能、異步事件驅動的NIO框架,Netty很是適合構建上層的應用層協議。因爲絕大多數應用層協議都是公有的,這意味着底層的Netty須要向上層提供通訊層的安全傳輸功能。
SSL/TLS
Netty安全傳輸特性:
- 支持SSL V2和V3
- 支持TLS
- 支持SSL單向認證、雙向認證和第三方CA認證。
SSL單向認證流程圖以下:
Netty經過SslHandler提供了對SSL的支持,它支持的SSL協議類型包括:SSL V二、SSL V3和TLS。
#單向認證:單向認證,即客戶端只驗證服務端的合法性,服務端不驗證客戶端。
#雙向認證:與單向認證不一樣的是服務端也須要對客戶端進行安全認證。這就意味着客戶端的自簽名證書也須要導入到服務端的數字證書倉庫中。
#CA認證:基於自簽名的SSL雙向認證,只要客戶端或者服務端修改了密鑰和證書,就須要從新進行簽名和證書交換,這種調試和維護工做量是很是大的。所以,在實際的商用系統中每每會使用第三方CA證書頒發機構進行簽名和驗證。咱們的瀏覽器就保存了幾個經常使用的CA_ROOT。每次鏈接到網站時只要這個網站的證書是通過這些CA_ROOT簽名過的。就能夠經過驗證了。
可擴展的安全特性
經過Netty的擴展特性,能夠自定義安全策略:
- IP地址黑名單機制
- 接入認證
- 敏感信息加密或者過濾機制
IP地址黑名單是比較經常使用的弱安全保護策略,它的特色就是服務端在與客戶端通訊的過程當中,對客戶端的IP地址進行校驗,若是發現對方IP在黑名單列表中,則拒絕與其通訊,關閉鏈路。
接入認證策略很是多,一般是較強的安全認證策略,例如基於用戶名+密碼的認證,認證內容每每採用加密的方式,例如Base64+AES等。
Netty架構剖析之擴展性
經過Netty的擴展特性,能夠自定義安全策略:
- 線程模型可擴展
- 序列化方式可擴展
- 上層協議棧可擴展
- 提供大量的網絡事件切面,方便用戶功能擴展
Netty的架構可擴展性設計理念以下:
\1. 判斷擴展點,事先預留相關擴展接口,給用戶二次定製和擴展使用
\2. 主要功能點都基於接口編程,方便用戶定製和擴展。
TCP粘包是指發送方發送的若干包數據到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。
出現粘包現象的緣由是多方面的,它既可能由發送方形成,也可能由接收方形成。發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一包數據。若連續幾回發送的數據都不多,一般TCP會根據優化算法把這些數據合成一包後一次發送出去,這樣接收方就收到了粘包數據。接收方引發的粘包是因爲接收方用戶進程不及時接收數據,從而致使粘包現象。這是由於接收方先把收到的數據放在系統接收緩衝區,用戶進程從該緩衝區取數據,若下一包數據到達時前一包數據還沒有被用戶進程取走,則下一包數據放到系統接收緩衝區時就接到前一包數據以後,而用戶進程根據預先設定的緩衝區大小從系統接收緩衝區取數據,這樣就一次取到了多包數據。
粘包狀況有兩種:
\1. 粘在一塊兒的包都是完整的數據包
\2. 粘在一塊兒的包有不完整的包
解決粘連包的方法大體分爲以下三種:
\1. 發送方開啓TCP_NODELAY
\2. 接收方簡化或者優化流程儘量快的接收數據
\3. 認爲強制分包每次只讀一個完整的包
對於以上三種方式,第一種會加劇網絡負擔,第二種治標不治本,第三種算比較合理的。
第三種又能夠分兩種方式:
\1. 每次都只讀取一個完整的包,若是不足一個完整的包,就等下次再接收,若是緩衝區有N個包要接受,那麼須要分N次才能接收完成
\2. 有多少接收多少,將接収的數據緩存在一個臨時的緩存中,交由後續的專門解碼的線程/進程處理
以上兩種分包方式,若是強制關閉程序,數據會存在丟失,第一種數據丟失在接收緩衝區;第二種丟失在程序自身緩存。
Netty自帶的幾種粘連包解決方案:
\1. DelimiterBasedFrameDecoder (帶分隔符)
\2. FixedLengthFrameDecoder (定長)
\3. LengthFieldBasedFrameDecoder(將消息分爲消息頭和消息體,消息頭中包含消息總長度的字段)
Netty解包組包
對於TCP編程最常遇到的就是根據具體的協議進行組包或者解包。
根據協議的不一樣大體能夠分爲以下幾種類型:
\1. JAVA平臺之間經過JAVA序列化進行解包組包(object->byte->object)
\2. 固定長度的包結構(定長每一個包都是M個字節的長度)
\3. 帶有明確分隔符協議的解包組包(例如HTTP協議\r\n\r\n)
\4. 可動態擴展的協議(每一個包都添加一個消息頭),此種協議一般遵循消息頭+消息體的機制,其中消息頭的長度是固定的,消息體的長度根據具體業務的不一樣長度可能不一樣。例如(SMPP協議、CMPP協議)
#序列化協議組包解包
可使用的有:MessagePack、Google Protobuf、Hessian2
#固定長度解包組包
FixedLengthFrameDecoder 解包,MessageToByteEncoder 組包
#帶有分隔符協議的解包組包
DelimiterBasedFrameDecoder 解包,MessageToByteEncoder 組包
#HTTP
io.netty.codec.http
#消息頭固定長度,消息體不固定長度協議解包組包
LengthFieldBasedFrameDecoder
須要注意的是:對於解碼的Handler必須作到在將ByteBuf解析成Object以後,須要將ByteBuf release()。
對於長鏈接的程序斷網重連幾乎是程序的標配。
斷網重連具體能夠分爲兩類:
CONNECT失敗,須要重連
程序運行過程當中斷網、遠程強制關閉鏈接、收到錯誤包必須重連
對於第一種解決方案是:實現ChannelFutureListener 用來啓動時監測是否鏈接成功,不成功的話重試。
Future-Listener機制
在併發編程中,咱們一般會用到一組非阻塞的模型:Promise,Future,Callback。
其中的Future表示一個可能尚未實際完成的異步任務的結果,針對這個結果添加Callback以便在執行任務成功或者失敗後作出響應的操做。而經由Promise交給執行者,任務執行者經過Promise能夠標記任務完成或者失敗。以上這套模型是不少異步非阻塞框架的基礎。具體的理解可參見JDK的FutureTask和Callable。JDK的實現版本,在獲取最終結果的時候,不得不作一些阻塞的方法等待最終結果的到來。Netty的Future機制是JDK機制的一個子版本,它支持給Future添加Listener,以方便EventLoop在任務調度完成以後調用。
數據安全性之滑動窗口協議
咱們假設一個場景,客戶端每次請求服務端必須獲得服務端的一個響應,因爲TCP的數據發送和數據接收是異步的,就存在必須存在一個等待響應的過程。該過程根據實現方式不一樣能夠分爲一下幾類(部分是錯誤案例):
\1. 每次發送一個數據包,而後進入休眠(sleep)或者阻塞(await)狀態,直到響應回來或者超時,整個調用鏈結束。此場景是典型的一問一答的場景,效率極其低下
\2. 讀寫分離,寫模塊只負責寫,讀模塊則負責接收響應,而後作後續的處理。此種場景能儘量的利用帶寬進行讀寫。可是此場景不作控速操做可能致使大量報文丟失或者重複發送。
\3. 實現相似於Windowed Protocol。此窗口是以上兩種方案的折中版,即容許必定數量的批量發送,又能保證數據的完整性。
做者:@xys1228 本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-pededtnv-bu.html Email:yongshun1228@gmail.com
目錄
經過 CompositeByteBuf 實現零拷貝
經過 wrap 操做實現零拷貝
經過 slice 操做實現零拷貝
經過 FileRegion 實現零拷貝
此文章已同步發佈在個人 segmentfault 專欄.
根據 Wiki 對 Zero-copy 的定義:
"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.
即所謂的 Zero-copy
, 就是在操做數據時, 不須要將數據 buffer 從一個內存區域拷貝到另外一個內存區域. 由於少了一次內存的拷貝, 所以 CPU 的效率就獲得的提高.
在 OS 層面上的 Zero-copy
一般指避免在 用戶態(User-space)
與 內核態(Kernel-space)
之間來回拷貝數據. 例如 Linux 提供的 mmap
系統調用, 它能夠將一段用戶空間內存映射到內核空間, 當映射成功後, 用戶對這段內存區域的修改能夠直接反映到內核空間; 一樣地, 內核空間對這段區域的修改也直接反映用戶空間. 正由於有這樣的映射關係, 咱們就不須要在 用戶態(User-space)
與 內核態(Kernel-space)
之間拷貝數據, 提升了數據傳輸的效率.
而須要注意的是, Netty 中的 Zero-copy
與上面咱們所提到到 OS 層面上的 Zero-copy
不太同樣, Netty的 Zero-coyp
徹底是在用戶態(Java 層面)的, 它的 Zero-copy
的更多的是偏向於 優化數據操做
這樣的概念.
Netty 的 Zero-copy
體如今以下幾個個方面:
Netty 提供了 CompositeByteBuf
類, 它能夠將多個 ByteBuf 合併爲一個邏輯上的 ByteBuf, 避免了各個 ByteBuf 之間的拷貝.
經過 wrap 操做, 咱們能夠將 byte[] 數組、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝操做.
ByteBuf 支持 slice 操做, 所以能夠將 ByteBuf 分解爲多個共享同一個存儲區域的 ByteBuf, 避免了內存的拷貝.
經過 FileRegion
包裝的FileChannel.tranferTo
實現文件傳輸, 能夠直接將文件緩衝區的數據發送到目標 Channel
, 避免了傳統經過循環 write 方式致使的內存拷貝問題.
下面咱們就來簡單瞭解一下這幾種常見的零拷貝操做.
假設咱們有一份協議數據, 它由頭部和消息體組成, 而頭部和消息體是分別存放在兩個 ByteBuf 中的, 即:
`ByteBuf header = ...``ByteBuf body = ...`
咱們在代碼處理中, 一般但願將 header 和 body 合併爲一個 ByteBuf, 方便處理, 那麼一般的作法是:
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
能夠看到, 咱們將 header 和 body 都拷貝到了新的 allBuf 中了, 這無形中增長了兩次額外的數據拷貝操做了.
那麼有沒有更加高效優雅的方式實現相同的目的呢? 咱們來看一下 CompositeByteBuf
是如何實現這樣的需求的吧.
`ByteBuf header = ...`
`ByteBuf body = ...`
`CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();`
`compositeByteBuf.addComponents(``true``, header, body);`
上面代碼中, 咱們定義了一個 CompositeByteBuf
對象, 而後調用
`public` `CompositeByteBuf addComponents(``boolean` `increaseWriterIndex, ByteBuf... buffers) {``...``}`
方法將 header
與 body
合併爲一個邏輯上的 ByteBuf, 即:
不過須要注意的是, 雖然看起來 CompositeByteBuf 是由兩個 ByteBuf 組合而成的, 不過在 CompositeByteBuf 內部, 這兩個 ByteBuf 都是單獨存在的, CompositeByteBuf 只是邏輯上是一個總體.
上面 CompositeByteBuf
代碼還以一個地方值得注意的是, 咱們調用 addComponents(boolean increaseWriterIndex, ByteBuf... buffers)
來添加兩個 ByteBuf, 其中第一個參數是 true
, 表示當添加新的 ByteBuf 時, 自動遞增 CompositeByteBuf 的 writeIndex
. 若是咱們調用的是
`compositeByteBuf.addComponents(header, body);`
那麼其實 compositeByteBuf
的 writeIndex
仍然是0, 所以此時咱們就不可能從 compositeByteBuf
中讀取到數據, 這一點但願你們要特別注意.
除了上面直接使用 CompositeByteBuf
類外, 咱們還可使用 Unpooled.wrappedBuffer
方法, 它底層封裝了 CompositeByteBuf
操做, 所以使用起來更加方便:
`ByteBuf header = ...``ByteBuf body = ...`
`ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);`
例如咱們有一個 byte 數組, 咱們但願將它轉換爲一個 ByteBuf 對象, 以便於後續的操做, 那麼傳統的作法是將此 byte 數組拷貝到 ByteBuf 中, 即:
`byte``[] bytes = ...
``ByteBuf byteBuf = Unpooled.buffer();`
`byteBuf.writeBytes(bytes);`
顯然這樣的方式也是有一個額外的拷貝操做的, 咱們可使用 Unpooled 的相關方法, 包裝這個 byte 數組, 生成一個新的 ByteBuf 實例, 而不須要進行拷貝操做. 上面的代碼能夠改成:
`byte``[] bytes = ...` `ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);`
能夠看到, 咱們經過 Unpooled.wrappedBuffer
方法來將 bytes 包裝成爲一個 UnpooledHeapByteBuf 對象, 而在包裝的過程當中, 是不會有拷貝操做的. 即最後咱們生成的生成的 ByteBuf 對象是和 bytes 數組共用了同一個存儲空間, 對 bytes 的修改也會反映到 ByteBuf 對象中.
Unpooled 工具類還提供了不少重載的 wrappedBuffer 方法:
`public` `static` `ByteBuf wrappedBuffer(``byte``[] array)` `public` `static` `ByteBuf wrappedBuffer(``byte``[] array, ``int` `offset, ``int` `length)` `public` `static` `ByteBuf wrappedBuffer(ByteBuffer buffer)` `public` `static` `ByteBuf wrappedBuffer(ByteBuf buffer)` `public` `static` `ByteBuf wrappedBuffer(``byte``[]... arrays)` `public` `static` `ByteBuf wrappedBuffer(ByteBuf... buffers)` `public` `static` `ByteBuf wrappedBuffer(ByteBuffer... buffers)` `public` `static` `ByteBuf wrappedBuffer(``int` `maxNumComponents, ``byte``[]... arrays)` `public` `static` `ByteBuf wrappedBuffer(``int` `maxNumComponents, ByteBuf... buffers)` `public` `static` `ByteBuf wrappedBuffer(``int` `maxNumComponents, ByteBuffer... buffers)`
這些方法能夠將一個或多個 buffer 包裝爲一個 ByteBuf 對象, 從而避免了拷貝操做.
slice 操做和 wrap 操做恰好相反, Unpooled.wrappedBuffer
能夠將多個 ByteBuf 合併爲一個, 而 slice 操做能夠將一個 ByteBuf 切片
爲多個共享一個存儲區域的 ByteBuf 對象. ByteBuf 提供了兩個 slice 操做方法:
`public` `ByteBuf slice();` `public` `ByteBuf slice(``int` `index, ``int` `length);`
不帶參數的 slice
方法等同於 buf.slice(buf.readerIndex(), buf.readableBytes())
調用, 即返回 buf 中可讀部分的切片. 而 slice(int index, int length)
方法相對就比較靈活了, 咱們能夠設置不一樣的參數來獲取到 buf 的不一樣區域的切片.
下面的例子展現了 ByteBuf.slice
方法的簡單用法:
`ByteBuf byteBuf = ...` `ByteBuf header = byteBuf.slice(``0``, ``5``);` `ByteBuf body = byteBuf.slice(``5``, ``10``);`
用 slice
方法產生 header 和 body 的過程是沒有拷貝操做的, header 和 body 對象在內部實際上是共享了 byteBuf 存儲空間的不一樣部分而已. 即:
Netty 中使用 FileRegion 實現文件傳輸的零拷貝, 不過在底層 FileRegion 是依賴於 Java NIO FileChannel.transfer
的零拷貝功能.
首先咱們從最基礎的 Java IO 開始吧. 假設咱們但願實現一個文件拷貝的功能, 那麼使用傳統的方式, 咱們有以下實現:
`public` `static` `void` `copyFile(String srcFile, String destFile) ``throws` `Exception {`` ``byte``[] temp = ``new` `byte``[``1024``];`` ``FileInputStream in = ``new` `FileInputStream(srcFile);`` ``FileOutputStream out = ``new` `FileOutputStream(destFile);`` ``int` `length;`` ``while` `((length = in.read(temp)) != -``1``) {`` ``out.write(temp, ``0``, length);`` ``}` ` ``in.close();`` ``out.close();` `}`
上面是一個典型的讀寫二進制文件的代碼實現了. 不用我說, 你們確定都知道, 上面的代碼中不斷中源文件中讀取定長數據到 temp 數組中, 而後再將 temp 中的內容寫入目的文件, 這樣的拷貝操做對於小文件卻是沒有太大的影響, 可是若是咱們須要拷貝大文件時, 頻繁的內存拷貝操做就消耗大量的系統資源了. 下面咱們來看一下使用 Java NIO 的 FileChannel
是如何實現零拷貝的:
`public` `static` `void` `copyFileWithFileChannel(String srcFileName, String destFileName) ``throws` `Exception {`` ``RandomAccessFile srcFile = ``new` `RandomAccessFile(srcFileName, ``"r"``);`` ``FileChannel srcFileChannel = srcFile.getChannel();` ` ``RandomAccessFile destFile = ``new` `RandomAccessFile(destFileName, ``"rw"``);`` ``FileChannel destFileChannel = destFile.getChannel();` ` ``long` `position = ``0``;`` ``long` `count = srcFileChannel.size();` ` ``srcFileChannel.transferTo(position, count, destFileChannel);``}`
能夠看到, 使用了 FileChannel
後, 咱們就能夠直接將源文件的內容直接拷貝(transferTo
) 到目的文件中, 而不須要額外借助一個臨時 buffer, 避免了沒必要要的內存操做.
有了上面的一些理論知識, 咱們來看一下在 Netty 中是怎麼使用 FileRegion
來實現零拷貝傳輸一個文件的:
`@Override` `public` `void` `channelRead0(ChannelHandlerContext ctx, String msg)``throws` `Exception {`` ``RandomAccessFile raf = ``null``;`` ``long` `length = -``1``;`` ``try` `{`` ``// 1. 經過 RandomAccessFile 打開一個文件.`` ``raf = ``new` `RandomAccessFile(msg, ``"r"``);`` ``length = raf.length();`` ``} ``catch` `(Exception e) {`` ``ctx.writeAndFlush(``"ERR: "` `+ e.getClass().getSimpleName() + ``": "` `+ e.getMessage() + ``'\n'``);`` ``return``;`` ``} ``finally` `{`` ``if` `(length < ``0` `&& raf != ``null``) {`` ``raf.close();`` ``}`` ``}` ` ``ctx.write(``"OK: "` `+ raf.length() + ``'\n'``);`` ``if` `(ctx.pipeline().get(SslHandler.``class``) == ``null``) {`` ``// SSL not enabled - can use zero-copy file transfer.`` ``// 2. 調用 raf.getChannel() 獲取一個 FileChannel.`` ``// 3. 將 FileChannel 封裝成一個 DefaultFileRegion`` ``ctx.write(``new` `DefaultFileRegion(raf.getChannel(), ``0``, length));`` ``} ``else` `{`` ``// SSL enabled - cannot use zero-copy file transfer.`` ``ctx.write(``new` `ChunkedFile(raf));`` ``}`` ``ctx.writeAndFlush(``"\n"``);``}`
上面的代碼是 Netty 的一個例子, 其源碼在 netty/example/src/main/java/io/netty/example/file/FileServerHandler.java 能夠看到, 第一步是經過 RandomAccessFile
打開一個文件, 而後 Netty 使用了 DefaultFileRegion
來封裝一個 FileChannel
即:
`new` `DefaultFileRegion(raf.getChannel(), ``0``, length)`
當有了 FileRegion 後, 咱們就能夠直接經過它將文件的內容直接寫入 Channel 中, 而不須要像傳統的作法: 拷貝文件內容到臨時 buffer, 而後再將 buffer 寫入 Channel. 經過這樣的零拷貝操做, 無疑對傳輸大文件頗有幫助.
問:據我以前瞭解到,Java的NIO selector底層在Windows下的實現是起兩個隨機端口互聯來監測鏈接或讀寫事件,在Linux上是利用管道實現的;我有遇到過這樣的需求,須要佔用不少個固定端口作服務端,若是在Windows下,利用NIO框架(Mina或Netty)就有可能會形成端口衝突,這種狀況有什麼好的解決方案嗎?
你說的問題確實存在,Linux使用Pipe實現網絡監聽,Windows要啓動端口。目前沒有更好的辦法,建議的方式是做爲服務端的端口能夠規劃一個範圍,而後根據節點和進程信息動態生成,若是發現端口衝突,能夠在規劃範圍內基於算法從新生成一個新的端口。
問:請我,我如今將Spring與Netty作了整合,使用Spring的Service開啓 Netty主線程,可是中止整個運行容器的時候,Netty的TCP Server端口不能釋放?退出處理時,有什麼好的辦法釋放Netty Server端口麼?
實際上,由誰拉起Netty 主線程並不重要。咱們須要作的就是當應用容器退出的時候(Spring Context銷燬),在退出以前調用Netty 的優雅退出接口便可實現端口、NIO線程資源的釋放。請參考這篇文章:http://www.infoq.com/cn/articles/netty-elegant-exit-mechanism-and-principles
問:適合用Netty寫Web通訊麼?
Netty不是Web框架,沒法解析JSP、HTML、JS等,可是它能夠作Web 通訊,例如可使用Netty重寫Tomcat的HTTP/HTTPS 通訊協議棧。
問:能不能講解一下Netty的串行無鎖化設計,如何在串行和並行中達到最優?
爲了儘量提高性能,Netty採用了串行無鎖化設計,在IO線程內部進行串行操做,避免多線程競爭致使的性能降低。表面上看,串行化設計彷佛CPU利用率不高,併發程度不夠。可是,經過調整NIO線程池的線程參數,能夠同時啓動多個串行化的線程並行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工做線程模型性能更優。Netty的NioEventLoop讀取到消息以後,直接調用ChannelPipeline的fireChannelRead(Object msg),只要用戶不主動切換線程,一直會由NioEventLoop調用到用戶的Handler,期間不進行線程切換,這種串行化處理方式避免了多線程操做致使的鎖的競爭