1. 程式人生 > 實用技巧 >IO、NIO、AIO理解

IO、NIO、AIO理解

參考:

https://www.cnblogs.com/study-makes-me-happy/p/9603290.html

https://blog.csdn.net/u013851082/article/details/53942947

https://blog.csdn.net/lisha006/article/details/82856906

https://www.cnblogs.com/sxkgeek/p/9488703.html

https://www.cnblogs.com/liushui-sky/p/12917347.html

IO、NIO、AIO理解

摘要: 關於BIO和NIO的理解

最近大概看了ZooKeeper和Mina的原始碼發現都是用Java NIO實現的,所以有必要搞清楚什麼是NIO。下面是我結合網路資料自己總結的,為了節約時間圖示隨便畫的,能達意就行。

簡介:

BIO:同步阻塞式IO,伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,當然可以通過執行緒池機制改善。
NIO:同步非阻塞式IO,伺服器實現模式為一個請求一個執行緒,即客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。
AIO(NIO.2):非同步非阻塞式IO,伺服器實現模式為一個有效請求一個執行緒,客戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動執行緒進行處理。

BIO
同步阻塞式IO,相信每一個學習過作業系統網路程式設計或者任何語言的網路程式設計的人都很熟悉,在while迴圈中服務端會呼叫accept方法等待接收客戶端的連線請求,一旦接收到一個連線請求,就可以建立通訊套接字在這個通訊套接字上進行讀寫操作,此時不能再接收其他客戶端連線請求,只能等待同當前連線的客戶端的操作執行完成。
如果BIO要能夠同時處理多個客戶端請求,就必須使用多執行緒,即每次accept阻塞等待來自客戶端請求,一旦受到連線請求就建立通訊套接字同時開啟一個新的執行緒來處理這個套接字的資料讀寫請求,然後立刻又繼續accept等待其他客戶端連線請求,即為每一個客戶端連線請求都建立一個執行緒來單獨處理,大概原理圖就像這樣:

雖然此時伺服器具備了高併發能力,即能夠同時處理多個客戶端請求了,但是卻帶來了一個問題,隨著開啟的執行緒數目增多,將會消耗過多的記憶體資源,導致伺服器變慢甚至崩潰,NIO可以一定程度解決這個問題。


NIO
同步非阻塞式IO,關鍵是採用了事件驅動的思想來實現了一個多路轉換器。
NIO與BIO最大的區別就是隻需要開啟一個執行緒就可以處理來自多個客戶端的IO事件,這是怎麼做到的呢?
就是多路複用器,可以監聽來自多個客戶端的IO事件:
A.若服務端監聽到客戶端連線請求,便為其建立通訊套接字(java中就是通道),然後返回繼續監聽,若同時有多個客戶端連線請求到來也可以全部收到,依次為它們都建立通訊套接字。
B.若服務端監聽到來自已經建立了通訊套接字的客戶端傳送來的資料,就會呼叫對應介面處理接收到的資料,若同時有多個客戶端發來資料也可以依次進行處理。
C.監聽多個客戶端的連線請求和接收資料請求同時還能監聽自己時候有資料要傳送。


總之就是在一個執行緒中就可以呼叫多路複用介面(java中是select)阻塞同時監聽來自多個客戶端的IO請求,一旦有收到IO請求就呼叫對應函式處理。

各自應用場景

到這裡你也許已經發現,一旦有請求到來(不管是幾個同時到還是隻有一個到),都會呼叫對應IO處理函式處理,所以:

(1)NIO適合處理連線數目特別多,但是連線比較短(輕操作)的場景,Jetty,Mina,ZooKeeper等都是基於java nio實現。

(2)BIO方式適用於連線數目比較小且固定的場景,這種方式對伺服器資源要求比較高,併發侷限於應用中。

(3)AIO新的IO2.0,即NIO2.0,jdk1.7開始應用,叫做非同步不阻塞的IO。AIO引入異常通道的概念,採用了Proactor模式,簡化了程式編寫,一個有效的請求才啟動一個執行緒,它的特點是先由作業系統完成後才通知服務端程式啟動執行緒去處理,一般適用於連線數較多且連線時間長的應用。


附錄:下面附上一個別人寫的java NIO的例子。
服務端:
  1. 1.packagecn.nio;
  2. 2.
  3. 3.importjava.io.IOException;
  4. 4.importjava.net.InetSocketAddress;
  5. 5.importjava.nio.ByteBuffer;
  6. 6.importjava.nio.channels.SelectionKey;
  7. 7.importjava.nio.channels.Selector;
  8. 8.importjava.nio.channels.ServerSocketChannel;
  9. 9.importjava.nio.channels.SocketChannel;
  10. 10.importjava.util.Iterator;
  11. 11.
  12. 12./**
  13. 13.*NIO服務端
  14. 14.*
  15. 15.*/
  16. 16.publicclassNIOServer{
  17. 17.//通道管理器
  18. 18.privateSelectorselector;
  19. 19.
  20. 20./**
  21. 21.*獲得一個ServerSocket通道,並對該通道做一些初始化的工作
  22. 22.*@paramport繫結的埠號
  23. 23.*@throwsIOException
  24. 24.*/
  25. 25.publicvoidinitServer(intport)throwsIOException{
  26. 26.//獲得一個ServerSocket通道
  27. 27.ServerSocketChannelserverChannel=ServerSocketChannel.open();
  28. 28.//設定通道為非阻塞
  29. 29.serverChannel.configureBlocking(false);
  30. 30.//將該通道對應的ServerSocket繫結到port埠
  31. 31.serverChannel.socket().bind(newInetSocketAddress(port));
  32. 32.//獲得一個通道管理器
  33. 33.this.selector=Selector.open();
  34. 34.//將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_ACCEPT事件,註冊該事件後,
  35. 35.//當該事件到達時,selector.select()會返回,如果該事件沒到達selector.select()會一直阻塞。
  36. 36.serverChannel.register(selector,SelectionKey.OP_ACCEPT);
  37. 37.}
  38. 38.
  39. 39./**
  40. 40.*採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理
  41. 41.*@throwsIOException
  42. 42.*/
  43. 43.@SuppressWarnings("unchecked")
  44. 44.publicvoidlisten()throwsIOException{
  45. 45.System.out.println("服務端啟動成功!");
  46. 46.//輪詢訪問selector
  47. 47.while(true){
  48. 48.//當註冊的事件到達時,方法返回;否則,該方法會一直阻塞
  49. 49.selector.select();
  50. 50.//獲得selector中選中的項的迭代器,選中的項為註冊的事件
  51. 51.Iteratorite=this.selector.selectedKeys().iterator();
  52. 52.while(ite.hasNext()){
  53. 53.SelectionKeykey=(SelectionKey)ite.next();
  54. 54.//刪除已選的key,以防重複處理
  55. 55.ite.remove();
  56. 56.//客戶端請求連線事件
  57. 57.if(key.isAcceptable()){
  58. 58.ServerSocketChannelserver=(ServerSocketChannel)key
  59. 59..channel();
  60. 60.//獲得和客戶端連線的通道
  61. 61.SocketChannelchannel=server.accept();
  62. 62.//設定成非阻塞
  63. 63.channel.configureBlocking(false);
  64. 64.
  65. 65.//在這裡可以給客戶端傳送資訊哦
  66. 66.channel.write(ByteBuffer.wrap(newString("向客戶端傳送了一條資訊").getBytes()));
  67. 67.//在和客戶端連線成功之後,為了可以接收到客戶端的資訊,需要給通道設定讀的許可權。
  68. 68.channel.register(this.selector,SelectionKey.OP_READ);
  69. 69.
  70. 70.//獲得了可讀的事件
  71. 71.}elseif(key.isReadable()){
  72. 72.read(key);
  73. 73.}
  74. 74.
  75. 75.}
  76. 76.
  77. 77.}
  78. 78.}
  79. 79./**
  80. 80.*處理讀取客戶端發來的資訊的事件
  81. 81.*@paramkey
  82. 82.*@throwsIOException
  83. 83.*/
  84. 84.publicvoidread(SelectionKeykey)throwsIOException{
  85. 85.//伺服器可讀取訊息:得到事件發生的Socket通道
  86. 86.SocketChannelchannel=(SocketChannel)key.channel();
  87. 87.//建立讀取的緩衝區
  88. 88.ByteBufferbuffer=ByteBuffer.allocate(10);
  89. 89.channel.read(buffer);
  90. 90.byte[]data=buffer.array();
  91. 91.Stringmsg=newString(data).trim();
  92. 92.System.out.println("服務端收到資訊:"+msg);
  93. 93.ByteBufferoutBuffer=ByteBuffer.wrap(msg.getBytes());
  94. 94.channel.write(outBuffer);//將訊息回送給客戶端
  95. 95.}
  96. 96.
  97. 97./**
  98. 98.*啟動服務端測試
  99. 99.*@throwsIOException
  100. 100.*/
  101. 101.publicstaticvoidmain(String[]args)throwsIOException{
  102. 102.NIOServerserver=newNIOServer();
  103. 103.server.initServer(8000);
  104. 104.server.listen();
  105. 105.}
  106. 106.
  107. 107.}

