要想學習netty就先要了解:(網絡編程模型:BIO、NIO、AIO)
IO
上圖的工作模式:
- 開始時應用程序會發一個請求給CPU,CPU得到通知后,此時CPU就需要調用操作系統內核程序(磁盤控制器)。這就是用戶態->內核態。
- 磁盤控制器接到通知,使用DMA拷貝技術將數據放到PageCache內核緩沖區中,再由CPU把內核緩沖區中的數據傳回用戶緩沖區中(buffer)。這就是內核態->用戶態。
請求數據時,IO操作通常包括兩個部分(使用到的IO模型如下)
- 等待內核緩沖區中的數據准備好
- 將內核緩沖區中的數據拷貝到用戶緩沖區中(需要使用零拷貝進行優化,詳情見零拷貝)
一、IO模型
阻塞IO:程序請求操作系統IO,如果內核緩沖區中沒有准備好數據,進程則會進行等待。
非阻塞IO:程序請求操作系統IO,如果內核緩沖區中沒有准備好數據,進程會繼續執行,不斷進行系統調用直到IO數據准備好。
同步IO:操作系統收到程序請求后,如果內核緩沖區中沒有准備好數據,進程不會響應,直到數據准備好才會響應往下執行。
異步IO:操作系統收到程序請求后,如果內核緩沖區中沒有准備好數據,會返回一個標記,程序繼續往下執行。當數據准備好之后,會以事件的方式通知。
1.1同步IO
1、阻塞式IO(兩個步驟都需要等待)
應用進程被阻塞,直到內核緩沖區中的數據復制到應用進程緩沖區中才返回。
2、非阻塞式IO(第一個步驟不斷詢問數據是否准備好不需要等待
,第二步需要等待)
應用進程執行系統調用之后,內核返回一個錯誤碼。應用進程可以繼續執行,但是需要不斷的執行系統調用來獲知I/O是否完成,這種方式稱為輪詢。(polling)。
3、I/O多路復用(兩個步驟都需要等待)
1、使用select或者poll等待數據,並且可以等待多個套接字中的任何一個變為可讀,這一過程會被阻塞,當某一個套接字可讀時返回,之后再使用recvfrom 把數據從內核復制到進程中。
2、使用select或者poll,可以讓單個線程具有處理多個I/O事件的能力。
IO多路復用的實現(詳情查看大佬:IO多路復用)
多路復用的實現:select、poll、epoll,這些函數都是系統調用函數。
select(一次select系統調用+N次就緒的客戶端的read系統調用)
select工作原理:
- 當通過一個線程調用select,將保存客戶端連接的數組
拷貝
一份給內核態,在內核層遍歷該數組看那個客戶端已經准備好數據,將准備好的客戶端的個數
返回給用戶態,用戶態需要遍歷
數組,找到准備好數據的客戶端,然后進行read系統調用將內核緩沖區中的數據拷貝到用戶緩沖區中。
select存在的問題:
- select 調用需要傳入保存客戶端連接的數組,需要拷貝一份到內核,高並發場景下這樣的拷貝消耗的資源是驚人的。(可優化為不復制)
- select 在內核層仍然是通過遍歷的方式檢查客戶端的就緒狀態,是個同步過程,只不過無系統調用切換上下文的開銷,只在內核態遍歷。(內核層可優化為異步事件通知)
- select 僅僅返回就緒的客戶端的個數,具體哪個可讀還是要用戶自己遍歷。(可優化為只返回給用戶就緒的客戶端標識,無需用戶做無效的遍歷)
poll
它和select 的主要區別就是,去掉了select只能監聽1024個客戶端的限制。
epoll
epoll對select和poll進行了優化:
- 只在內核中存儲保存客戶端連接的數組,無需用戶每次都重新傳入,只需告訴內核修改的部分即可。
- 內核不再通過輪詢的方式找到就緒的客戶端,而是通過異步IO事件通知用戶態已經就緒的客戶端。
- 內核僅會將有IO事件的客戶端標識返回給用戶,用戶也無需遍歷整個文件描述符集合。
由於Linux下沒有Windows下的IOCP技術提供真正的異步IO支持,所以Linux下使用epoll模擬異步IO
4、信號驅動IO(第一步不會等待,第二步會等待)
1、應用進程使用sigaction系統調用,內核立即返回,應用進程可以繼續執行,也就是說等待數據階段應用進程是非阻塞的。
2、內核在數據到達時向應用進程發送SIGIO信號,應用進程收到之后在信號處理程序中調用 recvfrom 將數據從內核復制到應用進程中。
1.2異步IO(兩個步驟都不需要等待)
進行aio_read系統調用會立即返回,應用進程繼續執行,不會被阻塞,內核會在所有操作完成之后向應用進程發送信號。
二、IO的發展
2.1 BIO、NIO、AIO的區別
Java BIO:同步並阻塞,服務器采用線程池為一個客戶端連接創建一個線程,即客戶端有連接請求時服務器端就需要啟動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷。
連接數目比較小且固定的架構,對服務器資源要求比較高
Java NIO:同步非阻塞(IO多路復用),服務器實現模式為一個線程處理多個請求(連接),即客戶端發送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有 I/O 請求就進行處理。
連接數目多且連接比較短(輕操作)的架構:聊天服務器,彈幕系統,服務器間通訊
Java AIO(NIO.2):異步非阻塞,AIO 引入異步通道的概念,采用了 Rector/Proactor 模式,當用戶態訪問內核會立即返回一個標記,由內核會在所有操作完成之后向應用進程發送信號,一般適用於連接數較多且連接時間較長的應用。
連接數目多且連接比較長(重操作)的架構:相冊服務器
2.2 NIO的三個核心部分
- BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據,塊 I/O 的效率比流 I/O 高很多。
- BIO 是阻塞的,NIO 則是非阻塞的。
- BIO 基於字節流和字符流進行操作,而 NIO 基於 Channel(通道)和 Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用於監聽多個通道的事件(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道。Buffer和Channel之間的數據流向是雙向的。
channel
- 通道可以同時進行讀寫,而流只能讀或者只能寫
- 通道可以實現異步讀寫數據
- 通道可以從緩沖讀數據,也可以寫數據到緩沖
channel類
- FileChannel:用於文件的數據讀寫
- DatagramChannel:用於 UDP 的數據讀寫
- SocketChannel:用於 TCP 的數據讀寫
- ServerSocketChannel:用於UDP和TCP數據的讀寫
channel類中幾個重要的方法
- 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):把數據從當前通道復制給目標通道
buffer
緩沖區(Buffer):緩沖區本質上是一個可以讀寫數據的內存塊,可以理解成是一個容器對象(含數組),該對象提供了一組方法,可以更輕松地使用內存塊,緩沖區對象內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況。對文件的讀取或寫入都必須經由Buffer。
buffer的子類
Buffer緩沖區有兩種工作模式
- 寫模式:當通道中的數據讀取到buffer中時,buffer為寫模式
- 讀模式:當把buffer中的數據寫入到通道時,buffer為讀模式
buffer緩沖區中的四大屬性
需要使用buffer.flip反轉讀模式和寫模式
寫模式
讀模式
- position: 緩存區分配下一個要被讀或寫的元素的索引,每次讀寫緩沖區改值會自動加1
- limit: 緩存區最大可以進行操作的index位置。緩存區的讀寫狀態正式由這個屬性控制的,可以改變值。
- capacity: 緩存區的最大容量。這個容量是在緩存區創建時進行指定的。不可以改變值。
- mark:在當前緩沖區進行position時設置一個標記mark1,當執行buffer的reset方法時,可以把position復位到mark1
selector
1、Selector能夠檢測多個注冊的通道上是否有事件發生(注意:多個Channel 以事件的方式可以注冊到同一個Selector),如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求。
2、使用selector檢測,當通道中有事件發生時會主動通知selector,可以減少多線程之間的上下文切換(1個selector系統調用+N次就緒的客戶端的read系統調用)。
傳統做法是需要循環訪問所有通道是否有事件發生,這樣在數據量大的情況下,會導致消耗內存資源嚴重
Selector相關方法說明
selector.select(); //阻塞
selector.select(1000); //阻塞 1000 毫秒,在 1000 毫秒后返回
selector.wakeup(); //喚醒 selector
selector.selectNow(); //不阻塞,立馬返還
2.3 Selector、Channel 和 Buffer 關系圖
- 每個 Channel 都會對應一個 Buffer。
- Selector對應一個線程,一個線程對應多個 Channel(連接)。
- 該圖反應了有三個 Channel 注冊到該 Selector上程序
- Selector 會根據不同的事件,在各個通道上切換。程序切換到哪個Channel 是由事件決定的,Event 就是一個重要的概念。
- Buffer 就是一個內存塊,底層是有一個數組。
- 數據的讀取寫入是通過Buffer,這個和BIO是不同的,BIO中要么是輸入流,或者是輸出流,不能雙向,但是NIO的Buffer是可以讀也可以寫,需要flip方法切換Channel是雙向的。