首页 > 编程语言 >Java I/O(4):AIO和NIO中的Selector

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

时间:2022-10-21 09:59:28浏览次数:36  
标签:Java NIO buffer Selector AIO ByteBuffer new public channel

您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来~

 

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

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

 

 

 

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

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

/**
 * NIO中的Selector
 *
 * @author xiangwang
 */
public class TestSelector {
    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服务,可以看到我这里已经启动成功:

 

 

 

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

 

 

 

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

 

 

 

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

 

 

 

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

 

 

 

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

 

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

先来看个图:

 

 

 

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

还是用代码来解释。

/**
 * NIO中的Pipe
 *
 * @author xiangwang
 */
public class TestPipe {
    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();
    }
}

 

 

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

 

 

 

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

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

/**
 * AIO客户端
 *
 * @author xiangwang
 */
public class AioClient {
    public void start() throws IOException, InterruptedException {
        AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
        if (channel.isOpen()) {
            // socket接收缓冲区recbuf大小
            channel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
            // socket发送缓冲区recbuf大小
            channel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
            // 保持长连接状态
            channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
            // 连接到服务端
            channel.connect(new InetSocketAddress(8080), null,
                    new AioClientHandler(channel));
            // 阻塞主进程
            for(;;) {
                TimeUnit.SECONDS.sleep(1);
            }
        } else {
            throw new RuntimeException("Channel not opened!");
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        new AioClient().start();
    }
}

 

/**
 * AIO客户端CompletionHandler
 *
 * @author xiangwang
 */
public class AioClientHandler implements CompletionHandler<Void, AioClient> {
    private final AsynchronousSocketChannel channel;
    private final CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
    private final BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

    public AioClientHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }
    @Override
    public void failed(Throwable exc, AioClient attachment) {
        throw new RuntimeException("channel not opened!");
    }
    @Override
    public void completed(Void result, AioClient attachment) {
        System.out.println("send message to server: ");
        try {
            // 将输入内容写到buffer
            String line = input.readLine();
            channel.write(ByteBuffer.wrap(line.getBytes()));
            // 在操作系统中的Java本地方法native已经把数据写到了buffer中
            // 这里只需要一个缓冲区能接收就行了
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (channel.read(buffer).get() != -1) {
                buffer.flip();
                System.out.println("from server: " + decoder.decode(buffer).toString());
                if (buffer.hasRemaining()) {
                    buffer.compact();
                } else {
                    buffer.clear();
                }
                // 将输入内容写到buffer
                line = input.readLine();
                channel.write(ByteBuffer.wrap(line.getBytes()));
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

 

/**
 * AIO服务端
 *
 * @author xiangwang
 */
public class AioServer {
    public void start() throws InterruptedException, IOException {
        AsynchronousServerSocketChannel channel = AsynchronousServerSocketChannel.open();
        if (channel.isOpen()) {
            // socket接受缓冲区recbuf大小
            channel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
            // 端口重用,防止进程意外终止,未释放端口,重启时失败
            // 因为直接杀进程,没有显式关闭套接字来释放端口,会等待一段时间后才可以重新use这个关口
            // 解决办法就是用SO_REUSEADDR
            channel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            channel.bind(new InetSocketAddress(8080));
        } else {
            throw new RuntimeException("channel not opened!");
        }
        // 处理client连接
        channel.accept(null, new AioServerHandler(channel));
        System.out.println("server started");
        // 阻塞主进程
        for(;;) {
            TimeUnit.SECONDS.sleep(1);
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        AioServer server = new AioServer();
        server.start();
    }
}

 

/**
 * AIO服务端CompletionHandler
 *
 * @author xiangwang
 */
public class AioServerHandler implements CompletionHandler<AsynchronousSocketChannel, Void> {
    private final AsynchronousServerSocketChannel serverChannel;
    private final CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
    private final BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

    public AioServerHandler(AsynchronousServerSocketChannel serverChannel) {
        this.serverChannel = serverChannel;
    }
    @Override
    public void failed(Throwable exc, Void attachment) {
        // 处理下一次的client连接
        serverChannel.accept(null, this);
    }
    @Override
    public void completed(AsynchronousSocketChannel result, Void attachment) {
        // 处理下一次的client连接,类似链式调用
        serverChannel.accept(null, this);
        try {
            // 将输入内容写到buffer
            String line = input.readLine();
            result.write(ByteBuffer.wrap(line.getBytes()));
            // 在操作系统中的Java本地方法native已经把数据写到了buffer中
            // 这里只需要一个缓冲区能接收就行了
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (result.read(buffer).get() != -1) {
                buffer.flip();
                System.out.println("from client: " + decoder.decode(buffer).toString());
                if (buffer.hasRemaining()) {
                    buffer.compact();
                } else {
                    buffer.clear();
                }
                // 将输入内容写到buffer
                line = input.readLine();
                result.write(ByteBuffer.wrap(line.getBytes()));
            }
        } catch (InterruptedException | ExecutionException | IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

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

 

 


 

 

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

 

标签:Java,NIO,buffer,Selector,AIO,ByteBuffer,new,public,channel
From: https://www.cnblogs.com/xiangwang1111/p/16811760.html

相关文章

  • java 入土--集合详解
    java集合集合是对象的容器,实现了对对象的常用的操作,类似数组功能。和数组的区别:数组长度固定,集合长度不固定数组可以存储基本类型和引用类型,集合只能存储引用类型使......
  • A* 自动寻路算法-JavaScript
    效果图代码<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"......
  • sentinel运行报错问题处理:Unable to make protected final java.lang.Class java.lan
    使用命令:java-jarsentinel-dashboard-1.8.3.jar启动后异常情况如下java.lang.IllegalStateException:Cannotloadconfigurationclass:com.alibaba.csp.sentinel.das......
  • 对于利用Java Script实现的判断的基础浅层总结
    各种判断,请收好啦!判断输入框中的数据是否为整数:点击删除按钮,出现判断是否删除的弹窗在button里面加上这样一句话就能够实现删除判断弹窗啦!......
  • java中的多线程的常用方法
    java中的多线程的常用方法/*Java中Thread(线程)中的常用方法。1.start():用来启动当前线程,调用当前线程的run()2.run():通常需要重写Thread中的此方法,将创建的线程......
  • 力扣605(java&python)-种花问题(简单)
    题目:假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。给你一个整数数组  flowerbed表示花坛,由若......
  • java----Collection集合
    Collection集合1、集合与数组的相同点是什么?  都是容器,可以存储多个数据2、集合与数组的不同点是什么?  ①.数组的长度是不可变的,集合的长度是可变的  ②.......
  • JavaScript实现数据结构 -- 栈
    栈栈是一种==后进先出==的数据结构。JS模拟栈虽然JavaScript中没有栈,但是我们可以用数组来实现栈的功能。 //定义一个数组用来模拟栈 conststack=[]; //用数组......
  • JavaScript实现数据结构 -- 队列
    队列队列是一个先进先出的数据结构。JS模拟队列虽然JavaScript中没有队列,但是我们可以用数组来实现队列的功能。 //用数组来模拟队列 constqueue=[]; //入队 q......
  • JavaScript实现数据结构 -- 链表
    链表链表和数组一样是有多个元素组成的列表;不同的是链表元素存储不连续,用next指针连接在一起;链表的特点插入、删除不需要移动元素;不必事先分配存储空间;所需空间与长......