客戶端:
  1. 1.packagecn.nio;
  2. 2.
  3. 3.importjava.io.IOException;
  4. 4.importjava.net.InetSocketAddress;
  5. 5.importjava.nio.ByteBuffer;
  6. 6.importjava.nio.channels.SelectionKey;
  7. 7.importjava.nio.channels.Selector;
  8. 8.importjava.nio.channels.SocketChannel;
  9. 9.importjava.util.Iterator;
  10. 10.
  11. 11./**
  12. 12.*NIO客戶端
  13. 13.*
  14. 14.*/
  15. 15.publicclassNIOClient{
  16. 16.//通道管理器
  17. 17.privateSelectorselector;
  18. 18.
  19. 19./**
  20. 20.*獲得一個Socket通道,並對該通道做一些初始化的工作
  21. 21.*@paramip連線的伺服器的ip
  22. 22.*@paramport連線的伺服器的埠號
  23. 23.*@throwsIOException
  24. 24.*/
  25. 25.publicvoidinitClient(Stringip,intport)throwsIOException{
  26. 26.//獲得一個Socket通道
  27. 27.SocketChannelchannel=SocketChannel.open();
  28. 28.//設定通道為非阻塞
  29. 29.channel.configureBlocking(false);
  30. 30.//獲得一個通道管理器
  31. 31.this.selector=Selector.open();
  32. 32.
  33. 33.//客戶端連線伺服器,其實方法執行並沒有實現連線,需要在listen()方法中調
  34. 34.//用channel.finishConnect();才能完成連線
  35. 35.channel.connect(newInetSocketAddress(ip,port));
  36. 36.//將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_CONNECT事件。
  37. 37.channel.register(selector,SelectionKey.OP_CONNECT);
  38. 38.}
  39. 39.
  40. 40./**
  41. 41.*採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理
  42. 42.*@throwsIOException
  43. 43.*/
  44. 44.@SuppressWarnings("unchecked")
  45. 45.publicvoidlisten()throwsIOException{
  46. 46.//輪詢訪問selector
  47. 47.while(true){
  48. 48.selector.select();
  49. 49.//獲得selector中選中的項的迭代器
  50. 50.Iteratorite=this.selector.selectedKeys().iterator();
  51. 51.while(ite.hasNext()){
  52. 52.SelectionKeykey=(SelectionKey)ite.next();
  53. 53.//刪除已選的key,以防重複處理
  54. 54.ite.remove();
  55. 55.//連線事件發生
  56. 56.if(key.isConnectable()){
  57. 57.SocketChannelchannel=(SocketChannel)key
  58. 58..channel();
  59. 59.//如果正在連線,則完成連線
  60. 60.if(channel.isConnectionPending()){
  61. 61.channel.finishConnect();
  62. 62.
  63. 63.}
  64. 64.//設定成非阻塞
  65. 65.channel.configureBlocking(false);
  66. 66.
  67. 67.//在這裡可以給服務端傳送資訊哦
  68. 68.channel.write(ByteBuffer.wrap(newString("向服務端傳送了一條資訊").getBytes()));
  69. 69.//在和服務端連線成功之後,為了可以接收到服務端的資訊,需要給通道設定讀的許可權。
  70. 70.channel.register(this.selector,SelectionKey.OP_READ);
  71. 71.
  72. 72.//獲得了可讀的事件
  73. 73.}elseif(key.isReadable()){
  74. 74.read(key);
  75. 75.}
  76. 76.
  77. 77.}
  78. 78.
  79. 79.}
  80. 80.}
  81. 81./**
  82. 82.*處理讀取服務端發來的資訊的事件
  83. 83.*@paramkey
  84. 84.*@throwsIOException
  85. 85.*/
  86. 86.publicvoidread(SelectionKeykey)throwsIOException{
  87. 87.//和服務端的read方法一樣
  88. 88.}
  89. 89.
  90. 90.
  91. 91./**
  92. 92.*啟動客戶端測試
  93. 93.*@throwsIOException
  94. 94.*/
  95. 95.publicstaticvoidmain(String[]args)throwsIOException{
  96. 96.NIOClientclient=newNIOClient();
  97. 97.client.initClient("localhost",8000);
  98. 98.client.listen();
  99. 99.}
  100. 100.
  101. 101.}

NIo、Bio、aio、 的原理及區別與應用場景

在高效能的IO體系設計中,有幾個名詞概念常常會使我們感到迷惑不解。具體如下:

序號 問題
1 什麼是同步?
2 什麼是非同步?
3 什麼是阻塞?
4 什麼是非阻塞?
5 什麼是同步阻塞?
6 什麼是同步非阻塞?
7 什麼是非同步阻塞?
8 什麼是非同步非阻塞?



散仙不才,在查了一部分資料後,願試著以通俗易懂的方式解釋下這幾個名詞。如有不足之處,還望告知。



在弄清楚上面的幾個問題之前,我們首先得明白什麼是同步,非同步,阻塞,非阻塞,只有這幾個單個概念理解清楚了,然後在組合理解起來,就相對比較容易了。

1,同步和非同步是針對應用程式和核心的互動而言的。同步/非同步是在時間上強調處理事情的結果/機會成本的兩種處理策略;強調結果意味著對結果的迫不急待,不過結果是正確的還是錯誤的,反正你要立即給我一個結果響應;強調時間機會成本意味著對等待結果浪費的時間極其難接受,而對結果並不是那麼急切,暫時不管結果(讓處理方處理完主動通知結果/自己空閒的時候主動去獲取結果)轉而去處理其他事情

2,阻塞和非阻塞是針對於程序在訪問資料的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作函式的實現方式,阻塞方式下讀取或者寫入函式將一直等待,而非阻塞方式下,讀取或者寫入函式會立即返回一個狀態值。

3,同步/非同步是巨集觀上(程序間通訊,通常表現為網路IO的處理上),阻塞/非阻塞是微觀上(程序內資料傳輸,通常表現為對本地IO的處理上);阻塞和非阻塞是同步/非同步的表現形式

由上描述基本可以總結一句簡短的話,同步和非同步是目的,阻塞和非阻塞是實現方式。

編號 名詞 解釋 舉例
1 同步 指的是使用者程序觸發IO操作並等待或者輪詢的去檢視IO操作是否就緒 自己上街買衣服,自己親自幹這件事,別的事幹不了。
2 非同步 非同步是指使用者程序觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知(非同步的特點就是通知) 告訴朋友自己合適衣服的尺寸,大小,顏色,讓朋友委託去賣,然後自己可以去幹別的事。(使用非同步IO時,Java將IO讀寫委託給OS處理,需要將資料緩衝區地址和大小傳給OS)
3 阻塞 所謂阻塞方式的意思是指, 當試圖對該檔案描述符進行讀寫時, 如果當時沒有東西可讀,或者暫時不可寫, 程式就進入等待 狀態, 直到有東西可讀或者可寫為止 去公交站充值,發現這個時候,充值員不在(可能上廁所去了),然後我們就在這裡等待,一直等到充值員回來為止。(當然現實社會,可不是這樣,但是在計算機裡確實如此。)
4 非阻塞 非阻塞狀態下, 如果沒有東西可讀, 或者不可寫, 讀寫函式馬上返回, 而不會等待, 銀行裡取款辦業務時,領取一張小票,領取完後我們自己可以玩玩手機,或者與別人聊聊天,當輪我們時,銀行的喇叭會通知,這時候我們就可以去了。






