前言
其實作爲我個人,NIO,我前面也自己學過一些,但是吧,總覺得沒有個概念,也就是說這個知識吧,他不是我的東西,我沒法用自己的話去描述,只是簡單的說,哦,NIO=New IO,和IO的區別就是不阻塞。然後好像有個選擇器,和銀行大堂經理一樣的,引導人流,沒了。我爭取通過我的學習,向大家描述一個概念,建立起一個概念,NIO到底是個啥。
NIO基本概念
要說NIO的基本概念,和IO的對比是繞不過去的,因爲與自己固有心智模式的對比,才能理解的更透徹,更有助於理解。
上面這個圖,大家應該都很瞭解了,IO的圖,一般來說,IO就是說的輸入輸出,一般輸入指從文件(來自網絡、本地磁盤)到程序的流過程,輸出是指從程序到文件的流過程。在傳輸的時候,一般是單向的。底層實際上是一個個的小包進行傳輸。在生活中的類比可以是自來水流或者電流,不過,一般生活中的流都是從廠流到家裏,一般不會從家裏流回電廠。
OK,我們現在來看一下NIO的圖
上面介紹過的部分,不再介紹,說說沒有介紹過的部分。這裏有三個詞,緩衝區、通道和選擇器。分別用生活中的例子來介紹一下。我們回到事情的本身來看,爲什麼要IO,爲什麼要輸入輸出呢?因爲程序可能需要文件中的東西,也有可能是程序產生的東西要存在文件中。這個過程就像極了生活中的這個場景。
工廠生產完東西,不能堆在工廠吧?更通常的做法就是,存放到倉庫中。那倉庫中的東西也不能一輩子放那兒吧?要不就發到實體店再去賣,要不然就快遞直接給買家。那如果我們把倉庫類比成文件,從工廠到倉庫的過程就是輸出,從倉庫到實體店或者買家的過程就是輸入。OK,背景解釋清楚了。
那具體的運輸過程不就是類比於我們數據傳輸的過程嗎?我們想想咯,在現實生活中,我們總不能說,發貨時,當貨沒有全到目的地,這段過程,我們倉庫就啥事兒不幹,擱那兒等着吧?生意不做啦?
肯定不是吧?我們一般都是,貨物裝車,或者,我不管你怎麼給我整,我託快遞公司幫我整,我一發貨我就幹其他事兒了。快遞公司這個事兒我們以後再說,現在暫時沒有它的角色,但是大家都知道快遞公司,效率比自己運高。如果自己運呢?整輛車嘛,裝車,發貨。注意了!這個車 就是我們的緩衝區。通道呢?就是運輸的路線,有人解釋爲鐵軌,可以。解釋成國道、省道、高速公路我覺得也未嘗不可,就是連接於發貨地和目的地之間的路徑嘛。
最後,選擇器呢?也簡單,你看哦,我們貨物到達目的地之後,總不能又堆那兒吧?實體店怎麼做?店長總歸要讓小李、小王、小張,誰閒着呢?誰去理貨。而這些店員就是幾條不同的線程,而店長就是選擇器。
OK,文字很多,但是我相信如果你真的認真的看一遍,結合個人生活常識,很快就能理解的。
緩衝區
接下來要講得就是緩衝區的基礎概念,你以爲我又要畫圖?
http://blog.csdn.net/abc_key/article/details/29909375
http://www.cnblogs.com/chenpi/p/6475510.html
這部分收工啦~(誒,對,我就是懶)
這裏再說兩嘴Buffer的直接緩衝區和間接緩衝區吧~
間接緩衝區的建立Buffer方法
ByteBuffer.allocate(capacity);
直接緩衝區建立Buffer方法
ByteBuffer.allocateDirect()
以上的兩個方法,我都是以ByteBuffer爲例
畫圖說說有何不同。
好啦好啦,這張圖和下張圖都不是我畫的啦, 一點我的風格都沒有。
這是間接緩衝區,爲什麼間接,因爲其實文件從磁盤到內存的過程,其實要在內存的兩個地方導一下手。其實可以考慮一下,這一份內存中的複製有沒有必要。
那直接緩衝區就簡單解釋了,就是在內存中只有一份。
這個圖嘛,糊是糊了點,畢竟不是我手畫的,將就看吧~
直接字節緩衝區可以通過調用此類的allocateDirect() 工廠方法來創建。此方法返回的緩衝區進行分配和取消分配所需成本通常高於非直接緩衝區。直接緩衝區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對應用程序的內存需求量造成的影響可能並不明顯。所以,建議將直接緩衝區主要分配給那些易受基礎系統的本機I/O 操作影響的大型、持久的緩衝區。一般情況下,最好僅在直接緩衝區能在程序性能方面帶來明顯好處時分配它們。
白話
直接緩衝的創建和銷燬消耗大,雖然快一點,但是慎用。
用法
好像不放幾段代碼,就完全沒有技術博客的意思嘛,用一個簡單的例子,複製文件來看看這個東西到底是怎麼用的吧~
package com.pochi.nio;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* 這裏我是想做一個對比,對比一下,NIO(直接緩衝/間接緩衝) IO的複製效率差別有多大
*/
public class NIOCopy {
public static void main(String[] args) throws Exception {
String filePath="E:/1.zip";
// IO 78821ms
copyByIO(filePath);
// NIO(間接)
copyByNIO(filePath);
// NIO(直接)
copyByNIO2(filePath);
}
private static void copyByNIO2(String filePath) throws Exception {
long start=System.currentTimeMillis();
// 首先要拿到通道,入通道出通道
FileChannel readChannel = FileChannel.open(Paths.get(filePath), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Paths.get("e:/NIO2.zip"), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.READ);
readChannel.transferTo(0,readChannel.size(),writeChannel);
long end=System.currentTimeMillis();
System.out.println("NIO2:::"+(end-start));
}
private static void copyByNIO(String filePath) throws Exception {
long start=System.currentTimeMillis();
// 首先要拿到通道,入通道出通道
ReadableByteChannel inChannel = Channels.newChannel(new FileInputStream(filePath));
WritableByteChannel outChannel = Channels.newChannel(new FileOutputStream("e:/nio.zip"));
ByteBuffer buf = ByteBuffer.allocate(1024);
int len=0;
while((len=inChannel.read(buf))!=-1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
inChannel.close();
outChannel.close();
long end=System.currentTimeMillis();
System.out.println("NIO:::"+(end-start));
}
private static void copyByIO(String filePath) throws IOException {
long start=System.currentTimeMillis();
FileOutputStream fileOutputStream = new FileOutputStream("e:/io.zip");
FileInputStream fileInputStream = new FileInputStream(filePath);
byte [] buf=new byte[1024];
int len=0;
while((len=fileInputStream.read(buf))!=-1){
fileOutputStream.write(buf,0,len);
}
fileInputStream.close();
fileOutputStream.close();
long end=System.currentTimeMillis();
System.out.println("IO:::"+(end-start));
}
}
結果
IO:::63649
NIO:::84639
NIO2:::96589
令人驚訝的一點,就是其實NIO的複製文件速度,並沒有比IO快,直接緩衝區,並沒有比間接緩衝區快,這我就凌亂了。
分散讀取和聚合寫入
就是說,我們現在呢,讀和寫用的都是一個buffer,一個buffer不是慢嘛。
你這一輛車,不夠運的,那麼就把貨物分一分嘛,桌子分成桌子面和桌子腿,分別運輸嘛,到底目的地再組合成桌子。
具體怎麼做:
http://blog.csdn.net/u013063153/article/details/76562221
別人寫的不錯,到時候用的時候,再看吧,再說,最後大家也不怎麼會這麼用了。
非阻塞式Socket操作
看這裏,太麻煩了,難怪要有Netty
http://www.cnblogs.com/chengJAVA/p/5715629.html
雖然哦,這個操作複雜不想copy重現,但是!
原理咱們還是要分析一波
public class HelloWorldServer {
// 緩衝區容量
static int BLOCK = 1024;
static String name = "";
protected Selector selector;
protected ByteBuffer clientBuffer = ByteBuffer.allocate(BLOCK);
protected CharsetDecoder decoder;
static CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();
// 初始化
public HelloWorldServer(int port) throws IOException {
selector = this.getSelector(port);
Charset charset = Charset.forName("GB2312");
decoder = charset.newDecoder();
}
// 獲取Selector
protected Selector getSelector(int port) throws IOException {
ServerSocketChannel server = ServerSocketChannel.open();
Selector sel = Selector.open();
server.socket().bind(new InetSocketAddress(port));
// 非阻塞式
server.configureBlocking(false);
// 註冊選擇器
server.register(sel, SelectionKey.OP_ACCEPT);
return sel;
}
// 監聽端口
public void listen() {
try {
for (;;) {
selector.select();
Iterator iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
iter.remove();
process(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 處理事件
protected void process(SelectionKey key) throws IOException {
if (key.isAcceptable()) { // 接收請求
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
//設置非阻塞模式
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 讀信息
SocketChannel channel = (SocketChannel) key.channel();
int count = channel.read(clientBuffer);
if (count > 0) {
clientBuffer.flip();
CharBuffer charBuffer = decoder.decode(clientBuffer);
name = charBuffer.toString();
// System.out.println(name);
SelectionKey sKey = channel.register(selector,
SelectionKey.OP_WRITE);
sKey.attach(name);
} else {
channel.close();
}
clientBuffer.clear();
} else if (key.isWritable()) { // 寫事件
SocketChannel channel = (SocketChannel) key.channel();
String name = (String) key.attachment();
ByteBuffer block = encoder.encode(CharBuffer
.wrap("Hello !" + name));
channel.write(block);
//channel.close();
}
}
public static void main(String[] args) {
int port = 8888;
try {
HelloWorldServer server = new HelloWorldServer(port);
System.out.println("listening on " + port);
server.listen();
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面的代碼就是服務器端的一套完整流程, 這套流程呢,我想用一幅靈魂畫作來解釋
解釋一下這張圖,圖的左邊是一堆客戶端,可能隨時會發各種消息,比如建立連接、發送消息等等。但是無論他什麼時候法,發什麼,都會有一個selector先判斷,就是像上面的代碼一樣,會有一個無限循環的listen(),如果監聽到寫操作,它就調用一個或多個線程做寫操作,如果是其他操作需求,就做不同的操作需求。這是一個無限的循環的過程。
後記
但是呢,其實這還是有點問題,就是。這樣的NIO只能說它是非阻塞的,但是卻不能說它是異步的,因爲其實不管到底selector返回一個什麼樣的結果,都要應用程序做一個read或者其他的處理,這個處理是主動的。能不能由操作系統完成呢?能,不過要在NIO2中完成,下次見~