Netty學習之I/O 模型和Java NIO 編程
一、簡介
1)Java 共支持 3 種網絡編程模型/IO 模式: BIO、 NIO、 AIO
2)Java BIO : 同步并阻塞(傳統阻塞型), 服務器實現模式為一個連接一個線程, 即客戶端有連接請求時服務器端就需要啟動一個線程進行處理, 如果這個連接不做任何事情會造成不必要的線程開銷
3)Java NIO : 同步非阻塞, 服務器實現模式為一個線程處理多個請求(連接), 即客戶端發送的連接請求都會注冊到多路復用器上, 多路復用器輪詢到連接有 I/O 請求就進行處理。
4)Java AIO(NIO.2) : 異步非阻塞, AIO 引入異步通道的概念, 采用了 Proactor 模式, 簡化了程序編寫, 有效的請求才啟動線程, 它的特點是先由操作系統完成后才通知服務端程序啟動線程去處理, 一般適用于連接數較多且連接時間較長的應用。
二、適用場景
1)BIO 方式適用于連接數目比較小且固定的架構, 這種方式對服務器資源要求比較高, 并發局限于應用中, JDK1.4以前的唯一選擇, 但程序簡單易理解。
2)NIO 方式適用于連接數目多且連接比較短(輕操作) 的架構, 比如聊天服務器, 彈幕系統, 服務器間通訊等。編程比較復雜, JDK1.4 開始支持。
- AIO 方式使用于連接數目多且連接比較長(重操作) 的架構, 比如相冊服務器, 充分調用 OS 參與并發操作,編程比較復雜, JDK7 開始支持。
三、Java NIO 編程
3.1 Java NIO 基本介紹
- Java NIO 全稱 java non-blocking IO, 是指 JDK 提供的新 API。 從 JDK1.4 開始, Java 提供了一系列改進的輸入/輸出的新特性, 被統稱為 NIO(即 New IO), 是同步非阻塞的。
- NIO 相關類都被放在 java.nio 包及子包下, 并且對原 java.io 包中的很多類進行改寫。
- NIO 有三大核心部分: Channel(通道), Buffer(緩沖區), Selector(選擇器)
- NIO 是 面向緩沖區 , 或者面向 塊 編程的。 數據讀取到一個它稍后處理的緩沖區, 需要時可在緩沖區中前后移動, 這就增加了處理過程中的靈活性, 使用它可以提供非阻塞式的高伸縮性網絡
- Java NIO 的非阻塞模式, 使一個線程從某通道發送請求或者讀取數據, 但是它僅能得到目前可用的數據, 如果目前沒有數據可用時, 就什么都不會獲取, 而不是保持線程阻塞, 所以直至數據變的可以讀取之前, 該線程可以繼續做其他的事情。 非阻塞寫也是如此, 一個線程請求寫入一些數據到某通道, 但不需要等待它完全寫入,這個線程同時可以去做別的事情。
3.2 NIO 和 BIO 的比較
- BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據,塊 I/O 的效率比流 I/O 高很多
- BIO 是阻塞的, NIO 則是非阻塞的
- BIO 基于字節流和字符流進行操作, 而 NIO 基于 Channel(通道)和 Buffer(緩沖區)進行操作, 數據總是從通道讀取到緩沖區中, 或者從緩沖區寫入到通道中。Selector(選擇器)用于監聽多個通道的事件(比如: 連接請求,數據到達等) , 因此使用單個線程就可以監聽多個客戶端通道
3.3 NIO三大核心原理圖
3.3.1 Selector 、 Channel 和 Buffer 的關系圖
每個 channel 都會對應一個 Buffer
- Selector 對應一個線程, 一個線程對應多個 channel(連接)
- 該圖反應了有三個 channel 注冊到 該 selector //程序
- 程序切換到哪個 channel 是由事件決定的, Event 就是一個重要的概念
- Selector 會根據不同的事件, 在各個通道上切換
- Buffer 就是一個內存塊 , 底層是有一個數組
- 數據的讀取寫入是通過 Buffer, 這個和 BIO , BIO 中要么是輸入流, 或者是
輸出流, 不能雙向, 但是 NIO 的 Buffer 是可以讀也可以寫, 需要 flip 方法切換
channel 是雙向的, 可以返回底層操作系統的情況, 比如 Linux , 底層的操作系統
通道就是雙向的
3.4 緩沖區(Buffer)
3.4.1基本介紹
緩沖區(Buffer) : 緩沖區本質上是一個可以讀寫數據的內存塊, 可以理解成是一個容器對象(含數組), 該對象提供了一組方法, 可以更輕松地使用內存塊,緩沖區對象內置了一些機制, 能夠跟蹤和記錄緩沖區的狀態變化情況。 Channel 提供從文件、 網絡讀取數據的渠道, 但是讀取或寫入的數據都必須經由 Buffer, 如圖:
3.4.2 Buffer 類及其子類
- Buffer 類定義了所有的緩沖區都具有的四個屬性來提供關于其所包含的數據元素的信息:
2)Buffer 類相關方法一覽
3.4.3 ByteBuffer
3.5 通道(Channel)
基本介紹
- NIO 的通道類似于流, 但有些區別如下:
? 通道可以同時進行讀寫, 而流只能讀或者只能寫
? 通道可以實現異步讀寫數據
? 通道可以從緩沖讀數據, 也可以寫數據到緩沖:
- 常 用 的 Channel 類 有 : FileChannel 、 DatagramChannel 、ServerSocketChannel 和 SocketChannel 。
- FileChannel 用于文件的數據讀寫, DatagramChannel 用于 UDP 的數據讀寫, ServerSocketChannel 和SocketChannel 用于 TCP 的數據讀寫。
3.6 Selector(選擇器)
3.6.1 基本介紹
- Java 的 NIO, 用非阻塞的 IO 方式。 可以用一個線程, 處理多個的客戶端連接, 就會使用到 Selector(選擇器)
- Selector 能夠檢測多個注冊的通道上是否有事件發生(注意:多個 Channel 以事件的方式可以注冊到同一個Selector), 如果有事件發生, 便獲取事件然后針對每個事件進行相應的處理。
- 只有在 連接/通道 真正有讀寫事件發生時, 才會進行讀寫, 就大大地減少了系統開銷, 并且不必為每個連接都創建一個線程, 不用去維護多個線程
- 避免了多線程之間的上下文切換導致的開銷
3.6.2 Selector 示意圖
- Netty 的 IO 線程 NioEventLoop 聚合了 Selector(選擇器, 也叫多路復用器), 可以同時并發處理成百上千個客戶端連接。
- 當線程從某客戶端 Socket 通道進行讀寫數據時, 若沒有數據可用時, 該線程可以進行其他任務。
- 線程通常將非阻塞 IO 的空閑時間用于在其他通道上執行 IO 操作, 所以單獨的線程可以管理多個輸入和輸出通道。
- 由于讀寫操作都是非阻塞的, 這就可以充分提升 IO 線程的運行效率, 避免由于頻繁 I/O 阻塞導致的線程掛起。
- 一個 I/O 線程可以并發處理 N 個客戶端連接和讀寫操作, 這從根本上解決了傳統同步阻塞 I/O 一連接一線程模型, 架構的性能、 彈性伸縮能力和可靠性都得到了極大的提升。
3.6.3 Selector方法
public static Selector open():得到一個選擇器對象
public int select(long timeout):監控所有注冊的通道,當其中IO操作可以進行時,將對應的SelectionKey加入到內部集合中返回,參數為超時時間
public Set<SlectionKey> selectedKeys():從內部集合中得到所有的SlectionKey
selector.select() //阻塞
selector.select(1000) //阻塞1000s
selector.wakeup(); //喚醒 selector
selector.selectNow(); //不阻塞, 立馬返還
3.7 NIO 非阻塞 網絡編程原理分析圖
NIO 非阻塞 網絡編程相關的(Selector、 SelectionKey、 ServerScoketChannel 和 SocketChannel) 關系梳理圖
- 當客戶端連接時, 會通過 ServerSocketChannel 得到 SocketChannel
- Selector 進行監聽 select 方法, 返回有事件發生的通道的個數.
- 將 socketChannel 注冊到 Selector 上, register(Selector sel, int ops), 一個 selector 上可以注冊多個 SocketChannel
- 注冊后返回一個 SelectionKey, 會和該 Selector 關聯(集合)
- 進一步得到各個 SelectionKey (有事件發生)
- 在通過 SelectionKey 反向獲取 SocketChannel , 方法 channel()
- 可以通過 得到的 channel , 完成業務處理
3.8 SelectionKey
SelectionKey, 表示 Selector 和網絡通道的注冊關系, 共四種:
int OP_ACCEPT: 有新的網絡連接可以 accept, 值為 16
int OP_CONNECT: 代表連接已經建立, 值為 8
int OP_READ: 代表讀操作, 值為 1
int OP_WRITE: 代表寫操作, 值為 4
3.9 ServerSocketChannel
ServerSocketChannel 在服務器端監聽新的客戶端 Socket 連接
3.10 SocketChannel
SocketChannel,網絡 IO 通道,具體負責進行讀寫操作。NIO 把緩沖區的數據寫入通道, 或者把通道里的數據讀到緩沖區。
3.11 NIO 與零拷貝
3.11.1 基本介紹
- 在 Java 程序中, 常用的零拷貝有 mmap(內存映射) 和 sendFile。
- 所謂零拷貝,是從操作系統角度來看沒有CPU拷貝。
3.11.2 傳統IO模型
DMA: direct memory access 直接內存拷貝(不使用 CPU)
3.11.4 mmap 優化
- mmap 通過內存映射, 將文件映射到內核緩沖區, 同時, 用戶空間可以共享內核空間的數據。
3.11.5 sendFile 優化
- Linux 2.1 版本 提供了 sendFile 函數, 其基本原理如下: 數據根本不經過用戶態, 直接從內核緩沖區進入到Socket Buffer,同時,由于和用戶態完全無關,就減少了一次上下文切換。
系統調用:需要進行線程上下文切換,但不是進程上下文切換
3.11.6 sendFile改進
Linux 在 2.4 版本中, 做了一些修改, 避免了從內核緩沖區拷貝到 Socket buffer 的操作, 直接拷貝到協議棧,從而再一次減少了數據拷貝。
這里還是有一次少量數據的CPU拷貝
kernel buffer → socket buffer
拷貝的信息很少,比如length,offset等,消耗很低,可以忽略。