Java IO:unix5種IO模型,及Java的3種IO

1.UNIX 5種IO模型

一個輸入操作通常包括兩個階段:

(1)等待數據準備好
(2)從內核向進程複製數據

對於一個套接字上的輸入操作,第一步通常涉及等待數據從網絡中到達。當所等待數據到達時,它被複制到內核中的某個緩衝區。第二步就是把數據從內核緩衝區複製到應用進程緩衝區。

UNIX 5種IO模型: 阻塞IO、非阻塞IO、IO複用、信號驅動IO和異步IO。

(1)阻塞與非阻塞:第一階段的區分。
阻塞的有:阻塞IO、IO複用。
非阻塞的有:非阻塞IO、信號驅動IO、異步IO。
(2)同步與異步:第二階段的區分,從內核拷貝數據到用戶進程這個階段,發生阻塞的是同步,不發生阻塞的是異步。
前面四種都是同步的,異步的只有最後一種。

UNIX網絡編程書中,使用的例子都是基於UDP的,因爲就UDP而言,數據準備好的概念比較簡單,要麼整個數據報已經收到,要麼還沒有,而對於TCP而言,需要額外的標記,如套接字低潮標記(low-water mark)等,額外開銷,變得複雜。
本節將 recvfrom函數 作爲 系統調用
阻塞IO
在阻塞IO中,進程調用recvfrom,其系統調用直到數據報到達內核,並且被拷貝到應用進程的緩衝區或者發生錯誤時才返回。整個階段都是被阻塞的。recvfrom成功返回後,應用進程處理數據報。
在這裏插入圖片描述
非阻塞IO中:在等待數據階段,程序一直調用recvfrom函數,如果數據沒有準備好,則系統返回一個錯誤碼,如果準備好了,就將數據拷貝到用戶程序緩衝區,併成功返回。
如上圖:前三次調用時,沒有數據可返回,則返回錯誤碼。第四次調用時,已經有一個數據報準備好了,將它拷貝到進程緩衝區,於是返回成功。
輪詢(polling):像上面那樣一直非阻塞地循環調用recvfrom的情況,就叫輪詢。進程持續輪詢內核,以查看操作系統是否就緒。這麼做往往消耗大量CPU時間。
在這裏插入圖片描述
IO複用:我們可以調用select或者poll,這樣阻塞就會阻塞在這兩個系統調用中的某一個上,而不是阻塞在真正的IO系統調用上。
如上圖:先啓動select調用,阻塞,等待數據報套接口變爲可讀。當select返回可讀條件時,再調用recvfrom把數據報拷貝到進程緩衝區。
在這裏插入圖片描述
信號驅動IO:先開啓套接口的信號驅動IO功能,並通過sigaction系統調用安裝一個信號處理函數。該系統調用立即返回,我們的進程可以繼續工作,就是說它沒有被阻塞。當數據報準備好,內核就爲該進程產生一個SIGIO信號,隨後,進程就可以在信號處理函數中調用recvfrom來讀取數據。
在這裏插入圖片描述
異步IO:異步IO和信號驅動IO的區別在於:信號驅動IO是內核準備好數據,就通知用戶進程。而異步IO是內核IO完成(即數據拷貝完成)才通知進程。
在這裏插入圖片描述

2. java 中的IO、NIO、AIO

一、IO流(同步、阻塞)

1、概述

IO流簡單來說就是input和output流,IO流主要是用來處理設備之間的數據傳輸,Java IO對於數據的操作都是通過流實現的,而java用於操作流的對象都在IO包中。

2、分類

按操作數據分爲:字節流(Reader、Writer)和字符流(InputStream、OutputStream)

按流向分:輸入流(Reader、InputStream)和輸出流(Writer、OutputStream)
在這裏插入圖片描述

3、字符流

(1)概述
  • 只用來處理文本數據

  • 數據最常見的表現形式是文件,字符流用來操作文件的子類一般是FileReader和FileWriter

  • 字符流讀寫文件注意事項:

(2)寫入文件必須要用flush()刷新
  • 用完流記得要關閉流
  • 使用流對象要拋出IO異常
  • 定義文件路徑時,可以用"/“或者”"
  • 在創建一個文件時,如果目錄下有同名文件將被覆蓋
  • 在讀取文件時,必須保證該文件已存在,否則拋出異常
  • 字符流的緩衝區
  • 緩衝區的出現是爲了提高流的操作效率而出現的
  • 需要被提高效率的流作爲參數傳遞給緩衝區的構造函數
  • 在緩衝區中封裝了一個數組,存入數據後一次取出

4、字節流

(1)概述

用來處理媒體數據

(2)字節流讀寫文件注意事項:
  • 字節流和字符流的基本操作是相同的,但是想要操作媒體流就需要用到字節流
  • 字節流因爲操作的是字節,所以可以用來操作媒體文件(媒體文件也是以字節存儲的)
  • 輸入流(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,然後從源文件讀取數據到緩衝區,然後再將緩衝區寫入目標文件。
在這裏插入圖片描述

(3)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);
發佈了20 篇原創文章 · 獲贊 2 · 訪問量 2607
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章