Java I/O(4):AIO和NIO中的Selector

165 阅读6分钟

您好,我是湘王,这是我的掘金小站,欢迎您来,欢迎您再来~

在Java NIO的三大核心中,除了Channel和Buffer,剩下的就是Selector了。有的地方叫它选择器,也有叫多路复用器的(比如Netty)。

之前提过,数据总是从Channel读取到Buffer,或者从Buffer写入到Channel,单个线程可以监听多个Channel——Selector就是这个线程背后的实现机制(所以得名Selector)。

01.Selector.png

 

Selector通过控制单个线程处理多个Channel,如果应用打开了多个Channel,但每次传输的流量都很低,使用Selector就会很方便(至于为什么,具体到Netty中再分析)。所以使用Selector的好处就显而易见:用最少的资源实现最多的操作,避免了线程切换带来的开销。

还是以代码为例来演示Selector的作用。新建一个类,在main()方法中输入下面的代码:


public static void main(String args[]) throws IOException {

    // 创建ServerSocketChannel
    ServerSocketChannel channel1 = ServerSocketChannel.open();
    channel1.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
    channel1.configureBlocking(false);
    ServerSocketChannel channel2 = ServerSocketChannel.open();
    channel2.socket().bind(new InetSocketAddress("127.0.0.1", 9090));
    channel2.configureBlocking(false);

    // 创建一个Selector对象
    Selector selector = Selector.open();
    // 按照字面意思理解,应该是这样的:selector.register(channel, event);
    // 但其实是这样的:channel.register(selector, SelectionKey.OP_READ);
    // 四种监听事件:
    // OP_CONNECT(连接就绪)
    // OP_ACCEPT(接收就绪)
    // OP_READ(读就绪)
    // OP_WRITE(写就绪)
    // 注册Channel到Selector,事件一旦被触发,监听随之结束
    SelectionKey key1 = channel1.register(selector, SelectionKey.OP_ACCEPT);
    SelectionKey key2 = channel2.register(selector, SelectionKey.OP_ACCEPT);

    // 模版代码:在编写程序时,大多数时间都是在模板代码中添加相应的业务代码
    while(true) {
        int readyNum = selector.select();
        if (readyNum == 0) {
            continue;
        }

        Set<SelectionKey> selectedKeys = selector.selectedKeys();
        // 轮询
        for (SelectionKey key : selectedKeys) {
            Channel channel = key.channel();
            if (key.isConnectable()) {
                if (channel == channel1) {
                    System.out.println("channel1连接就绪");
                } else {
                    System.out.println("channel2连接就绪");
                }
            } else if (key.isAcceptable()) {
                if (channel == channel1) {
                    System.out.println("channel1接收就绪");
                } else {
                    System.out.println("channel2接收就绪");
                }
            }
            // 触发后删除,这里不删
            // it.remove();
        }
    }

}

 

代码写好后启动ServerSocketChannel服务,可以看到我这里已经启动成功:

04.启动服务.png

 

然后在网上下载一个叫做SocketTest.jar的工具(在一些工具网站下载的时候当心中毒,如果不放心,可以私信我,给你地址),双击打开,并按下图方式执行:

05.启动SocketTest.png

 

点击「Connect」可以看到变化:

06.channel1接受就绪.png

 

然后点击「Disconnect」,再输入「9090」后,再点击「Connect」试试:

07.换成9090.png

 

可以看到结果显示结果变了:

08.显示结果变了.png

两次连接,打印了三条信息:说明selector的轮询在起作用(因为Set中包含了所有处于监听的SelectionKey)。但是「接收就绪」监听事件仅执行了一次就再不响应。如果感兴趣的话你可以把OP_READ、OP_WRITE这些事件也执行一下试试看。

 

因为Selector是单线程轮询监听多个Channel,那么如果Selector(线程)之间需要传递数据,怎么办呢?——Pipe登场了。Pipe就是一种用于Selector之间数据传递的「管道」。

先来看个图:

09.Pipe.png

 

可以清楚地看到它的工作方式。

还是用代码来解释。

public static void main(String args[]) throws IOException {

    // 打开管道
    Pipe pipe = Pipe.open();

    // 将Buffer数据写入到管道
    Pipe.SinkChannel sinkChannel = pipe.sink();
    ByteBuffer buffer = ByteBuffer.allocate(32);
    buffer.put("ByteBuffer".getBytes());
    // 切换到写模式
    buffer.flip();
    sinkChannel.write(buffer);

    // 从管道读取数据
    Pipe.SourceChannel sourceChannel = pipe.source();
    buffer = ByteBuffer.allocate(32);
    sourceChannel.read(buffer);
    System.out.println(new String(buffer.array()));

    // 关闭管道
    sinkChannel.close();
    sourceChannel.close();

}

 

之前说过,同步指的按顺序一次完成一个任务,直到前一个任务完成并有了结果以后,才能再执行后面的任务。而异步指的是前一个任务结束后,并不等待任务结果,而是继续执行后一个任务,在所有任务都「执行」完后,通过任务的回调函数去获得结果。所以异步使得应用性能有了极大的提高。为了更加生动地说明什么是异步,可以来做个实验:

11.异步效果.png

 