下面我們再來理解組合方式的IO型別,就好理解多了。

同步阻塞IO(JAVA BIO):
同步並阻塞,伺服器實現模式為一個連線一個執行緒,即客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,如果這個連線不做任何事情會造成不必要的執行緒開銷,當然可以通過執行緒池機制改善。

同步非阻塞IO(Java NIO) : 同步非阻塞,伺服器實現模式為一個請求一個執行緒,即客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。使用者程序也需要時不時的詢問IO操作是否就緒,這就要求使用者程序不停的去詢問。

非同步阻塞IO(Java NIO):
此種方式下是指應用發起一個IO操作以後,不等待核心IO操作的完成,等核心完成IO操作以後會通知應用程式,這其實就是同步和非同步最關鍵的區別,同步必須等待或者主動的去詢問IO是否完成,那麼為什麼說是阻塞的呢?因為此時是通過select系統呼叫來完成的,而select函式本身的實現方式是阻塞的,而採用select函式有個好處就是它可以同時監聽多個檔案控制代碼(如果從UNP的角度看,select屬於同步操作。因為select之後,程序還需要讀寫資料),從而提高系統的併發性!


(Java AIO(NIO.2))非同步非阻塞IO:
在此種模式下,使用者程序只需要發起一個IO操作然後立即返回,等IO操作真正的完成以後,應用程式會得到IO操作完成的通知,此時使用者程序只需要對資料進行處理就好了,不需要進行實際的IO讀寫操作,因為真正的IO讀取或者寫入操作已經由核心完成了。



BIO、NIO、AIO適用場景分析:

BIO方式適用於連線數目比較小且固定的架構,這種方式對伺服器資源要求比較高,併發侷限於應用中,JDK1.4以前的唯一選擇,但程式直觀簡單易理解。

NIO方式適用於連線數目多且連線比較短(輕操作)的架構,比如聊天伺服器,併發侷限於應用中,程式設計比較複雜,JDK1.4開始支援。

AIO方式使用於連線數目多且連線比較長(重操作)的架構,比如相簿伺服器,充分呼叫OS參與併發操作,程式設計比較複雜,JDK7開始支援。

搞清楚了以上概念以後,我們再回過頭來看看,Reactor模式和Proactor模式。

(其實阻塞與非阻塞都可以理解為同步範疇下才有的概念,對於非同步,就不會再去分阻塞非阻塞。對於使用者程序,接到非同步通知後,就直接操作程序使用者態空間裡的資料好了。)

首先來看看Reactor模式,Reactor模式應用於同步I/O的場景。我們分別以讀操作和寫操作為例來看看Reactor中的具體步驟:
讀取操作:
1. 應用程式註冊讀就緒事件和相關聯的事件處理器

2. 事件分離器等待事件的發生

3. 當發生讀就緒事件的時候,事件分離器呼叫第一步註冊的事件處理器

4. 事件處理器首先執行實際的讀取操作,然後根據讀取到的內容進行進一步的處理

寫入操作類似於讀取操作,只不過第一步註冊的是寫就緒事件。


下面我們來看看Proactor模式中讀取操作和寫入操作的過程:
讀取操作:
1. 應用程式初始化一個非同步讀取操作,然後註冊相應的事件處理器,此時事件處理器不關注讀取就緒事件,而是關注讀取完成事件,這是區別於Reactor的關鍵。

2. 事件分離器等待讀取操作完成事件

3. 在事件分離器等待讀取操作完成的時候,作業系統呼叫核心執行緒完成讀取操作(非同步IO都是作業系統負責將資料讀寫到應用傳遞進來的緩衝區供應用程式操作,作業系統扮演了重要角色),並將讀取的內容放入使用者傳遞過來的快取區中。這也是區別於Reactor的一點,Proactor中,應用程式需要傳遞快取區。

4. 事件分離器捕獲到讀取完成事件後,啟用應用程式註冊的事件處理器,事件處理器直接從快取區讀取資料,而不需要進行實際的讀取操作。

Proactor中寫入操作和讀取操作,只不過感興趣的事件是寫入完成事件。

從上面可以看出,Reactor和Proactor模式的主要區別就是真正的讀取和寫入操作是有誰來完成的,Reactor中需要應用程式自己讀取或者寫入資料,而Proactor模式中,應用程式不需要進行實際的讀寫過程,它只需要從快取區讀取或者寫入即可,作業系統會讀取快取區或者寫入快取區到真正的IO裝置.

綜上所述,同步和非同步是相對於應用和核心的互動方式而言的,同步 需要主動去詢問,而非同步的時候核心在IO事件發生的時候通知應用程式,而阻塞和非阻塞僅僅是系統在呼叫系統呼叫的時候函式的實現方式而已。


如果你想吃一份宮保雞丁蓋飯:

同步阻塞:你到飯館點餐,然後在那等著,還要一邊喊:好了沒啊!

同步非阻塞:在飯館點完餐,就去遛狗了。不過溜一會兒,就回飯館喊一聲:好了沒啊!

非同步阻塞:遛狗的時候,接到飯館電話,說飯做好了,讓您親自去拿。

非同步非阻塞:飯館打電話說,我們知道您的位置,一會給你送過來,安心遛狗就可以了。

“一個IO操作其實分成了兩個步驟:發起IO請求和實際的IO操作。
同步IO和非同步IO的區別就在於第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求程序,那麼就是同步IO。
阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞IO,如果不阻塞,那麼就是非阻塞IO。

同步和非同步是針對應用程式和核心的互動而言的,同步指的是使用者程序觸發IO操作並等待或者輪詢的去檢視IO操作是否就緒,而非同步是指使用者程序觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知而阻塞和非阻塞是針對於程序在訪問資料的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作函式的實現方式,阻塞方式下讀取或者寫入函式將一直等待,而非阻塞方式下,讀取或者寫入函式會立即返回一個狀態值。
所以,IO操作可以分為3類:同步阻塞(即早期的IO操作)、同步非阻塞(NIO)、非同步(AIO)。
同步阻塞:
在此種方式下,使用者程序在發起一個IO操作以後,必須等待IO操作的完成,只有當真正完成了IO操作以後,使用者程序才能執行。JAVA傳統的IO模型屬於此種方式。

同步非阻塞:
在此種方式下,使用者程序發起一個IO操作以後邊可返回做其它事情,但是使用者程序需要時不時的詢問IO操作是否就緒,這就要求使用者程序不停的去詢問,從而引入不必要的CPU資源浪費。其中目前JAVA的NIO就屬於同步非阻塞IO。
非同步:
此種方式下是指應用發起一個IO操作以後,不等待核心IO操作的完成,等核心完成IO操作以後會通知應用程式。”

這段話比較清楚


參考:http://blog.csdn.net/brainkick/article/details/9312407

原文:http://my.oschina.net/bluesky0leon/blog/132361

關於BIO | NIO | AIO的討論一直存在,有時候也很容易讓人混淆,就我的理解,給出一個解釋:

BIO | NIO | AIO,本身的描述都是在Java語言的基礎上的。而描述IO,我們需要從兩個層面:

  1. 程式語言
  2. 實現原理
  3. 底層基礎

從程式語言層面

BIO | NIO | AIO 以Java的角度,理解,linux c裡也有AIO的概念(庫),這些概念不知道什麼原因被炒火起來,這裡只從Java角度入手。

  • BIO,同步阻塞式IO,簡單理解:一個連線一個執行緒
  • NIO,同步非阻塞IO,簡單理解:一個請求一個執行緒
  • AIO,非同步非阻塞IO,簡單理解:一個有效請求一個執行緒

BIO

在JDK1.4之前,用Java編寫網路請求,都是建立一個ServerSocket,然後,客戶端建立Socket時就會詢問是否有執行緒可以處理,如果沒有,要麼等待,要麼被拒絕。即:一個連線,要求Server對應一個處理執行緒。

NIO

在Java裡的由來,在JDK1.4及以後版本中提供了一套API來專門操作非阻塞I/O,我們可以在java.nio包及其子包中找到相關的類和介面。由於這套API是JDK新提供的I/O API,因此,也叫New I/O,這就是包名nio的由來。這套API由三個主要的部分組成:緩衝區(Buffers)、通道(Channels)和非阻塞I/O的核心類組成。在理解NIO的時候,需要區分,說的是New I/O還是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。這裡講的是後面一種。

