參考教程
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對象給客戶端