在本章中,我們將研究 Netty 提供的核心功能以及他們是如何構(gòu)成一個(gè)完整的網(wǎng)絡(luò)應(yīng)用開(kāi)發(fā)堆棧頂部的核心。你閱讀本章時(shí),請(qǐng)把這個(gè)圖記住。
Netty 使用自建的 buffer API,而不是使用 NIO 的 ByteBuffer 來(lái)表示一個(gè)連續(xù)的字節(jié)序列。與 ByteBuffer 相比這種方式擁有明顯的優(yōu)勢(shì)。Netty 使用新的 buffer 類(lèi)型 ByteBuf,被設(shè)計(jì)為一個(gè)可從底層解決 ByteBuffer 問(wèn)題,并可滿足日常網(wǎng)絡(luò)應(yīng)用開(kāi)發(fā)需要的緩沖類(lèi)型。這些很酷的特性包括:
flip()
方法。更多信息請(qǐng)參考:io.netty.buffer 包描述
ByteBuf 具有豐富的操作集,可以快速的實(shí)現(xiàn)協(xié)議的優(yōu)化。例如,ByteBuf 提供各種操作用于訪問(wèn)無(wú)符號(hào)值和字符串,以及在緩沖區(qū)搜索一定的字節(jié)序列。你也可以擴(kuò)展或包裝現(xiàn)有的緩沖類(lèi)型用來(lái)提供方便的訪問(wèn)。自定義緩沖式仍然實(shí)現(xiàn)自 ByteBuf 接口,而不是引入一個(gè)不兼容的類(lèi)型
舉一個(gè)網(wǎng)絡(luò)應(yīng)用到極致的表現(xiàn),你需要減少內(nèi)存拷貝操作次數(shù)。你可能有一組緩沖區(qū)可以被組合以形成一個(gè)完整的消息。網(wǎng)絡(luò)提供了一種復(fù)合緩沖,允許你從現(xiàn)有的任意數(shù)的緩沖區(qū)創(chuàng)建一個(gè)新的緩沖區(qū)而無(wú)需沒(méi)有內(nèi)存拷貝。例如,一個(gè)信息可以由兩部分組成;header 和 body。在一個(gè)模塊化的應(yīng)用,當(dāng)消息發(fā)送出去時(shí),這兩部分可以由不同的模塊生產(chǎn)和裝配。
<pre> +--------+----------+
| header | body |
+--------+----------+
</pre>
如果你使用的是 ByteBuffer ,你必須要?jiǎng)?chuàng)建一個(gè)新的大緩存區(qū)用來(lái)拷貝這兩部分到這個(gè)新緩存區(qū)中?;蛘撸憧梢栽?NiO做一個(gè)收集寫(xiě)操作,但限制你將復(fù)合緩沖類(lèi)型作為 ByteBuffer 的數(shù)組而不是一個(gè)單一的緩沖區(qū),打破了抽象,并且引入了復(fù)雜的狀態(tài)管理。此外,如果你不從 NIO channel 讀或?qū)懀菦](méi)有用的。
// 復(fù)合類(lèi)型與組件類(lèi)型不兼容。
ByteBuffer[] message = new ByteBuffer[] { header, body };
通過(guò)對(duì)比, ByteBuf 不會(huì)有警告,因?yàn)樗峭耆蓴U(kuò)展并有一個(gè)內(nèi)置的復(fù)合緩沖區(qū)。
// 復(fù)合類(lèi)型與組件類(lèi)型是兼容的。
ByteBuf message = Unpooled.wrappedBuffer(header, body);
// 因此,你甚至可以通過(guò)混合復(fù)合類(lèi)型與普通緩沖區(qū)來(lái)創(chuàng)建一個(gè)復(fù)合類(lèi)型。
ByteBuf messageWithFooter = Unpooled.wrappedBuffer(message, footer);
// 由于復(fù)合類(lèi)型仍是 ByteBuf,訪問(wèn)其內(nèi)容很容易,
//并且訪問(wèn)方法的行為就像是訪問(wèn)一個(gè)單獨(dú)的緩沖區(qū),
//即使你想訪問(wèn)的區(qū)域是跨多個(gè)組件。
//這里的無(wú)符號(hào)整數(shù)讀取位于 body 和 footer
messageWithFooter.getUnsignedInt(
messageWithFooter.readableBytes() - footer.readableBytes() - 1);
許多協(xié)議定義可變長(zhǎng)度的消息,這意味著沒(méi)有辦法確定消息的長(zhǎng)度,直到你構(gòu)建的消息?;蛘撸谟?jì)算長(zhǎng)度的精確值時(shí),帶來(lái)了困難和不便。這就像當(dāng)你建立一個(gè)字符串。你經(jīng)常估計(jì)得到的字符串的長(zhǎng)度,讓 StringBuffer 擴(kuò)大了其本身的需求。
// 一種新的動(dòng)態(tài)緩沖區(qū)被創(chuàng)建。在內(nèi)部,實(shí)際緩沖區(qū)是被“懶”創(chuàng)建,從而避免潛在的浪費(fèi)內(nèi)存空間。
ByteBuf b = Unpooled.buffer(4);
// 當(dāng)?shù)谝粋€(gè)執(zhí)行寫(xiě)嘗試,內(nèi)部指定初始容量 4 的緩沖區(qū)被創(chuàng)建
b.writeByte('1');
b.writeByte('2');
b.writeByte('3');
b.writeByte('4');
// 當(dāng)寫(xiě)入的字節(jié)數(shù)超過(guò)初始容量 4 時(shí),
//內(nèi)部緩沖區(qū)自動(dòng)分配具有較大的容量
b.writeByte('5');
最頻繁使用的緩沖區(qū) ByteBuf 的實(shí)現(xiàn)是一個(gè)非常薄的字節(jié)數(shù)組包裝器(比如,一個(gè)字節(jié))。與 ByteBuffer 不同,它沒(méi)有復(fù)雜的邊界和索引檢查補(bǔ)償,因此對(duì)于 JVM 優(yōu)化緩沖區(qū)的訪問(wèn)更加簡(jiǎn)單。更多復(fù)雜的緩沖區(qū)實(shí)現(xiàn)是用于拆分或者組合緩存,并且比 ByteBuffer 擁有更好的性能。
傳統(tǒng)的 Java I/O API 在應(yīng)對(duì)不同的傳輸協(xié)議時(shí)需要使用不同的類(lèi)型和方法。例如:java.net.Socket
和 java.net.DatagramSocket
它們并不具有相同的超類(lèi)型,因此,這就需要使用不同的調(diào)用方式執(zhí)行 socket 操作。
這種模式上的不匹配使得在更換一個(gè)網(wǎng)絡(luò)應(yīng)用的傳輸協(xié)議時(shí)變得繁雜和困難。由于(Java I/O API)缺乏協(xié)議間的移植性,當(dāng)你試圖在不修改網(wǎng)絡(luò)傳輸層的前提下增加多種協(xié)議的支持,這時(shí)便會(huì)產(chǎn)生問(wèn)題。并且理論上講,多種應(yīng)用層協(xié)議可運(yùn)行在多種傳輸層協(xié)議之上例如TCP/IP,UDP/IP,SCTP和串口通信。
讓這種情況變得更糟的是,Java 新的 I/O(NIO)API與原有的阻塞式的I/O(OIO)API 并不兼容,NIO.2(AIO)也是如此。由于所有的API無(wú)論是在其設(shè)計(jì)上還是性能上的特性都與彼此不同,在進(jìn)入開(kāi)發(fā)階段,你常常會(huì)被迫的選擇一種你需要的API。
例如,在用戶(hù)數(shù)較小的時(shí)候你可能會(huì)選擇使用傳統(tǒng)的 OIO(Old I/O) API,畢竟與 NIO 相比使用 OIO 將更加容易一些。然而,當(dāng)你的業(yè)務(wù)呈指數(shù)增長(zhǎng)并且服務(wù)器需要同時(shí)處理成千上萬(wàn)的客戶(hù)連接時(shí)你便會(huì)遇到問(wèn)題。這種情況下你可能會(huì)嘗試使用 NIO,但是復(fù)雜的 NIO Selector 編程接口又會(huì)耗費(fèi)你大量時(shí)間并最終會(huì)阻礙你的快速開(kāi)發(fā)。
Netty 有一個(gè)叫做 Channel 的統(tǒng)一的異步 I/O 編程接口,這個(gè)編程接口抽象了所有點(diǎn)對(duì)點(diǎn)的通信操作。也就是說(shuō),如果你的應(yīng)用是基于 Netty 的某一種傳輸實(shí)現(xiàn),那么同樣的,你的應(yīng)用也可以運(yùn)行在 Netty 的另一種傳輸實(shí)現(xiàn)上。Netty 提供了幾種擁有相同編程接口的基本傳輸實(shí)現(xiàn):
切換不同的傳輸實(shí)現(xiàn)通常只需對(duì)代碼進(jìn)行幾行的修改調(diào)整,例如選擇一個(gè)不同的 ChannelFactory 實(shí)現(xiàn)。
此外,你甚至可以利用新的傳輸實(shí)現(xiàn)沒(méi)有寫(xiě)入的優(yōu)勢(shì),只需替換一些構(gòu)造器的調(diào)用方法即可,例如串口通信。而且由于核心 API 具有高度的可擴(kuò)展性,你還可以完成自己的傳輸實(shí)現(xiàn)。
一個(gè)定義良好并具有擴(kuò)展能力的事件模型是事件驅(qū)動(dòng)開(kāi)發(fā)的必要條件。Netty 具有定義良好的 I/O 事件模型。由于嚴(yán)格的層次結(jié)構(gòu)區(qū)分了不同的事件類(lèi)型,因此 Netty 也允許你在不破壞現(xiàn)有代碼的情況下實(shí)現(xiàn)自己的事件類(lèi)型。這是與其他框架相比另一個(gè)不同的地方。很多 NIO 框架沒(méi)有或者僅有有限的事件模型概念;在你試圖添加一個(gè)新的事件類(lèi)型的時(shí)候常常需要修改已有的代碼,或者根本就不允許你進(jìn)行這種擴(kuò)展。
在一個(gè) ChannelPipeline 內(nèi)部一個(gè) [ChannelEvent]()
被一組ChannelHandler 處理。這個(gè)管道是
Intercepting Filter (攔截過(guò)濾器)模式的一種高級(jí)形式的實(shí)現(xiàn),因此對(duì)于一個(gè)事件如何被處理以及管道內(nèi)部處理器間的交互過(guò)程,你都將擁有絕對(duì)的控制力。例如,你可以定義一個(gè)從 socket 讀取到數(shù)據(jù)后的操作:
public class MyReadHandler implements SimpleChannelHandler {
public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the received message.
...
// And forward the event to the next handler.
ctx.sendUpstream(evt);
}
}
同時(shí)你也可以定義一種操作響應(yīng)其他處理器的寫(xiě)操作請(qǐng)求:
public class MyWriteHandler implements SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the message to be written.
...
// And forward the event to the next handler.
ctx.sendDownstream(evt);
}
}
有關(guān)事件模型的更多信息,請(qǐng)參考 API 文檔 ChannelEvent 和ChannelPipeline 部分。
上述所提及的核心組件已經(jīng)足夠?qū)崿F(xiàn)各種類(lèi)型的網(wǎng)絡(luò)應(yīng)用,除此之外,Netty 也提供了一系列的高級(jí)組件來(lái)加速你的開(kāi)發(fā)過(guò)程。
就像“使用POJO代替ChannelBuffer”一節(jié)所展示的那樣,從業(yè)務(wù)邏輯代碼中分離協(xié)議處理部分總是一個(gè)很不錯(cuò)的想法。然而如果一切從零開(kāi)始便會(huì)遭遇到實(shí)現(xiàn)上的復(fù)雜性。你不得不處理分段的消息。一些協(xié)議是多層的(例如構(gòu)建在其他低層協(xié)議之上的協(xié)議)。一些協(xié)議過(guò)于復(fù)雜以致難以在一臺(tái)獨(dú)立狀態(tài)機(jī)上實(shí)現(xiàn)。
因此,一個(gè)好的網(wǎng)絡(luò)應(yīng)用框架應(yīng)該提供一種可擴(kuò)展,可重用,可單元測(cè)試并且是多層的 codec 框架,為用戶(hù)提供易維護(hù)的 codec 代碼。
Netty 提供了一組構(gòu)建在其核心模塊之上的 codec 實(shí)現(xiàn),這些簡(jiǎn)單的或者高級(jí)的 codec 實(shí)現(xiàn)幫你解決了大部分在你進(jìn)行協(xié)議處理開(kāi)發(fā)過(guò)程會(huì)遇到的問(wèn)題,無(wú)論這些協(xié)議是簡(jiǎn)單的還是復(fù)雜的,二進(jìn)制的或是簡(jiǎn)單文本的。
不同于傳統(tǒng)阻塞式的 I/O 實(shí)現(xiàn),在 NIO 模式下支持 SSL 功能是一個(gè)艱難的工作。你不能只是簡(jiǎn)單的包裝一下流數(shù)據(jù)并進(jìn)行加密或解密工作,你不得不借助于 javax.net.ssl.SSLEngine
,SSLEngine
是一個(gè)有狀態(tài)的實(shí)現(xiàn),其復(fù)雜性不亞于 SSL 自身。你必須管理所有可能的狀態(tài),例如密碼套件,密鑰協(xié)商(或重新協(xié)商),證書(shū)交換以及認(rèn)證等。此外,與通常期望情況相反的是 SSLEngine
甚至不是一個(gè)絕對(duì)的線程安全實(shí)現(xiàn)。
在 Netty 內(nèi)部,SslHandler 封裝了所有艱難的細(xì)節(jié)以及使用 SSLEngine
可 能帶來(lái)的陷阱。你所做的僅是配置并將該 SslHandler 插入到你的 ChannelPipeline 中。同樣
Netty 也允許你實(shí)現(xiàn)像 StartTlS 那樣所擁有的高級(jí)特性,這很容易。
HTTP無(wú) 疑是互聯(lián)網(wǎng)上最受歡迎的協(xié)議,并且已經(jīng)有了一些例如 Servlet 容器這樣的 HTTP 實(shí)現(xiàn)。因此,為什么 Netty 還要在其核心模塊之上構(gòu)建一套 HTTP 實(shí)現(xiàn)?
與現(xiàn)有的 HTTP 實(shí)現(xiàn)相比 Netty 的 HTTP 實(shí)現(xiàn)是相當(dāng)與眾不同的。在HTTP 消息的低層交互過(guò)程中你將擁有絕對(duì)的控制力。這是因?yàn)?Netty 的HTTP 實(shí)現(xiàn)只是一些 HTTP codec 和 HTTP 消息類(lèi)的簡(jiǎn)單組合,這里不存在任何限制——例如那種被迫選擇的線程模型。你可以隨心所欲的編寫(xiě)那種可以完全按照你期望的工作方式工作的客戶(hù)端或服務(wù)器端代碼。這包括線程模型,連接生命期,快編碼,以及所有 HTTP 協(xié)議允許你做的,所有的一切,你都將擁有絕對(duì)的控制力。
由于這種高度可定制化的特性,你可以開(kāi)發(fā)一個(gè)非常高效的HTTP服務(wù)器,例如:
WebSockets 允許雙向,全雙工通信信道,在 TCP socket 中。它被設(shè)計(jì)為允許一個(gè) Web 瀏覽器和 Web 服務(wù)器之間通過(guò)數(shù)據(jù)流交互。
WebSocket 協(xié)議已經(jīng)被 IETF 列為 RFC 6455規(guī)范。
Netty 實(shí)現(xiàn)了 RFC 6455 和一些老版本的規(guī)范。請(qǐng)參閱io.netty.handler.codec.http.websocketx包和相關(guān)的例子。
Google Protocol Buffers 是快速實(shí)現(xiàn)一個(gè)高效的二進(jìn)制協(xié)議的理想方案。通過(guò)使用 ProtobufEncoder 和 ProtobufDecoder,你可以把 Google Protocol Buffers 編譯器 (protoc) 生成的消息類(lèi)放入到 Netty 的codec 實(shí)現(xiàn)中。請(qǐng)參考“LocalTime”實(shí)例,這個(gè)例子也同時(shí)顯示出開(kāi)發(fā)一個(gè)由簡(jiǎn)單協(xié)議定義 的客戶(hù)及服務(wù)端是多么的容易。
譯者注:翻譯版本的項(xiàng)目源碼見(jiàn) https://github.com/waylau/netty-4-user-guide-demos
在這一章節(jié),我們從功能特性的角度回顧了 Netty 的整體架構(gòu)。Netty 有一個(gè)簡(jiǎn)單卻不失強(qiáng)大的架構(gòu)。這個(gè)架構(gòu)由三部分組成——緩沖(buffer),通道(channel),事件模型(event model)——所有的高級(jí)特性都構(gòu)建在這三個(gè)核心組件之上。一旦你理解了它們之間的工作原理,你便不難理解在本章簡(jiǎn)要提及的更多高級(jí)特性。
你可能對(duì) Netty 的整體架構(gòu)以及每一部分的工作原理仍舊存有疑問(wèn)。如果是這樣,最好的方式是告訴我們 應(yīng)該如何改進(jìn)這份指南。
譯者注:對(duì)本翻譯有任何疑問(wèn),在https://github.com/waylau/netty-4-user-guide/issues提問(wèn)
更多建議: