【2023】NIO从零到一入门学习——网络编程Selector

本文介绍了JavaNIO中的Selector如何用于实现多路复用网络通信,包括Selector的基本操作、事件类型、关注事件的方法,以及处理accept和read事件的具体步骤。文章还讨论了消息边界问题及其解决方案,如固定消息长度、分隔符拆分和TLV格式。
摘要由CSDN通过智能技术生成

使用Selector进行网络通信

在这里主要使用的是IO模型中的多路复用,想详细了解io模型可以看我上一篇

多路复用

单线程可以配合Selector完成多个Channel可读写事件的监控,这称之为多路复用

  • 多路复用仅针对网络IO、普通文件IO没法利用多路复用
    在这里插入图片描述

Selector事件

  • accept :会在有连接请求时触发 SelectionKey.*OP_ACCEPT = 16*
  • connect :是客户端,连接建立后触发 SelectionKey.OP_CONNECT = 8
  • read : 可读事件 SelectionKey.*OP_READ = 1*
  • write : 可写事件 SelectionKey.OP_WRITE = 4

1、Selector常用方法

Selector.open() :获取Selector 选择器

  • ssc.configureBlocking(false) :配置通道为非阻塞
  • SelectionKey sscKey = ssc.register(selector, 0, null);
    • 把ssc这个Channel注册到selector 上,并且表示关注事件为0(也可以添加要关注的事件),附件为null
    • socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
      • // 将套接字通过到注册到选择器,关注 readwrite事件
    • scKey.interestOps(SelectionKey.*OP_READ* | SelectionKey.*OP_WRITE*) //使用的是位运算符
      • 表示关注这两个事件,也可以使用 + 号代替,
    • scKey.attach(buffer);
      • 把buffer挂载到scKey这个事件上;

2、具体实现代码

简单获取事件

public static void main(String[] args) throws IOException {
//        1、创建selector,用于管理多个channel
        Selector selector = Selector.open();
        ByteBuffer buffer = ByteBuffer.allocate(16);
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);

//        2、建立selector 和 channel 的联系(注册)
//         SelectionKey 就是将来用于管理时间发生后,通过它可以知道事件和哪个Channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
//         key 只关注accept事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register key:{}"+sscKey);

        ssc.bind(new InetSocketAddress(8080));  //连接本机端口
        while (true){
//            3、select 方法,没有事件就阻塞,如果发生事件,线程恢复运行
//            (如果不处理这个事件时,不会进行阻塞;只有处理了这个事件或者取消了这个事件才会进入阻塞)
            selector.select();

//            4、处理事件,SelectionKey 内部包含了所有发生的事件
//                    获取迭代器,进行遍历事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                log.debug("key:{}",key);
/*                ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//                处理这个事件
                SocketChannel sc = channel.accept();
                log.debug("{}",sc);*/
//                取消这个事件
                key.cancel();
            }
        }
    }

流程图
在这里插入图片描述

3、accept和read事件

监听Channel事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少Channel发生了事件

方法1,阻塞直到绑定事件发生

selector.select();

方法2.阻塞直到绑定事件发生,或是超时(时间单位为ms)

selector.select(long timeout);

方法3.不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

selector.selectNow();

🔔select 何时不阻塞

  • 事件发生时
    • 客户端发起连接请求,会触发accept事件
    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于buffer 缓冲区,会触发多次读取事件
    • channel 可写,会触发write事件
    • 在linux下nio bug发生时
  • 调用selector.wakeup()
  • 调用selector.closel()
  • selector 所在线程interrupt
  • 使用Selector读取客户端数据
    • 处理完一个事件需要把事件从SelectionKey集合中删除掉,因为SelectionKey处理完这个事件后不会删除,只会做一个处理后的标记,就会一直循环执行这个已经处理过的事件(就会报错);需要手动的把这个事件从集合中remove()
    • 当客户端错误断开连接时,服务器会发生一个read事件,selector集合就会往SelectionKey集合中丢这个事件,然而因为客户端已经断开连接了,这个事件就无法处理成功,会报错,需要try catch 包住;并且需要把这个事件从selector集合中取消,避免一直往SelectionKey集合中丢;正常断开也是,执行channel.read(buffer1)会返回-1,也需要手动去取消这个事件。
    • 如果传输中文字符时,如果缓冲区大小不够,出现半包时,就会出现消息边界问题
public static void main(String[] args) throws IOException {
//        1、创建selector,用于管理多个channel
        Selector selector = Selector.open();
        ByteBuffer buffer = ByteBuffer.allocate(16);
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);

//        2、建立selector 和 channel 的联系(注册)
//         SelectionKey 就是将来用于管理事件发生后,通过它可以知道事件和哪个Channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
//         key 只关注accept事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register key:{}"+sscKey);

        ssc.bind(new InetSocketAddress(8080));
        while (true){
//            3、select 方法,没有事件就阻塞,如果发生事件,线程恢复运行
//            (如果不处理这个事件时,不会进行阻塞;只有处理了这个事件或者取消了这个事件才会进入阻塞)
            selector.select();

//            4、处理事件,SelectionKey 内部包含了所有发生的事件
//                    获取迭代器,进行遍历事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
//                当拿到key之后需要把key从sekectedkeys集合中删除掉,
//                      否则下次还会继续处理这个被处理过的key,就会报错
                iterator.remove();
                log.debug("key:{}",key);
//                5、区分事件类型
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//                处理这个事件
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}",sc);
                    log.debug("key{}",scKey);
                }else if (key.isReadable()){ //如果是read
//                    读取channel中的数据
                    try {
                        SocketChannel channel = (SocketChannel) key.channel();
                        ByteBuffer buffer1 = ByteBuffer.allocate(4);
                        int read = channel.read(buffer1);  //如果客户端正常断开,read的返回值是-1;
                        if (read==-1){
                            key.cancel();   //取消这个事件,避免一直往集合中放
                        }

                        buffer1.flip();
                        System.out.println(Charset.defaultCharset().decode(buffer1)); //直接打印会出现消息边界的问题
                    } catch (IOException e) {
                        e.printStackTrace();
                        key.cancel();   //当客户端异常断开时,需要把这个key取消,(从selector集合中删除,避免一直往执行集合写入)
                    }
                }
