浅析NIO Channel

Java NIO中,一个socket连接使用一个Channel(通道)来表示。然而,从更广泛的层面来说,一个通道封装了一个底层的文件描述符,例如硬件设备、文件、网络连接等。所以,与文件描述符相对应,Java NIO的通道分为很多类型。但是Java的通道更加的细化,例如,对应到不同的网络传输协议类型,在Java中都有不同的NIO Channel(通道)相对应。

Channel(通道)的主要类型

Channel(通道)的主要类型有:FileChannel、SocketChannel、ServerSocketChannel、 
DatagramChannel。

这四种通道的说明如下:

  1. FileChannel文件通道,用于文件的数据读写;
  2. SocketChannel套接字通道,用于Socket套接字TCP连接的数据读写;
  3. ServerSocketChannel服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道;
  4. DatagramChannel数据报通道,用于UDP协议的数据读写。

这个四种通道,涵盖了文件IO、TCP网络、UDP IO三类基础IO读写操作。

FileChannel文件通道

FileChannel是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数据,也可以将数据写入到文件中。特别申明一下,FileChannel为阻塞模式,不能设置为非阻塞模式。

获取FileChannel通道

        //创建一个文件输入流
        String srcFile = "a.txt";
        FileInputStream fis = new FileInputStream(srcFile);
        //获取文件流的通道
        FileChannel inChannel  = fis.getChannel();
        //创建一个文件输出流
        String destFile = "b.txt";
        FileOutputStream fos = new FileOutputStream(destFile);
        FileChannel outChannel = fos.getChannel();

也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道实例,代码如下:

        // 创建  RandomAccessFile 随机访问对象
        RandomAccessFile accessFile = new RandomAccessFile("a.txt","rw");
        //获取文件流的通道(可读可写)
        FileChannel channel = accessFile.getChannel();

读取FileChannel通道

        // 创建  RandomAccessFile 随机访问对象
        RandomAccessFile accessFile = new RandomAccessFile("a.txt","rw");
        //获取文件流的通道(可读可写)
        FileChannel channel = accessFile.getChannel();
        //获取字节缓冲区
        ByteBuffer buf = ByteBuffer.allocate(1024);
        int length = -1;
        //调用通道的  read 方法,读取数据并买入字节类型的缓冲区
        while ((length = channel.read(buf)) != -1) {
            //……省略  buf 中的数据处理
        }

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

写入FileChannel通道

        写入数据到通道,在大部分应用场景,都会调用通道的write(ByteBuffer)方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。

        write(ByteBuffer)方法的作用,是从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。

        //如果  buf 处于写入模式(如刚写完数据),需要  flip 翻转  buf,使其变成读取模式
        buf.flip();
        int outlength = 0;
        //调用  write 方法,将  buf 的数据写入通道
        while ((outlength = outChannel.write(buf)) != 0) {
            System.out.println("写入的字节数:" + outlength);
        }

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

关闭通道

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

    //关闭通道
    channel.close( );

强制刷新到磁盘

        在将缓冲区写入通道时,出于性能原因,操作系统不可能每次都实时将写入数据落地(或刷新)到磁盘,完成最终的数据保存。

        如果在将缓冲数据写入通道时,需要保证数据能落地写入到磁盘,可以在写入后调用一下FileChannel的force()方法。

    //强制刷新到磁盘 
    channel.force(true);

案例

    public static void main(String[] args) throws Exception {
        //新建  buf,处于写入模式
        ByteBuffer buf = ByteBuffer.allocate(1024);

        FileInputStream in = new FileInputStream("a.txt");
        FileOutputStream out = new FileOutputStream("b.txt");

        FileChannel inChannel = in.getChannel();
        FileChannel outChannel  = out.getChannel();

        int length = -1;
        //从输入通道读取到  buf
        while ((length=inChannel.read(buf))!=-1){
            //buf 第一次模式切换:翻转  buf,从写入模式变成读取模式
            buf.flip();
            int outLength = 0;
            //将  buf 写入到输出的通道
            while ((outLength=outChannel.write(buf))!=0){
                System.out.println("写入的字节数:"+outLength);
            }
            //将  buf 写入到输出的通道
            buf.clear();
        }
        //强制刷新到磁盘
        outChannel.force(true);
        //关闭所有的可关闭对象
        outChannel.close();
        out.close();
        inChannel.close();
        in.close();
    }

        除了FileChannel的通道操作外,还需要注意代码执行过程中隐藏的ByteBuffer的模式切换。由于新建的ByteBuffer是写入模式,才可作为inChannel.read(ByteBuffer)方法的参数,inChannel.read(…)方法将从通道inChannel读到的数据写入到ByteBuffer。然后,需要调用缓冲区的flip方法,将ByteBuffer从写入模式切换成读取模式,才能作为outchannel.write(ByteBuffer)方法的参数,以便从ByteBuffer读取数据,最终写入到outchannel输出通道。

        完成一次复制之后,在进入下一次复制前,还要进行一次缓冲区的模式切换。此时,需要将通过clear方法将Buffer切换成写入模式,才能进入下一次的复制。所以,在示例代码中,每一轮外层的while循环,都需要两次ByteBuffer模式切换:第一次模式切换时,翻转buf,变成读取模式;第二次模式切换时,清除buf,变成写入模式。

        作为文件复制的程序来说,以上实战代码的效率不是最高的。更高效的文件复制,可以调用 
