1.Netty 是什么?
Netty 是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務
器和客戶端。Netty 是基於 nio 的,它封裝了 jdk 的 nio,讓我們使用起來更加方法靈活。
2.Netty 的特點是什么?
高並發:Netty 是一款基於 NIO(Nonblocking IO,非阻塞 IO)開發的網絡通信框架,
對比於 BIO(Blocking I/O,阻塞 IO),他的並發性能得到了很大提高。
傳輸快:Netty 的傳輸依賴於零拷貝特性,盡量減少不必要的內存拷貝,實現了更高效率
的傳輸。
封裝好:Netty 封裝了 NIO 操作的很多細節,提供了易於使用調用接口。
3.Netty 的優勢有哪些?
使用簡單:封裝了 NIO 的很多細節,使用更簡單。
功能強大:預置了多種編解碼功能,支持多種主流協議。
定制能力強:可以通過 ChannelHandler 對通信框架進行靈活地擴展。
性能高:通過與其他業界主流的 NIO 框架對比,Netty 的綜合性能最優。
穩定:Netty 修復了已經發現的所有 NIO 的 bug,讓開發人員可以專注於業務本身。
社區活躍:Netty 是活躍的開源項目,版本迭代周期短,bug 修復速度快。
4.Netty 的應用場景有哪些?
典型的應用有:阿里分布式服務框架 Dubbo,默認使用 Netty 作為基礎通信組件,還有
RocketMQ 也是使用 Netty 作為通訊的基礎。
5.Netty 高性能表現在哪些方面?IO 線程模型:同步非阻塞,用最少的資源做更多的事。
內存零拷貝:盡量減少不必要的內存拷貝,實現了更高效率的傳輸。
內存池設計:申請的內存可以重用,主要指直接內存。內部實現是用一顆二叉查找樹管理內
存分配情況。
串形化處理讀寫:避免使用鎖帶來的性能開銷。
高性能序列化協議:支持 protobuf 等高性能序列化協議。
6.BIO、NIO 和 AIO 的區別?
BIO:一個連接一個線程,客戶端有連接請求時服務器端就需要啟動一個線程進行處理。線
程開銷大。
偽異步 IO:將請求連接放入線程池,一對多,但線程還是很寶貴的資源。
NIO:一個請求一個線程,但客戶端發送的連接請求都會注冊到多路復用器上,多路復用器
輪詢到連接有 I/O 請求時才啟動一個線程進行處理。
AIO:一個有效請求一個線程,客戶端的 I/O 請求都是由 OS 先完成了再通知服務器應用去
啟動線程進行處理,
BIO 是面向流的,NIO 是面向緩沖區的;BIO 的各種流是阻塞的。而 NIO 是非阻塞的;BIO
的 Stream 是單向的,而 NIO 的 channel 是雙向的。
NIO 的特點:事件驅動模型、單線程處理多任務、非阻塞 I/O,I/O 讀寫不再阻塞,而是返
回 0、基於 block 的傳輸比基於流的傳輸更高效、更高級的 IO 函數 zero-copy、IO 多路復用大大提高了 Java 網絡應用的可伸縮性和實用性。基於 Reactor 線程模型。
在 Reactor 模式中,事件分發器等待某個事件或者可應用或個操作的狀態發生,事件分發
器就把這個事件傳給事先注冊的事件處理函數或者回調函數,由后者來做實際的讀寫操作。
如在 Reactor 中實現讀:注冊讀就緒事件和相應的事件處理器、事件分發器等待事件、事
件到來,激活分發器,分發器調用事件對應的處理器、事件處理器完成實際的讀操作,處理
讀到的數據,注冊新的事件,然后返還控制權。
7.NIO 的組成?
Buffer:與 Channel 進行交互,數據是從 Channel 讀入緩沖區,從緩沖區寫入 Channel
中的
flip 方法 : 反轉此緩沖區,將 position 給 limit,然后將 position 置為 0,其實就是切換
讀寫模式
clear 方法 :清除此緩沖區,將 position 置為 0,把 capacity 的值給 limit。
rewind 方法 : 重繞此緩沖區,將 position 置為 0
DirectByteBuffer 可減少一次系統空間到用戶空間的拷貝。但 Buffer 創建和銷毀的成本更
高,不可控,通常會用內存池來提高性能。直接緩沖區主要分配給那些易受基礎系統的本機
I/O 操作影響的大型、持久的緩沖區。如果數據量比較小的中小應用情況下,可以考慮使用heapBuffer,由 JVM 進行管理。
Channel:表示 IO 源與目標打開的連接,是雙向的,但不能直接訪問數據,只能與 Buffer
進行交互。通過源碼可知,FileChannel 的 read 方法和 write 方法都導致數據復制了兩次!
Selector 可使一個單獨的線程管理多個 Channel,open 方法可創建 Selector,register
方法向多路復用器器注冊通道,可以監聽的事件類型:讀、寫、連接、accept。注冊事件
后會產生一個 SelectionKey:它表示 SelectableChannel 和 Selector 之間的注冊關系,
wakeup 方法:使尚未返回的第一個選擇操作立即返回,喚醒的
原因是:注冊了新的 channel 或者事件;channel 關閉,取消注冊;優先級更高的事件觸
發(如定時器事件),希望及時處理。
Selector 在 Linux 的實現類是 EPollSelectorImpl,委托給 EPollArrayWrapper 實現,其
中三個 native 方法是對 epoll 的封裝,而 EPollSelectorImpl. implRegister 方法,通過調
用 epoll_ctl 向 epoll 實例中注冊事件,還將注冊的文件描述符(fd)與 SelectionKey 的對應
關系添加到 fdToKey 中,這個 map 維護了文件描述符與 SelectionKey 的映射。
fdToKey 有時會變得非常大,因為注冊到 Selector 上的 Channel 非常多(百萬連接);過
期或失效的 Channel 沒有及時關閉。fdToKey 總是串行讀取的,而讀取是在 select 方法中
進行的,該方法是非線程安全的。Pipe:兩個線程之間的單向數據連接,數據會被寫到 sink 通道,從 source 通道讀取
NIO 的 服 務 端 建 立 過 程 : Selector.open() : 打 開 一 個 Selector ;
ServerSocketChannel.open():創建服務端的 Channel;bind():綁定到某個端口上。並
配置非阻塞模式;register():注冊 Channel 和關注的事件到 Selector 上;select()輪詢拿
到已經就緒的事件
8.Netty 的線程模型?
Netty 通過 Reactor 模型基於多路復用器接收並處理用戶請求,內部實現了兩個線程池,
boss 線程池和 work 線程池,其中 boss 線程池的線程負責處理請求的 accept 事件,當接
收到 accept 事件的請求時,把對應的 socket 封裝到一個 NioSocketChannel 中,並交給
work 線程池,其中 work 線程池負責請求的 read 和 write 事件,由對應的 Handler 處理。
單線程模型:所有 I/O 操作都由一個線程完成,即多路復用、事件分發和處理都是在一個
Reactor 線程上完成的。既要接收客戶端的連接請求,向服務端發起連接,又要發送/讀取請
求或應答/響應消息。一個 NIO 線程同時處理成百上千的鏈路,性能上無法支撐,速度慢,
若線程進入死循環,整個程序不可用,對於高負載、大並發的應用場景不合適。
多線程模型:有一個 NIO 線程(Acceptor) 只負責監聽服務端,接收客戶端的 TCP 連接
請求;NIO 線程池負責網絡 IO 的操作,即消息的讀取、解碼、編碼和發送;1 個 NIO 線
程可以同時處理 N 條鏈路,但是 1 個鏈路只對應 1 個 NIO 線程,這是為了防止發生並發
操作問題。但在並發百萬客戶端連接或需要安全認證時,一個 Acceptor 線程可能會存在性能不足問題。
主從多線程模型:Acceptor 線程用於綁定監聽端口,接收客戶端連接,將 SocketChannel
從主線程池的 Reactor 線程的多路復用器上移除,重新注冊到 Sub 線程池的線程上,用於
處理 I/O 的讀寫等操作,從而保證 mainReactor 只負責接入認證、握手等操作;
9.TCP 粘包/拆包的原因及解決方法?
TCP 是以流的方式來處理數據,一個完整的包可能會被 TCP 拆分成多個包進行發送,也可
能把小的封裝成一個大的數據包發送。
TCP 粘包/分包的原因:
應用程序寫入的字節大小大於套接字發送緩沖區的大小,會發生拆包現象,而應用程序寫入
數據小於套接字緩沖區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘包現
象;
進行 MSS 大小的 TCP 分段,當 TCP 報文長度-TCP 頭部長度>MSS 的時候將發生拆包
以太網幀的 payload(凈荷)大於 MTU(1500 字節)進行 ip 分片。
解決方法
消息定長:FixedLengthFrameDecoder 類包尾增加特殊字符分割:
行分隔符類:LineBasedFrameDecoder
或自定義分隔符類 :DelimiterBasedFrameDecoder
將消息分為消息頭和消息體:LengthFieldBasedFrameDecoder 類。分為有頭部的拆包與
粘包、長度字段在前且有頭部的拆包與粘包、多擴展頭部的拆包與粘包。
10.什么是 Netty 的零拷貝?
Netty 的零拷貝主要包含三個方面:
Netty 的接收和發送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接內存進行
Socket 讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(HEAP BUFFERS)
進行 Socket 讀寫,
JVM 會將堆內存 Buffer 拷貝一份到直接內存中,然后才寫入 Socket
中。相比於堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。
Netty 提供了組合 Buffer 對象,可以聚合多個 ByteBuffer 對象,用戶可以像操作一個
Buffer 那樣方便的對組合 Buffer 進行操作,避免了傳統通過內存拷貝的方式將幾個小
Buffer 合並成一個大的 Buffer。
Netty 的文件傳輸采用了 transferTo 方法,它可以直接將文件緩沖區的數據發送到目標
Channel,避免了傳統通過循環 write 方式導致的內存拷貝問題。
11.Netty 中有哪種重要組件?
Channel:Netty 網絡操作抽象類,它除了包括基本的 I/O 操作,如 bind、connect、
read、write 等。
EventLoop:主要是配合 Channel 處理 I/O 操作,用來處理連接的生命周期中所發生的
事情。
ChannelFuture : Netty 框 架 中 所 有 的 I/O 操 作 都 為 異 步 的 , 因 此 我 們 需 要
ChannelFuture 的 addListener()注冊一個 ChannelFutureListener 監聽事件,當操作執
行成功或者失敗時,監聽就會自動觸發返回結果。
ChannelHandler:充當了所有處理入站和出站數據的邏輯容器。ChannelHandler 主要用
來處理各種事件,這里的事件很廣泛,比如可以是連接、數據接收、異常、數據轉換等。
ChannelPipeline:為 ChannelHandler 鏈提供了容器,當 channel 創建時,就會被自
動分配到它專屬的 ChannelPipeline,這個關聯是永久性的。
12.Netty 發送消息有幾種方式?
Netty 有兩種發送消息的方式:
直接寫入 Channel 中,消息從 ChannelPipeline 當中尾部開始移動;
寫 入 和
ChannelHandler 綁 定 的
ChannelHandlerContext 中 , 消 息 從
ChannelPipeline 中的下一個 ChannelHandler 中移動。
13.默認情況 Netty 起多少線程?何時啟動?
Netty 默認是 CPU 處理器數的兩倍,bind 完之后啟動。
14.了解哪幾種序列化協議?
序列化(編碼)是將對象序列化為二進制形式(字節數組),主要用於網絡傳輸、數據持久
化等;而反序列化(解碼)則是將從網絡、磁盤等讀取的字節數組還原成原始對象,主要用於網絡傳輸對象的解碼,以便完成遠程調用。
影響序列化性能的關鍵因素:序列化后的碼流大小(網絡帶寬的占用)、序列化的性能(CPU
資源占用);是否支持跨語言(異構系統的對接和開發語言切換)。
Java 默認提供的序列化:無法跨語言、序列化后的碼流太大、序列化的性能差
XML,優點:人機可讀性好,可指定元素或特性的名稱。缺點:序列化數據只包含數據本
身以及類的結構,不包括類型標識和程序集信息;只能序列化公共屬性和字段;不能序列化
方法;文件龐大,文件格式復雜,傳輸占帶寬。適用場景:當做配置文件存儲數據,實時數
據轉換。
JSON,是一種輕量級的數據交換格式,優點:兼容性高、數據格式比較簡單,易於讀寫、
序列化后數據較小,可擴展性好,兼容性好、與 XML 相比,其協議比較簡單,解析速度比
較快。缺點:數據的描述性比 XML 差、不適合性能要求為 ms 級別的情況、額外空間開銷
比較大。適用場景(可替代XML):跨防火牆訪問、可調式性要求高、基於 Web browser
的 Ajax 請求、傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。
Fastjson,采用一種“假定有序快速匹配”的算法。優點:接口簡單易用、目前 java 語言
中最快的 json 庫。缺點:過於注重快,而偏離了“標准”及功能性、代碼質量不高,文檔
不全。適用場景:協議交互、Web 輸出、Android 客戶端Thrift,不僅是序列化協議,還是一個 RPC 框架。優點:序列化后的體積小, 速度快、支持
多種語言和豐富的數據類型、對於數據字段的增刪具有較強的兼容性、支持二進制壓縮編碼。
缺點:使用者較少、跨防火牆訪問時,不安全、不具有可讀性,調試代碼時相對困難、不能
與其他傳輸層協議共同使用(例如 HTTP)、無法支持向持久層直接讀寫數據,即不適合做
數據持久化序列化協議。適用場景:分布式系統的 RPC 解決方案
Avro,Hadoop 的一個子項目,解決了 JSON 的冗長和沒有 IDL 的問題。優點:支持豐富
的數據類型、簡單的動態語言結合功能、具有自我描述屬性、提高了數據解析速度、快速可
壓縮的二進制數據形式、可以實現遠程過程調用 RPC、支持跨編程語言實現。缺點:對於
習慣於靜態類型語言的用戶不直觀。適用場景:在 Hadoop 中做 Hive、Pig 和 MapReduce
的持久化數據格式。
Protobuf,將數據結構以.proto 文件進行描述,通過代碼生成工具可以生成對應數據結構
的 POJO 對象和 Protobuf 相關的方法和屬性。優點:序列化后碼流小,性能高、結構化數
據存儲格式(XML JSON 等)、通過標識字段的順序,可以實現協議的前向兼容、結構化的
文檔更容易管理和維護。缺點:需要依賴於工具生成代碼、支持的語言相對較少,官方只支
持 Java 、C++ 、python。適用場景:對性能要求高的 RPC 調用、具有良好的跨防火牆
的訪問屬性、適合應用層對象的持久化
其它
protostuff 基於 protobuf 協議,但不需要配置 proto 文件,直接導包即可Jboss marshaling 可以直接序列化 java 類, 無須實 java.io.Serializable 接口
Message pack 一個高效的二進制序列化格式
Hessian 采用二進制協議的輕量級 remoting onhttp 工具
kryo 基於 protobuf 協議,只支持 java 語言,需要注冊(Registration),然后序列化
(Output),反序列化(Input)
15.如何選擇序列化協議?
具體場景
對於公司間的系統調用,如果性能要求在 100ms 以上的服務,基於 XML 的 SOAP 協議是
一個值得考慮的方案。
基於 Web browser 的 Ajax,以及 Mobile app 與服務端之間的通訊,JSON 協議是首選。
對於性能要求不太高,或者以動態類型語言為主,或者傳輸數據載荷很小的的運用場景,
JSON 也是非常不錯的選擇。
對於調試環境比較惡劣的場景,采用 JSON 或 XML 能夠極大的提高調試效率,降低系統開
發成本。
當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro 之間具有一定的競爭關系。
對於 T 級別的數據的持久化應用場景,Protobuf 和 Avro 是首要選擇。如果持久化后的數
據存儲在 hadoop 子項目里,Avro 會是更好的選擇。
對於持久層非 Hadoop 項目,以靜態類型語言為主的應用場景,Protobuf 會更符合靜態類
型語言工程師的開發習慣。由於 Avro 的設計理念偏向於動態類型語言,對於動態語言為主的應用場景,Avro 是更好的選擇。
如果需要提供一個完整的 RPC 解決方案,Thrift 是一個好的選擇。
如果序列化之后需要支持不同的傳輸層協議,或者需要跨防火牆訪問的高性能場景,
Protobuf 可以優先考慮。
protobuf 的數據類型有多種:bool、double、float、
int32、
int64、string、bytes、enum、
message。protobuf 的限定符:required: 必須賦值,不能為空、optional:字段可以賦值,
也可以不賦值、repeated: 該字段可以重復任意次數(包括 0 次)、枚舉;只能用指定的常
量集中的一個值作為其值;
protobuf 的基本規則:每個消息中必須至少留有一個 required 類型的字段、包含 0 個或
多個 optional 類型的字段;repeated 表示的字段可以包含 0 個或多個數據;[1,15]之內的
標識號在編碼的時候會占用一個字節(常用),[16,2047]之內的標識號則占用 2 個字節,標
識號一定不能重復、使用消息類型,也可以將消息嵌套任意多層,可用嵌套消息類型來代替
組。
protobuf 的消息升級原則:不要更改任何已有的字段的數值標識;不能移除已經存在的
required 字段,optional 和 repeated 類型的字段可以被移除,但要保留標號不能被重用。
新添加的字段必須是 optional 或 repeated。因為舊版本程序無法讀取或寫入新增的
required 限定符的字段。
編譯器為每一個消息類型生成了一個.java 文件,以及一個特殊的 Builder 類(該類是用來
創 建 消 息 類 接 口 的 ) 。 如 : UserProto.User.Builder builder =UserProto.User.newBuilder();builder.build();
Netty 中的使用:ProtobufVarint32FrameDecoder 是用於處理半包消息的解碼類;
ProtobufDecoder(UserProto.User.getDefaultInstance())這是創建的 UserProto.java 文
件中的解碼類;ProtobufVarint32LengthFieldPrepender 對 protobuf 協議的消息頭上加
上一個長度為 32 的整形字段,用於標志這個消息的長度的類;ProtobufEncoder 是編碼
類
將 StringBuilder 轉換為 ByteBuf 類型:copiedBuffer()方法
16.Netty 支持哪些心跳類型設置?
readerIdleTime:為讀超時時間(即測試端一定時間內未接受到被測試端消息)。
writerIdleTime:為寫超時時間(即測試端一定時間內向被測試端發送消息)。
allIdleTime:所有類型的超時時間。
17.Netty 和 Tomcat 的區別?
作用不同:Tomcat 是 Servlet 容器,可以視為 Web 服務器,而 Netty 是異步事件驅
動的網絡應用程序框架和工具用於簡化網絡編程,例如 TCP 和 UDP 套接字服務器。
協議不同:Tomcat 是基於 http 協議的 Web 服務器,而 Netty 能通過編程自定義各種
協議,因為 Netty 本身自己能編碼/解碼字節流,所有 Netty 可以實現,HTTP 服務器、
FTP 服務器、UDP 服務器、RPC 服務器、WebSocket 服務器、Redis 的 Proxy 服務器、
MySQL 的 Proxy 服務器等等。
18.NIOEventLoopGroup 源碼?NioEventLoopGroup(其實是 MultithreadEventExecutorGroup) 內部維護一個類型為
EventExecutor children [], 默認大小是處理器核數 * 2, 這樣就構成了一個線程池,初始
化 EventExecutor 時 NioEventLoopGroup 重載 newChild 方法,所以 children 元素的實
際類型為 NioEventLoop。
線程啟動時調用 SingleThreadEventExecutor 的構造方法,執行 NioEventLoop 類的 run
方法,首先會調用 hasTasks()方法判斷當前 taskQueue 是否有元素。如果 taskQueue 中
有元素,執行 selectNow() 方法,最終執行 selector.selectNow(),該方法會立即返回。
如果 taskQueue 沒有元素,執行 select(oldWakenUp) 方法
select ( oldWakenUp) 方法解決了 Nio 中的 bug,selectCnt 用來記錄 selector.select
方法的執行次數和標識是否執行過 selector.selectNow(),若觸發了 epoll 的空輪詢 bug,
則會反復執行 selector.select(timeoutMillis),變量 selectCnt 會逐漸變大,當 selectCnt
達到閾值(默認 512),則執行 rebuildSelector 方法,進行 selector 重建,解決 cpu 占用
100%的 bug。
rebuildSelector 方法先通過 openSelector 方法創建一個新的 selector。然后將 old
selector 的 selectionKey 執行 cancel。最后將 old selector 的 channel 重新注冊到新的
selector 中。 rebuild 后, 需要重新 執行方 法 selectNow ,檢 查是否有 已 ready 的
selectionKey。
接下來調用 processSelectedKeys 方法(處理 I/O 任務),當 selectedKeys != null 時,調用 processSelectedKeysOptimized 方法,迭代 selectedKeys 獲取就緒的 IO 事件的
selectkey 存放在數組 selectedKeys 中, 然后為每個事件都調用 processSelectedKey 來
處理它,processSelectedKey 中分別處理 OP_READ;OP_WRITE;OP_CONNECT 事件。
最 后 調 用 runAllTasks 方 法 ( 非 IO 任 務 ), 該 方 法 首 先 會 調 用
fetchFromScheduledTaskQueue 方法,把 scheduledTaskQueue 中已經超過延遲執行時
間的任務移到 taskQueue 中等待被執行,然后依次從 taskQueue 中取任務執行,每執行
64 個任務,進行耗時檢查,如果已執行時間超過預先設定的執行時間,則停止執行非 IO 任
務,避免非 IO 任務太多,影響 IO 任務的執行。
每個 NioEventLoop 對應一個線程和一個 Selector,NioServerSocketChannel 會主動注
冊到某一個 NioEventLoop 的 Selector 上,NioEventLoop 負責事件輪詢。
Outbound 事件都是請求事件, 發起者是 Channel,處理者是 unsafe,通過 Outbound
事件進行通知,傳播方向是 tail 到 head。Inbound 事件發起者是 unsafe,事件的處理者
是 Channel, 是通知事件,傳播方向是從頭到尾。
內存管理機制,首先會預申請一大塊內存 Arena,Arena 由許多 Chunk 組成,而每個 Chunk
默認由 2048 個 page 組成。Chunk 通過 AVL 樹的形式組織 Page,每個葉子節點表示一個
Page,而中間節點表示內存區域,節點自己記錄它在整個 Arena 中的偏移地址。當區域被
分配出去后,中間節點上的標記位會被標記,這樣就表示這個中間節點以下的所有節點都已
被分配了。大於 8k 的內存分配在 poolChunkList 中,而 PoolSubpage 用於分配小於 8k的內存,它會把一個 page 分割成多段,進行內存分配。
ByteBuf 的特點:支持自動擴容(4M),保證 put 方法不會拋出異常、通過內置的復合緩
沖類型,實現零拷貝(zero-copy);不需要調用 flip()來切換讀/寫模式,讀取和寫入索引
分開;方法鏈;引用計數基於 AtomicIntegerFieldUpdater 用於內存回收;PooledByteBuf
采用二叉樹來實現一個內存池,集中管理內存的分配和釋放,不用每次使用都新建一個緩沖
區對象。UnpooledHeapByteBuf 每次都會新建一個緩沖區對象。
Netty 簡介
Netty 是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務
器和客戶端。
JDK 原生 NIO 程序的問題
JDK 原生也有一套網絡應用程序 API,但是存在一系列問題,主要如下:
NIO 的類庫和 API 繁雜,使用麻煩,你需要熟練掌握 Selector、ServerSocketChannel、
SocketChannel、ByteBuffer 等
需要具備其它的額外技能做鋪墊,例如熟悉 Java 多線程編程,因為 NIO 編程涉及到 Reactor
模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的 NIO 程序
可靠性能力補齊,開發工作量和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等,NIO 編程的特點是功能開發相對容易,
但是可靠性能力補齊工作量和難度都非常大
JDK NIO 的 BUG,例如臭名昭著的 epoll bug,它會導致 Selector 空輪詢,最終導致 CPU
100%。官方聲稱在 JDK1.6 版本的 update18 修復了該問題,但是直到 JDK1.7 版本該問
題仍舊存在,只不過該 bug 發生概率降低了一些而已,它並沒有被根本解決
Netty 的特點
Netty 的對 JDK 自帶的 NIO 的 API 進行封裝,解決上述問題,主要特點有:
設計優雅 適用於各種傳輸類型的統一 API - 阻塞和非阻塞 Socket 基於靈活且可擴展的事
件模型,可以清晰地分離關注點 高度可定制的線程模型 - 單線程,一個或多個線程池 真
正的無連接數據報套接字支持(自 3.1 起)
使用方便 詳細記錄的 Javadoc,用戶指南和示例 沒有其他依賴項,JDK 5(Netty 3.x)或
6(Netty 4.x)就足夠了
高性能 吞吐量更高,延遲更低 減少資源消耗 最小化不必要的內存復制
安全 完整的 SSL / TLS 和 StartTLS 支持
社區活躍,不斷更新 社區活躍,版本迭代周期短,發現的 BUG 可以被及時修復,同時,
更多的新功能會被加入
Netty 常見使用場景
Netty 常見的使用場景如下:
互聯網行業 在分布式系統中,各個節點之間需要遠程服務調用,高性能的 RPC 框架必不可
少,Netty 作為異步高新能的通信框架,往往作為基礎通信組件被這些 RPC 框架使用。 典型的應用有:阿里分布式服務框架 Dubbo 的 RPC 框架使用 Dubbo 協議進行節點間通信,
Dubbo 協議默認使用 Netty 作為基礎通信組件,用於實現各進程節點之間的內部通信。
游戲行業 無論是手游服務端還是大型的網絡游戲,Java 語言得到了越來越廣泛的應用。
Netty 作為高性能的基礎通信組件,它本身提供了 TCP/UDP 和 HTTP 協議棧。 非常方便
定制和開發私有協議棧,賬號登錄服務器,地圖服務器之間可以方便的通過 Netty 進行高
性能的通信
大數據領域 經典的Hadoop的高性能通信和序列化組件Avro的RPC框架,默認采用Netty
進行跨界點通信,它的 Netty Service 基於 Netty 框架二次封裝實現
有興趣的讀者可以了解一下目前有哪些開源項目使用了 Netty:Related projects
Netty 高性能設計
Netty 作為異步事件驅動的網絡,高性能之處主要來自於其 I/O 模型和線程處理模型,前者
決定如何收發數據,后者決定如何處理數據
I/O 模型
用什么樣的通道將數據發送給對方,BIO、NIO 或者 AIO,I/O 模型在很大程度上決定了框
架的性能
阻塞 I/O
傳統阻塞型 I/O(BIO)可以用下圖表示:特點
每個請求都需要獨立的線程完成數據 read,業務處理,數據 write 的完整操作
問題
當並發數較大時,需要創建大量線程來處理連接,系統資源占用較大
連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 read 操作上,造成線程資
源浪費
I/O 復用模型
在 I/O 復用模型中,會用到 select,這個函數也會使進程阻塞,但是和阻塞 I/O 所不同的
的,這兩個函數可以同時阻塞多個 I/O 操作,而且可以同時對多個讀操作,多個寫操作的
I/O 函數進行檢測,直到有數據可讀或可寫時,才真正調用 I/O 操作函數
Netty 的非阻塞 I/O 的實現關鍵是基於 I/O 復用模型,這里用 Selector 對象表示:
Netty 的 IO 線程 NioEventLoop 由於聚合了多路復用器 Selector,可以同時並發處理成百
上千個客戶端連接。當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。線程通常將非阻塞 IO 的空閑時間用於在其他通道上執行 IO
操作,所以單獨的線程可以管理多個輸入和輸出通道。
由於讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由於頻繁 I/O 阻
塞導致的線程掛起,一個 I/O 線程可以並發處理 N 個客戶端連接和讀寫操作,這從根本上
解決了傳統同步阻塞 I/O 一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了
極大的提升。
基於 buffer
傳統的 I/O 是面向字節流或字符流的,以流式的方式順序地從一個 Stream 中讀取一個或
多個字節, 因此也就不能隨意改變讀取指針的位置。
在 NIO 中, 拋棄了傳統的 I/O 流, 而是引入了 Channel 和 Buffer 的概念. 在 NIO 中, 只能
從 Channel 中讀取數據到 Buffer 中或將數據 Buffer 中寫入到 Channel。
基於 buffer 操作不像傳統 IO 的順序操作, NIO 中可以隨意地讀取任意位置的數據
線程模型
數據報如何讀取?讀取之后的編解碼在哪個線程進行,編解碼后的消息如何派發,線程模型
的不同,對性能的影響也非常大。
事件驅動模型通常,我們設計一個事件處理模型的程序有兩種思路
輪詢方式 線程不斷輪詢訪問相關事件發生源有沒有發生事件,有發生事件就調用事件處理
邏輯。
事件驅動方式 發生事件,主線程把事件放入事件隊列,在另外線程不斷循環消費事件列表
中的事件,調用事件對應的處理邏輯處理事件。事件驅動方式也被稱為消息通知方式,其實
是設計模式中觀察者模式的思路。
以 GUI 的邏輯處理為例,說明兩種邏輯的不同:
輪詢方式 線程不斷輪詢是否發生按鈕點擊事件,如果發生,調用處理邏輯
事件驅動方式 發生點擊事件把事件放入事件隊列,在另外線程消費的事件列表中的事件,
根據事件類型調用相關事件處理邏輯
這里借用 O’Reilly 大神關於事件驅動模型解釋圖
主要包括 4 個基本組件:
事件隊列(event queue):接收事件的入口,存儲待處理事件
分發器(event mediator):將不同的事件分發到不同的業務邏輯單元
事件通道(event channel):分發器與處理器之間的聯系渠道
事件處理器(event processor):實現業務邏輯,處理完成后會發出事件,觸發下一步操作
可以看出,相對傳統輪詢模式,事件驅動有如下優點:
可擴展性好,分布式的異步架構,事件處理器之間高度解耦,可以方便擴展事件處理邏輯
高性能,基於隊列暫存事件,能方便並行異步處理事件
Reactor 線程模型
Reactor 是反應堆的意思,Reactor 模型,是指通過一個或多個輸入同時傳遞給服務處理器
的服務請求的事件驅動處理模式。 服務端程序處理傳入多路請求,並將它們同步分派給請
求對應的處理線程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了復用統一監聽事件,
收到事件后分發(Dispatch 給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor 模型中有 2 個關鍵組成:
Reactor Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序
來對 IO 事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移
到適當的聯系人
Handlers 處理程序執行 I/O 事件要完成的實際事件,類似於客戶想要與之交談的公司中的
實際官員。Reactor 通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作
取決於 Reactor 的數量和 Hanndler 線程數量的不同,Reactor 模型有 3 個變種單 Reactor 單線程
單 Reactor 多線程
主從 Reactor 多線程
可以這樣理解,Reactor 就是一個執行 while (true) { selector.select(); …}循環的線程,會
源源不斷的產生新的事件,稱作反應堆很貼切。
篇幅關系,這里不再具體展開 Reactor 特性、優缺點比較,有興趣的讀者可以參考我之前
另外一篇文章:《理解高性能網絡模型》
Netty 線程模型
Netty 主要基於主從 Reactors 多線程模型(如下圖)做了一定的修改,其中主從 Reactor
多線程模型有多個 Reactor:MainReactor 和 SubReactor:
MainReactor 負責客戶端的連接請求,並將請求轉交給 SubReactor
SubReactor 負責相應通道的 IO 讀寫請求
非 IO 請求(具體邏輯處理)的任務則會直接寫入隊列,等待 worker threads 進行處理
這里引用 Doug Lee 大神的 Reactor 介紹:Scalable IO in Java 里面關於主從 Reactor 多
線程模型的圖
特別說明的是: 雖然 Netty 的線程模型基於主從 Reactor 多線程,借用了 MainReactor和 SubReactor 的結構,但是實際實現上,SubReactor 和 Worker 線程在同一個線程池中:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
上面代碼中的 bossGroup 和 workerGroup 是 Bootstrap 構造方法中傳入的兩個對象,這
兩個 group 均是線程池
bossGroup 線程池則只是在 bind 某個端口后,獲得其中一個線程作為 MainReactor,專
門處理端口的 accept 事件,每個端口對應一個 boss 線程
workerGroup 線程池會被各個 SubReactor 和 worker 線程充分利用
異步處理
異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理
這個調用的部件在完成后,通過狀態、通知和回調來通知調用者。
Netty 中的 I/O 操作是異步的,包括 bind、write、connect 等操作會簡單的返回一個
ChannelFuture,調用者並不能立刻獲得結果,通過 Future-Listener 機制,用戶可以方便
的主動獲取或者通過通知機制獲得 IO 操作結果。當 future 對象剛剛創建時,處於非完成狀態,調用者可以通過返回的 ChannelFuture 來獲
取操作執行的狀態,注冊監聽函數來執行完成后的操,常見有如下操作:
通過 isDone 方法來判斷當前操作是否完成
通過 isSuccess 方法來判斷已完成的當前操作是否成功
通過 getCause 方法來獲取已完成的當前操作失敗的原因
通過 isCancelled 方法來判斷已完成的當前操作是否被取消
通過 addListener 方法來注冊監聽器,當操作已完成(isDone 方法返回完成),將會通知指
定的監聽器;如果 future 對象已完成,則理解通知指定的監聽器
例如下面的的代碼中綁定端口是異步操作,當綁定操作處理完,將會調用相應的監聽器處理
邏輯
serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
} else {
System.err.println("端口[" + port + "]綁定失敗!");
}
});相比傳統阻塞 I/O,執行 I/O 操作后線程會被阻塞住, 直到操作完成;異步處理的好處是不
會造成線程阻塞,線程在 I/O 操作期間可以執行別的程序,在高並發情形下會更穩定和更高
的吞吐量。
Netty 架構設計
前面介紹完 Netty 相關一些理論介紹,下面從功能特性、模塊組件、運作過程來介紹 Netty
的架構設計
功能特性
傳輸服務 支持 BIO 和 NIO
容器集成 支持 OSGI、JBossMC、Spring、Guice 容器
協議支持 HTTP、Protobuf、二進制、文本、WebSocket 等一系列常見協議都支持。 還
支持通過實行編碼解碼邏輯來實現自定義協議
Core 核心 可擴展事件模型、通用通信 API、支持零拷貝的 ByteBuf 緩沖對象
模塊組件
Bootstrap、ServerBootstrap
Bootstrap 意思是引導,一個 Netty 應用通常由一個 Bootstrap 開始,主要作用是配置整
個 Netty 程序,串聯各個組件,Netty 中 Bootstrap 類是客戶端程序的啟動引導類,
ServerBootstrap 是服務端啟動引導類。Future、ChannelFuture
正如前面介紹,在 Netty 中所有的 IO 操作都是異步的,不能立刻得知消息是否被正確處理,
但是可以過一會等它執行完成或者直接注冊一個監聽,具體的實現就是通過 Future 和
ChannelFutures,他們可以注冊一個監聽,當操作執行成功或失敗時監聽會自動觸發注冊
的監聽事件。
Channel
Netty 網絡通信的組件,能夠用於執行網絡 I/O 操作。 Channel 為用戶提供:
當前網絡連接的通道的狀態(例如是否打開?是否已連接?)
網絡連接的配置參數 (例如接收緩沖區大小)
提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味着任何 I / O 調用
都將立即返回,並且不保證在調用結束時所請求的 I / O 操作已完成。調用立即返回一個
ChannelFuture 實例,通過注冊監聽器到 ChannelFuture 上,可以 I / O 操作成功、失敗
或取消時回調通知調用方。
支持關聯 I/O 操作與對應的處理程序
不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,下面是一些常用的
Channel 類型
NioSocketChannel,異步的客戶端 TCP Socket 連接
NioServerSocketChannel,異步的服務器端 TCP Socket 連接
NioDatagramChannel,異步的 UDP 連接NioSctpChannel,異步的客戶端 Sctp 連接
NioSctpServerChannel,異步的 Sctp 服務器端連接 這些通道涵蓋了 UDP 和 TCP 網絡
IO 以及文件 IO.
Selector
Netty 基於 Selector 對象實現 I/O 多路復用,通過 Selector, 一個線程可以監聽多個連接
的 Channel 事件, 當向一個 Selector 中注冊 Channel 后,Selector 內部的機制就可以自
動不斷地查詢(select) 這些注冊的 Channel 是否有已就緒的 I/O 事件(例如可讀, 可寫, 網
絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel 。
NioEventLoop
NioEventLoop 中維護了一個線程和任務隊列,支持異步提交執行任務,線程啟動時會調用
NioEventLoop 的 run 方法,執行 I/O 任務和非 I/O 任務:
I/O 任務 即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由
processSelectedKeys 方法觸發。
非 IO 任務 添加到 taskQueue 中的任務,如 register0、bind0 等任務,由 runAllTasks
方法觸發。
兩種任務的執行時間比由變量 ioRatio 控制,默認為 50,則表示允許非 IO 任務執行的時間
與 IO 任務的執行時間相等。
NioEventLoopGroup
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解為一個線程池,內部維護了一組線程,每個線程(NioEventLoop)負責處理多個 Channel 上的事件,而一個
Channel 只對應於一個線程。
ChannelHandler
ChannelHandler 是一個接口,處理 I / O 事件或攔截 I / O 操作,並將其轉發到其
ChannelPipeline(業務處理鏈)中的下一個處理程序。
ChannelHandler 本身並沒有提供很多方法,因為這個接口有許多的方法需要實現,方便使
用期間,可以繼承它的子類:
ChannelInboundHandler 用於處理入站 I / O 事件
ChannelOutboundHandler 用於處理出站 I / O 操作
或者使用以下適配器類:
ChannelInboundHandlerAdapter 用於處理入站 I / O 事件
ChannelOutboundHandlerAdapter 用於處理出站 I / O 操作
ChannelDuplexHandler 用於處理入站和出站事件
ChannelHandlerContext
保存 Channel 相關的所有上下文信息,同時關聯一個 ChannelHandler 對象
ChannelPipline
保存 ChannelHandler 的 List,用於處理或攔截 Channel 的入站事件和出站操作。ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理
方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
下圖引用 Netty 的 Javadoc4.1 中 ChannelPipline 的說明,描述了 ChannelPipeline 中
ChannelHandler 通常如何處理 I/O 事件。 I/O 事件由 ChannelInboundHandler 或
ChannelOutboundHandler 處理,並通過調用 ChannelHandlerContext 中定義的事件傳
播 方 法 ( 例 如 ChannelHandlerContext.fireChannelRead ( Object ) 和
ChannelOutboundInvoker.write(Object))轉發到其最近的處理程序。
I/O Request
via Channel or
ChannelHandlerContext
|
+---------------------------------------------------+---------------+
|
ChannelPipeline
|
|
|
\|/
|
|
+---------------------+
+-----------+----------+
|
|
| Inbound Handler N |
| Outbound Handler 1 |
|
|
+----------+----------+
+-----------+----------+
|
|
/|\
|
|
|
|
\|/
|
|
+----------+----------+
+-----------+----------+
||
| Inbound Handler N-1 |
| Outbound Handler 2 |
|
|
+----------+----------+
+-----------+----------+
|
|
/|\
.
|
|
.
.
|
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
|
[ method call]
[method call]
|
|
.
.
|
|
.
\|/
|
|
+----------+----------+
+-----------+----------+
|
|
| Inbound Handler 2 |
| Outbound Handler M-1 |
|
|
+----------+----------+
+-----------+----------+
|
|
/|\
|
|
|
|
\|/
|
|
+----------+----------+
+-----------+----------+
|
|
| Inbound Handler 1 |
| Outbound Handler M |
|
|
+----------+----------+
+-----------+----------+
|
|
/|\
|
|
+---------------+-----------------------------------+---------------+
|
\|/
+---------------+-----------------------------------+---------------+
|
|
|
|
|
[ Socket.read() ]
[ Socket.write() ]
||
|
| Netty Internal I/O Threads (Transport Implementation)
|
+-------------------------------------------------------------------+
入站事件由自下而上方向的入站處理程序處理,如圖左側所示。 入站 Handler 處理程序通
常處 理由 圖底 部的 I / O 線程 生成 的入 站數 據。 通常 通過 實際 輸入 操作 (例 如
SocketChannel.read(ByteBuffer))從遠程讀取入站數據。
出站事件由上下方向處理,如圖右側所示。 出站 Handler 處理程序通常會生成或轉換出站
傳輸,例如 write 請求。 I/O 線程通常執行實際的輸出操作,例如 SocketChannel.write
(ByteBuffer)。
在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應, 它們的組成關
系如下:
一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由
ChannelHandlerContext 組成的雙向鏈表, 並且每個 ChannelHandlerContext 中又關
聯着一個 ChannelHandler。入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表
head 往后傳遞到最后一個入站的 handler,出站事件會從鏈表 tail 往前傳遞到最前一個出站的 handler,兩種類型的 handler 互不干擾。
工作原理架構
初始化並啟動 Netty 服務端過程如下:
public static void main(String[] args) {
// 創建 mainReactor
NioEventLoopGroup boosGroup = new NioEventLoopGroup();
// 創建工作線程組
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
// 組裝 NioEventLoopGroup
.group(boosGroup, workerGroup)
// 設置 channel 類型為 NIO 類型
.channel(NioServerSocketChannel.class)
// 設置連接配置參數
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
// 配置入站、出站事件 handler.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
// 配置入站、出站事件 channel
ch.pipeline().addLast(...);
ch.pipeline().addLast(...);
}
});
// 綁定端口
int port = 8080;
serverBootstrap.bind(port).addListener(future -> {
if (future.isSuccess()) {
System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
} else {
System.err.println("端口[" + port + "]綁定失敗!");
}
});
}
基本過程如下:1 初始化創建 2 個 NioEventLoopGroup,其中 boosGroup 用於 Accetpt 連接建立事件
並分發請求, workerGroup 用於處理 I/O 讀寫事件和業務邏輯
2 基於 ServerBootstrap(服務端啟動引導類),配置 EventLoopGroup、Channel 類型,
連接參數、配置入站、出站事件 handler
3 綁定端口,開始工作
結合上面的介紹的 Netty Reactor 模型,介紹服務端 Netty 的工作架構圖:
server 端包含 1 個 Boss NioEventLoopGroup 和 1 個 Worker NioEventLoopGroup,
NioEventLoopGroup 相當於 1 個事件循環組,這個組里包含多個事件循環 NioEventLoop,
每個 NioEventLoop 包含 1 個 selector 和 1 個事件循環線程。
每個 Boss NioEventLoop 循環執行的任務包含 3 步:
1 輪詢 accept 事件
2 處 理 accept I/O 事 件 , 與 Client 建 立 連 接 , 生 成 NioSocketChannel , 並 將
NioSocketChannel 注冊到某個 Worker NioEventLoop 的 Selector 上 *3 處理任務隊列
中的任務,runAllTasks。任務隊列中的任務包括用戶調用 eventloop.execute 或 schedule
執行的任務,或者其它線程提交到該 eventloop 的任務。
每個 Worker NioEventLoop 循環執行的任務包含 3 步:1 輪詢 read、write 事件;
2 處 I/O 事件,即 read、write 事件,在 NioSocketChannel 可讀、可寫事件發生時進行
處理
3 處理任務隊列中的任務,runAllTasks。
其中任務隊列中的 task 有 3 種典型使用場景
1 用戶程序自定義的普通任務
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
//...
}
});
2 非當前 reactor 線程調用 channel 的各種方法 例如在推送系統的業務線程里面,根據用
戶的標識,找到對應的 channel 引用,然后調用 write 類方法向該用戶推送消息,就會進
入到這種場景。最終的 write 會提交到任務隊列中后被異步消費。
3 用戶自定義定時任務
ctx.channel().eventLoop().schedule(new Runnable() {
@Override
public void run() {}
}, 60, TimeUnit.SECONDS);
總結
現在穩定推薦使用的主流版本還是 Netty4,Netty5 中使用了 ForkJoinPool,增加了代碼
的復雜度,但是對性能的改善卻不明顯,所以這個版本不推薦使用,官網也沒有提供下載鏈
接。
Netty 入門門檻相對較高,其實是因為這方面的資料較少,並不是因為他有多難,大家其
實都可以像搞透 Spring 一樣搞透 Netty。在學習之前,建議先理解透整個框架原理結構,
運行過程,可以少走很多彎路。
