3.NIO选择器(基于NIO的服务器与客户端通讯)

Posted PacosonSWJTU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3.NIO选择器(基于NIO的服务器与客户端通讯)相关的知识,希望对你有一定的参考价值。

【README】

本文总结自B站《尚硅谷netty》,很不错;


【1】选择器Selector(多路复用器)

【1.1】基本介绍

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

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

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

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

5)特点再说明:

  • 5.1) Netty 的 IO 线程NioEventLoop 聚合了 Selector(选择器,也叫多路复用器*),可以同时并发处理成百上千个客户端连接;
  • 5.2)当线程从某客户端Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务(即线程不会在没有数据可用的客户端连接上阻塞,而是处理其他客户端请求);
  • 5.3) 线程通常将非阻塞IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道;
  • 5.4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起;
  • 5.5)一个 I/O 线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升;

【1.2】Selector类相关方法

1)Selector 类是一个抽象类, 常用方法和说明如下:
 

public abstract class Selector implements Closeable 
    public static Selector open();//得到一个选择器对象
    public int select(long timeout);// 返回IO操作准备就绪的通道的key个数;
    // 监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
    public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey

【补充】

  • 补充1) SelectionKey 与 Channel  是映射关系;通过SelectionKey 可以获取到 Channel ;
  • 补充2) SelectionKey 是 Selector的一个重要对象;

 【注意事项】
1) NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket ;
2) selector 相关方法说明:

selector.select()//阻塞
selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回;
selector.wakeup();// 唤醒selector
selector.selectNow();// 不阻塞,立马返还

【1.3】 SelectionKey在NIO 体系

1)NIO 非阻塞网络编程原理分析图
NIO 非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel) 关系梳理图;

 【图解】 服务器采用NIO非阻塞处理客户端请求步骤:

  • Step1)当客户端连接时,会通过ServerSocketChannel  生成 SocketChannel;
  • Step2) 把 SocketChannel注册到Selector上,register(Selector sel, int ops),1个selector上可以注册多个SocketChannel
  • Step3)注册后返回一个SelectionKey,会和该Selector 关联(集合);
  • Step4) Selector 监听通道Channel的select方法,返回有事件发生的通道个数.;
  • Step5)进一步得到各个SelectionKey (有事件发生);
  • Step6)再通过SelectionKey反向获取SocketChannel , 通过方法channel() 得到;
  • Step7)可以通过得到的channel , 完成业务处理;

【2】基于NIO实现服务器端和客户端之间的数据简单通讯

1)服务器代码:

/**
 * @Description 编写一个 NIO 入门案例,
 *              实现服务器端和客户端之间的数据简单通讯(非阻塞)
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2022年08月17日
 */
public class Nioserver024 
    public static void main(String[] args) throws IOException 
        // 创建 ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 得到选择器 selector
        Selector selector = Selector.open();

        // 绑定端口 6666 ,在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 把 serverSocketChannel 注册到 Selector,关心事件为 OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 循环等待客户端连接
        while (true) 
            if (selector.select(3000) == 0)  // 没有事件发生
                System.out.println("服务器等待1秒,无客户端连接");
                continue;
            
            // 如果返回值大于0, 表示监听到有事件发生,
            // 1.获取相关的  SelectionKey 集合
            // 2. selector.selectedKeys 获取关注事件的集合
            // 3. 通过 selectionKeys 反向获取通道;
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 4. 遍历 selectionKeys ,使用迭代器遍历
            Iterator<SelectionKey> it = selectionKeys.iterator();
            while (it.hasNext()) 
                // 获取到 SelectionKey
                SelectionKey key = it.next();
                // 根据key 对应的通道,发生的事件做不同处理
                if (key.isAcceptable())  // 如果是 OP_ACCEPT,表示有新的客户端连接服务器,则需要产生新的 channel
                    // 给客户端生成一个 SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 需要把 SocketChannel 设置为 非阻塞
                    socketChannel.configureBlocking(false);
                    System.out.println("客户端连接成功,生成一个 socketChannel, hashcode=" + socketChannel.hashCode());
                    // 把 SocketChannel 注册到 Selector 上,监听 OP_READ 事件
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                
                if (key.isReadable()) // 只读
                    // 通过key反向获取 Channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    // 获取到该 Channel 关联的 buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    // 把channel里的读入到 ByteBuffer
                    channel.read(buffer);
                    System.out.println("客户端:" + new String(buffer.array(), StandardCharsets.UTF_8));
                
                // 手动从集合中移除 selectionKey,防止重复操作
                it.remove();
            
        

    

2)客户端:

/**
 * @Description NIO 客户端
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2022年08月17日
 */
public class NIOClient025 
    public static void main(String[] args) throws IOException 
        // 得到一个通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置非阻塞
        socketChannel.configureBlocking(false);
        // 提供服务器端的ip 和 端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        // 连接服务器
        if (!socketChannel.connect(inetSocketAddress)) 
            while(!socketChannel.finishConnect()) 
                System.out.println("连接需要时间,客户端不会阻塞,可以做其他工作");
            
        
        // 若连接到服务器成功,则发送数据到服务器
        String text = "hello 成都";
        ByteBuffer byteBuffer = ByteBuffer.wrap(text.getBytes(StandardCharsets.UTF_8));
        // 发送数据,把buffer中的数据写入到 channel
        socketChannel.write(byteBuffer);
        System.out.println("任意键结束");
        System.in.read();
    

【演示效果】

以上是关于3.NIO选择器(基于NIO的服务器与客户端通讯)的主要内容,如果未能解决你的问题,请参考以下文章

NIO浅析

网络I/o编程模型6 Nio之Selector以及NIO客户服务通讯

RPC高性能框架总结3.NIO示例代码编写和简析

网络i/o编程3 NIO

Java-----网络编程2

Java-----网络编程2