NIO本身是基於事件驅動思想來完成的,其主要想解決的是BIO的大併發問題: 在使用同步I/O的網路應用中,如果要同時處理多個客戶端請求,或是在客戶端要同時和多個伺服器進行通訊,就必須使用多執行緒來處理。也就是說,將每一個客戶端請求分配給一個執行緒來單獨處理。這樣做雖然可以達到我們的要求,但同時又會帶來另外一個問題。由於每建立一個執行緒,就要為這個執行緒分配一定的記憶體空間(也叫工作儲存器),而且作業系統本身也對執行緒的總數有一定的限制。如果客戶端的請求過多,服務端程式可能會因為不堪重負而拒絕客戶端的請求,甚至伺服器可能會因此而癱瘓。

NIO基於Reactor,當socket有流可讀或可寫入socket時,作業系統會相應的通知引用程式進行處理,應用再將流讀取到緩衝區或寫入作業系統。
也就是說,這個時候,已經不是一個連線就要對應一個處理執行緒了,而是有效的請求,對應一個執行緒,當連線沒有資料時,是沒有工作執行緒來處理的。

AIO

與NIO不同,當進行讀寫操作時,只須直接呼叫API的read或write方法即可。這兩種方法均為非同步的,對於讀操作而言,當有流可讀取時,作業系統會將可讀的流傳入read方法的緩衝區,並通知應用程式;對於寫操作而言,當作業系統將write方法傳遞的流寫入完畢時,作業系統主動通知應用程式。
即可以理解為,read/write方法都是非同步的,完成後會主動呼叫回撥函式。
在JDK1.7中,這部分內容被稱作NIO.2,主要在java.nio.channels包下增加了下面四個非同步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

其中的read/write方法,會返回一個帶回調函式的物件,當執行完讀取/寫入操作後,直接呼叫回撥函式。


實現原理

說道實現原理,還要從作業系統的IO模型上了解

按照《Unix網路程式設計》的劃分,IO模型可以分為:阻塞IO、非阻塞IO、IO複用、訊號驅動IO和非同步IO,按照POSIX標準來劃分只分為兩類:同步IO和非同步IO。如何區分呢?首先一個IO操作其實分成了兩個步驟:發起IO請求和實際的IO操作,同步IO和非同步IO的區別就在於第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求程序,那麼就是同步IO,因此阻塞IO、非阻塞IO、IO複用、訊號驅動IO都是同步IO,如果不阻塞,而是作業系統幫你做完IO操作再將結果返回給你,那麼就是非同步IO。阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞IO,如果不阻塞,那麼就是非阻塞IO。

收到作業系統的IO模型,又不得不提select/poll/epoll/iocp,關於這四個的理解,不多做解釋,自己還沒理解到位。

可以理解的說明是:在Linux 2.6以後,java NIO的實現,是通過epoll來實現的,這點可以通過jdk的原始碼發現。而AIO,在windows上是通過IOCP實現的,在linux上還是通過epoll來實現的。

這裡強調一點:AIO,這是I/O處理模式,而epoll等都是實現AIO的一種程式設計模型;換句話說,AIO是一種介面標準,各家作業系統可以實現也可以不實現。在不同作業系統上在高併發情況下最好都採用作業系統推薦的方式。Linux上還沒有真正實現網路方式的AIO。

底層基礎

說到底層,要說Linux系統程式設計,這裡自己也不熟悉,有待後來人補充了。
只籠統的說一個:AIO實現

在windows上,AIO的實現是通過IOCP來完成的,看JDK的原始碼,可以發現

WindowsAsynchronousSocketChannelImpl

看實現介面:

implements Iocp.OverlappedChannel

再看實現方法:裡面的read0/write0方法是native方法,呼叫的jvm底層實現,虛擬機器技術不熟悉,不獻醜了。

在linux上,AIO的實現是通過epoll來完成的,看JDK原始碼,可以發現,實現原始碼是:

UnixAsynchronousSocketChannelImpl

看實現介面:

implements Port.PollableChannel

這是與windows最大的區別,poll的實現,在linux2.6後,預設使用epoll。

這樣就可以理解了。


寫在最後:Java開發為基礎的,對於作業系統底層的認知是沒有C語言為基礎的大牛好的,語言決定了思維方式,古人誠不欺我


最後,幾篇解釋的不錯的文章:

BIO NIO AIO

NIO.2 入門,第 1 部分: 非同步通道 API

使用非同步 I/O 大大提高應用程式的效能

BIO、NIO、AIO 區別和應用場景

前邊簡單介紹過IO的基本情況Java IO流

簡單回顧

對於IO我們應該非常熟悉了,IO不僅僅針對檔案的操作,網路程式設計socket的通訊,就是IO操作。

輸入、輸出流(InputStream、OutputStream)用於讀取或寫入位元組,如操作圖片、視訊等。

Reader和Writer 則用於操作字元,增加了字元編碼功能。本質上計算機操作都是位元組,不管是網路或者檔案,Reader和Writer等於構建了應用邏輯和原始資料的另一層通道。

BufferedOutputStream、BufferedInputStream等帶有緩衝區的實現,可以避免頻繁的磁碟操作,通過設計緩衝區將批量資料進行一次操作。

NIO

能解決什麼問題?

為什麼要有NIO,NIO是什麼?

首先看一下BIO,如果有一臺伺服器,能承受簡單的客戶端請求,那麼使用io和net中的同步、阻塞式API應該是可以實現了。但是為了一個使用者的請求而單獨啟動一個執行緒,開銷應該不小吧。java語言對執行緒的實現是比較重量的,啟動或銷燬執行緒,都會有明顯開銷,每個執行緒都有單獨的執行緒棧佔用明顯的記憶體。引入執行緒池,就能很大程度的避免不必要的開銷。

這種情況適合連線數並不多,只有最多幾百個連線的普通應用,能比較好的進行工作,但如果連線數量劇增,這種實現方式就無法很好的工作了,對於併發量要求較高的企業,這種方案,肯定是不可取的。

NIO採用的是一種多路複用的機制,利用單執行緒輪詢事件,高效定位就緒的Channel來決定做什麼,只是Select階段是阻塞式的,能有效避免大量連線數時,頻繁執行緒的切換帶來的效能或各種問題。

上圖隨便畫的,只是方便理解,並不能作為實現的具體的參考。

首先,Requester方通過Selector.open()建立了一個Selector準備好了排程角色。

建立了SocketChannel(ServerSocketChannel) 並註冊到Selector中,通過設定key(SelectionKey)告訴排程者所應該關注的連線請求。

阻塞,Selector阻塞在select操作中,如果發現有Channel發生連線請求,就會喚醒處理請求。

NIO同步非阻塞式IO

對比BIO的同步阻塞IO操作,實際上NIO是同步非阻塞IO,一個執行緒在同步的進行輪詢檢查,Selector不斷輪詢註冊在其上的Channel,某個Channel上面發生讀寫連線請求,這個Channel就處於就緒狀態,被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。

同步和非同步說的是訊息的通知機制,這個執行緒仍然要定時的讀取stream,判斷資料有沒有準備好,client採用迴圈的方式去讀取(執行緒自己去抓去資訊),CPU被浪費。

非阻塞:體現在,這個執行緒可以去幹別的,不需要一直在這等著。Selector可以同時輪詢多個Channel,因為JDK使用了epoll()代替傳統的select實現,沒有最大連線控制代碼限制。所以只需要一個執行緒負責Selector的輪詢,就可以接入成千上萬的客戶端。

AIO

是在NIO的基礎上引入非同步通道的概念,實現非同步非阻塞式的IO處理。如下圖(網路截圖):

AIO不需要通過多路複用器對註冊的通道進行輪詢操作即可實現非同步讀寫。什麼意思呢?NIO採用輪詢的方式,一直在輪詢的詢問stream中資料是否準備就緒,如果準備就緒發起處理。但是AIO就不需要了,AIO框架在windows下使用windows IOCP技術,在Linux下使用epoll多路複用IO技術模擬非同步IO, 即:應用程式向作業系統註冊IO監聽,然後繼續做自己的事情。作業系統發生IO事件,並且準備好資料後,在主動通知應用程式,觸發相應的函式(這就是一種以訂閱者模式進行的改造)。由於應用程式不是“輪詢”方式而是訂閱-通知方式,所以不再需要selector輪詢,由channel通道直接到作業系統註冊監聽。

