概述:
NIO包是JDK1.4引入的新的I/O類庫环形,目的是為了提高文件讀寫的速度银室。NIO的讀寫模式和舊的I/O有一些不同磺浙,NIO是通過緩沖器和通道來對文件進行操作的护盈,當我們需要對文件進行數(shù)據(jù)的讀取時堂湖,我們先將數(shù)據(jù)讀取到緩沖器中闲先,再從緩沖器上讀取我們所要的數(shù)據(jù),當我們想往文件中寫入數(shù)據(jù)時无蜂,那么我們先朝著緩沖器寫入數(shù)據(jù)伺糠,再利用緩沖器往文件里進行寫入。在整個文件的讀寫操作時斥季,我們并沒有與文件進行直接的交互训桶。
我們先來看一個NIO讀寫文件的例子
public static void testChannel() throws IOException {
//1.利用RandomAccessFile打開文件data.txt
RandomAccessFile aFile = new RandomAccessFile(dir + "data.txt","rw");
//2.獲取文件的通道
FileChannel inChannel = aFile.getChannel();
//3.創(chuàng)建一個字節(jié)緩存器
ByteBuffer buf = ByteBuffer.allocate(48);
//4.根據(jù)緩存器的大小將文件內(nèi)容讀取到緩存中
int bytesRead = inChannel.read(buf);
//5.根據(jù)讀取到的字節(jié)數(shù)做判斷
while (bytesRead != -1){
System.out.println("Read " + bytesRead);
//切換緩沖區(qū)的模式
buf.flip();
//6.打印緩存中的內(nèi)容
while (buf.hasRemaining()){
System.out.print((char) buf.get());
}
System.out.println("");
//7.清空緩存的內(nèi)容,假如不執(zhí)行這一步,read(buf)將會返回0,因為無法讀入到buf中
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
}
如上所示,這是一個利用Channel和緩沖器從文件中讀取數(shù)據(jù)的代碼酣倾,上面的代碼分為這幾個步驟:
- 打開一個文件
- 獲取文件的Channel
- 創(chuàng)建一個固定大小的緩沖器
- 將文件內(nèi)容讀入到緩沖器中
- 讀出緩沖器的內(nèi)容舵揭,讀完之后清空緩沖器以便接下來繼續(xù)讀
NIO的組成
如下所示,一個NIO的系統(tǒng)的組成大致包含下面的三大部分躁锡,分別是通道(Channel)午绳、緩沖器(Buffers)以及Selector
其中Selector的作用是可以在單個線程中管理多個通道的讀寫
Buffers(緩沖器)
緩沖器是文件數(shù)據(jù)讀取和寫入的一段內(nèi)存,當我們需要從通道中取數(shù)據(jù)時映之,我么先將通道中的數(shù)據(jù)讀到緩沖器中拦焚,再從緩沖器中獲取數(shù)據(jù)。當我們需要往通道中寫入數(shù)據(jù)時杠输,我們先將數(shù)據(jù)寫到緩沖器中耕漱,再把數(shù)據(jù)寫入通道。
緩沖器的三個標識
capacity
抬伺、limit
和position
螟够,下面我們來分別介紹一下
capacity
表示緩存的容量,比如說我們使用下面的代碼創(chuàng)建了一個緩沖器
ByteBuffer buf = ByteBuffer.allocate(48);//分配48個Byte的緩沖器
那么緩沖器的capacity就是48,注意capacity的值與緩沖器的類型無關(guān)
limit
- 在讀模式下妓笙,limit表示緩沖器還有多少數(shù)據(jù)可以讀
- 在寫模式下若河,limit表示緩沖器還有多少空間可以往里寫
position
表示緩沖區(qū)的當前位置
- 在緩沖區(qū)切到寫模式時,position的值被設(shè)為0
- 在緩沖區(qū)切換到讀模式時寞宫,position的值被設(shè)為緩沖區(qū)中第一個能寫入的空間位置
buffer一些操作的方法
往buffer里面寫入數(shù)據(jù)
- 通過Channel往buffer里寫入數(shù)據(jù)萧福,如下所示
int byteRead = inChannel.read(buf);
上面是往緩存buffer里面寫入數(shù)據(jù),并返回寫入到緩存中的字節(jié)個數(shù)
- 通過buffer的put()方法往緩存中寫入數(shù)據(jù)
buf.put(127);
往緩存中寫入ASCII編碼為127的數(shù)辈赋,寫入之后鲫忍,limit減1,position加1
flip()
方法
這個方法是將緩存切換到讀模式以讀取緩存中的數(shù)據(jù)钥屈,具體的變化是將position
的值設(shè)為0悟民,limit
的值設(shè)為原先position
的值,在這樣的情況下篷就,就可以讀取buffer中從position
到limit
之間的所有數(shù)據(jù)
從buffer中讀取數(shù)據(jù)
- 從buffer中讀取數(shù)據(jù)到Channel
int bytesWritten = inChannel.write(buf);
上面是將buffer中的數(shù)據(jù)讀取到Channel中射亏,并返回讀取數(shù)據(jù)的個數(shù)
- 使用
get()
方法讀取數(shù)據(jù),如下所示
byte aByte = buf.get();
獲取buffer中當前位置的值竭业,并將結(jié)果賦值給aByte
rewind()
方法
這個方法的作用是重新讀取buffer里面的內(nèi)容智润,做法是,將position
置為0未辆,而limit
保持不變
clear()
和compact()
方法
clear()
方法是對buffer進行清空操作窟绷,但是并不是真正意義上的清空,而是修改其中的標識的值咐柜,將position
的值設(shè)為0
钾麸,而將limit
的值設(shè)為capacity
,這樣再往buffer里面寫入數(shù)據(jù)時就會覆蓋原先的內(nèi)容炕桨,已達到清空的目的饭尝,但是如果隨后沒有執(zhí)行寫操作,那么原來的數(shù)據(jù)還是能讀的出來
compact()
則是將buffer中所有未讀的數(shù)據(jù)拷貝到buffer的起始位置献宫,再將position
的值設(shè)置到最后一個未讀元素的后面
mark()
和reset()
方法
mark()
方法將buffer中的position
的值記錄下來(賦值給mark
)钥平,假如我們繼續(xù)往后面讀取數(shù)據(jù),這時候我們調(diào)用reset()
方法時姊途,我們會回到我們之前標記的位置涉瘾,實際上是將mark
的值回賦給position
equals()
和compareTo()
方法
這兩個方法都是對兩個buffer之間的比較
對于equals()
,滿足下面的幾個條件時捷兰,我們會返回true
值,表示兩個buffer相同
1. 兩個buffer有相同的類型
2. Buffer中剩余的byte立叛、char等的個數(shù)相等
3. Buffer中所有剩余的byte、char等都相同
從上面的條件我們可以看出贡茅,equals()
方法實際上是比較從position
到limit
之間的數(shù)據(jù)是否相等秘蛇,而對position
之前的數(shù)據(jù)則并不關(guān)心
compareTo()
方法在滿足下列所有條件時其做,表示一個Buffer小于另一個Buffer
1. 第一個不相等的元素小于另一個Buffer中對應(yīng)的元素
2. 所有元素都相等,但第一個Buffer比另一個先耗盡
compareTo()
方法返回的是第一個buffer和第二個buffer中第一個不相等值的差
scatter和gather
scatter
scatter是指在讀操作時將一個Channel中的數(shù)據(jù)讀出到多個buffer中去赁还,代碼如下所示
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
如上的代碼是一個將一個Channel中的數(shù)據(jù)讀出到多個buffer中的例子妖泄,在讀出的過程中,會沿著buffer數(shù)組的下標依次進行填滿艘策,這樣的做法不適合于動態(tài)的信息
gather
gather指在寫操作時將多個buffer的數(shù)據(jù)寫入到同一個Channel蹈胡,代碼如下所示
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
如上的代碼是將不同的buffer沿著buffer的下標依次寫入到Channel中去,這樣的操作很適合處理動態(tài)的信息
Channel之間的數(shù)據(jù)傳輸
利用Channel的transferFrom()
和transferTo()
方法可以在不借助額外的buffer而完成兩個Channel之間的數(shù)據(jù)傳輸
transferFrom()
將任何Channel中的數(shù)據(jù)傳輸?shù)紽ileChannel中去朋蔫,示例代碼如下所示
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(position, count, fromChannel);
transferTo()
將FileChannel中的數(shù)據(jù)傳輸?shù)饺我獾腃hannel中去罚渐,示例代碼如下
RandomAccessFile fromFile = new RandomAccessFile(dir + "data.txt","rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile(dir + "data2.txt","rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position,count,toChannel);
Selector
Selector的作用是在單一的線程下對多個Channel中的數(shù)據(jù)進行管理,這樣就很有利于對于單個Channel數(shù)據(jù)量少驯妄,但是Channel的總數(shù)多的情形進行管理
創(chuàng)建一個Selector
Selector selector = Selector.open();
向Selector注冊通道
channel.configureBlocking(false);//將Channel設(shè)為非阻塞
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);//將Channel注冊到Selector對象上去
注冊在Selector中的Channel必須處于非阻塞模式荷并,這意味著不能將Selector和FileChannel一起使用,因為FileChannel是阻塞通道
interest集合
Channel的register
方法的第二個參數(shù)是interest的集合富玷,意味著第二個參數(shù)是多個類型的疊加,類型包括:
Connect(OP_CONNECT)
Accept(OP_ACCEPT)
Read(OP_READ)
Write(OP_WRITE)
第二個參數(shù)不僅可以是上面4個單獨的類型,而且可以是上面4個類型的疊加既穆,即使用按位或的方式赎懦,例如: SelectionKey.OP_READ | SelectionKey.OP_ACCEPT,這表示對這個Channel的連接和讀取感興趣
SelectionKey
當Channel向Selector注冊時,會返回一個SelectionKey的對象幻工,該對象包含以下的屬性
interest集合(int interestSet = selectionKey.readOps();)
ready集合(int readySet = selectionKey.readyOps();)
Channel(Channel channel = selectionKey.channel();)
Selector(Selector selector = selectionKey.selector();)
附加的對象
select()
這個方法表示對注冊在Selector上的Channel進行選擇励两,select()的方法有幾種變體,如下
int select()
:阻塞并一直等到通道上有一個Channel中發(fā)生了其在注冊時指定的事件囊颅,比如定義了OP_READ屬性且出現(xiàn)了Channel的讀取行為
int select(long timeout)
:阻塞并且等到設(shè)置的時間后自動返回
int selectNow()
:立即返回当悔,不管有沒有選擇到正在發(fā)生注冊操作的Channel
selectedKeys()
在使用select()方法后會知道有一個或多個通道已經(jīng)處于就緒狀態(tài)了,那么此時可以使用selectedKeys()來獲取SelectionKey對象的集合踢代,如下所示
Set selectedKeys = selector.selectedKeys();
對這些結(jié)果集的操作如下列代碼所示
Set selectedKeys = selector.selectedKeys();
Iterator 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();//Selector不會自己移除SelectionKey實例盲憎,因此需要利用這個方法來進行移除操作
}
wakeUp()
如果一個線程正在執(zhí)行select()并且因此而處于阻塞的狀態(tài),那么可以讓其他線程在第一個處于調(diào)用select()而阻塞的線程上調(diào)用Selector.WakeUp()而立即返回
close()
用完Selector后可以利用close()方法來關(guān)閉Selector胳挎,這樣會使得注冊在其上的Channel無效饼疙,但是這并不能關(guān)閉Channel
FileChannel
FileChannel是文件操作的通道,其操作的方法見下面
打開FileChannel
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
從FileChannel讀取數(shù)據(jù)
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
向FileChannel中寫入數(shù)據(jù)
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
用完FileChannel之后關(guān)閉Channel
channel.close();
position方法
position方法有兩種形式慕爬,帶參數(shù)和不帶參數(shù)窑眯,例子如下
long pos = channel.position();//無參形式為獲取當前通道的當前位置
channel.position(pos + 123);//帶參形式為設(shè)置通道的當前位置
需注意的是,在設(shè)置通道當前位置的時候医窿,假如設(shè)置的位置超出了文件長度磅甩,并且會在設(shè)置的該位置處寫下數(shù)據(jù),那么就會造成文件的空洞
size()
獲取通道關(guān)聯(lián)的文件的大小
long fileSize = channel.size();
truncate(int)
從頭截取通道關(guān)聯(lián)文件的大小,并且丟棄掉后面的數(shù)據(jù)
channel.truncate(1024);//只需要關(guān)聯(lián)文件的頭1024個字節(jié)
force()
將通道中尚未寫入到磁盤里的數(shù)據(jù)寫入到磁盤里
channel.force(true);
SocketChannel
SocketChannel是一個連接到TCP套接字上的通道姥卢,創(chuàng)建一個SocketChannel的方式有下面兩種
1. 打開一個SocketChannel并連接到互聯(lián)網(wǎng)上的某臺服務(wù)器卷要。
2. 一個新連接到達ServerSocketChannel時渣聚,會創(chuàng)建一個SocketChannel
打開SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://www.baidu.com",80));
關(guān)閉SocketChannel
socketChannel.close();
從SocketChannel中讀取數(shù)據(jù)
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
寫入SocketChannel
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
非阻塞模式
socketChannel.configureBlocking(false);//將SocketChannel對象設(shè)為非阻塞
socketChannel.connect(new InetSocketAddress("http://jenkov.com",80));
while(! socketChannel.finishConnect()){
}
write()
在非阻塞模式下,write()方法在尚未寫出任何內(nèi)容時就可能返回了却妨,所以在循環(huán)中調(diào)用write()
read()
非阻塞模式下饵逐,read()方法在尚未讀取到任何數(shù)據(jù)時就可能返回了,所以要關(guān)注其返回的字節(jié)數(shù)
DatagramChannel
DatagramChannel的打開方式
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
打開一個DatagramChannel彪标,并將這個Channel綁定到UDP的9999端口
接收數(shù)據(jù)
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);//利用分配的buffer進行接收數(shù)據(jù)倍权,超出buffer大小的數(shù)據(jù)將被丟棄
發(fā)送數(shù)據(jù)
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("fanwn.com", 80));
以上一個發(fā)送數(shù)據(jù)的代碼,由于其發(fā)送的地址并沒有處于監(jiān)聽狀態(tài)捞烟,因此這樣不會發(fā)生任何反應(yīng)
連接到特定的地址
channel.connect(new InetSocketAddress("fanwn.com",80));
int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf);
將Channel連接到特定的地址薄声,可以讀取通道中的數(shù)據(jù)到buffer中,也可以將buffer中的數(shù)據(jù)讀取到通道中
Pipe
管道是對兩個線程之間交換數(shù)據(jù)的方式题画,一個管道包含了兩個通道類:sink通道和source通道,其中sink是往通道里面寫入數(shù)據(jù)默辨,source是從通道內(nèi)讀出數(shù)據(jù)
打開管道
Pipe pipe = Pipe.open();
向管道寫入數(shù)據(jù)
Pipe.SinkChannel sinkChannel = pipe.sink();
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
sinkChannel.write(buf);
}
從管道中讀出數(shù)據(jù)
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);