NIO 基础组件之 Channel

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

光说不干,事事落空,又说又干,马到成功

NIO 基础组件之 Channel

什么是Channel

    传输数据的通道,其实和数据流挺像的,不过数据流是单向的而Channel 是双向的,可以向channel中写数据,也可以从channel中读取数据

1. 网络连接通道 channel

    channel 分类有很多种,但是我们先来学习有关于网络连接的 channel,ServerSocketChannelSocketChannel

1. SocketChannel

    负责连接的数据传输,其实和java底层的Socket一样,同时处于服务端和客户端,所以对应于一个连接,两端都有一个负责传输的 SocketChannel

2. ServerSocketChannel

    负责连接监听,其实和 java 底层的 ServerSocket 一样,只应用于服务端

模式支持

    无论是 ServerSocketChannel还是 SocketChannel,都支持阻塞和非阻塞两种模式。如何进行模式的设置呢?调用configureBlocking()方法,具体如下:

1. 非阻塞

    socketChannel.configureBlocking(false) 设置为非阻塞模式

2. 阻塞

    socketChannel.configureBlocking(true) 设置为阻塞模式,不过一般的都是设置成false的,因为这个是非阻塞的效率比较高,如果设置称阻塞的那么就不用NIO了就直接用IO了

如何获取SocketChannel传输通道

    在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道,然后将socket设置为非阻塞模式,最后通过connect()实例方法对服务器的IP和端口发起连接

public class ChannelClientDemo {
    public static void main(String[] args) {
        try {
            // 获取一个套接字传输通道
            SocketChannel socketChannel = SocketChannel.open();
            // 将通道设置为非阻塞模式
            socketChannel.configureBlocking(false);
            // 对服务器的IP和端口发起连接,此时不一定连接成功,
            // 因为是非阻塞的,所以如果连接不成功也会返回,因此需要进行不断的自旋
            // 检查是否连接成功
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));
            while (!socketChannel.finishConnect()) {
                // 不停的进行阻塞等待,也可以做别的事情
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

读取SocketChannel传输通道

    当SocketChannel传输通道可读时,可以从SocketChannel读取数据,调用read方法,将数据读入到缓冲区的ByteBuffer

ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 分配一个 buffer 缓冲区
int bytesRead = socketChannel.read(byteBuffer);

    在读取时,因为是异步的,所以我们必须检查read的返回值,以便判断当前是否读到了数据,read方法的返回值是读取的字节数,如果是-1,那么表示读取到对方的输出结束标志,即对方已经输出结束,准备关闭连接。实际上,通过read() 方法读数据本身是很简单的,比较困难的是在非阻塞模式下如何知道通道何时是可读的。

写入SocketChannel传输通道

    调用write方法进行缓冲区的写入,写入之前需要读取缓冲区,要求ByteBuffer是读模式

buffer.flip();
socketChannel.write(buffer);

关闭socketChannel传输通道

    在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次shutdownOutput() 终止输出方法,向对方发送一个输出的结束标志 -1 。然后调用socketChannel.close()方法,关闭套接字连接

// 调用终止输出方法,向对方发送一个输出的结束标记
socketChannel.shutdownOutput();
// 关闭套接字链接
socketChannel.close();

2. 文件读写Channel

    专门操作文件的Channel.既可以从一个文件中读取数据,也可以将数据写入文件中,但是要注意,FileChannel为阻塞模式,不是非阻塞模式,也不能设置成非阻塞模式。

获取文件的Channel

    在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个 InputStreamOutputStream或RandomAccessFile 来获取一个FileChannel实例。

// 文件输入流
FileInputStream inputStream = new FileInputStream("文件路径");
// 获取文件流输入流的通道
FileChannel inputStreamChannel = inputStream.getChannel();
// 创建一个文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("文件输出路径");
// 创建一个文件输出流通道
FileChannel outputStreamChannel = fileOutputStream.getChannel();

// 通过RandomAccessFile(文件随机访问)类来获取FileChannel实例,可读可写
RandomAccessFile randomAccessFile = new RandomAccessFile("文件路径","rw");
FileChannel accessFileChannel = randomAccessFile.getChannel();

读取FileChannel

    在大部分应用场景中,从通道读取数据都会调用通道的read(ByteBufferbuf)方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。

// 创建一个缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
// 将通道中的数据写入buf缓冲区中
accessFileChannel.read(buf);

    以上代码中channel.read(buf)读取通道的数据时,对于通道来说是读模式,对于ByteBuffer缓冲区来说是写入数据,这时ByteBuffer缓冲区处于写模式。

写入FileChannel

    把数据写入通道,在大部分应用场景中都会调用通道的write(ByteBuffer)方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。write(ByteBuffer)方法的作用是从ByteBuffer缓冲区中读取数据,然后写入通道自身,而返回值是写入成功的字节数。注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

// 如果buffer处于写模式(比如刚写完数据),那么需要反转buffer,使其变成读模式
buf.flip();
accessFileChannel.write(buf);

    在以上的outchannel.write(buf)调用中,对于入参buf实例来说,需要从其中读取数据写入outchannel通道中,所以入参buf必须处于读模式,不能处于写模式。

关闭通道

    当通道使用完成后,必须将其关闭。关闭非常简单,调用close()方法即可。

accessFileChannel.close();

强制刷新到磁盘

    在将缓冲区写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据落地(或刷新)到磁盘,完成最终的数据保存。在将缓冲区数据写入通道时,要保证数据能写入磁盘,可以在写入后调用一下FileChannel的force()方法。

accessFileChannel.force(true);

总结

    NIO 的通道要比原生的流处理好用的多,毕竟他是双向的,流处理是单向的,而且结合了buffer缓存区的操作,这样一来大大的提高了文件或者网络传输的效率,后续我会继续更新NIO buffer、selector等核心内容之后再攻占netty这个模块,等netty这个模块搞定后就继续攻占中间件系列比如MQ、dubbo等

猜你喜欢

转载自juejin.im/post/7111903187216367647