Selector (死磕4)

【正文】JAVA NIO 死磕4: 

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群文件共享。


疯狂创客圈:如果说Java是一个武林,这里的聚集一群武痴, 交流编程体验心得
QQ群链接:
疯狂创客圈QQ群


无编程不创客,无案例不学习。 一定记得去跑一跑案例哦


JAVA NIO 死磕全目录


1. JAVA NIO简介
1.1. NIO 和 OIO 的对比
1.2. 阻塞和非阻塞
1.3. Channel
1.4. selector
1.5. Java NIO Buffer
2. Java NIO Buffer
2.1. Buffer类型的标记属性
2.1.1. capacity
2.1.2. position
2.1.3. limit
2.1.4. 总结:
2.2. Buffer 类型
2.3. Buffer中的方法
2.3.1. 获取allocate()方法
2.3.2. 写put()方法
2.3.3. 读切换flip()方法
2.3.4. 读get() 方法
2.3.5. 倒带rewind()方法
2.3.6. mark( )和reset( )
2.3.7. clear()清空
2.4. Buffer 的使用
2.4.1. 使用的基本步骤
2.4.2. 完整的实例代码
3. Java NIO Channel
3.1. Java NIO Channel的特点
3.2. Channel类型
3.3. FileChannel
3.4. SocketChannel
3.4.1. 监听连接
3.4.2. 非阻塞模式
3.5. DatagramChannel
4. NIO Selector
4.1. Selector入门
4.1.1. Selector的和Channel的关系
4.1.2. 可选择通道(SelectableChannel)
4.1.3. Channel注册到Selector
4.1.4. 选择键(SelectionKey)
4.2. Selector的使用流程
4.2.1. 创建Selector
4.2.2. 将Channel注册到Selector
4.2.3. 轮询查询就绪操作
4.3. 一个NIO 编程的简单实例

猜你喜欢

转载自www.cnblogs.com/crazymakercircle/p/9826906.html