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