//                取消这个事件
//                key.cancel();
            }
        }
    }
}

4、处理消息边界

消息边界出现的原因

  • ByteBufeer较小,但是消息比较大
  • ByteBufeer较大,消息比较小。会出现半包现象
  • ButeBuffer较小,但是容纳了多个消息。此时会出现黏包现象

在这里插入图片描述

处理方式

  • 一种思路是固定消息长度,数据包一样,服务器按预定长度读取,缺点是浪费宽带
  • 另一种是按分隔符拆分,缺点是效率低
  • TLV格式,即Type类型、Length长度、Value数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的buffer,缺点是buffer需要提前分配,如果内容过大,则影响server吞吐量
    • Http1.1是TLV格式:先传出类型,再传出长度,最后传出数据
    • Http2.0是LTV格式:先传出长度,再传出类型,最后传出数据

在这里插入图片描述

如果一次传输的数据过长,第一次分隔符的长度就大于了缓冲区长度,则会出现下面的情况

在这里插入图片描述

代码举例

按固定分隔符解决消息边界

  • 因为缓冲区不能够扩容,所以需要把缓冲区作为附件放入SelectionKey中,当容量不够时,进行一次扩容,更新掉附件中的缓冲区
public class Server2 {
    public static void main(String[] args) throws IOException {
//        1、创建selector,用于管理多个channel
        Selector selector = Selector.open();
        ByteBuffer buffer = ByteBuffer.allocate(16);
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);

//        2、建立selector 和 channel 的联系(注册)
//         SelectionKey 就是将来用于管理事件发生后,通过它可以知道事件和哪个Channel的事件
        SelectionKey sscKey = ssc.register(selector, 0, null);
//         key 只关注accept事件
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register key:{}"+sscKey);

        ssc.bind(new InetSocketAddress(8080));
        while (true){
//            3、select 方法,没有事件就阻塞,如果发生事件,线程恢复运行
//            (如果不处理这个事件时,不会进行阻塞;只有处理了这个事件或者取消了这个事件才会进入阻塞)
            selector.select();

//            4、处理事件,SelectionKey 内部包含了所有发生的事件
//                    获取迭代器,进行遍历事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
//                当拿到key之后需要把key从sekectedkeys集合中删除掉,
//                      否则下次还会继续处理这个被处理过的key,就会报错
                iterator.remove();
                log.debug("key:{}",key);
//                5、区分事件类型
                if (key.isAcceptable()) {
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
//                处理这个事件
                    SocketChannel sc = channel.accept();
                    sc.configureBlocking(false);
                    ByteBuffer buffer2 = ByteBuffer.allocate(5);
//                    register(注册的selector,事件数量,附件)
                    SelectionKey scKey = sc.register(selector, 0, buffer2);   //把buffer当作attachment(附件)放入SelectionKey中
                    scKey.interestOps(SelectionKey.OP_READ);
                    log.debug("{}",sc);
                    log.debug("key{}",scKey);
                }else if (key.isReadable()){ //如果是read
//                    读取channel中的数据
                    try {
                        SocketChannel channel = (SocketChannel) key.channel();
//                        获取放入SelectionKey中关联的附件
                        ByteBuffer buffer1 = (ByteBuffer) key.attachment();
                        int read = channel.read(buffer1);  //如果客户端正常断开,read的返回值是-1;
                        if (read==-1){
                            key.cancel();   //取消这个事件,避免一直往集合中放
                        }else {
                            System.out.println(buffer1);
                            split(buffer1);
                            if (buffer1.position()==buffer1.limit()){ //如果缓冲区的position和limit相等,则代表数据长度大于缓冲区,数据未读

                                ByteBuffer newBuffer = ByteBuffer.allocate(buffer1.capacity() * 2); //则把原来的容量进行扩容
                                newBuffer.flip();
                                newBuffer.put(buffer1);  //把原来缓冲区的数据放入新缓冲区
                                key.attach(newBuffer);   //再更新SelectionKey中的附件buffer
                            }
                        }
                        buffer1.flip();
                        System.out.println(Charset.defaultCharset().decode(buffer1)); //直接打印会出现消息边界的问题
                    } catch (IOException e) {
                        e.printStackTrace();
                        key.cancel();   //当客户端异常断开时,需要把这个key取消,(从selector集合中删除,避免一直往执行集合写入)
                    }
                }
//                取消这个事件
//                key.cancel();
            }
        }
    }

    private static void split(ByteBuffer buffer) {
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
//            通过\n找到一条完整消息
            if (buffer.get(i)=='\n'){
                int length = i+1-buffer.position();
//                  创建指定大小的缓冲区
                ByteBuffer target = ByteBuffer.allocate(length);
//                把完整数据写入新的ByteBuffer中
                for (int j = 0; j < length; j++) {
                    target.put(buffer.get());
                }
                target.flip();
//                String s = StandardCharsets.UTF_8.decode(target).toString();
//                System.out.println(s);
                System.out.println(buffer);
            }
        }
        buffer.compact();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值