Java网络编程之NIO编程-基础篇

136 阅读5分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

简述

Java传统的网络编程为BIO(blocking I/O)网络编程。其最大的特点就是同步阻塞的,不管是ServerSocket.accept(),还是Socket的read()和write(),都是同步阻塞的。

今天我们学习的NIO网络编程模型,可以理解为新的(new)网络编程模型,也可以理解为非阻塞(non-blocking)网络编程模型。

核心组成部分

Java NIO 三个核心部分:

  • Buffer 缓冲区,用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。

  • Channel 通道,类似「流」,我们可以从通道中读取数据,又可以写数据到通道。这点与「流」不同,「流」的读写通常都是单向的,比如流通常分为「输入流」和「输出流」。

  • Selector 选择器,是NIO模型中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

Buffer 缓冲区

Buffer(缓冲区)本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。Buffer在JDK里是一个抽象类,常见的实现类有:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

Buffer是可以任意读写的一块内存。内存有三个基本操作:分配内存、读、写。与之对应的Buffer,基本操作也是分配、读、写。

以ByteBuffer为例:

  1. 分配内存
ByteBuffer buffer = ByteBuffer.allocate(10);
  1. 写入操作
buffer.put((byte) 'a');
  1. 读操作
byte b = buffer.get();

通常我们需要把Buffer从写模式切换到读模式,调用flip()方法。

buffer.flip();

Buffer有三个重要属性:capacity,position和limit。Buffer的内存分配、读、写都离不开这三个属性。

  • capacity,分配内存的容量大小。一旦分配下来,其基本不会变。

  • position,当前内存操作的位置。当处于写模式下,其为下一个待写入的位置;当处于读模式下,其为当前可读的位置。

  • limit,限制位置(大小)。当处于写模式下,其等于capacity,即可写满;当处于读模式下,其为限制读最大的位置。

示例:

public class BufferDemo {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(10);
        System.out.println("init:" + buffer);
        buffer.put((byte) 'a');
        System.out.println("after write:" + buffer);

        buffer.flip();
        System.out.println("after flip:" + buffer);

        buffer.get();
        System.out.println("after read:" + buffer);
    }
}
// 输出结果
init:java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
after write:java.nio.HeapByteBuffer[pos=1 lim=10 cap=10]
after flip:java.nio.HeapByteBuffer[pos=0 lim=1 cap=10]
after read:java.nio.HeapByteBuffer[pos=1 lim=1 cap=10]

Selector 选择器

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

最基本的用法就是:向Selector注册通道,并设置感兴趣事件,其将会返回返回SelectionKey对象。

SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);

通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”;一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。分别用四个常量表示:

  • SelectionKey.OP_CONNECT 连接就绪
  • SelectionKey.OP_ACCEPT 接受就绪
  • SelectionKey.OP_READ 读就绪
  • SelectionKey.OP_WRITE 写就绪

如果想要监听多个就绪状态,可以用「位或」操作连起来。

int allInterestSet = SelectionKey.OP_CONNECT | SelectionKey.OP_ACCEPT | SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey 相关操作

获取通道在选择器选择的感兴趣的事件集合

int interestSet = selectionKey.interestOps();

通过简单的「与」操作,判断是否对相关事件感兴趣


    boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
    boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
    boolean isInterestedInRead    = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
    boolean isInterestedInWrite   = (interestSet & SelectionKey.OP_WRITE) ==  SelectionKey.OP_WRITE;

获取通道已准备就绪的操作的集合

int readyOps = selectionKey.readyOps();

可以像上面「与」操作来判断某个事件是否就绪。但我们可以使用以下方法直接获取相关事件是否就绪

        boolean acceptable = selectionKey.isAcceptable();
        boolean connectable = selectionKey.isConnectable();
        boolean readable = selectionKey.isReadable();
        boolean writable = selectionKey.isWritable();

        // 获取Channel和Selector
        SelectableChannel selectableChannel = selectionKey.channel();
        Selector selector1 = selectionKey.selector();

完整代码:

public class SelectorDemo {
    public static void main(String[] args) throws IOException {
        
        Selector selector = Selector.open();

        SocketChannel channel = SocketChannel.open();
        // Channel设置为非阻塞模式
        channel.configureBlocking(false);

        // 向Selector注册通道,并设置感兴趣事件,OP_READ表示读就绪事件,并返回SelectionKey对象
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_READ);

       
        // 获取通道在选择器选择的感兴趣的事件集合
        int interestSet = selectionKey.interestOps();
        // 通过简单的「与」操作,判断是否对相关事件感兴趣
        boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
        boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
        boolean isInterestedInRead    = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;
        boolean isInterestedInWrite   = (interestSet & SelectionKey.OP_WRITE) ==  SelectionKey.OP_WRITE;

        // 获取通道已准备就绪的操作的集合
        int readyOps = selectionKey.readyOps();

        // 可以像上面「与」操作来判断某个事件是否就绪。但我们可以使用以下方法直接获取相关事件是否就绪
        boolean acceptable = selectionKey.isAcceptable();
        boolean connectable = selectionKey.isConnectable();
        boolean readable = selectionKey.isReadable();
        boolean writable = selectionKey.isWritable();

        // 获取Channel和Selector
        SelectableChannel selectableChannel = selectionKey.channel();
        Selector selector1 = selectionKey.selector();

        // 附加对象设置方式1:selectionKey.attach(theObject);
        // 附加对象设置方式2:selectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
        // 获取附加对象方式:Object attachedObj = selectionKey.attachment();


        // 获取准备就绪(自上次调用select()方法后有多少个通道变成就绪状态)的通道数。如果当前还未有就绪的通道,则会阻塞到至少一个通道在你注册的事件上就绪。
        int selectCount = selector.select();

        // int select(long timeout); 可以设置timeout毫秒阻塞事件

        // selectNow() 不会阻塞,不管什么通道就绪都立刻返回

        // 获取已就绪「键」集
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
        while (keyIterator.hasNext()) {
            SelectionKey next = keyIterator.next();
            // SelectionKey相关操作
            next.interestOps();
            next.readyOps();
            next.isAcceptable();
            next.cancel();
            // Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中
            keyIterator.remove();
        }
        // 当某个线程调用select方法阻塞时,其他线程可通过调用wakeUp()方法唤醒
//        selector.wakeup();
        // 关闭Selector,注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
        selector.close();
    }
}