參考教程
002.Netty是什么
- Netty 是由 JBOSS 提供的一個 Java 開源框架,現為 Github 上的獨立項目
- Netty 是一個異步的、基於事件驅動的網絡應用框架,用以快速開發高性能、高可靠性的網絡 IO 程序。
- Netty 主要針對在 TCP 協議下,面向 Clients 端的高並發應用,或者 Peer to Peer 場景下的大量數據持續傳輸的應用。
- Netty 本質是一個 NIO 框架,適用於服務器通訊相關的多種應用場景
- 要徹底理解 Netty,需要先學習 NIO,這樣我們才能閱讀 Netty 的源碼
同步

如上圖所示,瀏覽器執行了請求1,只有響應2到來之后,才能進行后續的操作3
異步

瀏覽器發送請求之后,指定消息來到時的回調方法,這樣即使消息沒有返回也能去做其它的事情了
體系圖

003.Netty的應用場景
互聯網行業
- 互聯網行業:在分布式系統中,各個節點之間需要遠程服務調用,高性能的 RPC 框架必不可少,Netty 作為異步高性能的通信框架,往往作為基礎通信組件被這些 RPC 框架使用
- 典型的應用有:阿里分布式服務框架 Dubbo 的 RPC 框架使用 Dubbo 協議進行節點間通信,Dubbo 協議默認使用 Netty 作為基礎通信組件,用於實現各進程節點之間的內部通信
游戲行業
- 無論是手游服務器還是大型的網絡游戲,Java 語言得到了越來越廣泛的應用
- Netty 作為高性能的基礎通信組件,提供了 TCP/UDP 和 HTTP 協議棧,方便定制和開發私有協議棧,賬號登錄服務器
- 地圖服務器之間可以方便的通過 Netty 進行高性能的通信
大數據領域
- 經典的 Hadoop 的高性能通信和序列化組件(AVRO 實現數據文件共享)的 RPC 框架,默認采用 Netty 進行跨界點通信
- 它的 Netty Service 基於 Netty 框架二次封裝實現
其它
https://netty.io/wiki/related-projects.html
推薦書籍
《Netty In Action》
《Netty 權威指南》
004.IO模型
I/O 模型基本說明
- I/O 模型簡單的理解:就是用什么樣的通道進行數據的發送和接收,很大程度上決定了程序通信的性能
- Java 共支持 3種網絡編程模型/IO 模式:BIO、NIO、AIO
- Java BIO:同步並阻塞(傳統阻塞型),服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷

- Java NIO:同步非阻塞,服務器實現模式為一個線程處理多個請求(連接),即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求就進行處理

- Java AIO(NIO.2):異步非阻塞,AIO 引入異步通道的概念,采用了 Proactor 模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,一般適用於連接數較多且連接時間較長的應用
使用場景分析
- BIO 方式適用於連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,並發局限於應用中,JDK 1.4 以前的唯一選擇,但程序簡單易理解
- NIO 方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,彈幕系統,服務器間通訊等。編程比較復雜,JDK 1.4 開始支持。
- AIO 方式適用於*連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用 OS 參與並發操作,編程比較復雜,JDK7 開始支持
005.BIO基本介紹
基本介紹
- Java BIO 就是傳統的 java io 編程,其相關的類和接口在 java.io
- BIO(blocking I/O):同步阻塞,服務器實現模式為一個連接一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制改善(實現多個客戶連接服務器)
- BIO 方式適用於連接數目比較小且固定的架構,這種方式對服務器資源比較高,並發局限於應用中,JDK 1.4 以前的唯一選擇,程序簡單易理解
工作原理圖

BIO 編程簡單流程
- 服務器端啟動一個 ServerSocket
- 客戶端啟動一個 Socket 對服務器進行通信,默認情況下服務器端需要對每個客戶建立一個線程與之通訊
- 客戶端發出請求后,先咨詢服務器是否有線程響應,如果沒有則會等待,或者被拒絕
- 如果有響應,客戶端線程會等待請求結束后,才繼續執行
應用實例
實例說明:
- 使用 BIO 模型編寫一個服務器端,監聽 6666 端口,當有客戶端連接時,就啟動一個線程與之通訊
- 要求使用線程池機制改善,可以連接多個客戶端
- 服務器端可以接收客戶端發送的數據(telnet 方式即可)
代碼示例:
com.hellozjf.project.study.netty.bio.BIOServer
問題分析
- 每個請求都需要創建獨立的線程,與對應的客戶端進行數據 read,業務處理,數據 write
- 當並發較大時,需要創建大量線程來處理連接,系統資源占用較大
- 連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 read 操作上,造成資源浪費
007. NIO基本介紹
基本介紹
- Java NIO 全稱 java non-blocking IO,是指 JDK 提供的新 API。從 JDK 1.4 開始,Java 提供了一系列改進的輸入/輸出的新特性,被統稱為 NIO(即 New IO),是同步非阻塞的
- NIO 相關類都被放在 java.nio 包及子包下,並且對原 java.io 包中的很多類進行改寫
- NIO 有三大核心部分:Channel(通道),Buffer(緩沖區),Selector(選擇器)