文件通道的transferFrom方法。具体的代码如下:

    public static void testCopyQuick() throws Exception {
        FileInputStream inputStream = new FileInputStream("a.txt");
        FileOutputStream outputStream = new FileOutputStream("b.txt");
        FileChannel inChannel = inputStream.getChannel();
        FileChannel outChannel = outputStream.getChannel();
        outChannel.transferFrom(inChannel,0,inChannel.size());
        outChannel.close();
        inChannel.close();
    }

SocketChannel套接字通道

在NIO中,涉及网络连接的通道有两个:

  1. SocketChannel负责连接的数据传输
  2. ServerSocketChannel负责连接的监听

        其中,NIO中的SocketChannel传输通道,与OIO中的Socket类对应;NIO中的ServerSocketChannel监听通道,对应于OIO中的ServerSocket类。

        ServerSocketChannel仅仅应用于服务器端,而SocketChannel则同时处于服务器端和客户端,所以,对应于一个连接,两端都有一个负责传输的SocketChannel传输通道。

        无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式,其设置方法如下:

  1. socketChannel.configureBlocking(false)设置为非阻塞模式。
  2. socketChannel.configureBlocking(true)设置为阻塞模式。

        在阻塞模式下,SocketChannel通道的connect连接、read读、write写操作,都是同步的和阻塞式的,在效率上与Java旧的OIO的面向流的阻塞式读写操作相同。

        在非阻塞模式下,通道的操作是异步、高效率的,这也是相对于传统的OIO的优势所在。

获取SocketChannel传输通道

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

        //获得一个套接字传输通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);
        //对服务器的  IP 和端口发起连接
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));

非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect方法就返回了,因此需要不断地自旋,检查当前是否是连接到了主机:

        //获得一个套接字传输通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞模式
        socketChannel.configureBlocking(false);
        //对服务器的  IP 和端口发起连接
        boolean connect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        while (!socketChannel.finishConnect()){
            //不断地自旋、等待,或者做一些其他的事情……
        }

在服务器端,获取与客户端对应的传输套接字方式如下:

        在连接建立的事件到来时,服务器端的ServerSocketChannel能成功地查询出这个新连接事件,并且通过调用服务器端ServerSocketChannel监听套接字的accept()方法,来获取新连接的套接字通道:

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        SocketChannel socketChannel1 = serverSocketChannel.accept();
        socketChannel1.configureBlocking(false);

NIO 套 接 字 通 道 , 主 要 用 于 非 阻 塞 的 传 输 场 景 。 所 以 , 基 本 上 都 需 要 调 用 通 道 的configureBlocking(false)方法,将通道从阻塞模式切换为非阻塞模式。

读取SocketChannel传输通道

当SocketChannel传输通道可读时,可以从SocketChannel读取数据,具体方法与前面的文件通道读取方法是相同的。调用read方法,将数据读入缓冲区ByteBuffer。

        ByteBuffer buf = ByteBuffer.allocate(1024);
        int bytesRead = socketChannel.read(buf);

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

写入到SocketChannel传输通道

和前面的把数据写入到FileChannel文件通道一样,大部分应用场景都会调用通道的int write(ByteBuffer buf)方法。

        //写入前需要读取缓冲区,要求  ByteBuffer 是读取模式 
        buf.flip();
        socketChannel.write(buf);

关闭SocketChannel传输通道

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

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

案例

使用FileChannel文件通道读取本地文件内容,然后在客户端使用SocketChannel套接字通道,把文件信息和文件内容发送到服务器。客户端的完整代码如下:

    public  static  void sendFile() throws Exception {
        File file = new File("a.txt");
        FileChannel fileChannel = new FileInputStream(file).getChannel();
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8888));
        socketChannel.configureBlocking(false);
        //发送文件名称
        ByteBuffer fileNameByteBuffer = Charset.defaultCharset().encode("b.txt");
        ByteBuffer buffer = ByteBuffer.allocate(100000);
        //发送文件名称长度
        int fileNameLen = fileNameByteBuffer.capacity();
        buffer.putInt(fileNameLen);
        buffer.flip();
        socketChannel.write(buffer);
        buffer.clear();

        //发送文件名称
        socketChannel.write(fileNameByteBuffer);

        //发送文件长度
        buffer.putLong(file.length());
        buffer.flip();
        socketChannel.write(buffer);
        buffer.clear();

        //发送文件内容
        int length = 0;
        long progress = 0;
        while ((length=fileChannel.read(buffer))>0){
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
            progress += length;
        } 
        
        fileChannel.close();
        socketChannel.shutdownOutput();
        socketChannel.close();
    }

DatagramChannel数据报通道

