前文開了高併發學習的頭,文末說了將會選擇NIO、RPC相關資料作進一步學習,因此本文開始學習NIO知識。html
IO知識回顧java
在學習NIO前,有必要先回顧一下IO的一些知識。 算法
IO中的流編程
Java程序經過流(Stream)來完成輸入輸出。流是生產或者消費信息的抽象,流經過Java的輸入輸出與物理設備鏈接,儘管與之相連的物理設備不盡相同,可是全部的流的行爲都是同樣的,因此相同的輸入輸出類的功能和方法適用於全部的外部設備。這意味着一個輸入流能夠抽象多種類型的輸入,好比文件、鍵盤或者網絡套接字等,一樣的,一個輸出流也能夠輸出到控制檯、文件或者相連的網絡。segmentfault
流的分類數組
從功能上能夠將流分爲輸入流和輸出流。輸入和輸出是相對於程序來講的,程序在使用數據時所扮演的角色有兩個:一個是源,一個是目的。若程序是數據的源,對外輸出數據,咱們就稱這個數據流相對於程序來講是輸出流,若程序是數據的目的地,咱們就稱這個數據流相對於程序來講是輸入流。緩存
從結構上能夠將流分爲字節流和字符流,字節流以字節爲處理單位,字符流以字符爲處理單位。安全
從角色上能夠將流分爲節點流和過濾流。從特定的地方讀寫的流叫作節點流,如磁盤或者一塊內存區域,而過濾流則以節點流做爲輸入或者輸出,過濾流是使用一個已經存在的輸入流或者輸出流鏈接來建立的。 服務器
字節流的輸入流和輸出流的基礎是InputStream和OutputStream,字節流的輸入輸出操做由這兩個類的子類實現。字符流是Java1.1後新增的以字符爲單位進行輸入和輸出的流,字符流的輸入輸出的基礎是Reader和Writer。網絡
字節流(byte stream)爲處理字節的輸入和輸出提供了方便的方法,好比使用字節流讀取或者寫入二進制的數據。字符流(character stream)爲字符的輸入和輸出提供了方便,採用了統一的編碼標準,於是能夠國際化。值得注意的,在計算機的底層實現中,全部的輸入輸出其實都是以字節的形式進行的,字符流只是爲字符的處理提供了具體的方法。
輸入流
讀數據的邏輯爲:
open a stream
while more information
read information
close the stream
忽略掉異常處理,相關的代碼實現大體以下:
InputStream input = new FileInputStream("c:\\data\\input-text.txt"); int data = input.read(); while(data != -1) { //do something with data... doSomethingWithData(data); data = input.read(); } input.close();
輸出流
寫數據的邏輯爲:
open a stream
while more information
write information
close the stream
忽略掉異常處理,相關的代碼實現大體以下:
OutputStream output = new FileOutputStream("c:\\data\\output-text.txt"); while(hasMoreData()) { int data = getMoreData(); output.write(data); } output.close();
輸入流的類層次
輸出流的類層次
過濾流
在InputStream、OutputStream類的子類中,FilterInputStream和FilterOutputStream過濾流又派生出DataInputStream和DataOutputStream數據輸入輸出流等子類。
IO流的連接
Input Stream Chain:從外部文件往程序中寫入數據,因此第一步須要構造輸入流,同時也是節點流,FileInputStream,爲了使這個流具有緩衝的特性,須要從節點流轉成過濾流,BufferedInputStream,僅僅有緩衝特性可能還不能知足平常需求,還須要有讀取基本數據類型的特性,能夠基於現有的過濾流再轉成其餘的過濾流,DataInputStream,此時即可以方便的從文件中讀取數據;
Output Stream Chain:往外部文件中寫出數據,首先對於外部文件來講,是FileOutputStream,爲了使這個流具有緩衝的特性,須要從節點流轉成過濾流,BufferedOutputStream,僅僅有緩衝特性可能還不能知足平常需求,還須要有寫出基本數據類型的特性,能夠基於現有的過濾流再轉成其餘的過濾流,DataOutputStream,此時即可以方便的從寫各類數據類型;
Reader的類層次
Writer的類層次
至此,大概回顧了一下IO的部分基礎知識。
IO與裝飾模式
回到IO流的連接中,Input Stream Chain的通常代碼是這樣寫的:
InputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream("c:\\data\\input-text.txt")))
Output Stream Chain的通常代碼是這樣寫的:
OutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("c:\\data\\output-text.txt")))
實際上,這種一個流與另外一個流首尾相接,造成一個流管道的實現機制,其實就是裝飾模式的一種應用。
裝飾模式的套路
抽象構件角色(Component):給出一個抽象接口,以規範準備接收附加責任的對象
具體構件角色(ConcreteComponent):定義一個將要接收附加責任的類
裝飾角色(Decorator):持有一個構件(Component)對象引用,並定義一個與抽象構建接口一致的接口
具體裝飾角色(ConcreteDecorator):負責給構件對象貼上附加的責任
裝飾模式的代碼實現
讓咱們來看下代碼:
public interface Component { void doSomething(); } public class ConcreteComponent implements Component{ @Override public void doSomething() { System.out.println("功能A"); } } public class Decorator implements Component{//重點1 定義抽象構件接口一致的接口 private Component component;//重點2 持有構件對象的引用 public Decorator(Component component) { this.component = component; } @Override public void doSomething() { component.doSomething(); } }
最後是具體裝飾角色代碼:
public class ConcreteDecorator1 extends Decorator{ public ConcreteDecorator1(Component component) { super(component); } @Override public void doSomething() { super.doSomething(); this.doAnotherThing(); } public void doAnotherThing() { System.out.println("功能B"); } } public class ConcreteDecorator2 extends Decorator{ public ConcreteDecorator2(Component component) { super(component); } @Override public void doSomething() { super.doSomething(); this.doAnotherThing(); } public void doAnotherThing() { System.out.println("功能C"); } }
對於客戶端來講只須要以下簡單的代碼,便可完成對構件對象ConcreteComponent的裝飾:
Component component = new ConcreteDecorator1(new ConcreteDecorator2(new ConcreteComponent())); component.doSomething();
IO中對應的裝飾模式解釋
DataInputStream、BufferedInputStream的角色就像上述的ConcreteDecorator1和ConcreteDecorator2,FilterInputStream相似Decorator,InputStream就是Component。
在JDK的源碼中:
public class FilterInputStream extends InputStream { protected volatile InputStream in; protected FilterInputStream(InputStream in) { this.in = in;} public int read() throws IOException { return in.read();}
再看下Decorator:
public class Decorator implements Component{//重點1 定義抽象構件接口一致的接口 private Component component;//重點2 持有構件對象的引用 public Decorator(Component component) { this.component = component; } @Override public void doSomething() { component.doSomething(); } }
至此,咱們能夠知道IO是如何體現的裝飾模式。
爲何須要NIO
IO主要面向流數據,常常爲了處理個別字節或字符,就要執行好幾個對象層的方法調用。這種面向對象的處理方法,將不一樣的 IO 對象組合到一塊兒,提供了高度的靈活性(IO裏面的裝飾模式),但須要處理大量數據時,卻可能對執行效率形成致命傷害。IO的終極目標是效率,而高效的IO每每又沒法與對象造成一一對應的關係。高效的 IO 每每意味着您要選擇從A到B的最短路徑,而執行大量IO 操做時,複雜性毀了執行效率。傳統Java平臺上的IO抽象工做良好,適應用途普遍。可是當移動大量數據時,這些IO類可伸縮性不強,也沒有提供當今大多數操做系統廣泛具有的經常使用IO功能,如文件鎖定、非塊IO、就緒性選擇和內存映射。這些特性對實現可伸縮性是相當重要的,對保持與非 Java 應用程序的正常交互也能夠說是必不可少的,尤爲是在企業應用層面,而傳統的Java IO 機制卻沒有模擬這些通用IO服務。Java規範請求#51(JSR 51, https://jcp.org/en/jsr/detail?id=51)包含了對高速、可伸縮 I/O 特性的詳盡描述,藉助這一特性,底層操做系統的IO性能能夠獲得更好發揮。 JSR 51 的實現,其結果就是新增類組合到一塊兒,構成了 java.nio 及其子包,以及 java.util.regex 軟件包,同時現存軟件包也相應做了幾處修改。JCP 網站詳細介紹了 JSR 的運做流程,以及 NIO 從最初的提議到最終實現併發布的演進歷程。隨着 Merlin(Jdk1.4) 的發佈,操做系統強大的IO特性終於能夠藉助 Java 提供的工具獲得充分發揮。論及IO性能,Java不再遜於任何一款編程語言。
到這裏,咱們知道Java NIO的目的是爲了提升效率,充分利用操做系統提供的IO特性,因此爲了應對更多的處理請求,咱們須要新的IO模型(NIO)。
NIO的核心組件
這一節,咱們將開始學習NIO。
上文中曾提到,NIO的三個核心組件:Selector、Channel和Buffer。用一張圖來抽象這三者之間的關係。
在沒有Java NIO以前,傳統的IO對於網絡鏈接的處理,一般會採用Thread Per Task,即一線程一鏈接的模式,這種模式在中小量業務處理時基本上是能知足要求的,可是隨着鏈接數不斷增長,所建立的線程會不斷佔用內存空間,同時大量的線程也會帶來頻繁的上下文切換,CPU都用來操做上下文切換了,必然影響實際的業務處理。有了Java NIO以後,就能夠花少許的線程來處理大量的鏈接,在上圖中,Selector是對Linux下的select/poll/epoll的包裝,Channel是可IO操做的硬件、文件、socket、其餘程序組件的包裝,咱們能夠把Channel當作是網絡鏈接,網絡鏈接上主要有connect、accept、read和write等事件,Selector負責監聽這些事件的發生,一旦某個Channel上發生了某個事件,Thread切換到該Channel進行事件處理。類比到操做系統層面,若是是select/poll方式,應用進程對每一個socket(Channel)的文件描述符順序掃描,查看是否就緒,阻塞在select(Selector)上,若是就緒,調用recvfrom,若是是epoll方式,就不是順序掃描,而是提供回調函數,當文件描述符就緒時,直接調用回調函數,進一步提升效率。圖中還有一個組件就是Buffer,它實際上就是一塊內存,底層是基於數組來實現的,通常和Channel成對出現,數據的讀寫都是經過Buffer來實現的。
接下來,依次來看下Selector、Channel和Buffer。
Selector模塊
Selector
Selector是一個多路傳輸的SelectableChannel對象。
Selector能夠經過調用自身的open方法來進行建立,在open方法裏面是經過系統默認的selector provider來建立Selector的。固然也能夠經過調用openSelector來自定義一個Selector。一個Selector將會一直保持open的狀態直到調用close方法。
一個可選擇的Channel對象註冊到Selector的行爲是經過SelectionKey對象來體現的。Selector維護了三種SelectionKey的集合:
key set包含了當前的註冊到Selector的Channel對應的全部的keys,這些keys能夠經過keys()返回;
selected-key set的每一個成員都是相關的通道被選擇器(在前一個選擇操做
中)判斷爲已經準備好的,而且包含於鍵的 interest 集合中的操做。這個集合經過 selectedKeys()方法返回。selected-key set是key set的子集;
cancelled-key set是一個被取消的Channel可是還未被註銷的key的集合,這個集合沒法直接訪問,cancelled-key set也是key set的子集。
對於一個剛建立的Selector,上述三個集合默認都是空的。
經過調用Channel的register方法,一個新的key將會被添加到Selector的key set中。在執行selection操做時,cancelled keys將會從key set中移除,key set自己是不能被直接修改的。
無論是直接關閉Channel,仍是調用SelectionKey的close方法,都會有一個key被添加到cancelled-key set中。在下一個selection操做中,取消一個key的動做都會致使其對應的Channel被註銷掉,同時這個key也會從Selector的key set中移除。
在執行selection操做時,keys會被添加到selected-key set中,經過Set的remove方法或者是Iterator的remove方法,key能夠直接從selected-key set中移除,除此之外,沒有其餘的方法能夠達到這樣的效果。特別須要強調的是, 移除操做不是selection的反作用。key也不能直接添加到selected-key set中。
Selection
在每一次Selection操做中,keys有可能從selected-key set、key set或者cancelled-key set中增長或者刪除。Selection操做是經過select()、select(long)、selectNow()方法來執行的,通常包含以下三步:
一、cancelled-key set中的每個key均可以從它所屬的key set中移除,同時它所屬的Channel也會註銷,這一步將會使cancelled-key set爲空;
二、底層操做系統開始被查詢以更新剩餘的Channel通道的就緒狀態來執行這個key感興趣的事件,對於一個至少有一種這樣操做的Channel來講,下面2個動做將會被執行:
2.1 若是Channel的key不在selected-key set中,而後key會被增長到selected-key set中,同時它的就緒操做會被修改出來準確的標記出哪個Channel已完成準備工做,任意以前的ready set的就緒信息將會被丟棄;
2.2 若是Channel的key在selected-key set中,同時它的就緒操做會被修改出來準確的標記出哪個Channel已完成準備工做,任意以前的ready set的就緒信息將會被保留,換句話來講,這個底層操做系統的ready set將會按位寫入到當前的key的ready set。
若是全部的key set裏面的key一開始就沒有interest set,那麼無論是selected-key set仍是它對應的就緒操做都不會被更新。
三、若是執行步驟2時有新的key加入到cancelled-key,則按步驟1進行處理。
這三個方法的本質區別就是選擇操做是否阻塞等待一個或多個通道準備就緒,或者等待了多長時間。
Concurrency
選擇器自己能夠安全地供多個併發線程使用。可是,它們的key set不是。
在執行選擇操做時,選擇器在 Selector 對象上進行同步,而後是key set,最後是selected-key set,按照這樣的順序。cancelled-key set也在選擇過程的的第 1步和第 3 步之間保持同步。
在進行選擇操做時,對選擇器的interest sets所作的更改對該操做沒有影響,他們將在下一個選擇操做中看到。
選擇器的一個或多個鍵集中的鍵的存在並不表示該鍵有效或其通道已打開。若是其餘線程有可能取消鍵或關閉通道,則應用程序代碼應謹慎同步並在必要時檢查這些條件。
線程會阻塞在select()或者select(long)方法上,若是其餘線程想中斷這個阻塞,能夠經過以下三種方式:
經過調用選擇器的wakeup方法;
經過調用選擇器的close方法;
經過調用被阻塞線程的interrupt方法,在這種狀況下,將設置其中斷狀態,並調用選擇器的wakeup方法。
close方法以與選擇操做相同的順序在選擇器和全部三個鍵集上同步。
一般,選擇器的key和selected-key不能安全地供多個併發線程使用。若是此類線程能夠直接修改這些集合之一,則應經過在集合自己上進行同步來控制訪問。 這些集合的迭代器方法返回的迭代器是快速失敗的:若是在建立迭代器以後修改了集合,則除了經過調用迭代器本身的remove方法以外,其餘任何方式都會拋出java.util.ConcurrentModificationException。
以下爲Selector提供的全部方法:
SelectionKey
表示SelectableChannel向Selector的註冊的令牌。
每次將Channel註冊到選擇器中時,都會建立一個選擇鍵。這個鍵一直有效,直到經過調用其cancel方法,關閉其通道或關閉其選擇器將其取消。取消鍵不會當即將其從選擇器中刪除,而是將其添加到選擇器的「取消鍵」集合中,以便在下一次選擇操做期間將其刪除。能夠經過調用isValid方法來測試這個鍵是否有效性。
選擇鍵包含兩個表示爲整數值的操做集。操做集的每一個位表示鍵的通道支持的可選操做的類別。
興趣集肯定下一次調用選擇器的選擇方法之一時,將測試那些操做類別是否準備就緒。使用建立鍵時給定的值來初始化興趣集,之後能夠經過interestOps(int)方法對其進行更改。
準備集標識鍵的選擇器已檢測到鍵的通道已準備就緒的操做類別。 建立key時,準備集將初始化爲零。它可能稍後會在選擇操做期間由選擇器更新,但沒法直接更新。
選擇鍵的就緒集指示其通道已爲某個操做類別作好了提示,但不是保證,此類類別中的操做能夠由線程執行而不會致使線程阻塞。準備工做極可能在選擇操做完成後當即準確。外部事件和在相應通道上調用的I/O操做可能會使它不許確。
此類定義了全部已知的操做集位,可是精確地給定通道支持哪些位取決於通道的類型。SelectableChannel的每一個子類都定義一個validOps()方法,該方法返回一個集合,該集合僅標識通道支持的那些操做。嘗試設置或測試鍵通道不支持的操做集位將致使run-time exception。
一般有必要將一些特定於應用程序的數據與選擇鍵相關聯,例如,一個對象表明一個更高級別協議的狀態並處理就緒通知,以實現該協議。所以,選擇鍵支持將單個任意對象附加到鍵上。能夠經過attach方法附加對象,而後再經過attach方法檢索對象。
選擇鍵可安全用於多個併發線程。一般,讀取和寫入興趣集的操做將與選擇器的某些操做同步。確切地說,如何執行此同步取決於實現方式:在低性能的實現方式中,若是選擇操做已在進行中,則興趣組的讀寫可能會無限期地阻塞;在高性能實現中,讀取或寫入興趣集可能會短暫阻塞(若是有的話)。不管如何,選擇操做將始終使用該操做開始時當前的興趣設置值。
以下爲SelectionKey提供的全部方法:
Channel模塊
Channel用來表示諸如硬件設備、文件、網絡套接字或程序組件之類的實體的開放鏈接,該實體可以執行一個或多個不一樣的I/O操做(例如讀取或寫入)。I/O 能夠分爲廣義的兩大類別:File I/O和Stream I/O。那麼相應地有兩種類型的通道,它們是文件(file)通道和套接字(socket)通道。文件通道有一個FileChannel類,而套接字則有三個socket通道類:SocketChannel、 ServerSocketChannel和DatagramChannel。通道能夠以阻塞(blocking)或非阻塞(nonblocking)模式運行。非阻塞模式的通道永遠不會讓調用的線程休眠。請求的操做要麼當即完成,要麼返回一個結果代表未進行任何操做。只有面向流的(stream-oriented)的通道,如 SocketChannel、ServerSocketChannel才能使用非阻塞模式。SocketChannel、ServerSocketChannel從 SelectableChannel引伸而來。從 SelectableChannel 引伸而來的類能夠和支持有條件的選擇(readiness selectio)的選擇器(Selector)一塊兒使用。將非阻塞I/O 和選擇器組合起來就可使用多路複用 I/O(multiplexed I/O),也就是前面提到的select/poll/epoll。因爲FileChannel不是從SelectableChannel類引伸而來,因此FileChannel,也就是文件IO,是沒法使用非阻塞模型的。
FileChannel
用於讀取、寫入、映射和操做文件的通道。
文件通道是能夠鏈接到文件的SeekableByteChannel。它在文件中具備當前位置,支持查詢和修改。文件自己包含一個可變長度的字節序列,能夠讀取和寫入這些字節,而且能夠查詢其當前大小。當寫入字節超過當前大小時,文件大小會增長; 文件的大小在被截斷時會減少。該文件可能還具備一些關聯的元數據,例如訪問權限、內容類型和最後修改時間等,此類未定義用於元數據訪問的方法。
除了熟悉的字節讀取、寫入和關閉操做以外,此類還定義瞭如下特定於文件的操做:
能夠以不影響通道當前位置的方式在文件中的絕對位置讀取或寫入字節;
文件的區域能夠直接映射到內存中。對於大文件,這一般比調用傳統的讀取或寫入方法要有效得多;
對文件所作的更新可能會被強制發送到基礎存儲設備,以確保在系統崩潰時不會丟失數據;
字節能夠從文件傳輸到其餘通道,反之亦然,能夠經過操做系統進行優化,將字節快速傳輸到文件系統緩存或從文件系統緩存快速傳輸;
文件的區域可能被鎖定,以防止其餘程序訪問;
文件通道能夠安全地供多個併發線程使用。如Channel接口所指定的,close方法能夠隨時調用。在任何給定時間,可能僅在進行涉及通道位置或能夠更改其文件大小的一項操做。當第一個此類操做仍在進行時,嘗試啓動第二個此類操做的嘗試將被阻止,直到第一個操做完成。其餘操做,尤爲是採起明確立場的操做,能夠同時進行。它們是否實際上執行取決於底層實現。
此類的實例提供的文件視圖保證與同一程序中其餘實例提供的相同文件的其餘視圖一致。可是,因爲底層操做系統執行的緩存和網絡文件系統協議引發的延遲,此類實例提供的視圖可能與其餘併發運行的程序所見的視圖一致,也可能不一致。 無論這些其餘程序的編寫語言是什麼,以及它們是在同一臺計算機上仍是在其餘計算機上運行,都是如此。任何此類不一致的確切性質都取決於底層操做系統如何實現。
經過調用此類定義的open方法來建立文件通道。也能夠經過調用後續類的getChannel方法從現有的FileInputStream,FileOutputStream或RandomAccessFile對象得到文件通道,該方法返回鏈接到相同基礎文件的文件通道。若是文件通道是從現有流或隨機訪問文件得到的,則文件通道的狀態與其getChannel方法返回該通道的對象的狀態緊密相連。不管是顯式更改通道位置,仍是經過讀取或寫入字節來更改通道位置,都會更改原始對象的文件位置,反之亦然。經過文件通道更改文件的長度將更改經過原始對象看到的長度,反之亦然。經過寫入字節來更改文件的內容將更改原始對象看到的內容,反之亦然。
在各個點上,此類都指定須要一個「可讀取」、「可寫入」或「可讀取和寫入」的實例。經過FileInputStream實例的getChannel方法得到的通道將打開以供讀取。經過FileOutputStream實例的getChannel方法得到的通道將打開以進行寫入。最後,若是實例是使用模式「 r」建立的,則經過RandomAccessFile實例的getChannel方法得到的通道將打開以供讀取;若是實例是使用模式「 rw」建立的,則將打開以進行讀寫。打開的用於寫入的文件通道可能處於附加模式,例如,若是它是從經過調用FileOutputStream(File,boolean)構造函數併爲第二個參數傳遞true建立的文件輸出流中得到的。在這種模式下,每次調用相對寫入操做都會先將位置前進到文件末尾,而後再寫入請求的數據。位置的提高和數據的寫入是否在單個原子操做中完成取決於操做系統的具體實現。
SocketChannel
一個可選擇的Channel,用於面向流的鏈接socket。
經過調用此類的open方法來建立套接字通道。沒法爲任意現有的套接字建立通道。新建的套接字通道一打開,是處於還沒有鏈接的狀態的。嘗試在未鏈接的通道上調用I/O操做將致使引起NotYetConnectedException。套接字通道能夠經過調用其connect方法進行鏈接,鏈接後,套接字通道將保持鏈接狀態,直到關閉爲止。套接字通道是否已鏈接能夠經過調用其isConnected方法來肯定。
套接字通道支持非阻塞鏈接,建立一個套接字通道,並能夠經過connect方法啓動創建到遠程套接字的連接的過程,而後由finishConnect方法完成。能夠經過調用isConnectionPending方法來肯定鏈接操做是否正在進行。
套接字通道支持異步關閉,這相似於Channel類中指定的異步關閉操做。若是套接字的輸入端被一個線程關閉,而另外一個線程在套接字通道的讀取操做中被阻塞,則阻塞線程中的讀取操做將完成而不會讀取任何字節,而且將返回-1。若是套接字的輸出端被一個線程關閉,而另外一個線程在套接字通道的寫操做中被阻塞,則被阻塞的線程將收到AsynchronousCloseException。
套接字選項是使用setOption方法配置的。套接字通道支持如下選項:
選項名稱 描述
SO_SNDBUF 套接字發送緩衝區的大小
SO_RCVBUF 套接字接收緩衝區的大小
SO_KEEPALIVE 保持鏈接活躍
SO_REUSEADDR 重複使用地址
SO_LINGER 若是有數據,則在關閉時徘徊(僅在阻塞模式下配置)
TCP_NODELAY 禁用Nagle算法
也能夠支持其餘(特定於實現的)選項。
套接字通道能夠安全地供多個併發線程使用。它們支持併發讀取和寫入,儘管在任何給定時間最多能夠讀取一個線程,而且最多能夠寫入一個線程。connect和finishConnect方法彼此相互同步,而且在這些方法之一的調用正在進行時嘗試啓動讀取或寫入操做將被阻止,直到該調用完成爲止。
ServerSocketChannel
一個可選擇的Channel,用於面向流的監聽socket。
經過調用此類的open方法能夠建立服務器套接字通道。沒法爲任意現有的ServerSocket建立通道。新建立的服務器套接字通道一打開,是處於還沒有綁定的狀態的。嘗試調用未綁定的服務器套接字通道的accept方法將致使引起NotYetBoundException。能夠經過調用此類定義的bind方法之一來綁定服務器套接字通道。
套接字選項是使用setOption方法配置的。服務器套接字通道支持如下選項:
選項名稱 描述
SO_RCVBUF 套接字接收緩衝區的大小
SO_REUSEADDR 重複使用地址
也能夠支持其餘(特定於實現的)選項。
服務器套接字通道可安全用於多個併發線程。
Buffer模塊
Buffer
一個特定原始類型數據的容器。
緩衝區是特定原始類型元素的線性有限序列。除了其內容以外,緩衝區的基本屬性還包括capacity、limit和position:
緩衝區的capacity是它包含的元素數量。緩衝區的capacity永遠不會爲負,也不會改變。
緩衝區的limit是不該讀取或寫入的第一個元素的索引。緩衝區的limit永遠不會爲負,也永遠不會大於緩衝區的capacity。
緩衝區的position是下一個要讀取或寫入的元素的索引。緩衝區的position永遠不會爲負,也不會大於limit。
對於每一個非布爾基本類型,此類都有一個子類,即IntBuffer、ShortBuffer、LongBuffer、CharBuffer、ByteBuffer、DoubleBuffer和FloatBuffer。
Transferring data
此類的每一個子類定義了get和put操做的兩類:
相對操做從當前位置開始讀取或寫入一個或多個元素,而後將該位置增長所傳送元素的數量。若是請求的傳輸超出limit,則相對的get操做將引起BufferUnderflowException,而相對的put操做將引起BufferOverflowException; 不管哪一種狀況,都不會傳輸數據。
絕對運算採用顯式元素索引,而且不影響位置。若是index參數超出limit,則絕對的get和put操做將引起IndexOutOfBoundsException。
固然,也能夠經過始終相對於當前位置的通道的I/O操做將數據移入或移出緩衝區。
Marking and resetting
緩衝區的mark標記是在調用reset方法時將其position重置的索引。mark並不是老是定義的,可是定義時,它永遠不會爲負,也永遠不會大於position。若是定義了mark,則在將position或limit調整爲小於mark的值時將mark標記丟棄。若是未定義mark,則調用reset方法將引起InvalidMarkException。
Invariants
對於mark,position,limit和capacity,如下不變式成立:
0 <=mark<= position <=limit<=capacity
新建立的緩衝區始終具備零位置和未定義的標記。 初始時limit能夠爲零,也能夠是其餘一些值,具體取決於緩衝區的類型及其構造方式。新分配的緩衝區的每一個元素都初始化爲零。
Clearing, flipping, and rewinding
除了訪問position,limit和capacity以及mark和reset的方法以外,此類還定義瞭如下對緩衝區的操做:
clear使緩衝區爲新的通道讀取或相對put操做序列作好準備:將limit設置爲capacity,並將位置position爲零。
flip使緩衝區爲新的通道寫入或相對get操做序列作好準備:將limit設置爲當前position,而後將position設置爲零。
rewind使緩衝區準備好從新讀取它已經包含的數據:保留limit不變,並將position設置爲零。
Read-only buffers
每一個緩衝區都是可讀的,但並不是每一個緩衝區都是可寫的。每一個緩衝區類的變異方法都指定爲可選操做,當對只讀緩衝區調用時,該方法將引起ReadOnlyBufferException。只讀緩衝區不容許更改其內容,但其mark,positoin和limit是可變的。緩衝區是否爲只讀能夠經過調用isReadOnly方法來肯定。
Thread safety
緩衝區不能安全用於多個併發線程。若是一個緩衝區將由多個線程使用,則應經過適當的同步來控制對該緩衝區的訪問。
Invocation chaining
此類中沒有其餘要返回值的方法被指定爲返回在其上調用它們的緩衝區。這使得方法調用能夠連接在一塊兒,例如,語句序列:
b.flip();
b.position(23);
b.limit(42);
能夠用一個更緊湊的語句代替
b.flip().position(23).limit(42);
基於NIO實現一個簡單的聊天程序
上述總結了NIO的基礎知識,知道了NIO能夠處理文件IO和流IO(網絡IO),NIO最大的魅力仍是在於網絡IO的處理,接下來將經過NIO實現一個簡單的聊天程序來繼續瞭解Java的NIO,這個簡單的聊天程序是一個服務端多個客戶端,客戶端相互之間能夠實現數據通訊。
服務端:
public class NioServer { //經過Map來記錄客戶端鏈接信息 private static Map<String,SocketChannel> clientMap = new HashMap<String,SocketChannel>(); public static void main(String[] args) throws Exception { //建立ServerSocketChannel 用來監聽端口 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //配置爲非阻塞 serverSocketChannel.configureBlocking(false); //獲取服務端的socket ServerSocket serverSocket = serverSocketChannel.socket(); //監聽8899端口 serverSocket.bind(new InetSocketAddress(8899)); //建立Selector Selector selector = Selector.open(); //serverSocketChannel註冊到selector 初始時關注客戶端的鏈接事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { try { //阻塞 關注感興趣的事件 selector.select(); //獲取關注事件的SelectionKey集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //根據不一樣的事件作不一樣的處理 selectionKeys.forEach(selectionKey -> { final SocketChannel client; try { //鏈接創建起來以後 開始監聽客戶端的讀寫事件 if (selectionKey.isAcceptable()) { //如何監聽客戶端讀寫事件 首先須要將客戶端鏈接註冊到selector //如何獲取客戶端創建的通道 能夠經過selectionKey.channel() //前面只註冊了ServerSocketChannel 因此進入這個分支的通道一定是ServerSocketChannel ServerSocketChannel server = (ServerSocketChannel)selectionKey.channel(); //獲取到真實的客戶端 client = server.accept(); client.configureBlocking(false); //客戶端鏈接註冊到selector client.register(selector,SelectionKey.OP_READ); //selector已經註冊上ServerSocketChannel(關注鏈接)和SocketChannel(關注讀寫) //UUID表明客戶端標識 此處爲業務信息 String key = "[" + UUID.randomUUID().toString() + "]"; clientMap.put(key,client); }else if (selectionKey.isReadable()) { //處理客戶端寫過來的數據 對於服務端是可讀數據 此處一定是SocketChannel client = (SocketChannel)selectionKey.channel(); //Channel不能讀寫數據 必須經過Buffer來讀寫數據 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); //服務端讀數據到Buffer int count = client.read(byteBuffer); if(count > 0) { //讀寫轉換 byteBuffer.flip(); //寫數據到其餘客戶端 Charset charset = Charset.forName("utf-8"); String receiveMessage = String.valueOf(charset.decode(byteBuffer).array()); System.out.println("client:" + client + receiveMessage); String sendKey = null; for(Map.Entry<String,SocketChannel> entry: clientMap.entrySet()) { if(client == entry.getValue()) { //拿到發送者的UUID 用於模擬客戶端的聊天發送信息 sendKey = entry.getKey(); break; } } //給全部的客戶端發送信息 for(Map.Entry<String,SocketChannel> entry: clientMap.entrySet()) { //拿到全部創建鏈接的客戶端對象 SocketChannel value = entry.getValue(); ByteBuffer writeBuffer = ByteBuffer.allocate(1024); //這個put操做是Buffer的讀操做 writeBuffer.put((sendKey + ":" + receiveMessage).getBytes()); //write以前須要讀寫轉換 writeBuffer.flip(); //寫出去 value.write(writeBuffer); } } } }catch (Exception ex) { ex.printStackTrace(); } }); //處理完成該key後 必須刪除 不然會重複處理報錯 selectionKeys.clear(); }catch (Exception e) { e.printStackTrace(); } } } }
客戶端:
public class NioClient { public static void main(String[] args) throws Exception { //建立SocketChannel 用來請求端口 SocketChannel socketChannel = SocketChannel.open(); //配置爲非阻塞 socketChannel.configureBlocking(false); //建立Selector Selector selector = Selector.open(); //socketChannel註冊到selector 初始時關注向服務端創建鏈接的事件 socketChannel.register(selector,SelectionKey.OP_CONNECT); //向遠程發起鏈接 socketChannel.connect(new InetSocketAddress("127.0.0.1",8899)); while (true) { //阻塞 關注感興趣的事件 selector.select(); //獲取關注事件的SelectionKey集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //根據不一樣的事件作不一樣的處理 for(SelectionKey selectionKey : selectionKeys) { final SocketChannel channel; if(selectionKey.isConnectable()) { //與服務端創建好鏈接 獲取通道 channel = (SocketChannel)selectionKey.channel(); //客戶端與服務端是否正處於鏈接中 if(channel.isConnectionPending()) { //完成鏈接的創建 channel.finishConnect(); //發送鏈接創建的信息 ByteBuffer writeBuffer = ByteBuffer.allocate(1024); //讀入 writeBuffer.put((LocalDateTime.now() + "鏈接成功").getBytes()); writeBuffer.flip(); //寫出 channel.write(writeBuffer); //TCP雙向通道創建 //鍵盤做爲標準輸入 避免主線程的阻塞 新起線程來作處理 ExecutorService service = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory()); service.submit(() -> { while (true) { writeBuffer.clear(); //IO操做 InputStreamReader inputStreamReader = new InputStreamReader(System.in); BufferedReader reader = new BufferedReader(inputStreamReader); String readLine = reader.readLine(); //讀入 writeBuffer.put(readLine.getBytes()); writeBuffer.flip(); //寫出 channel.write(writeBuffer); } }); } //客戶端也須要監聽服務端的寫出信息 因此須要關注READ事件 channel.register(selector,SelectionKey.OP_READ); }else if(selectionKey.isReadable()) { //從服務端讀取事件 channel = (SocketChannel)selectionKey.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int count = channel.read(readBuffer); if(count > 0) { readBuffer.flip(); Charset charset = Charset.forName("utf-8"); String receiveMessage = String.valueOf(charset.decode(readBuffer).array()); System.out.println("client:" + receiveMessage); } } //處理完成該key後 必須刪除 不然會重複處理報錯 selectionKeys.clear(); } } } }
演示效果:
最後咱們來總結一下:
一、IO面向流,NIO面向緩衝區,流只能單向傳輸,而緩衝區能夠雙向傳輸,雙向傳輸的模型除了吞吐量獲得增長外,這個模型也更接近操做系統和網絡的底層;
二、對於網絡IO,Selector和Channel組合在一塊兒,實現了IO多路複用,這樣少許的線程也能處理大量的鏈接,適用於應對高併發大流量的場景;而對於文件IO,就談不上IO多路複用,可是FileChannel經過提供transferTo、transferFrom方法來減小底層拷貝的次數也能大幅提高文件IO的性能;
三、Buffer緩衝區用來存儲數據,除了沒有布爾類型外,其餘基礎數據類型和Java裏面的基礎類型是同樣的,Buffer的核心屬性是position、limit和capacity,讀寫數據時是這幾個變量在不斷翻轉變化,可是其實這個設計並不優雅,Netty的ByteBuf提供讀寫索引分離的方式使實現更加優雅;
四、NIO的編程模式總結:
將Socket通道註冊到Selector中,監聽感興趣的事件;
當感興趣的事件就緒時,則會進去咱們處理的方法進行處理;
每處理完一次就緒事件,刪除該選擇鍵(由於咱們已經處理完了)。
參考資料:
http://ifeve.com/java-nio-all/
https://segmentfault.com/a/1190000014932357?utm_source=tag-newest
https://www.zhihu.com/question/29005375?sort=created
部分圖片截圖自某學習視頻,若有侵權請告知。
原文出處:https://www.cnblogs.com/iou123lg/p/12497586.html