NIO之Channel

1、基本概念

Java NIO中,channel用于数据的传输,类似于传统BIO中的流(IOStream)的概念。

我们都知道,系统的I/O都分为两个阶段:

  • 等待就绪:从IO设备将数据读取到内核中的过程;
  • 操作:将数据从内核复制到进程缓冲区的过程。

Channel就可以看作是IO设备和内核区域的一个桥梁,凡是与IO设备交互都必须通过channel,而Buffer就可以看作是内核缓冲区。这样整个过程就很好理解了,我们举个读写的例子:

数据读取过程:

  • 先从IO设备(网卡或者磁盘)将内容读取到内核中,对应于NIO就是从网卡或磁盘利用channel将数据读到buffer中;
  • 然后就是内核中的数据复制到进程缓冲区,对应于就是从buffer中读取数据;

数据写入过程:

  • 先从进程将数据写到内核中,对应于就是进程将数据写入到buffer中;
  • 然后内核中的数据再写入到网卡或者磁盘中,对应于就是,buffer中的数据利用channel传输到IO设备中。

所以,在NIO中数据的读取和写入必须经过Buffer和Channel。也就是说,在读取数据的时候,先利用channel将IO设备中的数据读取到buffer,然后从buffer中读取;在写入数据的时候,先将数据写入到buffer,然后buffer中的数据再通过channel传到IO设备中。

1)Channel特点:

  • 与传统IO中的流不同,channel是双向的(可读可写),channel从buffer中读取数据,写入数据也是先写入到buffer;
  • channel可以实现异步读写操作;
  • channel可以设置为阻塞和非阻塞的模式
  • 只有socket的channel可以设置为非阻塞模式,文件的channel是无法设置的,文件的IO一定是阻塞的;
  • 如果是文件channel的话,channel可以在channel之间传输数据;

注:非阻塞模式意味着,当读不到数据或者缓冲区已满无法写入的时候,不会把线程睡眠

2)常用的Channel :

  • FileChannel :从文件中读写数据
  • DatagramChannel: 从 UDP 中读写数据
  • SocketChannel :从TCP 中读写数据
  • ServerSocketChannel:监听新进来的 TCP 连接,每一个新进来的连接都会创建一个 SocketChannel

2、FileChannel介绍

FileChannel是操作文件的Channel,我们可以通过FileChannel 从一个文件中读取数据,也可以将数据写入到文件中。

此外,FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或者直接把另外一个Channel中的数据拷贝到FileChannel,该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,其性能一般高于Java IO中提供的方法。

注意:FileChannel 不能设置为非阻塞模式!

2.1)常用方法:

  • 打开一个FileChannel需要通过输入/输出(FileInputStream)流或者RandomAccessFile的getChannel()方法
  • int read(ByteBuffer dst) :从 Channel 中读取数据写入 Buffer;
  • int write(ByteBuffer src) :从 Buffer 中读取数据写入 Channel;
  • long size() :返回FileChannel对应的文件的文件大小;
  • void close():对 FileChannel 的操作完成后, 必须将其关闭;
  • void truncate(int n):将文件截断到指定长度;
  • void force(bool b):强制将缓存中未写入的数据写入到文件中;
  • long position() :得到 Channel 中文件的当前操作位置;
  • position(long newPosition) :设置 Channel 中文件的当前操作位置;

注:假设我们把当前位置设置为文件结尾之后,从通道中读取数据时返回值是-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) {
			}
		}
	}

说明:

  • 上面例子使用了RandomAccessFile打开了FileChannel;
  • 代码中1.2两处对应了IO写数据的两个步骤;
  • Buffer有两种模式,在使用时要注意切换;
  • ByteBuffer申请capacity时不能小于要写入的数据量(单位字节),否则put时报java.nio.BufferOverflowException;
  • channel的write方法不能保证有多少数据真实被写入channel,因此需要循环写入直到没有更多数据

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) {
			}
		}
	}

说明:

  • 上面例子使用了FileInputStream打开了FileChannel;
  • 代码中划线的两处对应了IO读数据的两个步骤;
  • Buffer有两种模式,在使用时要注意切换;
  • 从IO设备读取数据到内核缓冲区时,如果缓冲区的大小恰好分隔了一个字符,就会出现乱码;

注:有些时候,从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();
  }
}

3、SocketChannel介绍

SocketChannel是一个客户端用来进行TCP连接的Channel,相当于Java网络编程中的Socket套接字接口。创建一个 SocketChannel 的方法有两种:

  • 在客户端使用SocketChannel.open()打开一个socketChannel,然后使用scoketChannel.connect()将其连接到某个服务器中;
  • 在服务端当一个 ServerSocketChannel 接受到连接请求时,会返回一个 SocketChannel 对象

3.1)常用方法:

  • open()、connect():打开一个SocketChannel,并且设置连接;
  • int read(ByteBuffer dst) :从 Channel 中读取数据写入 Buffer;
  • int write(ByteBuffer src) :从 Buffer 中读取数据写入 Channel;
  • void close():对ScokcetChannel 的操作完成后, 必须将其关闭;
  • configureBlocking(false):设置 SocketChannel 为非阻塞模式,这样我们的connect、read、write 都是非阻塞的了;

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();
			}
		}
	}

说明:

  • 构造http请求的时候,最少需要的协议或者说参数是:GET / HTTP/1.1 \r\n Host: www.ifeng.com \r\n
  • 通过BIO、NIO方式创建http请求设计到IO的写入(http请求)、和IO的读取(http响应);除了这种方式,更简单的方式是通过HttpURLConnection类完成http的操作,后者是基于HTTP协议的,其底层是通脱socket通信实现的。
  • 从这里也可以得出结论,http协议是在TCP只上的应用层协议,性能没有基于TCP(socket)的rpc协议高

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。

4、ServerSocketChannel介绍

ServerSocketChannel是用于服务端监听TCP链接请求的通道,正如Java网络编程中的ServerSocket一样。

4.1)常用方法:

  • ServerSocketChannel.open():打开一个ServerSocketChannel通道;
  • close():关闭;
  • accept():监听客户端的TCP请求,返回一个SocketChannel;该方法可以设置成阻塞、非阻塞模式;

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是否为空.

5、DatagramChannel介绍

DatagramChannel 是用来处理 UDP 连接的。略

参考:

https://segmentfault.com/a/1190000006824107

https://wiki.jikexueyuan.com/project/java-nio-zh/java-nio-socketchannel.html

发布了800 篇原创文章 · 获赞 460 · 访问量 436万+

猜你喜欢

转载自blog.csdn.net/liuxiao723846/article/details/104465470