Java NIO與IO詳解

概述

NIO主要有三大核心部分:Channel(通道),Buffer(緩沖區),Selector。

?????? 傳統IO基于字節流和字符流進行操作,而NIO基于Channel和Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇區)用于監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個線程可以監聽多個數據通道。

NIO和傳統IO(一下簡稱IO)之間第一個最大的區別是,IO是面向流的,NIO是面向緩沖區的。 Java

IO面向流意味著每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前后移動流中的數據。如果需要前后移動從流中讀取的數據,需要先將它緩存到一個緩沖區。NIO的緩沖導向方法略有不同。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩沖區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩沖區時,不要覆蓋緩沖區里尚未處理的數據。IO的各種流是阻塞的。這意味著,當一個線程調用read() 或write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再干任何事情了。NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。線程通常將非阻塞IO的空閑時間用于在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

Channel

首先說一下Channel,國內大多翻譯成“通道”。Channel和IO中的Stream(流)是差不多一個等級的。只不過Stream是單向的,譬如:InputStream, OutputStream.而Channel是雙向的,既可以用來進行讀操作,又可以用來進行寫操作。

NIO中的Channel的主要實現有:

FileChannel

DatagramChannel

SocketChannel

ServerSocketChannel

這里看名字就可以猜出個所以然來:分別可以對應文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是圍繞這4個類型的Channel進行陳述的。

Buffer

NIO中的關鍵Buffer實現有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer,

IntBuffer, LongBuffer, ShortBuffer,分別對應基本數據類型: byte, char, double,

float, int, long, short。當然NIO中還有MappedByteBuffer, HeapByteBuffer,

DirectByteBuffer等這里先不進行陳述。

Selector

Selector運行單線程處理多個Channel,如果你的應用打開了多個通道,但每個連接的流量都很低,使用Selector就會很方便。例如在一個聊天服務器中。要使用Selector,

得向Selector注冊Channel,然后調用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新的連接進來、數據接收等。

一.java NIO 和阻塞I/O的區別

1. 阻塞I/O通信模型

假如現在你對阻塞I/O已有了一定了解,我們知道阻塞I/O在調用InputStream.read()方法時是阻塞的,它會一直等到數據到來時(或超時)才會返回;同樣,在調用ServerSocket.accept()方法時,也會一直阻塞到有客戶端連接才會返回,每個客戶端連接過來后,服務端都會啟動一個線程去處理該客戶端的請求。阻塞I/O的通信模型示意圖如下:


如果你細細分析,一定會發現阻塞I/O存在一些缺點。根據阻塞I/O通信模型,我總結了它的兩點缺點:

1. 當客戶端多時,會創建大量的處理線程。且每個線程都要占用棧空間和一些CPU時間

2. 阻塞可能帶來頻繁的上下文切換,且大部分上下文切換可能是無意義的。

在這種情況下非阻塞式I/O就有了它的應用前景。

2.java NIO原理及通信模型

Java NIO是在jdk1.4開始使用的,它既可以說成“新I/O”,也可以說成非阻塞式I/O。下面是java NIO的工作原理:

1. 由一個專門的線程來處理所有的 IO 事件,并負責分發。

2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。

3. 線程通訊:線程之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的線程切換。

閱讀過一些資料之后,下面貼出我理解的java NIO的工作原理圖:


注:每個線程的處理流程大概都是讀取數據、解碼、計算處理、編碼、發送響應。

Java NIO的服務端只需啟動一個專門的線程來處理所有的 IO

事件,這種通信模型是怎么實現的呢?呵呵,我們一起來探究它的奧秘吧。java

NIO采用了雙向通道(channel)進行數據傳輸,而不是單向的流(stream),在通道上可以注冊我們感興趣的事件。一共有以下四種事件:

事件名對應值

服務端接收客戶端連接事件? SelectionKey.OP_ACCEPT(16)

客戶端連接服務端事件???? SelectionKey.OP_CONNECT(8)

讀事件??????? SelectionKey.OP_READ(1)

寫事件???????? SelectionKey.OP_WRITE(4)

服務端和客戶端各自維護一個管理通道的對象,我們稱之為selector,該對象能檢測一個或多個通道

(channel)

上的事件。我們以服務端為例,如果服務端的selector上注冊了讀事件,某時刻客戶端給服務端發送了一些數據,阻塞I/O這時會調用read()方法阻塞地讀取數據,而NIO的服務端會在selector中添加一個讀事件。服務端的處理線程會輪詢地訪問selector,如果訪問selector時發現有感興趣的事件到達,則處理這些事件,如果沒有感興趣的事件到達,則處理線程會一直阻塞直到感興趣的事件到達為止。下面是我理解的java

NIO的通信模型示意圖:


二.java NIO服務端和客戶端代碼實現

為了更好地理解java NIO,下面貼出服務端和客戶端的簡單代碼實現。

服務端:

package cn.nio;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.util.Iterator;

/**

* NIO服務端

* @author kewei.zhang

*/