NIO(AIO)中幾個概念

緩衝區 Buffer

NIO基於塊進行資料處理,在NIO中所有資料的讀取都是通過緩衝Buffer進行處理。

具體的快取區有這些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他們實現了相同的介面:Buffer。

通道 Channel

對資料的讀取和寫入要通過Channel通道。通道不同於流的地方就是通道是雙向的,用於讀、寫和同時讀寫操作。底層的作業系統的通道一般都是全雙工的,全雙工的Channel比流能更好的對映底層作業系統的API。

多路複用器 Selector

Selector提供選擇已經就緒的任務的能力:

Selector輪詢註冊在其上的Channel,如果某個Channel發生讀寫請求並且Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。(同步)

一個Selector可以同時輪詢多個Channel,因為JDK使用了epoll()代替傳統的select實現,所以沒有最大連線控制代碼1024/2048的限制。所以,只需要一個執行緒負責Selector的輪詢,就可以接入成千上萬的客戶端。(非阻塞)

NIO和AIO

NIO:會等資料準備好後,再交由應用進行處理,資料的讀取/寫入過程依然在應用執行緒中完成,只是將等待的時間剝離到單獨的執行緒中去,節省了資料準備時間,因為多路複用機制,Selector會得到複用,對於那些讀寫過程時間長的,NIO就不太適合。

AIO:讀完(核心記憶體拷貝到使用者記憶體)了系統再通知應用,使用回撥函式,進行業務處理,AIO能夠勝任那些重量級,讀寫過程長的任務。

java IO、NIO、AIO詳解

正文

回到頂部

概述

在我們學習Java的IO流之前,我們都要了解幾個關鍵詞

  • 同步與非同步(synchronous/asynchronous):同步是一種可靠的有序執行機制,當我們進行同步操作時,後續的任務是等待當前呼叫返回,才會進行下一步;而非同步則相反,其他任務不需要等待當前呼叫返回,通常依靠事件、回撥等機制來實現任務間次序關係
  • 阻塞與非阻塞:在進行阻塞操作時,當前執行緒會處於阻塞狀態,無法從事其他任務,只有當條件就緒才能繼續,比如ServerSocket新連線建立完畢,或者資料讀取、寫入操作完成;而非阻塞則是不管IO操作是否結束,直接返回,相應操作在後臺繼續處理

同步和非同步的概念:實際的I/O操作

同步是使用者執行緒發起I/O請求後需要等待或者輪詢核心I/O操作完成後才能繼續執行

非同步是使用者執行緒發起I/O請求後仍需要繼續執行,當核心I/O操作完成後會通知使用者執行緒,或者呼叫使用者執行緒註冊的回撥函式

阻塞和非阻塞的概念:發起I/O請求

阻塞是指I/O操作需要徹底完成後才能返回使用者空間

非阻塞是指I/O操作被呼叫後立即返回一個狀態值,無需等I/O操作徹底完成

BIO、NIO、AIO的概述

首先,傳統的 java.io包,它基於流模型實現,提供了我們最熟知的一些 IO 功能,比如 File 抽象、輸入輸出流等。互動方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,執行緒會一直阻塞在那裡,它們之間的呼叫是可靠的線性順序。

java.io包的好處是程式碼比較簡單、直觀,缺點則是 IO 效率和擴充套件性存在侷限性,容易成為應用效能的瓶頸。

很多時候,人們也把 java.net下面提供的部分網路 API,比如 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,因為網路通訊同樣是 IO 行為。

第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以構建多路複用的、同步非阻塞 IO 程式,同時提供了更接近作業系統底層的高效能資料操作方式。

第三,在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了非同步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。非同步 IO 操作基於事件和回撥機制,可以簡單理解為,應用操作直接返回,而不會阻塞在那裡,當後臺處理完成,作業系統會通知相應執行緒進行後續工作。

回到頂部

一、IO流(同步、阻塞)

1、概述

IO流簡單來說就是input和output流,IO流主要是用來處理裝置之間的資料傳輸,Java IO對於資料的操作都是通過流實現的,而java用於操作流的物件都在IO包中。

2、分類

按操作資料分為:位元組流(InputStream、OutputStream)和字元流(Reader、Writer)

按流向分:輸入流(Reader、InputStream)和輸出流(Writer、OutputStream)

3、字元流

概述

只用來處理文字資料

資料最常見的表現形式是檔案,字元流用來操作檔案的子類一般是FileReader和FileWriter

字元流讀寫檔案注意事項:

  • 寫入檔案必須要用flush()重新整理
  • 用完流記得要關閉流
  • 使用流物件要丟擲IO異常
  • 定義檔案路徑時,可以用"/"或者"\"
  • 在建立一個檔案時,如果目錄下有同名檔案將被覆蓋
  • 在讀取檔案時,必須保證該檔案已存在,否則丟擲異常

字元流的緩衝區

  • 緩衝區的出現是為了提高流的操作效率而出現的
  • 需要被提高效率的流作為引數傳遞給緩衝區的建構函式
  • 在緩衝區中封裝了一個數組,存入資料後一次取出

4、位元組流

概述

用來處理媒體資料

位元組流讀寫檔案注意事項:

  • 位元組流和字元流的基本操作是相同的,但是想要操作媒體流就需要用到位元組流
  • 位元組流因為操作的是位元組,所以可以用來操作媒體檔案(媒體檔案也是以位元組儲存的)
  • 輸入流(InputStream)、輸出流(OutputStream)
  • 位元組流操作可以不用重新整理流操作
  • InputStream特有方法:int available()(返回檔案中的位元組個數)

位元組流的緩衝區
位元組流緩衝區跟字元流緩衝區一樣,也是為了提高效率

5、Java Scanner類

Java 5添加了java.util.Scanner類,這是一個用於掃描輸入文字的新的實用程式

關於nextInt()、next()、nextLine()的理解

nextInt():只能讀取數值,若是格式不對,會丟擲java.util.InputMismatchException異常

next():遇見第一個有效字元(非空格,非換行符)時,開始掃描,當遇見第一個分隔符或結束符(空格或換行符)時,結束掃描,獲取掃描到的內容

nextLine():可以掃描到一行內容並作為字串而被捕獲到

關於hasNext()、hasNextLine()、hasNextxxx()的理解

就是為了判斷輸入行中是否還存在xxx的意思

與delimiter()有關的方法

應該是輸入內容的分隔符設定,

回到頂部

二、NIO(同步、非阻塞)

NIO之所以是同步,是因為它的accept/read/write方法的核心I/O操作都會阻塞當前執行緒

首先,我們要先了解一下NIO的三個主要組成部分:Channel(通道)、Buffer(緩衝區)、Selector(選擇器)

(1)Channel(通道)

Channel(通道):Channel是一個物件,可以通過它讀取和寫入資料。可以把它看做是IO中的流,不同的是:

  • Channel是雙向的,既可以讀又可以寫,而流是單向的
  • Channel可以進行非同步的讀寫
  • 對Channel的讀寫必須通過buffer物件

正如上面提到的,所有資料都通過Buffer物件處理,所以,您永遠不會將位元組直接寫入到Channel中,相反,您是將資料寫入到Buffer中;同樣,您也不會從Channel中讀取位元組,而是將資料從Channel讀入Buffer,再從Buffer獲取這個位元組。

因為Channel是雙向的,所以Channel可以比流更好地反映出底層作業系統的真實情況。特別是在Unix模型中,底層作業系統通常都是雙向的。

在Java NIO中的Channel主要有如下幾種型別:

  • FileChannel:從檔案讀取資料的
  • DatagramChannel:讀寫UDP網路協議資料
  • SocketChannel:讀寫TCP網路協議資料
  • ServerSocketChannel:可以監聽TCP連線

(2)Buffer

Buffer是一個物件,它包含一些要寫入或者讀到Stream物件的。應用程式不能直接對 Channel 進行讀寫操作,而必須通過 Buffer 來進行,即 Channel 是通過 Buffer 來讀寫資料的。