- NIO 是面向緩沖區,或者面向塊編程的。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞的高伸縮性網絡
- Java NIO 的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取,而不是保持數據阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此,一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情
- 通俗理解:NIO 是可以做到用一個線程來處理多個操作的。假設有 10000 個請求過來,根據實際情況,可以分配 50 或者 100 個線程來處理。不像之前的阻塞 IO 那樣,非得分配 10000 個
- HTTP 2.0 使用多路復用的技術,做到同一個連接並發請求處理多個請求,而且並發請求的數量比 HTTP 1.1 大了好幾個數量級
Buffer 介紹
參考代碼
com.hellozjf.project.study.netty.nio.BasicBuffer
NIO 和 BIO 的比較
- BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據,塊 I/O 的效率比流 I/O 高很多
- BIO 是阻塞的,NIO 是非阻塞的
- BIO 基於字節流和字符流進行操作,而 NIO 基於 Channel(通道)和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用於監聽多個通道的事件(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道
NIO 三大核心原理示意圖

Selector、Channel、Buffer的關系
關系圖的說明:
- 每個 Channel 都會對應一個 Buffer
- Selector 會對應一個線程,一個線程對應多個 Channel(連接)
- 該圖反應了有三個 Channel 注冊到了該 Selector
- 程序切換到哪個 Channel 是由事件決定,Event 就是一個重要的概念
- Selector 會根據不同的事件,在各個通道上切換
- Buffer 就是一個內存塊,底層是有一個數組
- 數據的讀取寫入是通過 Buffer,這個和 BIO 是由本質不同的,BIO 中那么是輸入流,或者是輸出流,不能雙向。但是 NIO 的 Buffer 是可以讀也可以寫,需要 flip 方法切換
- Channel 是雙向的,可以反應底層操作系統的情況,比如 Linux,底層的操作系統通道就是雙向的
緩沖區(Buffer)
基本介紹
緩沖區(Buffer):緩沖區本質上是一個可以讀寫數據的內存塊,可以理解是一個容器對象(含數組),該對象提供了一組方法,可以更輕松地使用內存塊,緩沖區對象內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況。Channel 提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據必須經由 Buffer,如圖:

Buffer 類及其子類
- 在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類,累的層級關系圖

- Buffer 類定義了所有的緩沖區都具有的四個屬性,來提供關於其所包含的數據元素的信息

- Buffer 類相關方法一覽

ByteBuffer
從前面可以看出對於 Java 中的基本數據類型(boolean 除外),都有一個 Buffer 類型與之相對應,最常用的自然是 ByteBuffer 類(二進制數據),該類的主要方法如下:

通道(Channel)
基本介紹
-
NIO 的通道類似於流,但有些區別如下:
- 通道可以同時進行讀寫,而流只能讀或者只能寫
- 通道可以實現異步讀寫數據
- 通道可以從緩沖讀數據,也可以寫數據到緩沖

-
BIO 中的 stream 是單向的,例如 FileInputStream 對象只能進行讀取數據的操作,而 NIO 中的通道(Channel)是雙向的,可以讀操作,也可以寫操作
-
Channel 在 NIO 中是一個接口
public interface Channel extends Closeable -
常用的 Channel 類有:FileChannel、DatagramChannel、ServerSocketChannel(類似 ServerSocket) 和 SocketChannel(類似 Socket)
-
FileChannel 用於文件的數據讀寫,DatagramChannel 用於 UDP 的數據讀寫,ServerSocketChannel 和 SocketChannel 用於 TCP 的數據讀寫
FileChannel 類
FileChannel 主要用來對本地文件進行 IO 操作,常見的方法有
public int read(ByteBuffer dst),從通道讀取數據,並放到緩沖區中public int write(ByteBuffer src),把緩沖區的數據寫到通道中public long transferFrom(ReadableByteChannel src, long position, long count),從目標通道中復制數據到當前通道public long transferTo(long position, long count, WritableByteChannel target),把數據從當前通道復制到目標通道


參考代碼
com.hellozjf.project.study.netty.nio.NIOFileChannel01
com.hellozjf.project.study.netty.nio.NIOFileChannel02
com.hellozjf.project.study.netty.nio.NIOFileChannel03
com.hellozjf.project.study.netty.nio.NIOFileChannel04
關於 Buffer 和 Channel 的注意事項和細節
- ByteBuffer 支持類型化的 put 和 get,put 放入的是什么數據類型,get 就應該使用相應的數據類型來取出,否則可能會有 BufferUnderflowException 異常。參考代碼
com.hellozjf.project.study.netty.nio.NIOByteBufferPutGet - 可以將一個普通 Buffer 轉成只讀 Buffer。參考代碼
com.hellozjf.project.study.netty.nio.ReadOnlyBuffer - NIO 還提供了 MappedByteBuffer,可以讓文件直接在內存(堆外的內存)中進行修改,而如何同步到文件由 NIO 來完成。參考代碼
com.hellozjf.project.study.netty.nio.MappedByteBufferTest - 前面我們講的讀寫操作,都是通過一個 Buffer 完成的,NIO 還支持 通過多個 Buffer(即 Buffer 數組)完成讀寫操作,即 Scattering 和 Gatering。參考代碼
com.hellozjf.project.study.netty.nio.ScatteringAndGatheringTest
Selector(選擇器)
基本介紹
- Java 的 NIO,用非阻塞的 IO 方式。可以用一個線程,處理多個的客戶端連接,就會使用Selector(選擇器)
- Selector 能夠檢測多個注冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以注冊到同一個 Selector),如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求

- 只有在連接真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,並且不必為每個連接都創建一個線程,不用去維護多個線程
- 避免了多線程之間的上下文切換導致的開銷
特點說明
- Netty 的 IO 線程 NioEventLoop 聚合了 Selector (選擇器,也叫多路復用器),可以同時並發處理成百上千個客戶端連接
- 當線程從某客戶端 Socket 通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務
- 線程通常將非阻塞 IO 的空閑時間用於在其它通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道
- 由於讀寫操作都是非阻塞的,這就可以充分提升 IO 線程的運行效率,避免由於頻繁 I/O 阻塞導致的線程掛起
- 一個 I/O 線程可以並發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞 I/O —— 連接 —— 線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升
類相關方法
Selector 類是一個抽象類,常用方法和說明如下:

注意事項
-
NIO 中的 ServerSocketChannel 功能類似於 ServerSocket,SocketChannel 功能類似於 Socket
-
selector 相關方法說明
selector.select(); // 阻塞 selector.select(1000); // 阻塞1000毫秒,在1000毫秒后返回 selector.wakeup(); // 喚醒selector selector.selectNow(); // 不阻塞,立馬返回
NIO 非阻塞網絡編程原理分析圖

