先了解一些基本概念,什么是socket?什么是I/O操作
-
unix(like)世界里,一切皆文件,而文件是什么呢?文件就是一串二進制流而已,不管socket,還是FIFO、管道、終端,對我們來說,一切都是文件,一切都是流
-
在信息交換的過程中,我們都是對這些流進行數據的收發操作,簡稱為I/O操作(input and output)
-
計算機里有這么多的流,我怎么知道要操作哪個流呢?對,就是文件描述符,即通常所說的fd,一個fd就是一個整數,所以,對這個整數的操作,就是對這個文件(流)的操作。我們創建一個socket,通過系統調用會返回一個文件描述符,那么剩下對socket的操作就會轉化為對這個描述符的操作
然后看看一下幾個概念
-
BIO:同步阻塞IO,一個客戶端連接,對應一個服務端線程
-
BIO還有一種變種,偽異步IO,當有新的客戶端接入時,將客戶端的socket封裝成一個task,丟到線程池中處理。優化了后續處理線程的方式
-
NIO:同步非阻塞IO
-
AIO:異步非阻塞IO(異步一定是非阻塞)
再看看以下幾個區別
-
同步和異步針對應用程序來,關注的是程序中間的協作關系
-
阻塞與非阻塞更關注的是單個進程的執行狀態
再看看I/O處理的過程
-
數據通過網關到達內核,內核准備好數據
-
數據從內核緩存寫入用戶緩存
再來講講同步異步就清晰了
-
同步:不管是BIO,NIO,還是IO多路復用,從內核緩存寫入用戶緩存一定是由 用戶線程自行讀取數據,處理數據
-
異步:數據是內核寫入的,並放在了用戶線程指定的緩存區,寫入完畢后通知用戶線程
-
阻塞:數據從網關寫到內核,如果沒寫好,線程就一直在等待
-
非阻塞:數據總網關寫到內核,用一個線程輪詢的去查看所有的數據是否准備好(I/O多路復用,監聽多個socket)
再來看看I/O多路復用的三種形式
-
select:知道了有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。所以select具有O(n)的無差別輪詢復雜度,同時處理的流越多,無差別輪詢時間就越長
-
poll:本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態, 但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的
-
epoll(Linux內核所特有):可以理解為event poll,不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(復雜度降低到了O(1))(Epoll最大的優點就在於它只管你“活躍”的連接,而跟連接總數無關,因此在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll)
-
注意:表面上看epoll的性能最好,但是在連接數少並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調
最后回來看看java內核的NIO的實現
-
緩沖區Buffer
-
緩沖區實際上是一個數組,封裝了對數據結構化訪問以及維護讀寫位置等信息
-
在NIO庫中,所有數據都是用緩沖區處理的,在讀取數據時,直接讀取到緩沖區。寫入數據時,直接寫入寫緩沖區。任何時候訪問NIO中的數據,都是 通過緩沖區進行操作
-
最常用的的緩沖區是ByteBuffer。大部分Java基本類型都對應一種緩沖區
-
通道channel
-
Channel 是一個通道,可以通過它讀取和寫入數據。InputStream和OutputStream各自只能在一個方向上操作
-
Channel是全雙工的,所以它可以比流更好地映射底層的api
-
多路復用器Selector
-
Selector是NIO的編程基礎。多路復用器提供選擇已經就緒的任務的能力
-
Selector會不斷輪詢注冊在其上的Channel,如果channel上面有了新的TCP連接、讀取或者寫事件,這個channel就是就緒狀態,會被Selector輪詢出來。然后通過SelectionKey集合可以獲取就緒的Channel集合,進行IO操作
-
一個Selector可以同時輪詢多個Channel,由於JDK使用了epoll()代替傳統的select實現,所以沒有最大連接句柄1024/2048的限制。這意味着只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端
-
NIO服務端序列圖
-
-
NIO客服端序列圖
-
-
簡單版本的交互圖
-