在NIO中,所有的資料都是用Buffer處理的,它是NIO讀寫資料的中轉池。Buffer實質上是一個數組,通常是一個位元組資料,但也可以是其他型別的陣列。但一個緩衝區不僅僅是一個數組,重要的是它提供了對資料的結構化訪問,而且還可以跟蹤系統的讀寫程序。

使用 Buffer 讀寫資料一般遵循以下四個步驟:

1.寫入資料到 Buffer;

2.呼叫 flip() 方法;

3.從 Buffer 中讀取資料;

4.呼叫 clear() 方法或者 compact() 方法。

當向 Buffer 寫入資料時,Buffer 會記錄下寫了多少資料。一旦要讀取資料,需要通過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到 Buffer 的所有資料。

一旦讀完了所有的資料,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:呼叫 clear() 或 compact() 方法。clear() 方法會清空整個緩衝區。compact() 方法只會清除已經讀過的資料。任何未讀的資料都被移到緩衝區的起始處,新寫入的資料將放到緩衝區未讀資料的後面。

Buffer主要有如下幾種:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

copyFile例項(NIO)

CopyFile是一個非常好的讀寫結合的例子,我們將通過CopyFile這個實力讓大家體會NIO的操作過程。CopyFile執行三個基本的操作:建立一個Buffer,然後從原始檔讀取資料到緩衝區,然後再將緩衝區寫入目標檔案。

public static void copyFileUseNIO(String src,String dst) throws IOException{
//宣告原始檔和目標檔案
        FileInputStream fi=new FileInputStream(new File(src));
        FileOutputStream fo=new FileOutputStream(new File(dst));
        //獲得傳輸通道channel
        FileChannel inChannel=fi.getChannel();
        FileChannel outChannel=fo.getChannel();
        //獲得容器buffer
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        while(true){
            //判斷是否讀完檔案
            int eof =inChannel.read(buffer);
            if(eof==-1){
                break;  
            }
            //重設一下buffer的position=0,limit=position
            buffer.flip();
            //開始寫
            outChannel.write(buffer);
            //寫完要重置buffer,重設position=0,limit=capacity
            buffer.clear();
        }
        inChannel.close();
        outChannel.close();
        fi.close();
        fo.close();
}   

(三)Selector(選擇器物件)

首先需要了解一件事情就是執行緒上下文切換開銷會在高併發時變得很明顯,這是同步阻塞方式的低擴充套件性劣勢。

Selector是一個物件,它可以註冊到很多個Channel上,監聽各個Channel上發生的事件,並且能夠根據事件情況決定Channel讀寫。這樣,通過一個執行緒管理多個Channel,就可以處理大量網路連線了。

selector優點

有了Selector,我們就可以利用一個執行緒來處理所有的channels。執行緒之間的切換對作業系統來說代價是很高的,並且每個執行緒也會佔用一定的系統資源。所以,對系統來說使用的執行緒越少越好。

1.如何建立一個Selector

Selector 就是您註冊對各種 I/O 事件興趣的地方,而且當那些事件發生時,就是這個物件告訴您所發生的事件。

Selector selector = Selector.open();

2.註冊Channel到Selector

為了能讓Channel和Selector配合使用,我們需要把Channel註冊到Selector上。通過呼叫 channel.register()方法來實現註冊:

channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

注意,註冊的Channel 必須設定成非同步模式 才可以,否則非同步IO就無法工作,這就意味著我們不能把一個FileChannel註冊到Selector,因為FileChannel沒有非同步模式,但是網路程式設計中的SocketChannel是可以的。

3.關於SelectionKey

請注意對register()的呼叫的返回值是一個SelectionKey。 SelectionKey 代表這個通道在此 Selector 上註冊。當某個 Selector 通知您某個傳入事件時,它是通過提供對應於該事件的 SelectionKey 來進行的。SelectionKey 還可以用於取消通道的註冊。

SelectionKey中包含如下屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

(1)Interest set

就像我們在前面講到的把Channel註冊到Selector來監聽感興趣的事件,interest set就是你要選擇的感興趣的事件的集合。你可以通過SelectionKey物件來讀寫interest set:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE; 

通過上面例子可以看到,我們可以通過用AND 和SelectionKey 中的常量做運算,從SelectionKey中找到我們感興趣的事件。

(2)Ready Set

ready set 是通道已經準備就緒的操作的集合。在一次選Selection之後,你應該會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:

int readySet = selectionKey.readyOps();

可以用像檢測interest集合那樣的方法,來檢測Channel中什麼事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布林型別:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

(3)Channel 和 Selector

我們可以通過SelectionKey獲得Selector和註冊的Channel:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector(); 

(4)Attach一個物件

可以將一個物件或者更多資訊attach 到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集資料的某個物件。使用方法如下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

還可以在用register()方法向Selector註冊Channel的時候附加物件。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

4.關於SelectedKeys()

生產系統中一般會額外進行就緒狀態檢查

一旦呼叫了select()方法,它就會返回一個數值,表示一個或多個通道已經就緒,然後你就可以通過呼叫selector.selectedKeys()方法返回的SelectionKey集合來獲得就緒的Channel。請看演示方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

當你通過Selector註冊一個Channel時,channel.register()方法會返回一個SelectionKey物件,這個物件就代表了你註冊的Channel。這些物件可以通過selectedKeys()方法獲得。你可以通過迭代這些selected key來獲得就緒的Channel,下面是演示程式碼:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) { 
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}

這個迴圈遍歷selected key的集合中的每個key,並對每個key做測試來判斷哪個Channel已經就緒。

請注意迴圈中最後的keyIterator.remove()方法。Selector物件並不會從自己的selected key集合中自動移除SelectionKey例項。我們需要在處理完一個Channel的時候自己去移除。當下一次Channel就緒的時候,Selector會再次把它新增到selected key集合中。

SelectionKey.channel()方法返回的Channel需要轉換成你具體要處理的型別,比如是ServerSocketChannel或者SocketChannel等等。

(4)NIO多路複用

主要步驟和元素:

  • 首先,通過 Selector.open() 建立一個 Selector,作為類似排程員的角色。

  • 然後,建立一個 ServerSocketChannel,並且向 Selector 註冊,通過指定 SelectionKey.OP_ACCEPT,告訴排程員,它關注的是新的連線請求。

  • 注意,為什麼我們要明確配置非阻塞模式呢?這是因為阻塞模式下,註冊操作是不允許的,會丟擲 IllegalBlockingModeException 異常。

  • Selector 阻塞在 select 操作,當有 Channel 發生接入請求,就會被喚醒。

  • 在 具體的 方法中,通過 SocketChannel 和 Buffer 進行資料操作

IO 都是同步阻塞模式,所以需要多執行緒以實現多工處理。而 NIO 則是利用了單執行緒輪詢事件的機制,通過高效地定位就緒的 Channel,來決定做什麼,僅僅 select 階段是阻塞的,可以有效避免大量客戶端連線時,頻繁執行緒切換帶來的問題,應用的擴充套件能力有了非常大的提高

回到頂部

三、NIO2(非同步、非阻塞)

AIO是非同步IO的縮寫,雖然NIO在網路操作中,提供了非阻塞的方法,但是NIO的IO行為還是同步的。對於NIO來說,我們的業務執行緒是在IO操作準備好時,得到通知,接著就由這個執行緒自行進行IO操作,IO操作本身是同步的。

但是對AIO來說,則更加進了一步,它不是在IO準備好時再通知執行緒,而是在IO操作已經完成後,再給執行緒發出通知。因此AIO是不會阻塞的,此時我們的業務邏輯將變成一個回撥函式,等待IO操作完成後,由系統自動觸發。

與NIO不同,當進行讀寫操作時,只須直接呼叫API的read或write方法即可。這兩種方法均為非同步的,對於讀操作而言,當有流可讀取時,作業系統會將可讀的流傳入read方法的緩衝區,並通知應用程式;對於寫操作而言,當作業系統將write方法傳遞的流寫入完畢時,作業系統主動通知應用程式。 即可以理解為,read/write方法都是非同步的,完成後會主動呼叫回撥函式。 在JDK1.7中,這部分內容被稱作NIO.2,主要在Java.nio.channels包下增加了下面四個非同步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket程式設計中,服務端通道是AsynchronousServerSocketChannel,這個類提供了一個open()靜態工廠,一個bind()方法用於繫結服務端IP地址(還有埠號),另外還提供了accept()用於接收使用者連線請求。在客戶端使用的通道是AsynchronousSocketChannel,這個通道處理提供open靜態工廠方法外,還提供了read和write方法。