說明:
- 當客戶端連接時,會通過 ServerSocketChannel 得到對應的 SocketChannel
- Selector 進行監聽,select 方法,返回有事件發生的通道的個數
- 將 SocketChannel 注冊到 Selector 上,registor(Selector sel, int ops),一個 Selector 上可以注冊多個 SocketChannel
- 注冊后返回一個 SelectionKey,會和該 Selector 關聯(集合)
- 進一步得到各個 SelectionKey(有事件發生)
- 再通過 SelectionKey 反向獲取 SocketChannel channel()
- 可以通過得到的 channel,完成業務處理
- 代碼撐腰
NIO 非阻塞網絡編程快速入門
案例要求:
- 編寫一個 NIO 入門案例,實現服務器端和客戶端之間的數據簡單通訊(非阻塞)
- 目的:理解 NIO 非阻塞網絡編程機制
參考代碼
com.hellozjf.project.study.netty.nio.NIOServer
com.hellozjf.project.study.netty.nio.NIOClient
SelectionKey
-
SelectionKey,表示 Selector 和網絡通道的注冊關系,共四種:
int OP_ACCEPT:有新的網絡連接可以 accept,值為16 int OP_CONNECT:代表連接已經建立,值為8 int OP_READ:代表讀操作,值為1 int OP_WRITE:代表寫操作,值為4 源碼中 public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4; -
SelectionKey 相關方法

ServerSocketChannel
- ServerSocketChannel 在服務器端監聽新的客戶端 Socket 連接
- 相關方法如下

SocketChannel
-
SocketChannel,網絡 IO 通道,具體負責進行讀寫操作。NIO 把緩沖區的數據寫入通道,或者把通道里的數據讀到緩沖區
-
相關方法如下:

029.NIO群聊系統
實例要求
- 編寫一個 NIO 群聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞)
- 實現多人群聊
- 服務器端:可以監測用戶上線,離線,並實現消息轉發功能
- 客戶端:通過 channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(由服務器轉發得到)
- 目的:進一步理解 NIO 非阻塞網絡編程機制
步驟
- 先編寫服務器端
- 服務器啟動並監聽 6667
- 服務器接收客戶端信息,並實現轉發【處理上線和離線】
- 編寫客戶端
- 連接服務器
- 發送消息
- 接收服務器端消息
033.NIO與零拷貝
零拷貝基本介紹
- 零拷貝是網絡編程的關鍵,很多性能優化都離不開
- 在 Java 程序中,常用的零拷貝有 mmap(內存映射)和 sendFile。那么,它們在 OS 里,到底是怎么樣的一個設計?我們分析 mmap 和 sendFile 這兩個零拷貝
- 另外我們看下 NIO 中如何使用零拷貝
提示,零拷貝從操作系統角度看,是沒有 CPU 拷貝
傳統 IO 數據讀寫
-
Java 傳統 IO 和網絡編程的一段代碼
File file = new File("test.txt"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); byte[] arr = new byte[(int) file.length()]; raf.read(arr); Socket socket = new ServerSocket(8080).accept(); socket.getOutputStream().write(arr); -
IO 示意圖,藍線是用戶態,紅線是內核態。
其中 DMA(direct memory access),直接內存拷貝,不使用CPU。經過四次拷貝,三次狀態切換

mmap優化
-
mmap 通過內存映射,將文件映射到內核緩沖區,同時,用戶空間可以共享內核空間的數據。這樣,在進行網絡傳輸時,就可以減少內核空間到用戶空間的拷貝次數
-
mmap示意圖
經過3次拷貝,3次狀態切換

sendFile 優化
-
Linux 2.1 版本提供了 sendFile 函數,其基本原理如下:數據根本不經過用戶態,直接從內核緩沖區進入到 SocketBuffer ,同時,由於和用戶態完全無關,就減少了一次上下文切換
-
示意圖和小結
經過3次拷貝,2次狀態切換
-
Linux 在 2.4 版本中,做了一些修改,避免了從內核緩沖區拷貝到 Socket Buffer 的操作,直接拷貝到協議棧,從而再一次減少了數據拷貝。具體如下圖和小結:
這里其實還是有一次 CPU 拷貝的,kernel buffer -> socket buffer。但是拷貝的信息很少,比如 length,offset,消耗少,可以忽略
經過2次拷貝,2次狀態切換
零拷貝的再次理解
- 我們說零拷貝,是從操作系統的角度來說的。因為內核緩沖區之間,沒有數據是重復的(只有 kernel buffer 有一份數據)
- 零拷貝不僅僅帶來更少的數據復制,還能帶來其他的性能優勢,例如更少的上下文切換,更少的 CPU 緩存偽共享以及無 CPU 校驗和計算
mmap 和 sendFile 的區別
- mmap 適合小數據量讀寫,sendFile 適合大文件傳輸
- mmap 需要 3 次上下文切換,3 次數據拷貝;sendFile 需要 2 次上下文切換,最少 2 次數據拷貝
- sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket 緩沖區)
NIO 零拷貝案例
案例要求:
- 使用傳統的 IO 方式傳遞一個大文件
- 使用 NIO 零拷貝方式傳遞(transferTo)一個大文件
- 看看兩種傳遞方式耗時時間分別是多少
035.零拷貝AIO內容梳理
Java AIO 基本介紹
- JDK 7 引入了 Asynchronous I/O,即 AIO。在進行 I/O 編程中,常用到的兩種模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,當有事件觸發時,服務器端得到通知,進行相應的處理
- AIO 即 NIO 2.0,叫做異步不阻塞的 IO。AIO 引入異步通道的概念,采用了 Proactor 模式,簡化了程序編寫,有效的請求才啟動線程,它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理,一般適用於連接數較多且連接時間較長的應用
- 目前 AIO 還沒有廣泛應用,Netty 也是基於 NIO,而不是 AIO,因此我們就不詳解 AIO 了,有興趣的同學可以參考《Java 新一代網絡編程模型 AIO 原理及 Linux 系統 AIO 介紹》http://www.52im.net/thread-306-1-1.html
BIO、NIO、AIO 對比表
| BIO | NIO | AIO | |
|---|---|---|---|
| IO 模型 | 同步阻塞 | 同步非阻塞(多路復用) | 異步非阻塞 |
| 編程難度 | 簡單 | 復雜 | 復雜 |
| 可靠性 | 差 | 好 | 好 |
| 吞吐量 | 低 | 高 | 高 |
舉例說明
- 同步阻塞:到理發店理發,就一直等理發師,直到輪到自己理發
- 同步非阻塞:到理發店理發,發現前面有其它人理發,給理發師說下,先干其它事情,一會過來看是否輪到自己
- 異步非阻塞:給理發師打電話,讓理發師上門服務,自己干其它事情,理發師自己來家里給你理發
036.Netty概述
原生 NIO 存在的問題
- NIO 的類庫和 API 繁雜,使用麻煩:需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等
- 需要具備其它的額外技能:要熟悉 Java 多線程編程,因為 NIO 編程涉及到 Reactor 模式,你必須對多線程和網絡編程非常熟悉,才能編寫高質量的 NIO 程序
- 開發工作量和難度都非常大:例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡阻塞和異常流的處理等等
- JDK NIO 的 Bug:例如臭名昭著的Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。直到 JDK 1.7 版本該問題依舊存在,沒有被根本解決。
Netty 官網說明
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients

