Java NIO中,channel用于数据的传输,类似于传统BIO中的流(IOStream)的概念。
我们都知道,系统的I/O都分为两个阶段:
Channel就可以看作是IO设备和内核区域的一个桥梁,凡是与IO设备交互都必须通过channel,而Buffer就可以看作是内核缓冲区。这样整个过程就很好理解了,我们举个读写的例子:
数据读取过程:
数据写入过程:
所以,在NIO中数据的读取和写入必须经过Buffer和Channel。也就是说,在读取数据的时候,先利用channel将IO设备中的数据读取到buffer,然后从buffer中读取;在写入数据的时候,先将数据写入到buffer,然后buffer中的数据再通过channel传到IO设备中。
1)Channel特点:
注:非阻塞模式意味着,当读不到数据或者缓冲区已满无法写入的时候,不会把线程睡眠
2)常用的Channel :
FileChannel是操作文件的Channel,我们可以通过FileChannel 从一个文件中读取数据,也可以将数据写入到文件中。
此外,FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或者直接把另外一个Channel中的数据拷贝到FileChannel,该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,其性能一般高于Java IO中提供的方法。
注意:FileChannel 不能设置为非阻塞模式!
2.1)常用方法:
注:假设我们把当前位置设置为文件结尾之后,从通道中读取数据时返回值是-1,表示已经到达文件结尾了。 如果把当前位置设置为文件结尾之后,在向通道中写入数据,文件会自动扩展以便写入数据,但是这样会导致文件中出现类似空洞,即文件的一些位置是没有数据的。
2.2)写入文件示例:
public static void fileChannelWrite() {
RandomAccessFile aFile = null;
FileChannel channel = null;
try {
aFile = new RandomAccessFile("D:\\testFileChannel.txt", "rw");
channel = aFile.getChannel();//通过RandomAccessFile打开FileChannel
//写入数据:这里的capacity如果设置小了,下面的put方法会报java.nio.BufferOverflowException
ByteBuffer buf = ByteBuffer.allocate(newData.getBytes().length-1);
buf.clear();//切换到接收模式
buf.put(newData.getBytes());//1.将数据从用户空间写入内核缓冲区(ByteBuffer)
buf.flip();//切换到输出模式
while(buf.hasRemaining()) {//因为write不能保证有多少数据真实被写入,因此需要循环写入直到没有更多数据。
channel.write(buf);//2.将数据从内核缓冲(ByteBuffer)区写到到io(channel)
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (channel != null) channel.close();
if (aFile != null) aFile.close();
} catch (IOException e) {
}
}
}
说明:
2.3)读取文件示例:
public static void fileChannelRead() {
FileInputStream ins = null;
FileChannel channel = null;
try {
ins = new FileInputStream("D:\\test1.txt");
channel = ins.getChannel();//通过FileInputStream打开FileChannel
//读取数据:这里设置的capacity,下面read方法如果10字节恰好分割了一个字符将出现乱码
ByteBuffer buf = ByteBuffer.allocate(1024);
while (channel.read(buf) != -1) {//1.从io设备(Channel)读取到内核缓冲区(ByteBuffer)
buf.flip();//切换到输出模式
System.out.print(charset.decode(buf));//2.从缓冲区(ByteBuffer)读取到用户空间;这里可能会乱码
buf.clear();//切换到输入模式
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (channel != null) channel.close();
if (ins != null) ins.close();
} catch (IOException e) {
}
}
}
说明:
注:有些时候,从ByteBuffer中读数据时也会有一个while循环,例如:
while(buffer.hasRemaining()) {
System.out.println((char)buffer.get());
}
2.4)数据拷贝示例:
public static void main(String[] args) {
//通过FileInputStream构造FileChannel
FileChannel in = new FileInputStream("a.txt").getChannel();
FileChannel out = new FileOutputStream("b.txt").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//调用channel的write方法往buffer中写数据
while(in.read(buffer) != -1) {
//从buffer中读取数据之前先切换模式
buffer.flip();
//从buffer中读数据写到channel
out.write(buffer);
//往buffer中写数据前先切换模式
buffer.clear();
}
//或者调用如下代码
//out.transferFrom(in,0,in.size());
}
2.5)零拷贝示例:
将本地文件内容传输到网络的示例代码如下所示
public class NIOClient {
public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel socketChannel = SocketChannel.open();
InetSocketAddress address = new InetSocketAddress(1234);
socketChannel.connect(address);
RandomAccessFile file = new RandomAccessFile(
NIOClient.class.getClassLoader().getResource("test.txt").getFile(), "rw");
FileChannel channel = file.getChannel();
channel.transferTo(0, channel.size(), socketChannel);
channel.close();
file.close();
socketChannel.close();
}
}
SocketChannel是一个客户端用来进行TCP连接的Channel,相当于Java网络编程中的Socket套接字接口。创建一个 SocketChannel 的方法有两种:
3.1)常用方法:
3.2)利用socketChannel发送http请求,并解析http的响应:
private static Charset charset = Charset.forName("UTF-8");// 创建GBK字符集
public static void socketChannelTest() {
SocketChannel socketChannel = null;
try {
socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.ifeng.com", 80));
String line="GET / HTTP/1.1 \r\n";
line+="HOST:www.ifeng.com\r\n";
line+="\r\n";
//1.发送http请求:写数据到io设备
ByteBuffer requestBuf = charset.encode(line);//1.1 用户空间数据写入到缓存(ByteBuffer)
while (requestBuf.hasRemaining()) {
socketChannel.write(requestBuf);//1.2 缓存数据写入到io设备(socketChannel)
}
//2.接收http响应读取数据:读取数据到用户空间
ByteBuffer buf = ByteBuffer.allocate(1024);//capacity设置的恰好分隔一个字符时会出现乱码
while (socketChannel.read(buf) != -1) {//2.1 io设备数据写入到缓存(ByteBuffer)
buf.flip();//切换到输出模式
System.out.print(charset.decode(buf));//2.2 从缓存数据读取到用户空间;可能会乱码
buf.clear();//切换到输入模式
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (socketChannel != null) {
socketChannel.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
说明:
3.3)非阻塞设置:
我们可以吧SocketChannel设置为non-blocking(非阻塞)模式。这样的话在调用connect(), read(), write()时都是异步的。
1)connect():
如果我们设置了一个SocketChannel是非阻塞的,那么调用connect()后,方法会在链接建立前就直接返回。为了检查当前链接是否建立成功,我们可以调用finishConnect(),如下:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://example.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
2)write():
在非阻塞模式下,调用write()方法不能确保方法返回后写入操作一定得到了执行。因此我们需要把write()调用放到循环内。这和前面代码中write始终是在一个while里一样。例如:
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);
}
3)read():
在非阻塞模式下,调用read()方法也不能确保方法返回后,确实读到了数据。因此我们需要自己检查的整型返回值,这个返回值会告诉我们实际读取了多少字节的数据。
4)和Selector结合非阻塞模式:
SocketChannel的非阻塞模式可以和Selector很好的协同工作。把一个或多个SocketChannel注册到一个Selector后,我们可以通过Selector指导哪些channels通道是处于可读,可写等等状态的。后续我们会再详细阐述如果联合使用Selector与SocketChannel。
ServerSocketChannel是用于服务端监听TCP链接请求的通道,正如Java网络编程中的ServerSocket一样。
4.1)常用方法:
4.2)阻塞方式监听TCP请求:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
//do something with socketChannel...
}
4.3)非阻塞方式监听TCP请求:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(socketChannel != null){
//do something with socketChannel...
}
}
在非阻塞模式下,调用accept()函数会立刻返回,如果当前没有请求的链接,那么返回值为空null。因此我们需要手动检查返回的SocketChannel是否为空.
DatagramChannel 是用来处理 UDP 连接的。略
参考:
https://segmentfault.com/a/1190000006824107
https://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-socketchannel.html