在AIO程式設計中,發出一個事件(accept read write等)之後要指定事件處理類(回撥函式),AIO中的事件處理類是CompletionHandler<V,A>,這個介面定義瞭如下兩個方法,分別在非同步操作成功和失敗時被回撥。

void completed(V result, A attachment);

void failed(Throwable exc, A attachment);

Linux的5種網路IO模型詳解

linux的五種IO模型,分別是:阻塞IO、非阻塞IO、多路複用IO、訊號驅動IO以及非同步IO。其中阻塞IO、非阻塞IO、多路複用IO、訊號驅動IO都屬於同步IO。

同步IO和非同步IO

同步IO:應用程式主動向核心查詢是否有可用資料,如果有自己負責把資料從核心copy到使用者空間。

非同步IO:應用程式向核心發起讀資料請求需要:(1)告訴核心資料存放位置(2)註冊回撥函式,當核心完成資料copy後呼叫回撥通知應用程式取資料。

同步IO/非同步IO最大區別:同步IO資料從核心空間到使用者空間的copy動作是由應用程式自己完成。而非同步IO則是註冊回撥函式並告知核心使用者空間緩衝區存放地址,資料copy由核心完成。

詳細介紹之前,援引網上《Linux 網路程式設計的5種IO模型:阻塞IO與非阻塞IO》中的一段話,便於簡單理解、記憶:

阻塞IO, 給女神發一條簡訊, 說我來找你了, 然後就默默的一直等著女神下樓, 這個期間除了等待你不會做其他事情, 屬於備胎做法.
非阻塞IO, 給女神發簡訊, 如果不回, 接著再發, 一直髮到女神下樓, 這個期間你除了發簡訊等待不會做其他事情, 屬於專一做法.
IO多路複用, 是找一個宿管大媽來幫你監視下樓的女生, 這個期間你可以些其他的事情. 例如可以順便看看其他妹子,玩玩王者榮耀, 上個廁所等等. IO複用又包括 select, poll, epoll 模式. 那麼它們的區別是什麼?

  • 1)select大媽每一個女生下樓, select大媽都不知道這個是不是你的女神, 她需要一個一個詢問, 並且select大媽能力還有限, 最多一次幫你監視1024個妹子

  • 2)poll大媽不限制盯著女生的數量, 只要是經過宿舍樓門口的女生, 都會幫你去問是不是你女神

  • 3)epoll大媽不限制盯著女生的數量, 並且也不需要一個一個去問. 那麼如何做呢? epoll大媽會為每個進宿舍樓的女生臉上貼上一個大字條,上面寫上女生自己的名字, 只要女生下樓了, epoll大媽就知道這個是不是你女神了, 然後大媽再通知你。
    上面這些同步IO有一個共同點就是, 當女神走出宿舍門口的時候, 你已經站在宿舍門口等著女神的, 此時你屬於阻塞狀態

    接下來是非同步IO的情況:
    你告訴女神我來了, 然後你就去打遊戲了, 一直到女神下樓了, 發現找不見你了, 女神再給你打電話通知你, 說我下樓了, 你在哪呢? 這時候你才來到宿舍門口。 此時屬於逆襲做法

以下每種模型都有三種圖示和描述,各人可以選擇自己容易理解的部分進行記憶。

1、阻塞IO模型

最傳統的一種IO模型,即在讀寫資料過程中會發生阻塞現象。

當用戶執行緒發出IO請求之後,核心會去檢視資料是否就緒,如果沒有就緒就會等待資料就緒,而使用者執行緒就會處於阻塞狀態,使用者執行緒交出CPU。當資料就緒之後,核心會將資料拷貝到使用者執行緒,並返回結果給使用者執行緒,使用者執行緒才解除block狀態。

程式碼如下:

printf("Calling recv(). \n");
ret =  recv(socket, recv_buf, sizeof(recv_buf), 0); 
printf("Had called recv(). \n")

也許有人會說,可以採用多執行緒+ 阻塞IO 來解決效率問題,但是由於在多執行緒 + 阻塞IO 中,每個socket對應一個執行緒,這樣會造成很大的資源佔用,並且尤其是對於長連線來說,執行緒的資源一直不會釋放,如果後面陸續有很多連線的話,就會造成效能上的瓶頸。


當用戶程序呼叫了recvfrom這個系統呼叫,kernel就開始了IO的第一個階段:準備資料(對於網路IO來說,很多時候資料在一開始還沒有到達。比如,還沒有收到一個完整的UDP包。這個時候kernel就要等待足夠的資料到來)。這個過程需要等待,也就是說資料被拷貝到作業系統核心的緩衝區中是需要一個過程的。而在使用者程序這邊,整個程序會被阻塞(當然,是程序自己選擇的阻塞)。當kernel一直等到資料準備好了,它就會將資料從kernel中拷貝到使用者記憶體,然後kernel返回結果,使用者程序才解除block的狀態,重新執行起來。

所以,blocking IO的特點就是在IO執行的兩個階段都被block了。

應用程式請求核心讀取資料,核心資料資料緩衝區無資料或者資料未就緒前,阻塞等待。核心系統等待資料準備就緒後,拷貝資料到使用者空間,待拷貝完成後,返回結果,使用者程式接觸阻塞,處理資料。

2、非阻塞IO模型

當用戶執行緒發起一個IO操作後,並不需要等待,而是馬上就得到了一個結果。如果結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送IO操作。一旦核心中的資料準備好了,並且又再次收到了使用者執行緒的請求,那麼核心它馬上就將資料拷貝到了使用者執行緒,然後返回。

在非阻塞IO模型中,使用者執行緒需要不斷地詢問核心資料是否就緒,也就說非阻塞IO不會交出CPU,而會一直佔用CPU

對於非阻塞IO就有一個非常嚴重的問題,在while迴圈中需要不斷地去詢問核心資料是否就緒,這樣會導致CPU佔用率非常高,因此一般情況下很少使用while迴圈這種方式來讀取資料。

while(1)
{
    printf("Calling recv(). \n");
    ret =  recv(socket, recv_buf, sizeof(recv_buf), 0); 
    if (EAGAIN == ret) {continue;}
    else if(ret > -1) { break;}
    printf("Had called recv(), retry.\n");
}

Linux下,可以通過設定socket使其變為non-blocking。

當用戶程序發出read操作時,如果kernel中的資料還沒有準備好,那麼它並不會block使用者程序,而是立刻返回一個error。從使用者程序角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。使用者程序判斷結果是一個error時,它就知道資料還沒有準備好,於是它可以再次傳送read操作。一旦kernel中的資料準備好了,並且又再次收到了使用者程序的system call,那麼核心它馬上就將資料拷貝到了使用者記憶體,然後返回。

所以,nonblocking IO的特點是使用者程序需要不斷的主動詢問kernel資料好了沒有

應用程式請求核心讀取資料,核心直接返回結果,如果資料未準備就緒,則返回error,應用程式繼續請求,周而復始,直到核心資料準備就緒後,當核心再次收到應用程式請求後,將資料拷貝到使用者空間,待拷貝完成後返回ok,應用程式處理資料。

3、IO多路複用模型