public class NIOServer {

//通道管理器

private Selector selector;

/**

* 獲得一個ServerSocket通道,并對該通道做一些初始化的工作

* @param port? 綁定的端口號

* @throws IOException

*/

public void initServer(int port) throws IOException {

// 獲得一個ServerSocket通道

ServerSocketChannel serverChannel = ServerSocketChannel.open();

// 設置通道為非阻塞

serverChannel.configureBlocking(false);

// 將該通道對應的ServerSocket綁定到port端口

serverChannel.socket().bind(new InetSocketAddress(port));

// 獲得一個通道管理器

this.selector = Selector.open();

//將通道管理器和該通道綁定,并為該通道注冊SelectionKey.OP_ACCEPT事件,注冊該事件后,

//當該事件到達時,selector.select()會返回,如果該事件沒到達selector.select()會一直阻塞。

serverChannel.register(selector, SelectionKey.OP_ACCEPT);

}

/**

* 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理

* @throws IOException

*/

@SuppressWarnings("unchecked")

public void listen() throws IOException {

System.out.println("服務端啟動成功!");

// 輪詢訪問selector

while (true) {

//當注冊的事件到達時,方法返回;否則,該方法會一直阻塞

selector.select();

// 獲得selector中選中的項的迭代器,選中的項為注冊的事件

Iterator ite = this.selector.selectedKeys().iterator();

while (ite.hasNext()) {

SelectionKey key = (SelectionKey) ite.next();

// 刪除已選的key,以防重復處理

ite.remove();

// 客戶端請求連接事件

if (key.isAcceptable()) {

ServerSocketChannel server = (ServerSocketChannel) key

.channel();

// 獲得和客戶端連接的通道

SocketChannel channel = server.accept();

// 設置成非阻塞

channel.configureBlocking(false);

//在這里可以給客戶端發送信息哦

channel.write(ByteBuffer.wrap(new String("向客戶端發送了一條信息").getBytes()));

//在和客戶端連接成功之后,為了可以接收到客戶端的信息,需要給通道設置讀的權限。

channel.register(this.selector, SelectionKey.OP_READ);

// 獲得了可讀的事件

} else if (key.isReadable()) {

read(key);

}

}

}

}

/**

* 處理讀取客戶端發來的信息 的事件

* @param key

* @throws IOException

*/

public void read(SelectionKey key) throws IOException{

// 服務器可讀取消息:得到事件發生的Socket通道

SocketChannel channel = (SocketChannel) key.channel();

// 創建讀取的緩沖區

ByteBuffer buffer = ByteBuffer.allocate(10);

channel.read(buffer);

byte[] data = buffer.array();

String msg = new String(data).trim();

System.out.println("服務端收到信息:"+msg);

ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());

channel.write(outBuffer);// 將消息回送給客戶端

}

/**

* 啟動服務端測試

* @throws IOException

*/

public static void main(String[] args) throws IOException {

NIOServer server = new NIOServer();

server.initServer(8000);

server.listen();

}

}

客戶端:

package cn.nio;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.SocketChannel;

import java.util.Iterator;

/**

* NIO客戶端

* @author kewei.zhang

*/

public class NIOClient {

//通道管理器

private Selector selector;

/**

* 獲得一個Socket通道,并對該通道做一些初始化的工作

* @param ip 連接的服務器的ip

* @param port? 連接的服務器的端口號

* @throws IOException

*/

public void initClient(String ip,int port) throws IOException {

// 獲得一個Socket通道

SocketChannel channel = SocketChannel.open();

// 設置通道為非阻塞

channel.configureBlocking(false);

// 獲得一個通道管理器

this.selector = Selector.open();

// 客戶端連接服務器,其實方法執行并沒有實現連接,需要在listen()方法中調

//用channel.finishConnect();才能完成連接

channel.connect(new InetSocketAddress(ip,port));

//將通道管理器和該通道綁定,并為該通道注冊SelectionKey.OP_CONNECT事件。

channel.register(selector, SelectionKey.OP_CONNECT);

}

/**

* 采用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理

* @throws IOException

*/

@SuppressWarnings("unchecked")

public void listen() throws IOException {

// 輪詢訪問selector

while (true) {

selector.select();

// 獲得selector中選中的項的迭代器

Iterator ite = this.selector.selectedKeys().iterator();

while (ite.hasNext()) {

SelectionKey key = (SelectionKey) ite.next();

// 刪除已選的key,以防重復處理

ite.remove();

// 連接事件發生

if (key.isConnectable()) {

SocketChannel channel = (SocketChannel) key

.channel();

// 如果正在連接,則完成連接

if(channel.isConnectionPending()){

channel.finishConnect();

}

// 設置成非阻塞

channel.configureBlocking(false);

//在這里可以給服務端發送信息哦

channel.write(ByteBuffer.wrap(new String("向服務端發送了一條信息").getBytes()));

//在和服務端連接成功之后,為了可以接收到服務端的信息,需要給通道設置讀的權限。

channel.register(this.selector, SelectionKey.OP_READ);

// 獲得了可讀的事件

} else if (key.isReadable()) {

read(key);

}

}

}

}

/**

* 處理讀取服務端發來的信息 的事件

* @param key

* @throws IOException

*/

public void read(SelectionKey key) throws IOException{

//和服務端的read方法一樣

}

/**

* 啟動客戶端測試

* @throws IOException

*/

public static void main(String[] args) throws IOException {

NIOClient client = new NIOClient();

client.initClient("localhost",8000);

client.listen();

}

}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容