在Java中使用UDP协议传输数据,比TCP协议更加简单。和Socket套接字的TCP传输协议不同,UDP协议不是面向连接的协议。使用UDP协议时,只要知道服务器的IP和端口,就可以直接向对方发送数据。在Java NIO中,使用DatagramChannel数据报通道来处理UDP协议的数据传输。

获取DatagramChannel数据报通道

获取数据报通道的方式很简单,调用DatagramChannel类的open静态方法即可。然后调用configureBlocking(false)方法,设置成非阻塞模式。

    //获取  DatagramChannel 数据报通道
    DatagramChannel channel = DatagramChannel.open(); 
    //设置为非阻塞模式
    datagramChannel.configureBlocking(false);

如果需要接收数据,还需要调用bind方法绑定一个数据报的监听端口,具体如下:

    //调用  bind 方法绑定一个数据报的监听端口
    channel.socket().bind(new InetSocketAddress(18080));

读取DatagramChannel数据报通道数据

当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的SocketChannel读取方式不同,这里不调用read方法,而是调用receive(ByteBufferbuf)方法将数据从DatagramChannel读入,再写入到ByteBuffer缓冲区中。

    //创建缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);
    //从  DatagramChannel 读入,再写入到  ByteBuffer 缓冲区 
    SocketAddress clientAddr= datagramChannel.receive(buf);

通道读取receive(ByteBufferbuf)方法虽然读取了数据到buf缓冲区,但是其返回值是 SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。通过receive方法读取 
数据非常简单,但是,在非阻塞模式下,如何知道DatagramChannel通道何时是可读的呢? 和SocketChannel一样,同样需要用到NIO的新组件—Selector通道选择器

写入DatagramChannel数据报通道

向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法也是不同的。这里不是调用write方法,而是调用send方法。示例代码如下:

    //把缓冲区翻转到读取模式 
    buffer.flip();
    //调用  send 方法,把数据发送到目标  IP+端口
    datagramChannel.send(buffer,  new InetSocketAddress("127.0.0.1",18899));
    //清空缓冲区,切换到写入模式
    buffer.clear();

由于UDP是面向非连接的协议,因此,在调用send方法发送数据的时候,需要指定接收方的地址(IP和端口)。

关闭DatagramChannel数据报通道

    //简单关闭即可
    datagramChannel.close();

案例

使用DatagramChannel数据包通到发送数据的客户端示例程序代码。其功能是:获取用户的输入数据,通过DatagramChannel数据报通道,将数据发送到远程的服务器。客户端的完整程序代码如下:

    public void send() throws IOException { 
        //获取  DatagramChannel 数据报通道
        DatagramChannel dChannel = DatagramChannel.open(); 
        //设置为非阻塞
        dChannel.configureBlocking(false); 
        ByteBuffer buffer = ByteBuffer.allocate(10000); 
        Scanner scanner = new Scanner(System.in);
        Print.tcfo("UDP 客户端启动成功!"); 
        Print.tcfo("请输入发送内容:"); 
        while (scanner.hasNext()) {
            String next = scanner.next();
            buffer.put((Dateutil.getNow() + " >>" + next).getBytes()); 
            buffer.flip();
            //通过DatagramChannel 数据报通道发送数据 
            dChannel.send(buffer,new InetSocketAddress("127.0.0.1",18899));
            buffer.clear(); 
        }
        //操作四:关闭  DatagramChannel 数据报通道 
        dChannel.close();
    }

服务器端通过DatagramChannel数据包通道接收数据的程序代码如下:

    public void receive() throws IOException { 
        //获取  DatagramChannel 数据报通道
        DatagramChannel datagramChannel = DatagramChannel.open(); 
        //设置为非阻塞模式
        datagramChannel.configureBlocking(false); 
        //绑定监听地址
        datagramChannel.bind(new InetSocketAddress("127.0.0.1",18899)); 
        Print.tcfo("UDP 服务器启动成功!");
        //开启一个通道选择器
        Selector selector = Selector.open(); 
        //将通道注册到选择器
        datagramChannel.register(selector, SelectionKey.OP_READ); 
        //通过选择器,查询  IO 事件
        while (selector.select() > 0) { 
            Iterator iterator = selector.selectedKeys().iterator(); 
            ByteBuffer buffer =ByteBuffer.allocate(1000); 
            //迭代  IO 事件
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next(); 
                //可读事件,有数据到来
                if (selectionKey.isReadable()) {
                //读取  DatagramChannel 数据报通道的数据 
                    SocketAddress client = datagramChannel.receive(buffer); 
                    buffer.flip();
                    Print.tcfo(new String(buffer.array(), 0, buffer.limit()));
                    buffer.clear(); 
                }
            }
            iterator.remove(); 
        }
        //关闭选择器和通道 
        selector.close(); 
        datagramChannel.close();
    }

在服务器端,首先调用了bind方法绑定datagramChannel的监听端口。当数据到来后,调用了receive方法,从datagramChannel数据包通道接收数据,再写入到ByteBuffer缓冲区中。

你可能感兴趣的:(高并发,nio,java,jvm)