NIO核心之选择器(Selector)与NIO网络通信原理

选择器(Selector)

选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。

 

 

 

 

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)

  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个Selector,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程

  • 避免了多线程之间的上下文切换导致的开销

  • 选择 器(Selector)的应用

    创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。

 

Selector selector = Selector.open();

   向选择器注册通道:SelectableChannel.register(Selector sel, int ops)

//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):

  • 读 : SelectionKey.OP_READ (1)

  • 写 : SelectionKey.OP_WRITE (4)

  • 连接 : SelectionKey.OP_CONNECT (8)

  • 接收 : SelectionKey.OP_ACCEPT (16)

若注册时不止监听一个事件,则可以使用“位或”操作符连接。

int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE 

 NIO网络通信原理

Selector 示意图和特点说明

Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

 

 

 

服务端流程

public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("----服务端启动---");
        // 1、获取通道
        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        // 2、切换为非阻塞模式
        ssChannel.configureBlocking(false);
        // 3、绑定连接的端口
        ssChannel.bind(new InetSocketAddress(9999));
        // 4、获取选择器Selector
        Selector selector = Selector.open();
        // 5、将通道都注册到选择器上去,并且开始指定监听接收事件
        ssChannel.register(selector , SelectionKey.OP_ACCEPT);
        // 6、使用Selector选择器轮询已经就绪好的事件
        while (selector.select() > 0){
            System.out.println("开始新一轮事件处理");
            // 7、获取选择器中的所有注册的通道中已经就绪好的事件 迭代器的方式获得:selectedKeys获取当前选择器轮询到的所有事件 iterator()迭代器
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            // 8、开始遍历这些准备好的事件 如果有下一个 就提取
            while (it.hasNext()){
                // 提取当前这个事件
                SelectionKey sk = it.next();
                // 9、判断这个事件具体是什么 是接入事件?是 则接入客户端通道
                if(sk.isAcceptable()){
                    // 10、直接获取当前接入的客户端通道
                    SocketChannel schannel = ssChannel.accept();
                    // 11 、切换成非阻塞模式
                    schannel.configureBlocking(false);
                    // 12、将本客户端通道注册到选择器 进行读的监听(客户端写过来 服务端负责读)
                    schannel.register(selector , SelectionKey.OP_READ);
                    //如果是读事件
                }else if(sk.isReadable()){
                    // 13、获取当前选择器上的读就绪事件  先获得通道 获得客户端连接 有通道则说明有数据过来
                    SocketChannel sChannel = (SocketChannel) sk.channel();
                    // 14、设置buffer读取数据
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = 0;
                    //循环读取通道中的缓冲区 >0 还有数据未读 循环读完
                    while((len = sChannel.read(buf)) > 0){
                        buf.flip();//归位
                        System.out.println(new String(buf.array() , 0, len));
                        buf.clear();// 清除之前的数据
                    }
                }
                it.remove(); // 处理完毕之后需要移除当前事件
            }
        }
    }
}

客户端流程

public class Client {
    public static void main(String[] args) throws Exception {
        // 1、获取通道
        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
        // 2、切换成非阻塞模式
        sChannel.configureBlocking(false);
        // 3、分配指定缓冲区大小
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 4、发送数据给服务端
        Scanner sc = new Scanner(System.in);
        while (true){
            System.out.println("请说:");
            String msg = sc.nextLine();
            buf.put(("theone:"+msg).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
        }
    }
}

总结:

BIO、NIO:

  • Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • Java NIO : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

BIO、NIO适用场景分析:

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

 
posted @ 2021-06-22 11:02  ChiHsien  阅读(286)  评论(0编辑  收藏  举报