- Netty 是由 JBOSS 提供的一個 Java 開源框架。Netty 提供異步的、基於事件驅動的網絡應用程序框架,用以快速開發高性能、高可靠性的網絡 IO 程序
- Netty 可以幫助你快速、簡單的開發出一個網絡應用,相當於簡化和流程化了 NIO 的開發過程
- Netty 是目前最流行的 NIO 框架,Netty 在互聯網領域、大數據分布式領域、游戲行業、通信行業等獲得了廣泛的應用,知名的 Elasticsearch、Dubbo 框架內部都采用了 Netty
Netty 的優點
Netty 對 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題
- 設計優雅:適用於各種傳輸類型的統一 API 阻塞和非阻塞 Socket;基於靈活且可擴展的事件模型,可以清晰地分離關注點;高度可定制的線程模型 - 單線程,一個或多個線程池
- 使用方便:詳細記錄的 JavaDoc,用戶指南和示例;沒有其他依賴項,JDK 5(Netty 3.x)或 6(Netty 4.x)就足夠了
- 高性能、吞吐量更高:延遲更低;減少資源消耗;最小化不必要的內存復制
- 安全:完整的 SSL/TLS 和 StartTLS 支持
- 社區活躍、不斷更新:社區活躍,版本迭代周期短,發現的 Bug 可以被及時修復,同時,更多的新功能會被加入
Netty 版本說明
- Netty 版本分為 netty 3.x 、netty 4.x 、netty 5.x
- 因為 Netty 5 出現重大 bug,已經被官網廢棄了,目前推薦使用的是 Netty 4.x 的穩定版本
- 目前在官網可下載的版本 netty 3.x、netty 4.0.x 和 netty 4.1.x
- 在本套課程中,我們講解 Netty 4.1.x 版本
- Netty 下載地址:https://netty.io/downloads.html
037.線程模型概述
線程模型基本介紹
- 不同的線程模式,對程序的性能有很大影響,為了搞清 Netty 線程模式,我們來系統地講解下各個線程模式,最后看看 Netty 線程模式有什么優越性
- 目前存在的線程模型有:傳統阻塞 I/O 服務模型,Reactor 模式
- 根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現
- 單 Reactor 單線程
- 單 Reactor 多線程
- 主從 Reactor 多線程
- Netty 線程模式(Netty 主要基於主從 Reactor 多線程模型做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor)
傳統阻塞 I/O 服務模型
工作原理圖

黃色框表示對象,藍色的框表示線程,白色的框表示方法(API)
模型特點
- 采用阻塞 IO 模式獲取輸入的數據
- 每個連接都需要獨立的線程完成數據的輸入,業務處理,數據返回
問題分析
- 當並發數很大,就會創建大量的線程,占用很大的系統資源
- 連接創建后,如果當前線程暫時沒有數據可讀,該線程會阻塞在 read 操作上,造成線程資源浪費
Reactor 模式
針對傳統阻塞 I/O 服務模型的 2 個缺點,解決方案:
- 基於 I/O 復用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理
Reactor 對應的叫法:1. 反應器模式,2. 分發者模式(Dispatcher),3. 通知者模式(notifier) - 基於線程池復用線程資源:不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務

I/O 復用結合線程池,就是 Reactor 模式,基本設計思想,如圖:

說明:
- Reactor 模式,通過一個或多個輸入同時傳遞給服務處理器的模式(基於事件驅動)
- 服務器端程序處理傳入的多個請求,並將它們同步分派到相應的處理線程,因此 Reactor 模式也叫 Dispatcher 模式
- Reactor 模式使用 IO 復用監聽事件,收到事件后,分發給某個線程(進程),這點就是網絡服務器高並發處理的關鍵
核心組成
- Reactor:Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對 IO 事件做出反應。它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯系人
- Handlers:處理程序執行 I/O 事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor 通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作
模式分類
根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現
- 單 Reactor 單線程
- 單 Reactor 多線程
- 主從 Reactor 多線程
單 Reactor 單線程
工作原理示意圖

方案說明
- Select 是前面 I/O 復用模型介紹的標准網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求
- Reactor 對象通過 Select 監聽客戶端請求事件,收到事件后通過 Dispatch 進行分發
- 如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后的后續業務處理
- 如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應
- Handler 會完成 Read -> 業務處理 -> Send 的完整業務流程
綜合實例:服務器端用一個線程通過多路復用搞定所有的 IO 操作(包括連接,讀、寫等),編碼簡單,清晰明了,但是如果客戶端連接數量較多,將無法支撐,前面的 NIO 案例就屬於這種模型
方案優缺點分析
- 優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成
- 缺點:性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸
- 缺點:可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障
- 適用場景:客戶端的數量有限,業務處理非常快速,比如 Redis 在業務處理的時間復雜度 O(1) 的情況
單 Reactor 多線程
工作原理示意圖

方案說明
- Reactor 對象通過 select 監控客戶端請求事件,收到事件后,通過 dispatch 進行分發
- 如果建立連接請求,則由 Acceptor 通過 accept 處理連接請求,然后創建一個 Handler 對象處理完成連接后的各種事件
- 如果不是連接請求,則由 reactor 分發調用連接對應的 handler 來處理
- handler 只負責響應事件,不做具體的業務處理,通過 read 讀取數據后,會分發給后面的 worker 線程池的某個線程處理業務
- worker 線程池會分配獨立線程完成真正的業務,並將結果返回給 handler
- handler 收到響應后,通過 send 將結果返回給 client
優點
可以充分的利用多核 CPU 的處理能力
缺點
多線程數據共享和訪問比較復雜,reactor 處理所有的事件的監聽和響應,在單線程運行,在高並發場景容易出現性能瓶頸
主從 Reactor 多線程
工作原理示意圖
針對單 Reactor 多線程模型中,Reactor 在單線程中運行,高並發場景下容易成為性能瓶頸,可以讓 Reactor 在多線程中運行

