前言java
怎樣才能說本身懂Netty ? 如何將Netty瞭解到必定的深度 ? 如何全面玩轉Netty的實戰 ? 不妨沿着下面的路徑,對Netty進行一個全面的認識和思考!算法
第一節、網絡編程NIO的Reactor模式數據庫
第二節、Netty和選擇Netty的理由
編程
第三節、Netty入門中三個基本特性
數組
第四節、核心概念和機制 - EventLoop、EventLoopGroup
緩存
第五節、主要組件ChannelHandler、ChannelHandlerContext和ChnnelPipeline
安全
第六節、Netty支持的網絡通信傳輸模式
服務器
第七節、操做系統層ChannelOption詳解
網絡
第八節、特有的緩衝封裝ByteBuf詳解
多線程
第九節、Netty中的重要機制引用計數
第十節、Netty如何解決粘包/半包問題
第十一節、Netty重量級組件編解碼器
第十二節、Netty內置及引入外部序列和反序列化部件
第十三節、如何獨立進行ChannelHandler的單元測試
全面認識Netty開始
第一節、網絡編程NIO的Reactor模式
Reactor反應器中的 "反應" 即倒置、控制反轉的意思。具體事件處理程序不調用反應器,而向反應器註冊一個事件處理器,表示本身對某些事件感興趣,有事件來了,具體事件處理程序經過事件處理器對某個指定的事件發生作出反應;這種控制逆轉又稱爲「好萊塢法則」(通俗點就是:你不要主動找我,有事我來找你)。
單線程Reactor模式
① 服務器端的Reactor是一個線程對象,該線程會啓動事件循環,並使用Selector(選擇器)來實現IO的多路複用。註冊一個Acceptor事件處理器到Reactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣Reactor會監聽客戶端向服務器端發起的鏈接請求事件(ACCEPT事件);
② 客戶端向服務器端發起一個鏈接請求,Reactor監聽到了該ACCEPT事件的發生並將該ACCEPT事件派發給相應的Acceptor處理器來進行處理。Acceptor處理器經過accept()方法獲得與這個客戶端對應的鏈接(SocketChannel),而後將該鏈接所關注的READ事件以及對應的READ事件處理器註冊到Reactor中,這樣一來Reactor就會監聽該鏈接的READ事件了;
③ 當Reactor監聽到有讀或者寫事件發生時,將相關的事件派發給對應的處理器進行處理。好比,讀處理器會經過SocketChannel的read()方法讀取數據,此時read()操做能夠直接讀取到數據,而不會堵塞與等待可讀的數據到來;
④ 每當處理完全部就緒的感興趣的I/O事件後,Reactor線程會再次執行select()阻塞等待新的事件就緒並將其分派給對應處理器進行處理;
注意,Reactor的單線程模式的單線程主要是針對於I/O操做而言,也就是全部的I/O的accept()、read()、write()以及connect()操做都在一個線程上完成的;
但在目前的單線程Reactor模式中,不只I/O操做在該Reactor線程上,連非I/O的業務操做也在該線程上進行處理了,這可能會大大延遲I/O請求的響應。因此咱們應該將非I/O的業務邏輯操做從Reactor線程上卸載,以此來加速Reactor線程對I/O請求的響應。
多線程Reactor模式
Reactor線程池中的每一Reactor線程都會有本身的Selector、線程和分發的事件循環邏輯。
mainReactor能夠只有一個,但subReactor通常會有多個。mainReactor線程主要負責接收客戶端的鏈接請求,而後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通訊。交互流程以下:
① 註冊一個Acceptor事件處理器到mainReactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣mainReactor會監聽客戶端向服務器端發起的鏈接請求事件(ACCEPT事件)。啓動mainReactor的事件循環;
② 客戶端向服務器端發起一個鏈接請求,mainReactor監聽到了該ACCEPT事件並將該ACCEPT事件派發給Acceptor處理器來進行處理。Acceptor處理器經過accept()方法獲得與這個客戶端對應的鏈接(SocketChannel),而後將這個SocketChannel傳遞給subReactor線程池;
③ subReactor線程池分配一個subReactor線程給這個SocketChannel,即,將SocketChannel關注的READ事件以及對應的READ事件處理器註冊到subReactor線程中。固然你也註冊WRITE事件以及WRITE事件處理器到subReactor線程中以完成I/O寫操做。Reactor線程池中的每一Reactor線程都會有本身的Selector、線程和分發的循環邏輯;
④ 當有I/O事件就緒時,相關的subReactor就將事件派發給響應的處理器處理。注意,這裏subReactor線程只負責完成I/O的read()操做,在讀取到數據後將業務邏輯的處理放入到線程池中完成,若完成業務邏輯後須要返回數據給客戶端,則相關的I/O的write操做仍是會被提交回subReactor線程來完成;
注意,因此的I/O操做(包括,I/O的accept()、read()、write()以及connect()操做)依舊仍是在Reactor線程(mainReactor線程 或 subReactor線程)中完成的。Thread Pool(線程池)僅用來處理非I/O操做的邏輯;
多Reactor線程模式將「接受客戶端的鏈接請求」和「與該客戶端的通訊」分在了兩個Reactor線程來完成。mainReactor完成接收客戶端鏈接請求的操做,它不負責與客戶端的通訊,而是將創建好的鏈接轉交給subReactor線程來完成與客戶端的通訊,這樣一來就不會由於read()數據量太大而致使後面的客戶端鏈接請求得不到即時處理的狀況。而且多Reactor線程模式在海量的客戶端併發請求的狀況下,還能夠經過實現subReactor線程池來將海量的鏈接分發給多個subReactor線程,在多核的操做系統中這能大大提高應用的負載和吞吐量,Netty服務端使用了多Reactor線程模式。
第二節、Netty和選擇Netty的理由
Netty是由jbss提供的一個java開源框架, 它提供異步的、事件驅動網絡編程框架和工具;支撐開發快速、高性能、高穩定性的端到端(服務端—網絡端)程序。咱們經常使用的Netty大版本是Netty4,而最新的Netty5嚴格地說,仍是alpha版本,還有一些問題且並沒通過實戰。
選擇Netty的理由
1、雖然JAVA NIO框架提供了 多路複用IO的支持,可是並無提供上層「信息格式」的良好封裝。例如前二者並無提供針對 Protocol Buffer、JSON這些信息格式的封裝,可是Netty框架提供了這些數據格式封裝(基於責任鏈模式的編碼和解碼功能);
2、NIO的類庫和API至關複雜,使用它來開發,須要很是熟練地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等,須要不少額外的編程技能來輔助使用NIO,例如,由於NIO涉及了Reactor線程模型,因此必須必須對多線程和網絡編程很是熟悉才能寫出高質量的NIO程序;
3、要編寫一個可靠的、易維護的、高性能的NIO服務器應用。除了框架自己要兼容實現各種操做系統的實現外。更重要的是它應該還要處理不少上層特有服務,例如:客戶端的權限、還有上面提到的信息格式封裝、簡單的數據讀取,斷連重連,半包讀寫,心跳等等,這些Netty框架都提供了響應的支持;
4、JAVA NIO框架存在一個poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能block意味着CPU的使用率會變成100%(這是底層JNI的問題,上層要處理這個異常實際上也好辦)。固然這個bug只有在Linux內核上才能重現;
這個問題在JDK 1.7版本中尚未被徹底解決,可是Netty已經將這個bug進行了處理。
這個Bug與操做系統機制有關係的,JDK雖然僅僅是一個兼容各個操做系統平臺的軟件,但在JDK5和JDK6最初的版本中(嚴格意義上來將,JDK部分版本都是),這個問題並無解決,而將這個帽子拋給了操做系統方,這也就是這個bug最終一直到2013年才最終修復的緣由(JDK7和JDK8之間)。
第三節、Netty入門中三個基本特性
事件和Channel
Netty 使用不一樣的事件來通知咱們狀態的改變或者是操做的狀態。這使得咱們可以基於已經發生的事件來觸發適當的動做。由入站數據或者相關的狀態更改而觸發的事件包括:鏈接被激活、非激活事件、數據讀取、用戶事件、異常事件等;出站事件是將來將會觸發的某個動做的操做結果,包括:打開或關閉到遠程節點的鏈接、將數據寫到或者沖刷到套接字等;
事件被分發給ChannelHandler 類中每一個覆蓋的方法中, 這些方法相似於回調函數。Netty 提供了大量開箱即用的ChannelHandler 實現,包括用於各類協議(如HTTP 和SSL/TLS)的ChannelHandler。
Channel 接口
基本的I/O 操做(bind()、connect()、read()和write())依賴於底層網絡傳輸所提供的原語。在基於Java 的網絡編程中,其基本的構造是類Socket。Netty 的Channel 接口所提供的API,被用於全部的I/O 操做。大大地下降了直接使用Socket 類的複雜性。此外,Channel 也是擁有許多預約義的、專門化實現的普遍類層次結構的根。
因爲Channel 是獨一無二的,因此爲了保證順序將Channel 聲明爲java.lang.Comparable 的一個子接口。所以,若是兩個不一樣的Channel 實例都返回了相同的散列碼,那麼AbstractChannel 中的compareTo()方法的實現將會拋出一個Error。
Channel 的生命週期狀態
ChannelUnregistered :Channel 已經被建立,但還未註冊到EventLoop
ChannelRegistered :Channel 已經被註冊到了EventLoop
ChannelActive :Channel 處於活動狀態(已經鏈接到它的遠程節點)。它如今能夠接收和發送數據了
ChannelInactive :Channel 沒有鏈接到遠程節點
當這些狀態發生改變時,將會生成對應的事件。這些事件將會被轉發給ChannelPipeline 中的ChannelHandler,其能夠隨後對它們作出響應。
第四節、核心概念和機制 - EventLoop、EventLoopGroup
在內部,當提交任務到若是(當前)調用線程正是支撐EventLoop 的線程,那麼所提交的代碼塊將會被(直接)執行。不然,EventLoop 將調度該任務以便稍後執行,並將它放入到內部隊列中。當EventLoop下次處理它的事件時,它會執行隊列中的那些任務/事件。
服務於Channel 的I/O 和事件的EventLoop 則包含在EventLoopGroup 中。
異步傳輸實現只使用了少許的EventLoop(以及和它們相關聯的Thread),並且在當前的線程模型中,它們可能會被多個Channel 所共享。這使得能夠經過儘量少許的Thread 來支撐大量的Channel,而不是每一個Channel 分配一個Thread。EventLoopGroup 負責爲每一個新建立的Channel 分配一個EventLoop。在當前實現中,使用順序循環(round-robin)的方式進行分配以獲取一個均衡的分佈,而且相同的EventLoop可能會被分配給多個Channel。
一旦一個Channel 被分配給一個EventLoop,它將在它的整個生命週期中都使用這個EventLoop(以及相關聯的Thread)。請牢記這一點,由於它可使你從擔心你的ChannelHandler 實現中的線程安全和同步問題中解脫出來。
須要注意EventLoop 的分配方式對ThreadLocal 的使用的影響。由於一個EventLoop 一般會被用於支撐多個Channel,因此對於全部相關聯的Channel 來講,ThreadLocal 都將是同樣的。這使得它對於實現狀態追蹤等功能來講是個糟糕的選擇。然而,在一些無狀態的上下文中,它仍然能夠被用於在多個Channel 之間共享一些重度的或者代價昂貴的對象,甚至是事件。
第五節、主要組件ChannelHandler、ChannelHandlerContext和ChnnelPipeline
ChannelHandler接口
Netty 的主要組件是ChannelHandler,它充當了全部處理入站和出站數據的應用程序邏輯的容器。ChannelHandler 的方法是由網絡事件觸發的。
Netty 定義了下面兩個重要的ChannelHandler 子接口:ChannelInboundHandler——處理入站數據以及各類狀態變化;ChannelOutboundHandler——處理出站數據而且容許攔截全部的操做。
ChannelInboundHandler接口的生命週期中重要的方法:
channelActive: 當Channel 處於活動狀態時被調用;Channel 已經鏈接/綁定而且已經就緒
channelReadComplete: 當Channel上的一個讀操做完成時被調用
channelRead: 當從Channel 讀取數據時被調用
userEventTriggered: 當ChannelnboundHandler.fireUserEventTriggered()方法被調用時被調用
ChannelOutboundHandler接口的生命週期中重要的方法:
bind:當請求將Channel 綁定到本地地址時被調用
connect:當請求將Channel 鏈接到遠程節點時被調用
close:當請求關閉Channel 時被調用
read: 當請求從Channel 讀取更多的數據時被調用
flush: 當請求經過Channel 將入隊數據沖刷到遠程節點時被調用
write:當請求經過Channel 將數據寫到遠程節點時被調用
ChannelHandlerContext經常使用API
bind: 綁定到給定的SocketAddress,並返回ChannelFuture
channel: 返回綁定到這個實例的Channel
close: 關閉Channel,並返回ChannelFuture
connect: 鏈接給定的SocketAddress,並返回ChannelFuture
fireChannelActive: 觸發對下一個ChannelInboundHandler 上的channelActive()方法(已鏈接)的調用
fireChannelRead: 觸發對下一個ChannelInboundHandler 上的channelRead()方法(已接收的消息)的調用
fireChannelReadComplete: 觸發對下一個ChannelInboundHandler 上的channelReadComplete()方法的調用
fireExceptionCaught: 觸發對下一個ChannelInboundHandler 上的fireExceptionCaught(Throwable)方法的調用
fireUserEventTriggered: 觸發對下一個ChannelInboundHandler 上的fireUserEventTriggered(Object evt)方法的調用
handler: 返回綁定到這個實例的ChannelHandler
pipeline: 返回這個實例所關聯的ChannelPipeline
read 將數據從Channel讀取到第一個入站緩衝區;若是讀取成功則觸發一個channelRead事件,並(在最後一個消息被讀取完成後)通知ChannelInboundHandler 的channelReadComplete
ChnnelPipeline接口
當Channel 被建立時,它將會被自動地分配一個新的ChannelPipeline。這項關聯是永久性的;Channel 既不能附加另一個ChannelPipeline,也不能分離其當前的。在Netty 組件的生命週期中,這是一項固定的操做,不須要開發人員的任何干預。
當Channel 被建立時,它將會被自動地分配一個新的ChannelPipeline。這項關聯是永久性的;Channel 既不能附加另一個ChannelPipeline,也不能分離其當前的。在Netty 組件的生命週期中,這是一項固定的操做,不須要開發人員的任何干預。
使得事件流經ChannelPipeline 是ChannelHandler 的工做,它們是在應用程序的初始化或者引導階段被安裝的。這些對象接收事件、執行它們所實現的處理邏輯,並將數據傳遞給鏈中的下一個ChannelHandler。它們的執行順序是由它們被添加的順序所決定的。
入站和出站ChannelHandler 能夠被安裝到同一個ChannelPipeline中。若是一個消息或者任何其餘的入站事件被讀取,那麼它會從ChannelPipeline 的頭部開始流動,最終,數據將會到達ChannelPipeline 的尾端,屆時,全部處理就都結束了。
數據的出站運動(即正在被寫的數據)在概念上也是同樣的。在這種狀況下,數據將從ChannelOutboundHandler 鏈的尾端開始流動,直到它到達鏈的頭部爲止。在這以後,出站數據將會到達網絡傳輸層,這裏顯示爲Socket。一般狀況下,這將觸發一個寫操做。
若是將兩個類別的ChannelHandler都混合添加到同一個ChannelPipeline 中會發生什麼。雖然ChannelInboundHandle 和ChannelOutboundHandle 都擴展自ChannelHandler,可是Netty 能區分ChannelInboundHandler實現和ChannelOutboundHandler 實現,並確保數據只會在具備相同定向類型的兩個ChannelHandler 之間傳遞。
ChannelPipeline上的重要的方法:addFirst、addBefore、addAfter、addLast。
第六節、Netty支持的網絡通信傳輸模式
NIO: 使用java.nio.channels 包做爲基礎——基於選擇器的方式;
Epoll: 由 JNI 驅動的 epoll()和非阻塞 IO。這個傳輸支持只有在Linux 上可用的多種特性,如SO_REUSEPORT,比NIO 傳輸更快,並且是徹底非阻塞的。將NioEventLoopGroup替換爲 EpollEventLoopGroup , 而且將NioServerSocketChannel.class 替換爲EpollServerSocketChannel.class 便可;
OIO: 使用java.net 包做爲基礎——使用阻塞流;
Local:能夠在VM 內部經過管道進行通訊的本地傳輸;
Embedded: 傳輸容許使用ChannelHandler 而又不須要一個真正的基於網絡的傳輸。在測試ChannelHandler 實現時很是有用;
第七節、操做系統層ChannelOption詳解
ChannelOption 的各類屬性在套接字選項中都有對應。
ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG 對應的是 tcp/ip 協議 listen 函數中的 backlog 參數,函數 listen(int socketfd,int backlog)用來初始化服務端可鏈接隊列, 服務端處理客戶端鏈接請求是順序處理的,因此同一時間只能處理一個客戶端鏈接,多 個客戶端來的時候,服務端將不能處理的客戶端鏈接請求放在隊列中等待處理,backlog 參 數指定了隊列的大小。
ChannelOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR 對應於套接字選項中的 SO_REUSEADDR,這個參數表示允 許重複使用本地地址和端口, 好比,某個服務器進程佔用了 TCP 的 80 端口進行監聽,此時再次監聽該端口就會返回 錯誤,使用該參數就能夠解決問題,該參數容許共用該端口,這個在服務器程序中比較常使 用,好比某個進程非正常退出,該程序佔用的端口可能要被佔用一段時間才能容許其餘進程 使用,並且程序死掉之後,內核一須要必定的時間纔可以釋放此端口,不設置 SO_REUSEADDR 就沒法正常使用該端口。
ChannelOption.SO_KEEPALIVE
Channeloption.SO_KEEPALIVE 參數對應於套接字選項中的 SO_KEEPALIVE,該參數用於設 置 TCP 鏈接,當設置該選項之後,鏈接會測試連接的狀態,這個選項用於可能長時間沒有數 據交流的鏈接。當設置該選項之後,若是在兩小時內沒有數據的通訊時,TCP 會自動發送一 個活動探測數據報文。
ChannelOption.SO_SNDBUF 和 ChannelOption.SO_RCVBUF
ChannelOption.SO_SNDBUF 參數對應於套接字選項中的 SO_SNDBUF, ChannelOption.SO_RCVBUF 參數對應於套接字選項中的 SO_RCVBUF 這兩個參數用於操做接 收緩衝區和發送緩衝區的大小,接收緩衝區用於保存網絡協議站內收到的數據,直到應用程 序讀取成功,發送緩衝區用於保存發送數據,直到發送成功。
ChannelOption.SO_LINGER
ChannelOption.SO_LINGER 參數對應於套接字選項中的 SO_LINGER,Linux 內核默認的處理 方式是當用戶調用 close()方法的時候,函數返回,在可能的狀況下,儘可能發送數據,不 必定保證會發生剩餘的數據,形成了數據的不肯定性,使用 SO_LINGER 能夠阻塞 close()的調 用時間,直到數據徹底發送 。
ChannelOption.TCP_NODELAY
ChannelOption.TCP_NODELAY 參數對應於套接字選項中的 TCP_NODELAY,該參數的使用 與 Nagle 算法有關,Nagle 算法是將小的數據包組裝爲更大的幀而後進行發送,而不是輸入 一次發送一次,所以在數據包不足的時候會等待其餘數據的到了,組裝成大的數據包進行發 送,雖然該方式有效提升網絡的有效負載,可是卻形成了延時,而該參數的做用就是禁止使 用 Nagle 算法,使用於小數據即時傳輸,於 TCP_NODELAY 相對應的是 TCP_CORK,該選項是 須要等到發送的數據量最大的時候,一次性發送數據,適用於文件傳輸。
第八節、特有的緩衝封裝ByteBuf詳解
ByteBuf是Netty在ByteBuffer基礎上作的二次封裝和擴展。它的API 具備如下優勢:
1. 它能夠被用戶自定義的緩衝區類型擴展;
2. 經過內置的複合緩衝區類型實現了透明的零拷貝;
3. 容量能夠按需增加(相似於 JDK 的 StringBuilder);
4. 在讀和寫這兩種模式之間切換不須要調用 ByteBuffer 的 flip()方法;
5. 讀和寫使用了不一樣的索引;
6. 支持方法的鏈式調用;
7. 支持引用計數; 支持池化;
ByteBuf 維護了兩個不一樣的索引,名稱以 read 或者 write 開頭的 ByteBuf 方法,將會推動其對應的索引,而名稱以 set 或者 get 開頭的操做則不會。
第九節、Netty中的重要機制引用計數
當某個 ChannelInboundHandler 的實現重寫 channelRead()方法時,它要負責顯式地釋放 與池化的 ByteBuf 實例相關的內存。Netty 爲此提供了一個實用方法 ReferenceCountUtil.release() Netty 將使用 WARN 級別的日誌消息記錄未釋放的資源,使得能夠很是簡單地在代碼 中發現違規的實例。可是以這種方式管理資源可能很繁瑣。一個更加簡單的方式是使用 SimpleChannelInboundHandler,SimpleChannelInboundHandler 會自動釋放資源。
一、對於入站請求,Netty 的 EventLoo 在處理 Channel 的讀操做時進行分配 ByteBuf,對 於這類 ByteBuf,須要咱們自行進行釋放,有三種方式,或者使用 SimpleChannelInboundHandler,或者在重寫 channelRead()方法使用 ReferenceCountUtil.release()或者使用 ctx.fireChannelRead 繼續向後傳遞;
二、對於出站請求,無論 ByteBuf 是否由咱們的業務建立的,當調用了 write 或者 writeAndFlush 方法後,Netty 會自動替咱們釋放,不須要咱們業務代碼自行釋放。
第十節、Netty如何解決粘包/半包問題
粘包半包
假設客戶端分別發送了兩個數據包 D1 和 D2 給服務端,因爲服務端一次讀取到的字節 數是不肯定的,故可能存在如下 4 種狀況。
1. 服務端分兩次讀取到了兩個獨立的數據包,分別是 D1 和 D2,沒有粘包和拆包;
2. 服務端一次接收到了兩個數據包,D1 和 D2 粘合在一塊兒,被稱爲 TCP 粘包;
3. 服務端分兩次讀取到了兩個數據包,第一次讀取到了完整的 D1 包和 D2 包的部分 內容,第二次讀取到了 D2 包的剩餘內容,這被稱爲 TCP 拆包;
4. 服務端分兩次讀取到了兩個數據包,第一次讀取到了 D1 包的部份內容 D1_1,第 二次讀取到了 D1 包的剩餘內容 D1_2 和 D2 包的整包。
若是此時服務端 TCP 接收滑窗很是小,而數據包 D1 和 D2 比較大,頗有可能會發生第 五種可能,即服務端分屢次才能將 D1 和 D2 包接收徹底,期間發生屢次拆包;
粘包半包發生的緣由分析
因爲 TCP 協議自己的機制(面向鏈接的可靠地協議-三次握手機制)客戶端與服務器會 維持一個鏈接(Channel),數據在鏈接不斷開的狀況下,能夠持續不斷地將多個數據包發 往服務器,可是若是發送的網絡數據包過小,那麼他自己會啓用 Nagle 算法(可配置是否啓 用)對較小的數據包進行合併(基於此,TCP 的網絡延遲要 UDP 的高些)而後再發送(超 時或者包大小足夠)。
那麼這樣的話,服務器在接收到消息(數據流)的時候就沒法區分哪 些數據包是客戶端本身分開發送的,這樣產生了粘包;服務器在接收到數據庫後,放到緩衝 區中,若是消息沒有被及時從緩存區取走,下次在取數據的時候可能就會出現一次取出多個 數據包的狀況,形成粘包現象。
粘包半包解決辦法
因爲底層的 TCP 沒法理解上層的業務數據,因此在底層是沒法保證數據包不被拆分和重組的,這個問題只能經過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案, 能夠概括以下:
1. 在包尾增長分割符,好比回車換行符進行分割,例如 FTP 協議; 參見 cn.enjoyedu.nettybasic.splicing.linebase 和 cn.enjoyedu.nettybasic.splicing.delimiter 下的代碼;
2. 消息定長,例如每一個報文的大小爲固定長度 200 字節,若是不夠,空位補空格; 參見 cn.enjoyedu.nettybasic.splicing.fixed 下的代碼 ;
3. 將消息分爲消息頭和消息體,消息頭中包含表示消息總長度(或者消息體長度) 的字段,一般設計思路爲消息頭的第一個字段使用 int32 來表示消息的總長度LengthFieldBasedFrameDecoder。
第十一節、Netty重量級組件編解碼器
將字節解碼爲消息
抽象類 ByteToMessageDecoder
將字節解碼爲消息(或者另外一個字節序列)是一項如此常見的任務,以致於 Netty 爲它 提供了一個抽象的基類:ByteToMessageDecoder。因爲你不可能知道遠程節點是否會一次性 地發送一個完整的消息,因此這個類會對入站數據進行緩衝,直到它準備好處理。
decode(ChannelHandlerContext ctx,ByteBuf in,List out)
這是你必須實現的惟一抽象方法。decode()方法被調用時將會傳入一個包含了傳入數據 的 ByteBuf,以及一個用來添加解碼消息的 List。
將一種消息類型解碼爲另外一種
decode(ChannelHandlerContext ctx,I msg,List out)
對於每一個須要被解碼爲另外一種格式的入站消息來講,該方法都將會被調用。解碼消息隨 後會被傳遞給 ChannelPipeline 中的下一個 ChannelInboundHandler
TooLongFrameException
因爲 Netty 是一個異步框架,因此須要在字節能夠解碼以前在內存中緩衝它們。所以, 不能讓解碼器緩衝大量的數據以致於耗盡可用的內存。爲了解除這個常見的顧慮,Netty 提 供了 TooLongFrameException 類,其將由解碼器在幀超出指定的大小限制時拋出。
第十二節、Netty內置及引入外部序列和反序列化部件
序列化的問題
Java 序列化的目的主要有兩個:
1.網絡傳輸
2.對象持久化
當選行遠程跨迸程服務調用時,須要把被傳輸的 Java 對象編碼爲字節數組或者 ByteBuffer 對象。而當遠程服務讀取到 ByteBuffer 對象或者字節數組時,須要將其解碼爲發 送時的 Java 對象。這被稱爲 Java 對象編解碼技術。
Java 序列化僅僅是 Java 編解碼技術的一種,因爲它的種種缺陷,衍生出了多種編解碼 技術和框架;
Java 序列化的缺點
1. 沒法跨語言
對於跨進程的服務調用,服務提供者可能會使用 C 十+或者其餘語言開發,當咱們須要 和異構語言進程交互時 Java 序列化就難以勝任。因爲 Java 序列化技術是 Java 語言內部的私 有協議,其餘語言並不支持,對於用戶來講它徹底是黑盒。對於 Java 序列化後的字節數組, 別的語言沒法進行反序列化,這就嚴重阻礙了它的應用。
2. 序列化後的碼流太大
經過不少驗證代碼證實:序列化後的碼流確實很大;
3. 序列化性能過低
不管是序列化後的碼流大小,仍是序列化的性能,JDK 默認的序列化機制表現得都不好。
所以,咱們邊常不會選擇 Java 序列化做爲遠程跨節點調用的編解碼框架。
Netty內置的對象序列和反序列化組件是:Protocol Buffers
外部業界比較高效地序列和反序列化的部件有:
protostuff 、 kryo 、 fast-serialization 、msgpack-databird、 hessian 等,尤爲是前三甲;
第十三節、如何獨立進行ChannelHandler的單元測試
EmbeddedChannel---ChannelHandler獨立單元測試工具
將入站數據或者出站數據寫入到 EmbeddedChannel 中,而後檢查是否有任何東西到達 了 ChannelPipeline 的尾端。以這種方式,你即可以肯定消息是否已經被編碼或者被解碼過 了,以及是否觸發了任何的 ChannelHandler 動做。
writeInbound(Object... msgs)
將入站消息寫到 EmbeddedChannel 中。若是能夠經過 readInbound()方法從 EmbeddedChannel 中讀取數據,則返回 true。
readInbound()
從 EmbeddedChannel 中讀取一個入站消息。任何返回的東西都穿越了整個 ChannelPipeline。若是沒有任何可供讀取的,則返回 null。
writeOutbound(Object... msgs)
將出站消息寫到 EmbeddedChannel 中。若是如今能夠經過 readOutbound()方法從 EmbeddedChannel 中讀取到什麼東西,則返回 true。
readOutbound()
從 EmbeddedChannel 中讀取一個出站消息。任何返回的東西都穿越了整個 ChannelPipeline。若是沒有任何可供讀取的,則返回 null。
finish()
將 EmbeddedChannel 標記爲完成,而且若是有可被讀取的入站數據或者出站數 據,則返回 true。這個方法還將會調用 EmbeddedChannel 上的 close()方法。
入站數據由 ChannelInboundHandler 處理,表明從遠程節點讀取的數據。出站數據由 ChannelOutboundHandler 處理,表明將要寫到遠程節點的數據。
使用 writeOutbound()方法將消息寫到 Channel 中,並經過 ChannelPipeline 沿着出站的 方向傳遞。隨後,你可使用 readOutbound()方法來讀取已被處理過的消息,以肯定結果是 否和預期同樣。 相似地,對於入站數據,你須要使用 writeInbound()和 readInbound()方法。
總結
咱們應該儘可能對以上Netty相關知識進行理解,尤爲是EventLoop(Group)、ChannelHandler、ChannelHandlerContext、ChannelPipeline、ByteBuf、Netty編解碼器、序列化和反序列化等作深刻理解。那麼有了這些知識儲備後,咱們就能夠基於一些業務來設計實現一個Netty端到端的網絡IO實戰了。下次咱們將運用以上知識或機制,逐步深刻給你們展現Netty的實戰相關課題。