通过调用CompletableFuture.supplyAsync()方法可以很明显地观察到,处于位置2的「这一步先执行」会最先显示,然后才执行位置1的代码。而这就是异步的具体实现。

NIO为了支持异步,升级到了NIO2,也就是AIO。而AIO引入了新的异步Channel的概念,并提供了异步FileChannel和异步SocketChannel的实现。AIO的异步SocketChannel是真正的异步非阻塞I/O。通过代码可以更好地说明:

/**
 * AIO客户端
 *
 * @author xiangwang
 */
public class AioClient {
    private AsynchronousSocketChannel channel;

    public AioClient(String host, int port) {
        try {
            // 初始化
            channel = AsynchronousSocketChannel.open();
            if (channel.isOpen()) {
                channel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
                channel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
                channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
            }
            channel.connect(new InetSocketAddress(8080), null, new AioClientHandler(channel));
            // 阻塞程序,防止自动退出
            while(true) {
                Thread.sleep(1000);
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 启动客户端
        new AioClient("localhost", 8080);
    }
}
/**
 * AIO客户端CompletionHandler
 *
 * @author xiangwang
 */
public class AioClientHandler implements CompletionHandler<Void, AioClient> {
    private AsynchronousSocketChannel channel;

    public AioClientHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void completed(Void result, AioClient attachment) {
        try {
            read(channel);
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void failed(Throwable exc, AioClient attachment) {
        exc.printStackTrace();
    }

    private void read(AsynchronousSocketChannel channel) throws ExecutionException, InterruptedException {
//        System.out.println("enter your message to server : ");
//        Scanner s = new Scanner(System.in);
//        String line = s.nextLine();
//        write(line);
//        ByteBuffer buffer = ByteBuffer.allocate(1024);
//        while (-1 != channel.read(buffer).get()) {
//            buffer.flip();
//            System.out.println("from server: " + new String(buffer.array(), StandardCharsets.UTF_8));
//        }
        System.out.println("enter your message to server : ");
        Scanner s = new Scanner(System.in);
        String line = s.nextLine();
        write(line);
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while(channel.read(buffer).get() != -1){
                buffer.flip();
                System.out.println("from server: " + new String(buffer.array(), StandardCharsets.UTF_8));
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void write(String line) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(line.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        channel.write(buffer);
    }
}
/**
 * AIO服务端
 *
 * @author xiangwang
 */
public class AioServer {
    private AsynchronousServerSocketChannel channel;

    public AioServer(int port) {
        System.out.println("server starting at port " + port + "..");
        try {
            // 初始化
            channel = AsynchronousServerSocketChannel.open();
            if (channel.isOpen()) {
                channel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
                channel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
                channel.bind(new InetSocketAddress(port));
            }
            // 监听客户端连接,需要在处理逻辑中再次调用accept用于开启下一次的监听
            // 类似于链式调用
            channel.accept(null, new AioServerHandler(channel));
            // 阻塞程序,防止自动退出
            while(true) {
                Thread.sleep(1000);
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        // 启动服务端
        new AioServer(8080);
    }
}
/**
 * AIO服务端CompletionHandler
 *
 * @author xiangwang
 */
public class AioServerHandler implements CompletionHandler<AsynchronousSocketChannel, Void> {
    private AsynchronousServerSocketChannel serverChannel;

    public AioServerHandler(AsynchronousServerSocketChannel serverChannel) {
        this.serverChannel = serverChannel;
    }

    @Override
    public void completed(AsynchronousSocketChannel channel, Void attachment) {
        // 处理下一次的client连接,类似链式调用
        serverChannel.accept(attachment, this);
        // 执行业务逻辑
        read(channel);
    }

    @Override
    public void failed(Throwable exc, Void attachment) {
        serverChannel.accept(attachment, this);
    }

    /**
     * 读取client发送的消息打印到控制台
     * AIO中OS已经帮助我们完成了read的IO操作,所以我们直接拿到了读取的结果
     *
     */
    private void read(AsynchronousSocketChannel channel) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 从client读取数据
        // 在调用channel.read()之前操作系统已经完成了I/O操作
        // 只需要用一个缓冲区来存放读取的内容即可
        channel.read(
                buffer,   // 用于数据中转缓冲区
                buffer,   // 用于存储client发送的数据的缓冲区
                new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer result, ByteBuffer attachment) {
                        // 切换模式
                        attachment.flip();
                        // 读取client发送的数据
                        System.out.println("from client: " + new String(attachment.array(), StandardCharsets.UTF_8));
                        // 向client写入数据
                        write(channel);
                    }

                    @Override
                    public void failed(Throwable exc, ByteBuffer attachment) {
                    }
                });
    }

    private void write(AsynchronousSocketChannel channel) {
        // 向client发送数据,clientChannel.write()是一个异步调用,该方法执行后会通知
        // OS执行写操作,并立即返回
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Scanner s = new Scanner(System.in);
        String line = s.nextLine();
        buffer.put(line.getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        channel.write(buffer);
    }
}

执行测试后显示,不管是在客户端还是在服务端,读写完全是异步的。

感谢您的大驾光临!咨询技术、产品、运营和管理相关问题,请关注后留言。欢迎骚扰,不胜荣幸~