方案說明
- Reactor 主線程,MainReactor 對象通過 select 監聽連接事件,收到事件后,通過 Acceptor 處理連接事件
- 當 Acceptor 處理連接事件后,MainReactor 將連接分配給 SubReactor
- SubReactor 將連接加入到連接隊列進行監聽,並創建 Handler 進行各種事件處理
- 當有新事件發生時,SubReactor 就會調用對應的 Handler 處理
- Handler 通過 Read 讀取數據,分發給后面的 Worker 線程處理
- Worker 線程池分配獨立的 Worker 線程進行業務處理,並返回結果
- Handler 收到響應的結果后,再通過 send 方法,將結果返回給 client
- Reactor 主線程可以對應多個 Reactor 子線程,即 MainReactor 可以關聯多個 SubReactor
Scalable IO in Java 對 Multiple Reactors 的原理圖解

優缺點說明
- 優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續的業務處理
- 優點:父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據
- 缺點:編程復雜度較高
結合實例:這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持
Reactor 模式小結
3 種模式用生活案例來理解
- 單 Reactor 單線程,前台接待員和服務員是同一個人,全程為顧客服務
- 單 Reactor 多線程,1 個前台接待員,多個服務員,接待員只負責接待
- 主從 Reactor 多線程,多個前台接待員,多個服務員
我覺得這個解釋不對,應該要這樣理解
- 單 Reactor 單線程,前台接待員、服務員、廚師是同一個人,全程為顧客服務。這個人會忙死,同時客戶也要等死。
- 單 Reactor 多線程,前台接待員、服務員是同一個人,廚師是多個人,接待員、服務員只負責接待和下單,由廚師執行具體的做菜操作,廚師做好的菜再交給服務員。這個接待員、服務員要忙死,但是客戶的體驗比第一種要稍微好一點
- 主從 Reactor 多線程,前台接待員是一個人,服務員是多個人,廚師也是多個人。接待員只負責接待,由服務員負責下單給廚師,廚師負責做菜,做好的菜返回給服務員,服務員進行上菜。這樣接待員、服務員、廚師的工作都比較輕松,客戶的體驗也更好
優點
- 響應快,不必為單個同步時間所阻塞,雖然 Reactor 本身依然是同步的
- 可以最大程度的避免復雜的多線程及同步問題,並且避免了多線程/進程的切換開銷
- 擴展性好,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源
- 復用性好,Reactor 模型本身與具體事件處理邏輯無關,具有很高的復用性
042.Netty模型
簡單版
Netty 主要基於主從 Reactor 多線程模型(如圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor

- BossGroup 線程維護 Selector,只關注 accept 事件
- 當接收到 accept 事件后,獲取到對應的 SocketChannel,封裝成 NIOSocketChannel,並注冊到 Worker 線程(事件循環),並進行維護
- 當 Worker 線程監聽到 selector 中的通道發生自己感興趣的事件后,就進行處理(就由 Handler),注意 Handler 已經加入到通道
進階版
Netty 抓喲基於主從 Reactors 多線程模型(如圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor

詳細版

- Netty 抽象出兩組線程池 BossGroup 專門負責接收客戶端的連接,WorkerGroup 專門負責網絡讀寫
- BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup
- NioEventLoopGroup 相當於一個事件循環組,這個組中含有多個事件循環,每一個事件循環是 NioEventLoop
- NioEventLoop 表示一個不斷循環的執行處理任務的線程,每個 NioEventLoop 都有一個 selector,用於監聽綁定在其上的 socket 的網絡通訊
- NioEventLoopGroup 可以有多個線程,即可以含有多個 NioEventLoop
- 每個 Boss NioEventLoop 循環執行的步驟有 3 步
- 輪詢 accept 事件
- 處理 accept 事件,與 client 建立連接,生成 NioSocketChannel,並將其注冊到某個 Worker NioEventLoop 上的 selector
- 處理任務隊列的任務,即 runAllTasks
- 每個 Worker NioEventLoop 循環執行的步驟有 3 步
- 輪詢 read、write 事件
- 處理 I/O 事件,即 read ,write 事件,在對應的 NioSocketChannel 讀寫
- 處理任務隊列的任務,即 runAllTasks
- 每個 Worker NioEventLoop 處理業務時,會使用 pipeline(管道),pipeline 中包含了 channel,即通過 pipeline 可以獲取到對應的通道,管道中維護了很多的處理器
044.Netty入門
Netty 快速入門實例 - TCP 服務
- 實例要求:使用 IDEA 創建 Netty 項目
- Netty 服務器在 6668 端口監聽,客戶端能發送消息給服務器 "hello, 服務器~"
- 服務器可以回復消息給客戶端 "hello, 客戶端~"
- 目的:對 Netty 線程模型 有一個初步認識,便於理解 Netty 模型理論
- 看老師代碼演示
- 編寫服務端
- 編寫客戶端
- 對 netty 程序進行分析,看看 netty 模型的特點
049.taskQueue自定義任務
任務隊列中的 Task 有 3 種典型使用場景
- 用戶程序自定義的普通任務
- 用戶自定義定時任務
- 非當前 Reactor 線程調用 Channel 的各種方法
例如在推送系統的業務線程里面,根據用戶的標識,找到對應的 Channel 引用,然后調用 Write 類方法向該用戶推送消息,就會進入到這種場景。最終的 Write 會提交到任務隊列中被異步消費
異步模型原理剖析
基本介紹
- 異步的概念和同步相對。當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的組件在完成后,通過狀態、通知和回調來通知調用者
- Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture
- 調用者並不能立刻獲得結果,而是通過 Future-Listener 機制,用戶可以方便的主動獲取或者通過通知機制獲得 IO 操作結果
- Netty 的異步模型是建立在 future 和 callback 之上的。callback 就是回調。重點說 Future,它的核心思想是:假設一個方法 fun,計算過程可能非常耗時,等待 fun 返回顯然不合適。那么可以在調用 fun 的時候,立馬返回一個 Future,后續可以通過 Future 去監控方法 fun 的處理過程(即:Future-Listener 機制)
Future 說明
- 表示異步的執行結果,可以通過它提供的方法來檢查執行是否完成,比如檢索計算等
- ChannelFuture 是一個接口:
public interface ChannelFuture extends Future<Void>,我們可以添加監聽器,當監聽的事件發生時,就會通知到監聽器
工作原理示意圖

說明:
- 在使用 Netty 進行編程時,攔截操作和轉換出入站數據只需要您提供 callback 或利用 future 即可。這使得鏈式操作簡單、高效,並有利於編寫可重用的、通用的代碼
- Netty 框架的目標就是讓你的業務邏輯從網絡基礎應用編碼中分離出來、解脫出來

Future-Listener 機制
-
當 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 操作期間可以執行別的程序,在高並發情形下會更穩定和更高的吞吐量
053.Http服務程序實例
HTTP服務
- 實例要求:使用 IDEA 創建 Netty 項目
- Netty 服務器在 6668 端口監聽,瀏覽器發出請求
http://localhost:6668/ - 服務器可以回復消息給客戶端
Hello!我是服務器5,並對特定請求資源進行過濾 - 目的:Netty 可以做 Http 服務開發,並且理解 Handler 實例和客戶端及其請求的關系

056.Netty核心組件
Bootstrap、ServerBootstrap
-
Bootstrap 意思是引導,一個 Netty 應用通常由一個 Bootstrap 開始,主要作用是配置整個 Netty 程序,串聯各個組件,Netty 中 Bootstrap 類是客戶端程序的啟動引導類,ServerBootstrap 是服務端啟動引導類
-
常見的方法有

Future、ChannelFuture
- Netty 中所有的 IO 操作都是異步的,不能立刻得知消息是否被正確處理。但是可以過一會等它執行完成或者直接注冊一個監聽,具體的實現就是通過 Future 和 ChannelFuture,他們可以注冊一個監聽,當操作執行成功或失敗時監聽會自動觸發注冊的監聽事件
- 常見的方法有
- Channel channel(),返回當前正在進行 IO 操作的通道
- ChannelFuture sync(),等待異步操作執行完畢
Channel
- Netty 網絡通信的組件,能夠用於執行網絡 I/O 操作
- 通過 Channel 可獲得當前網絡連接的通道的狀態
- 通過 Channel 可獲得網絡連接的配置參數(例如接收緩沖區大小)
- Channel 提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味着任何 I/O 調用都將立即返回,並且不保證在調用結束時所請求的 I/O 操作已完成
- 調用立即返回一個 ChannelFuture 實例,通過注冊監聽器到 ChannelFuture 上,可以直到 I/O 操作成功、失敗或取消時通知調用方
- 支持關聯 I/O 操作與對應的處理程序
- 不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,常用的Channel 類型

Selector
- Netty 基於 Selector 對象實現 I/O 多路復用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件
- 當向一個 Selector 中注冊 Channel 后,Selector 內部的機制就可以自動不斷地查詢(Select)這些注冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel
ChannelHandler 及其實現類
- ChannelHandler 是一個接口,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序
- ChannelHandler 本身並沒有提供很多方法,因為這個接口有許多的方法需要實現,為了方便使用,可以繼承它的子類
- ChannelHandler 及其實現類一覽圖
- ChannelInboundHandler 用於處理入站 I/O 事件
- ChannelOutboundHandler 用於處理出站 I/O 操作
- ChannelInboundHandlerAdaptor 用於處理入站 I/O 事件
- ChannelOutboundHandlerAdaptor 用於處理出站 I/O 操作
- ChannelDuplexHandler 用於處理入站和出站事件
- 我們經常需要自定義一個 Handler 類去繼承 ChannelInboundHandlerAdapter,然后通過重寫相應方法實現業務邏輯,我們接下來看看一般都需要重寫哪些方法


Pipeline 和 ChannelPipeline
ChannelPipeline 是一個重點
-
ChannelPipeline 是一個 Handler 的集合,它負責處理和攔截 inbound 或者 outbound 的事件和操作,相當於一個貫穿 Netty 的鏈。(也可以這樣理解:ChannelPipeline 是保存 ChannelHandler 的 List,用於處理或攔截 Channel 的入站事件和出站事件)
-
ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互
-
在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關系如下

- 一個 Channel 包含了一個ChannelPipeline,而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表,並且每個 ChannelHandlerContext 中又關聯着一個 ChannelHandler
- 入站事件和出站事件在一個雙向鏈表中,入站事件會從鏈表 head 往后傳遞到最后一個入站的 handler,出站事件會從鏈表 tail 往前傳遞到最前一個出站的handler,兩種類型的 handler 互不干擾
-
常用方法

ChannelHandlerContext
- 保存 Channel 相關的所有上下文信息,同時關聯一個 ChannelHandler 對象
- 即 ChannelHandlerContext 中包含一個具體的事件處理器 ChannelHandler,同時 ChannelHandlerContext 中也綁定了對應的 pipeline 和 Channel 的信息,方便對 ChannelHandler 進行調用
- 常用方法

ChannelOption
- Netty 在創建 Channel 實例后,一般都需要設置 ChannelOption 參數
- ChannelOption 參數如下

EventLoopGroup 和其實現類 NioEventLoopGroup
- EventLoopGroup 是一組 EventLoop 的抽象,Netty 為了更好的利用多核 CPU 資源,一般會有多個 EventLoop 同時工作,每個 EventLoop 維護着一個 Selector 實例
- EventLoopGroup 提供 next 接口,可以從組里面按照一定規則獲取其中一個 EventLoop 來處理任務。在 Netty 服務器端編程中,我們一般都需要提供兩個 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup
- 通常一個服務端口,即一個 ServerSocketChannel 對應一個 Selector 和一個 EventLoop 線程。BossEventLoop 負責接收客戶端的連接並將 SocketChannel 交給 WorkerEventLoopGroup 來進行 IO 處理,如下圖所示

- 常用方法

Unpooled 類
-
Netty 提供一個專門用來操作緩沖區(即 Netty 的數據容器)的工具類
-
常用方法如下所示
// 通過給定的數據和字符編碼返回一個 ByteBuf 對象(類似於 NIO 中的 ByteBuffer 但有區別) public static ByteBuf copiedBuffer(CharSequence string, Charset charset) -
舉例說明 Unpooled 獲取 Netty 的數據容器 ByteBuf 的基本使用

063.Netty群聊系統
實例要求:
- 編寫一個 Netty 群聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞)
- 實現多人群聊
- 服務器端:可以監測用戶上線,離線,並實現消息轉發功能
- 客戶端:通過 channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(由服務器轉發得到)
- 目的:進一步理解 Netty 非阻塞網絡編程機制
- 代碼演示

單聊思路

066.Netty心跳機制實例
實例要求
- 編寫一個 Netty 心跳檢測機制案例,當服務器超過 3 秒沒有讀時,就提示讀空閑
- 當服務器超過 5 秒沒有寫操作時,就提示寫空閑
- 實現當服務器超過 7 秒沒有讀或者寫操作時,就提示讀寫空閑
068.WebSocket長連接開發
實例要求
- Http 協議是無狀態的,瀏覽器和服務器間的請求響應一次,下一次會重新創建連接
- 要求:實現基於 websocket 的長連接的全雙工的交互
- 改變 http 協議多次請求的約束,實現長連接,服務器可以發送消息給瀏覽器
- 客戶端瀏覽器和服務器端會相互感知,比如服務器關閉了,瀏覽器會感知,同樣瀏覽器關閉了,服務器會感知

073.netty編解碼器機制
編碼和解碼的基本介紹
- 編寫網絡應用程序時,因為數據在網絡中傳輸的都是二進制字節碼數據,在發送數據時就需要編碼,接收數據時就需要解碼
- codec(編碼器)的組成部分有兩個:decoder(解碼器)和encoder(編碼器)。encoder 負責把業務數據轉換成字節碼數據,decoder 負責把字節碼數據轉換成業務數據

Netty 本身的編碼解碼的機制和問題分析
- Netty 自身提供了一些 codec(編解碼器)
- Netty 提供的編碼器
- StringEncoder,對字符串數據進行編碼
- ObjectEncoder,對 Java 對象進行編碼
- Netty 提供的解碼器
- StringDecoder,對字符串數據進行解碼
- ObjectDecoder,對 Java 對象進行解碼
- Netty 自身自帶的 ObjectDecoder 和 ObjectEncoder 可以用來實現 POJO 對象或各種業務對象的編碼和解碼,底層使用的仍是 Java 序列化技術,而 Java 序列化技術本身效率就不高,存在如下問題
- 無法跨語言
- 序列化后的體積太大,是二進制編碼的 5 倍多
- 序列化性能太低
- 引出新的解決方案 [ Google 的 Protobuf ]
074.ProtoBuf
Protobuf 基本介紹和使用示意圖
- Protobuf 是 Google 發布的開源項目,全稱 Google Protocol Buffers,是一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲或RPC[遠程過程調用 remote procedure call]數據交換格式
目前很多公司 http + json => tcp + protobuf - 參考文檔:https://developers.google.com/protocol-buffers/docs/proto,語言指南
- Protobuf 是以 message 的方式來管理數據的
- 支持跨平台、跨語言,即[客戶端和服務器端可以是不同的語言編寫的](支持目前絕大多數的語言,例如 C++、C#、Java、python 等)
- 高性能,高可靠性
- 使用 protobuf 編譯器能自動生成代碼,Protobuf 是將類的定義使用 .proto 文件進行描述。說明,在 idea 中編寫 .proto 文件時,會自動提示是否下載 .proto 編寫插件,可以讓語法高亮
- 然后通過 protoc.exe 編譯器根據 .proto 自動生成 .java 文件
- protobuf 使用示意圖

Protobuf 快速入門實例
編寫程序,使用 Protobuf 完成如下功能
- 客戶端可以發送一個 Student PoJo 對象到服務器(通過 Protobuf 編碼)
- 服務端能接收 Student PoJo 對象,並顯示信息(通過 Protobuf 解碼)
編寫流程如下:
-
在 maven 項目中引入 protobuf 坐標,下載相關的 jar 包
<dependencies> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>3.6.1</version> </dependency> </dependencies> -
第2步:編寫 proto 文件 Student.proto
syntax = "proto3"; // 協議的版本 option java_outer_classname = "StudentPOJO"; // 生成的外部類名,同時也是文件名 // protobuf 使用 message 管理數據 message Student { // 會在 StudentPOJO 外部類生成內部類 Student,它是真正發送的 POJO 對象 int32 id = 1; // Student 類中有一個屬性,名字為 id,類型為 int32(protobuf類型) 1表示屬性序號,不是值 string name = 2; }或者編寫 proto 文件 Student.proto
syntax = "proto3"; // 協議的版本 option optimize_for = SPEED; // 加快解析 option java_package = "com.atguigu.netty.codec2"; // 指定生成到哪個包下 option java_outer_classname = "MyDataInfo"; // 外部類名 // protobuf 可以使用 message 管理其它的 message message MyMessage { // 定義一個枚舉類型 enum DataType { StudentType = 0; // 在 proto3 要求 enum 的編號從 0 開始 WorkerType = 1; } // 用 data_type 來標識傳的是哪一個枚舉類型 DataType data_type = 1; // 表示每次枚舉類型最多只能出現其中的一個,節省空間 oneof dataBody { Student student = 2; Worker worker = 3; } } // protobuf 使用 message 管理數據 message Student { // 會在 StudentPOJO 外部類生成內部類 Student,它是真正發送的 POJO 對象 int32 id = 1; // Student 類中有一個屬性,名字為 id,類型為 int32(protobuf類型) 1表示屬性序號,不是值 string name = 2; // } message Worker { string name = 1; int32 age = 2; } -
從 https://github.com/protocolbuffers/protobuf/releases 下載 protobuf 程序,使用
protoc --java_out=. Student.proto
Protobuf 快速入門實例2
編寫程序,使用 Protobuf 完成如下功能
- 客戶端可以隨機發送 Student Pojo / Worker Projo 對象到服務器(通過 Protobuf 編碼)
- 服務端能接收 Student Pojo / Worker Pojo 對象(需要判斷是哪種類型),並顯示信息(通過 Protobuf 解碼)
079. Netty入站和出站機制
基本說明
- netty 的組件設計:Netty 的主要組件有 Channel、EventLoop、ChannelFuture、ChannelHandler、ChannelPipe 等
- ChannelHandler 充當了處理入站和出站數據的應用程序邏輯的容器。例如,實現 ChannelInboundHandler 接口(或 ChannelInboundHandlerAdaptor),你就可以接收入站事件和數據,這些數據會被業務邏輯處理。當要給客戶端發送響應時,也可以從 ChannelInboundHandler 沖刷數據。業務邏輯通常寫在一個或者多個 ChannelInboundHandler 中。ChannelOutboundHandler 原理一樣,只不過它是用來處理出站數據的
- ChannelPipeline 提供了 ChannelHandler 鏈的容器。以客戶端應用程序為例,如果事件的運動方向是從客戶端到服務端的,那么我們稱這些事件為出站的,即客戶端發送給服務端的數據會通過 pipeline 中的一系列 ChannelOutboundHandler,並被這些 Handler 處理,反之則稱為入站的

編碼解碼器
- 當 Netty 發送或者接收一個消息的時候,就將會發生一次數據轉換。入站消息會被解碼:從字節轉換為另一種格式(比如 Java 對象);如果是出站消息,它會被編碼成字節
- Netty 提供一系列實用的編解碼器,它們都實現了 ChannelInboundHandler 或者 ChannelOutboundHandler 接口。在這些類中,channelRead 方法已經被重寫了。以入站為例,對於每個從入站 Channel 讀取的消息,這個方法會被調用。隨后,它將調用由編碼器所提供的 decode() 方法進行編碼,並將已經解碼的字節轉發給 ChannelPipeline 中的下一個 ChannelInboundHandler
解碼器-ByteToMessageDecoder
- 關系繼承圖
- 由於不可能知道遠程節點是否會一次性發送一個完整的信息,tcp 有可能出現粘包拆包的問題,這個類會對入站數據進行緩沖,知道它准備好被處理

- 一個關於 ByteToMessageDecoder 實例分析


080.Handler鏈調用機制實例
實例要求
- 使用自定義的編碼器和解碼器來說明 Netty 的 handler 調用機制
客戶端發送 long -> 服務器
服務端發送 long -> 客戶端


結論
- 不論編碼器 handler 還是解碼器 handler 即接收的消息類型必須與待處理的消息類型一致,否則該 handler 不會被執行
- 在解碼器進行數據解碼時,需要判斷緩存取(ByteBuf)的數據是否足夠,否則接收到的結果會和期望結果可能不一樣
Netty其它常用編解碼器
解碼器 - ReplayingDecoder
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder- ReplayingDecoder 擴展了 ByteToMessageDecoder 類,使用這個類,我們不必調用 readableBytes() 方法。參數 S 指定了用戶狀態管理的類型,其中 Void 代表不需要狀態管理
- 應用實例:使用 ReplayingDecoder 編寫解碼器,對前面的案例進行簡化
- ReplayingDecoder 使用方便,但它也有一些局限性
- 並不是所有的 ByteBuf 操作都被支持,如果調用了一個不被支持的方法,將會拋出一個 UnsupportedOperationException
- ReplayingDecoder 在某些情況下可能稍慢於 ByteToMessageDecoder,例如 網絡緩慢並且消息格式復雜時,消息會被拆成多個碎片,速度變慢
其它解碼器
- LineBasedFrameDecoder:這個類在 Netty 內部也有使用,它使用行尾控制字符(\n 或者 \r\n)作為分隔符來解析數據
- DelimiterBasedFrameDecoder:使用自定義的特殊字符作為消息的分隔符
- HttpObjectDecoder:一個 HTTP 數據的解碼器
- LengthFieldBasedFrameDecoder:通過指定長度來標識整包消息,這樣就可以自動的處理黏包和半包消息
其它編碼器

085.Log4j整合到Netty
- 在 Maven 中添加對 Log4j 的依賴,在 pom.xml

- 配置 Log4j,在 resources/log4j.properties
087.Tcp粘包拆包原理
TCP粘包和拆包基本介紹
- TCP是面向連接的,面向流的,提供高可靠服務。收發兩端(客戶端和服務器端)都要有一一成對的 socket,因此,發送端為了將多個發給接收端的包,更有效的發給對方,使用了優化方法(Nagle 算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣做雖然提高了效率,但是接收端就難於分辨出完整的數據包了,因為面向流的通信是無消息保護邊界的
- 由於TCP無消息保護邊界,需要在接收端處理消息邊界問題,也就是我們所說的粘包、拆包問題
- TCP粘包、拆包圖解

假設客戶端分別發送了兩個數據包D1和D2給服務端,由於服務端一次讀取到字節數是不確定的,故可能存在以下四種情況:- 服務端分兩次讀取到了兩個獨立的數據包,分別是D1和D2,沒有粘包和拆包
- 服務端一次接受到了兩個數據包,D1和D2粘合在一起,稱之為TCP粘包
- 服務端分兩次讀取到了數據包,第一次讀取到了完整的D1包和D2包的部分內容,第二次讀取到了D2包的剩余內容,這稱之為TCP拆包
- 服務端分兩次讀取到了數據包,第一次讀取到了D1包的部分內容D1_1,第二次讀取到了D1包的剩余部分內容D1_2和完整的D2包
TCP粘包和拆包現象實例
在編寫 Netty 程序時,如果沒有做處理,就會發生粘包和拆包的問題
TCP粘包和拆包解決方案
- 使用自定義協議 + 編解碼器 來解決
- 關鍵就是要解決 服務器端每次讀取數據長度的問題,這個問題解決,就不會出現服務器多讀或少讀數據的問題,從而避免TCP的粘包、拆包
看一個具體的實例:
- 要求客戶端發送5個Message對象,客戶端每次發送一個Message對象
- 服務器端每次接收一個Message,分5次進行解碼,每讀取到一個Message,會回復一個Message對象給客戶端

