一 Channel(通道)介紹
通常來說NIO中的所有IO都是從 Channel(通道) 開始的。
從通道進(jìn)行數(shù)據(jù)讀取 :創(chuàng)建一個(gè)緩沖區(qū),然后請(qǐng)求通道讀取數(shù)據(jù)。
從通道進(jìn)行數(shù)據(jù)寫入 :創(chuàng)建一個(gè)緩沖區(qū),填充數(shù)據(jù),并要求通道寫入數(shù)據(jù)。
數(shù)據(jù)讀取和寫入操作圖示:
Java NIO Channel通道和流非常相似,主要有以下幾點(diǎn)區(qū)別:
- 通道可以讀也可以寫,流一般來說是單向的(只能讀或者寫,所以之前我們用流進(jìn)行IO操作的時(shí)候需要分別創(chuàng)建一個(gè)輸入流和一個(gè)輸出流)。
- 通道可以異步讀寫。
- 通道總是基于緩沖區(qū)Buffer來讀寫。
Java NIO中最重要的幾個(gè)Channel的實(shí)現(xiàn):
- FileChannel: 用于文件的數(shù)據(jù)讀寫
- DatagramChannel: 用于UDP的數(shù)據(jù)讀寫
- SocketChannel: 用于TCP的數(shù)據(jù)讀寫,一般是客戶端實(shí)現(xiàn)
- ServerSocketChannel: 允許我們監(jiān)聽TCP鏈接請(qǐng)求,每個(gè)請(qǐng)求會(huì)創(chuàng)建會(huì)一個(gè)SocketChannel,一般是服務(wù)器實(shí)現(xiàn)
類層次結(jié)構(gòu):
下面的UML圖使用Idea生成的。
二 FileChannel的使用
使用FileChannel讀取數(shù)據(jù)到Buffer(緩沖區(qū))以及利用Buffer(緩沖區(qū))寫入數(shù)據(jù)到FileChannel:
package filechannel;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelTxt {
public static void main(String args[]) throws IOException {
//1.創(chuàng)建一個(gè)RandomAccessFile(隨機(jī)訪問文件)對(duì)象,
RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
//通過RandomAccessFile對(duì)象的getChannel()方法。FileChannel是抽象類。
FileChannel inChannel=raf.getChannel();
//2.創(chuàng)建一個(gè)讀數(shù)據(jù)緩沖區(qū)對(duì)象
ByteBuffer buf=ByteBuffer.allocate(48);
//3.從通道中讀取數(shù)據(jù)
int bytesRead = inChannel.read(buf);
//創(chuàng)建一個(gè)寫數(shù)據(jù)緩沖區(qū)對(duì)象
ByteBuffer buf2=ByteBuffer.allocate(48);
//寫入數(shù)據(jù)
buf2.put("filechannel test".getBytes());
buf2.flip();
inChannel.write(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
//Buffer有兩種模式,寫模式和讀模式。在寫模式下調(diào)用flip()之后,Buffer從寫模式變成讀模式。
buf.flip();
//如果還有未讀內(nèi)容
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
//清空緩存區(qū)
buf.clear();
bytesRead = inChannel.read(buf);
}
//關(guān)閉RandomAccessFile(隨機(jī)訪問文件)對(duì)象
raf.close();
}
}
運(yùn)行效果:
通過上述實(shí)例代碼,我們可以大概總結(jié)出FileChannel的一般使用規(guī)則:
1. 開啟FileChannel
使用之前,F(xiàn)ileChannel必須被打開 ,但是你無法直接打開FileChannel(FileChannel是抽象類)。需要通過 InputStream , OutputStream 或 RandomAccessFile 獲取FileChannel。
我們上面的例子是通過RandomAccessFile打開FileChannel的:
//1.創(chuàng)建一個(gè)RandomAccessFile(隨機(jī)訪問文件)對(duì)象,
RandomAccessFile raf=new RandomAccessFile("D:\\niodata.txt", "rw");
//通過RandomAccessFile對(duì)象的getChannel()方法。FileChannel是抽象類。
FileChannel inChannel=raf.getChannel();
2. 從FileChannel讀取數(shù)據(jù)/寫入數(shù)據(jù)
從FileChannel中讀取數(shù)據(jù)/寫入數(shù)據(jù)之前首先要?jiǎng)?chuàng)建一個(gè)Buffer(緩沖區(qū))對(duì)象,Buffer(緩沖區(qū))對(duì)象的使用我們?cè)谏弦黄恼轮幸呀?jīng)詳細(xì)說明了,如果不了解的話可以看我的上一篇關(guān)于Buffer的文章。
使用FileChannel的read()方法讀取數(shù)據(jù):
//2.創(chuàng)建一個(gè)讀數(shù)據(jù)緩沖區(qū)對(duì)象
ByteBuffer buf=ByteBuffer.allocate(48);
//3.從通道中讀取數(shù)據(jù)
int bytesRead = inChannel.read(buf);
使用FileChannel的write()方法寫入數(shù)據(jù):
//創(chuàng)建一個(gè)寫數(shù)據(jù)緩沖區(qū)對(duì)象
ByteBuffer buf2=ByteBuffer.allocate(48);
//寫入數(shù)據(jù)
buf2.put("filechannel test".getBytes());
buf2.flip();
inChannel.write(buf);
3. 關(guān)閉FileChannel
完成使用后,F(xiàn)ileChannel您必須關(guān)閉它。
channel.close();
三 SocketChannel和ServerSocketChannel的使用
利用SocketChannel和ServerSocketChannel實(shí)現(xiàn)客戶端與服務(wù)器端簡(jiǎn)單通信:
SocketChannel 用于創(chuàng)建基于tcp協(xié)議的客戶端對(duì)象,因?yàn)镾ocketChannel中不存在accept()方法,所以,它不能成為一個(gè)服務(wù)端程序。通過 connect()方法 ,SocketChannel對(duì)象可以連接到其他tcp服務(wù)器程序。
客戶端:
package socketchannel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class WebClient {
public static void main(String[] args) throws IOException {
//1.通過SocketChannel的open()方法創(chuàng)建一個(gè)SocketChannel對(duì)象
SocketChannel socketChannel = SocketChannel.open();
//2.連接到遠(yuǎn)程服務(wù)器(連接此通道的socket)
socketChannel.connect(new InetSocketAddress("127.0.0.1", 3333));
// 3.創(chuàng)建寫數(shù)據(jù)緩存區(qū)對(duì)象
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello WebServer this is from WebClient".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
//創(chuàng)建讀數(shù)據(jù)緩存區(qū)對(duì)象
ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer);
//String 字符串常量,不可變;StringBuffer 字符串變量(線程安全),可變;StringBuilder 字符串變量(非線程安全),可變
StringBuilder stringBuffer=new StringBuilder();
//4.將Buffer從寫模式變?yōu)榭勺x模式
readBuffer.flip();
while (readBuffer.hasRemaining()) {
stringBuffer.append((char) readBuffer.get());
}
System.out.println("從服務(wù)端接收到的數(shù)據(jù):"+stringBuffer);
socketChannel.close();
}
}
ServerSocketChannel 允許我們監(jiān)聽TCP鏈接請(qǐng)求,通過ServerSocketChannelImpl的 accept()方法 可以創(chuàng)建一個(gè)SocketChannel對(duì)象用戶從客戶端讀/寫數(shù)據(jù)。
服務(wù)端:
package socketchannel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class WebServer {
public static void main(String args[]) throws IOException {
try {
//1.通過ServerSocketChannel 的open()方法創(chuàng)建一個(gè)ServerSocketChannel對(duì)象,open方法的作用:打開套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.通過ServerSocketChannel綁定ip地址和port(端口號(hào))
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 3333));
//通過ServerSocketChannelImpl的accept()方法創(chuàng)建一個(gè)SocketChannel對(duì)象用戶從客戶端讀/寫數(shù)據(jù)
SocketChannel socketChannel = ssc.accept();
//3.創(chuàng)建寫數(shù)據(jù)的緩存區(qū)對(duì)象
ByteBuffer writeBuffer = ByteBuffer.allocate(128);
writeBuffer.put("hello WebClient this is from WebServer".getBytes());
writeBuffer.flip();
socketChannel.write(writeBuffer);
//創(chuàng)建讀數(shù)據(jù)的緩存區(qū)對(duì)象
ByteBuffer readBuffer = ByteBuffer.allocate(128);
//讀取緩存區(qū)數(shù)據(jù)
socketChannel.read(readBuffer);
StringBuilder stringBuffer=new StringBuilder();
//4.將Buffer從寫模式變?yōu)榭勺x模式
readBuffer.flip();
while (readBuffer.hasRemaining()) {
stringBuffer.append((char) readBuffer.get());
}
System.out.println("從客戶端接收到的數(shù)據(jù):"+stringBuffer);
socketChannel.close();
ssc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
運(yùn)行效果:
客戶端:
服務(wù)端:
通過上述實(shí)例代碼,我們可以大概總結(jié)出SocketChannel和ServerSocketChannel的使用的一般使用規(guī)則:
考慮到篇幅問題,下面只給出大致步驟,不貼代碼,可以結(jié)合上述實(shí)例理解。
客戶端
1.通過SocketChannel連接到遠(yuǎn)程服務(wù)器
2.創(chuàng)建讀數(shù)據(jù)/寫數(shù)據(jù)緩沖區(qū)對(duì)象來讀取服務(wù)端數(shù)據(jù)或向服務(wù)端發(fā)送數(shù)據(jù)
3.關(guān)閉SocketChannel
服務(wù)端
1.通過ServerSocketChannel 綁定ip地址和端口號(hào)
2.通過ServerSocketChannelImpl的accept()方法創(chuàng)建一個(gè)SocketChannel對(duì)象用戶從客戶端讀/寫數(shù)據(jù)
3.創(chuàng)建讀數(shù)據(jù)/寫數(shù)據(jù)緩沖區(qū)對(duì)象來讀取客戶端數(shù)據(jù)或向客戶端發(fā)送數(shù)據(jù)
4. 關(guān)閉SocketChannel和ServerSocketChannel
四 ?DatagramChannel的使用
DataGramChannel,類似于java 網(wǎng)絡(luò)編程的DatagramSocket類;使用UDP進(jìn)行網(wǎng)絡(luò)傳輸, UDP是無連接,面向數(shù)據(jù)報(bào)文段的協(xié)議,對(duì)傳輸?shù)臄?shù)據(jù)不保證安全與完整 ;和上面介紹的SocketChannel和ServerSocketChannel的使用方法類似,所以這里就簡(jiǎn)單介紹一下如何使用。
1.獲取DataGramChannel
//1.通過DatagramChannel的open()方法創(chuàng)建一個(gè)DatagramChannel對(duì)象
DatagramChannel datagramChannel = DatagramChannel.open();
//綁定一個(gè)port(端口)
datagramChannel.bind(new InetSocketAddress(1234));
上面代碼表示程序可以在1234端口接收數(shù)據(jù)報(bào)。
2.接收/發(fā)送消息
接收消息:
先創(chuàng)建一個(gè)緩存區(qū)對(duì)象,然后通過receive方法接收消息,這個(gè)方法返回一個(gè)SocketAddress對(duì)象,表示發(fā)送消息方的地址:
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
發(fā)送消息:
由于UDP下,服務(wù)端和客戶端通信并不需要建立連接,只需要知道對(duì)方地址即可發(fā)出消息,但是是否發(fā)送成功或者成功被接收到是沒有保證的;發(fā)送消息通過send方法發(fā)出,改方法返回一個(gè)int值,表示成功發(fā)送的字節(jié)數(shù):
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put("datagramchannel".getBytes());
buf.flip();
int send = channel.send(buffer, new InetSocketAddress("localhost",1234));
這個(gè)例子發(fā)送一串字符:“datagramchannel”到主機(jī)名為”localhost”服務(wù)器的端口1234上。
五 Scatter / Gather
Channel 提供了一種被稱為 Scatter/Gather 的新功能,也稱為本地矢量 I/O。Scatter/Gather 是指在多個(gè)緩沖區(qū)上實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 I/O 操作。正確使用 Scatter / Gather可以明顯提高性能。
大多數(shù)現(xiàn)代操作系統(tǒng)都支持本地矢量I/O(native vectored I/O)操作。當(dāng)您在一個(gè)通道上請(qǐng)求一個(gè)Scatter/Gather操作時(shí),該請(qǐng)求會(huì)被翻譯為適當(dāng)?shù)谋镜卣{(diào)用來直接填充或抽取緩沖區(qū),減少或避免了緩沖區(qū)拷貝和系統(tǒng)調(diào)用;
Scatter/Gather應(yīng)該使用直接的ByteBuffers以從本地I/O獲取最大性能優(yōu)勢(shì)。
Scatter/Gather功能是通道(Channel)提供的 并不是Buffer。
Scatter: 從一個(gè)Channel讀取的信息分散到N個(gè)緩沖區(qū)中(Buufer).
Gather: 將N個(gè)Buffer里面內(nèi)容按照順序發(fā)送到一個(gè)Channel.
Scattering Reads
“scattering read”是把數(shù)據(jù)從單個(gè)Channel寫入到多個(gè)buffer,如下圖所示:
示例代碼:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
read()方法內(nèi)部會(huì)負(fù)責(zé)把數(shù)據(jù)按順序?qū)戇M(jìn)傳入的buffer數(shù)組內(nèi)。一個(gè)buffer寫滿后,接著寫到下一個(gè)buffer中。
舉個(gè)例子,假如通道中有200個(gè)字節(jié)數(shù)據(jù),那么header會(huì)被寫入128個(gè)字節(jié)數(shù)據(jù),body會(huì)被寫入72個(gè)字節(jié)數(shù)據(jù);
注意:
無論是scatter還是gather操作,都是按照buffer在數(shù)組中的順序來依次讀取或?qū)懭氲模?/p>
Gathering Writes
“gathering write”把多個(gè)buffer的數(shù)據(jù)寫入到同一個(gè)channel中,下面是示意圖:
示例代碼:
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
write()方法內(nèi)部會(huì)負(fù)責(zé)把數(shù)據(jù)按順序?qū)懭氲絚hannel中。
注意:
并不是所有數(shù)據(jù)都寫入到通道,寫入的數(shù)據(jù)要根據(jù)position和limit的值來判斷,只有position和limit之間的數(shù)據(jù)才會(huì)被寫入;
舉個(gè)例子,假如以上header緩沖區(qū)中有128個(gè)字節(jié)數(shù)據(jù),但此時(shí)position=0,limit=58;那么只有下標(biāo)索引為0-57的數(shù)據(jù)才會(huì)被寫入到通道中。
六 通道之間的數(shù)據(jù)傳輸
在Java NIO中如果一個(gè)channel是FileChannel類型的,那么他可以直接把數(shù)據(jù)傳輸?shù)搅硪粋€(gè)channel。
- transferFrom() :transferFrom方法把數(shù)據(jù)從通道源傳輸?shù)紽ileChannel
- transferTo() :transferTo方法把FileChannel數(shù)據(jù)傳輸?shù)搅硪粋€(gè)channel