zoukankan      html  css  js  c++  java
  • Java NIO Selector

    【正文】netty死磕1.4: 

    Java NIO Selector 一文全解


    1.1. Selector入门

    1.1.1. Selector的和Channel的关系

    Java NIO的核心组件包括:

    (1)Channel(通道)

    (2)Buffer(缓冲区)

    (3)Selector(选择器)

    其中Channel和Buffer比较好理解 ,联系也比较密切,他们的关系简单来说就是:数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。

    选择器和他们的关系又是什么?

    选择器(Selector) 是 Channel(通道)的多路复用器,Selector 可以同时监控多个 通道的 IO(输入输出) 状况。

    Selector的作用是什么?

    选择器提供选择执行已经就绪的任务的能力。从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。

    1.1.2. 可选择通道(SelectableChannel)

    并不是所有的Channel,都是可以被Selector 复用的。比方说,FileChannel就不能被选择器复用。为什么呢?

    判断一个Channel 能被Selector 复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能。

    SelectableChannel类是何方神圣?

    SelectableChannel类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有socket通道,都继承了SelectableChannel类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而FileChannel类,没有继承SelectableChannel,因此是不是可选通道。

    通道和选择器注册之后,他们是绑定的关系吗?

    答案是不是。不是一对一的关系。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。

    通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。

    wps3749.tmp

    1.1.3. Channel注册到Selector

    使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。第一个参数,指定通道要注册的选择器是谁。第二个参数指定选择器需要查询的通道操作。

    可以供选择器查询的通道操作,从类型来分,包括以下四种:

    (1)可读 : SelectionKey.OP_READ

    (2)可写 : SelectionKey.OP_WRITE

    (3)连接 : SelectionKey.OP_CONNECT

    (4)接收 : SelectionKey.OP_ACCEPT

    如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

    注意,操作一词,是一个是使用非常泛滥,也是一个容易混淆的词。特别提醒的是,选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。

    什么是操作的就绪状态?

    一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被Selector查询到,程序可以对通道进行对应的操作。比方说,某个SocketChannel通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT)。再比方说,一个ServerSocketChannel服务器通道准备好接收新进入的连接,则处于“接收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是“读就绪”(OP_READ)。一个等待写数据的通道可以说是“写就绪”(OP_WRITE)。

    1.1.4. 选择键(SelectionKey)

    Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。

    Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。

    一个选择键,首先是包含了注册在Selector的通道操作的类型,比方说SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。

    开发应用程序是,选择键是编程的关键。NIO的编程,就是根据对应的选择键,进行不同的业务逻辑处理。

    选择键的概念,有点儿像事件的概念。

    选择键和事件的关系是什么?

    一个选择键有点儿像监听器模式里边的一个事件,但是又不是。由于Selector不是事件触发的模式,而是主动去查询的模式,所以不叫事件Event,而是叫SelectionKey选择键。

    1.2. Selector的使用流程

    1.2.1. 创建Selector

    Selector对象是通过调用静态工厂方法open()来实例化的,如下:

      // 1、获取Selector选择器
    
                Selector selector = Selector.open();
    

    Selector的类方法open()内部是向SPI发出请求,通过默认的SelectorProvider对象获取一个新的实例。

    1.2.2. 将Channel注册到Selector

    要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:

                // 2、获取通道
    
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
                // 3.设置为非阻塞
    
                serverSocketChannel.configureBlocking(false);
    
                // 4、绑定连接
    
                serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));
    
                // 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
    
                serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
    

    上面通过调用通道的register()方法会将它注册到一个选择器上。

    首先需要注意的是:

    与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。这意味着,FileChannel不能与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字相关的所有的通道都可以。

    另外,还需要注意的是:

    一个通道,并没有一定要支持所有的四种操作。比如服务器通道ServerSocketChannel支持Accept 接受操作,而SocketChannel客户端通道则不支持。可以通过通道上的validOps()方法,来获取特定通道下所有支持的操作集合。

    1.2.3. 轮询查询就绪操作

    万事俱备,可以开干。下一步是查询就绪的操作。

    通过Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。

    下面是Selector几个重载的查询select()方法:

    (1)select():阻塞到至少有一个通道在你注册的事件上就绪了。

    (2)select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。

    (3)selectNow():非阻塞,只要有通道就绪就立刻返回。

    select()方法返回的int值,表示有多少通道已经就绪,更准确的说,是自前一次select方法以来到这一次select方法之间的时间段上,有多少通道变成就绪状态。

    一旦调用select()方法,并且返回值不为0时,下一步工干啥?

    通过调用Selector的selectedKeys()方法来访问已选择键集合,然后迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作:

    Set selectedKeys = selector.selectedKeys();
    
    Iterator keyIterator = selectedKeys.iterator();
    
    while(keyIterator.hasNext()) {
    
        SelectionKey key = keyIterator.next();
    
        if(key.isAcceptable()) {
    
            // a connection was accepted by a ServerSocketChannel.
    
        } else if (key.isConnectable()) {
    
            // a connection was established with a remote server.
    
        } else if (key.isReadable()) {
    
            // a channel is ready for reading
    
        } else if (key.isWritable()) {
    
            // a channel is ready for writing
    
        }
    
        keyIterator.remove();
    
    }
    

    处理完成后,直接将选择键,从这个集合中移除,防止下一次循环的时候,被重复的处理。键可以但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。

    1.3. 一个NIO 编程的简单实例

    package com.crazymakercircle.iodemo.base;
    
    import com.crazymakercircle.config.SystemConfig;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.util.Iterator;
    
    public class SelectorDemo
    {
    
        static class Client
        {
            /**
             * 客户端
             */
            public static void testClient() throws IOException
            {
                InetSocketAddress address= new InetSocketAddress(SystemConfig.SOCKET_SERVER_IP, SystemConfig.SOCKET_SERVER_PORT);
    
    
                // 1、获取通道(channel)
                SocketChannel socketChannel =  SocketChannel.open(address);
                // 2、切换成非阻塞模式
                socketChannel.configureBlocking(false);
    
                // 3、分配指定大小的缓冲区
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                byteBuffer.put("hello world  ".getBytes());
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
    
                socketChannel.close();
            }
    
            public static void main(String[] args) throws IOException
            {
                testClient();
            }
        }
        static class Server
        {
    
            public static void testServer() throws IOException
            {
    
                // 1、获取Selector选择器
                Selector selector = Selector.open();
    
                // 2、获取通道
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                // 3.设置为非阻塞
                serverSocketChannel.configureBlocking(false);
                // 4、绑定连接
                serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));
    
                // 5、将通道注册到选择器上,并注册的操作为:“接收”操作
                serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
                // 6、采用轮询的方式,查询获取“准备就绪”的注册过的操作
                while (selector.select() > 0)
                {
                    // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
                    Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                    while (selectedKeys.hasNext())
                    {
                        // 8、获取“准备就绪”的时间
                        SelectionKey selectedKey = selectedKeys.next();
    
                        // 9、判断key是具体的什么事件
                        if (selectedKey.isAcceptable())
                        {
                            // 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
                            SocketChannel socketChannel = serverSocketChannel.accept();
                            // 11、切换为非阻塞模式
                            socketChannel.configureBlocking(false);
                            // 12、将该通道注册到selector选择器上
                            socketChannel.register(selector, SelectionKey.OP_READ);
                        }
                        else if (selectedKey.isReadable())
                        {
                            // 13、获取该选择器上的“读就绪”状态的通道
                            SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
    
                            // 14、读取数据
                            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                            int length = 0;
                            while ((length = socketChannel.read(byteBuffer)) != -1)
                            {
                                byteBuffer.flip();
                                System.out.println(new String(byteBuffer.array(), 0, length));
                                byteBuffer.clear();
                            }
                            socketChannel.close();
                        }
    
                        // 15、移除选择键
                        selectedKeys.remove();
                    }
                }
    
                // 7、关闭连接
                serverSocketChannel.close();
            }
    
            public static void main(String[] args) throws IOException
            {
                testServer();
            }
        }
    }
    

    2. NIO编程小结

    NIO编程的难度比同步阻塞BIO大很多。

    请注意以上的代码中并没有考虑“半包读”和“半包写”,如果加上这些,代码将会更加复杂。

    (1)客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。

    (2)SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。

    (3)线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能、高负载的网络服务器。



    源码:


    代码工程:  JavaNioDemo.zip

    下载地址:在疯狂创客圈QQ群文件共享。



    无编程不创客,无案例不学习。疯狂创客圈,一大波高手正在交流、学习中!

    疯狂创客圈 Netty 死磕系列 10多篇深度文章博客园 总入口】  QQ群:104131248

  • 相关阅读:
    第3章 结束会话端点(EndSession Point)
    第2章 授权端点(Authorize Endpoint)
    第1章 发现端点(Discovery Endpoint)
    欢迎使用IdentityModel文档!- IdentityModel 中文文档(v1.0.0)
    IdentityModel 中文文档(v1.0.0) 目录
    第66章 视频
    第65章 博客帖子
    第64章 学习
    Jenkins Ant 自动编译部署测试环境
    jenkins+jmeter+ant自动化接口测试集成
  • 原文地址:https://www.cnblogs.com/crazymakercircle/p/9826906.html
Copyright © 2011-2022 走看看