I/O多路複用是作業系統級別的,屬於linux作業系統的五種I/O模型中的一種,是作業系統級別同步非阻塞的。作業系統級別的非同步I/O才是真正非同步非阻塞的。(參見:https://www.zhihu.com/question/59975081/answer/837766592

所謂I/O多路複用機制,就是說通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。這種機制的使用需要額外的功能來配合: select、poll、epoll。

  • select、poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的
  • select時間複雜度O(n),它僅僅知道了,有I/O事件發生了,卻並不知道是哪幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出資料,或者寫入資料的流,對他們進行操作。所以select具有O(n)的無差別輪詢複雜度,同時處理的流越多,無差別輪詢時間就越長。
  • poll(翻譯:輪詢)時間複雜度O(n),poll本質上和select沒有區別,它將使用者傳入的陣列拷貝到核心空間,然後查詢每個fd對應的裝置狀態,但是它沒有最大連線數的限制,原因是它是基於連結串列來儲存的.
  • epoll時間複雜度O(1),epoll可以理解為event poll,不同於忙輪詢和無差別輪詢,epoll會把哪個流發生了怎樣的I/O事件通知我們。所以我們說epoll實際上是事件驅動(每個事件關聯上fd)的,此時我們對這些流的操作都是有意義的。(複雜度降低到了O(1))。

在多路複用IO模型中,會有一個核心執行緒不斷去輪詢多個socket的狀態,只有當真正讀寫事件發生時,才真正呼叫實際的IO讀寫操作。因為在多路複用IO模型中,只需要使用一個執行緒就可以管理多個socket,系統不需要建立新的程序或者執行緒,也不必維護這些執行緒和程序,並且只有在真正有讀寫事件進行時,才會使用IO資源,所以它大大減少了資源佔用。

IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在於單個process就可以同時處理多個網路連線的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有資料到達了,就通知使用者程序。

當用戶程序呼叫了select,那麼整個程序就會被block,而同時,kernel會 “監視”所有select負責的socket,當任何一個socket中的資料準備好了,select就會返回。這個時候使用者程序再呼叫read操作,將資料從kernel拷貝到使用者程序。所以,IO多路複用的特點是通過一種機制一個程序能同時等待多個檔案描述符,而這些檔案描述符(套接字描述符)其中的任意一個進入就緒狀態,select()函式就可以返回。

這裡需要使用兩個system call(select 和 recvfrom),而blocking IO只調用了一個system call(recvfrom)。但是,用select的優勢在於它可以同時處理多個connection。

如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用mutil-threading + blocking IO的web server效能更好,可能延遲還更大。select/epoll 的優勢並不是對於單個連線能處理得更好,而是在於效能更多的連線。

應用程式請求核心讀取資料,首先呼叫了select,核心監控select監控的所有socket,當有任何一個socket資料準備就緒後,就返回給使用者程序可讀,然後使用者程序再次向核心傳送讀取指令,核心將資料拷貝到使用者空間,並返回結果,使用者程序獲得資料後進行處理。這裡關於select、poll、epoll的區別不在這裡描述,參見:《Linux 網路程式設計的5種IO模型:多路複用(select/poll/epoll)》、《select、poll、epoll之間的區別(搜狗面試)》

4.訊號驅動IO模型

在訊號驅動IO模型中,當用戶執行緒發起一個IO請求操作,會給對應的socket註冊一個訊號函式,然後使用者執行緒會繼續執行,當核心資料就緒時會發送一個訊號給使用者執行緒,使用者執行緒接收到訊號之後,便在訊號函式中呼叫IO讀寫操作來進行實際的IO請求操作。這個一般用於UDP中,對TCP套介面幾乎是沒用的,原因是該訊號產生得過於頻繁,並且該訊號的出現並沒有告訴我們發生了什麼事情。

在UDP上,SIGIO訊號會在下面兩個事件的時候產生:

1 資料報到達套接字

2 套接字上發生錯誤

因此我們很容易判斷SIGIO出現的時候,如果不是發生錯誤,那麼就是有資料報到達了。

而在TCP上,由於TCP是雙工的,它的訊號產生過於頻繁,並且訊號的出現幾乎沒有告訴我們發生了什麼事情。因此對於TCP套接字,SIGIO訊號是沒有什麼使用的。

有關函式

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
             struct sigaction *oldact);

關於有關內容的講解,請參考:Linux 系統程式設計 學習:程序間通訊-Unix IPC-訊號

5、非同步IO模型

前面四種IO模型實際上都屬於同步IO,只有最後一種是真正的非同步IO,因為無論是多路複用IO還是訊號驅動模型,IO操作的第2個階段都會引起使用者執行緒阻塞,也就是核心進行資料拷貝的過程都會讓使用者執行緒阻塞。

導言

兩種高效能IO設計模式

在傳統的網路服務設計模式中,有兩種比較經典的模式:多執行緒與執行緒池。

多執行緒

對於多執行緒模式,也就說來了client,伺服器就會新建一個執行緒來處理該client的讀寫事件,如下圖所示:

這種模式雖然處理起來簡單方便,但是由於伺服器為每個client的連線都採用一個執行緒去處理,使得資源佔用非常大。因此,當連線數量達到上限時,再有使用者請求連線,直接會導致資源瓶頸,嚴重的可能會直接導致伺服器崩潰。

執行緒池

因此,為了解決這種一個執行緒對應一個客戶端模式帶來的問題,提出了採用執行緒池的方式,也就說建立一個固定大小的執行緒池,來一個客戶端,就從執行緒池取一個空閒執行緒來處理,當客戶端處理完讀寫操作之後,就交出對執行緒的佔用。因此這樣就避免為每一個客戶端都要建立執行緒帶來的資源浪費,使得執行緒可以重用。

但是執行緒池也有它的弊端,如果連線大多是長連線,因此可能會導致在一段時間內,執行緒池中的執行緒都被佔用,那麼當再有使用者請求連線時,由於沒有可用的空閒執行緒來處理,就會導致客戶端連線失敗,從而影響使用者體驗。因此,執行緒池比較適合大量的短連線應用。

高效能IO模型

因此便出現了下面的兩種高效能IO設計模式:Reactor和Proactor。Proactor前攝器模式和Reactor反應器模式。兩個模式不同的地方在於,Proactor用於非同步IO,而Reactor用於同步IO。

Reactor

在Reactor模式中,會先對每個client註冊感興趣的事件,然後有一個執行緒專門去輪詢每個client是否有事件發生,當有事件發生時,便順序處理每個事件,當所有事件處理完之後,便再轉去繼續輪詢,如下圖所示:

從這裡可以看出,多路複用IO就是採用Reactor模式。

注意,上面的圖中展示的 是順序處理每個事件,當然為了提高事件處理速度,可以通過多執行緒或者執行緒池的方式來處理事件。

Proactor

在Proactor模式中:當檢測到有事件發生時,會新起一個非同步操作,然後交由核心執行緒去處理,當核心執行緒完成IO操作之後,傳送一個通知告知操作已完成;可以得知,非同步IO模型採用的就是Proactor模式。

非同步IO模型是比較理想的IO模型,在非同步IO模型中,當用戶執行緒發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從核心的角度,當它受到一個asynchronous read之後,它會立刻返回,說明read請求已經成功發起了,因此不會對使用者執行緒產生任何block。然後,核心會等待資料準備完成,然後將資料拷貝到使用者執行緒,當這一切都完成之後,核心會給使用者執行緒傳送一個訊號,告訴它read操作完成了。也就說使用者執行緒完全不需要關心實際的整個IO操作是如何進行的,只需要先發起一個請求,當接收核心返回的成功訊號時表示IO操作已經完成,可以直接去使用資料了。

也就說在非同步IO模型中,IO操作的兩個階段都不會阻塞使用者執行緒,這兩個階段都是由核心自動完成,然後傳送一個訊號告知使用者執行緒操作已完成。使用者執行緒中不需要再次呼叫IO函式進行具體的讀寫

使用者程序發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對使用者程序產生任何block。然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程序傳送一個signal,告訴它read操作完成了。

應用程式請求核心讀取資料後不等待,執行其它事項,核心待資料就緒後直接拷貝到使用者空間,併發送訊號給應用程式,應用程式收到訊號後處理資料。

參考:

https://www.cnblogs.com/schips/p/12583129.htmlLinux 網路程式設計的5種IO模型 總結

https://www.cnblogs.com/schips/p/12543650.htmlLinux 網路程式設計的5種IO模型:阻塞IO與非阻塞IO

https://www.cnblogs.com/schips/p/12568408.htmlLinux 網路程式設計的5種IO模型:多路複用(select/poll/epoll)

https://www.cnblogs.com/schips/p/12575493.html Linux 網路程式設計的5種IO模型:訊號驅動IO模型

https://www.cnblogs.com/schips/p/12575933.html Linux 網路程式設計的5種IO模型:非同步IO模型

https://www.cnblogs.com/natian-ws/p/10785649.htmlLinux IO模式及 select、poll、epoll詳解

https://blog.csdn.net/coolgw2015/article/details/79719328面試之多路複用

https://blog.csdn.net/yfkscu/article/details/38141635?locationNum=7常見Linux IO模型分析