NIO底层实现

背景

​ 当前,Java工程师需要开发网络应用程序,Netty几乎是首选,下面简单列一下Netty的使用场景:

  1. 构建高性能、低延迟的java中间件,如MQ,RPC框架等。

  2. 基于其它应用层协议的基础通信框架,如开发IM程序相关的WebSocket等。

  3. 其它领域:游戏等。

    而Netty底层是基于NIO开发的,NIO是java 1.4版本开始提供的非阻塞通信API。和之前的BIO相比,能够实现IO多路复用,实现单线程同时处理多个连接请求和IO。一个典型的NIO程序如下:

    public class Main {
    
    
        public static void main(String[] args) throws IOException {
    
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(9000));
            serverSocketChannel.configureBlocking(false);
            //注册selector事件
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            System.out.println("服务器启动...");
    
            while (true) {
    
                //阻塞等待事件发生
                selector.select();
    
                //获取selector中注册的全部事件的SelectionKey实例
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
    
                while (iterator.hasNext()) {
    
                    SelectionKey selectionKey = iterator.next();
    
                    if (selectionKey.isAcceptable()) {
    
                        ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        //注册读事件,需要向客户端发数据可注册写事件
                        client.register(selector, SelectionKey.OP_READ);
                        System.out.println("客户端连接成功");
    
                    } else if (selectionKey.isReadable()) {
    
                        SocketChannel client = (SocketChannel) selectionKey.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(128);
                        int len = client.read(buffer);
                        if (len > 0) {
                            System.out.println("收到客户端消息:" + new String(buffer.array(), StandardCharsets.UTF_8));
                        } else if (len == -1) {
                            System.out.println("客户端断开连接");
                            client.close();
                        }
                    }
    
                    //从事件集合中移除本次已处理的key,防止下次重复处理
                    iterator.remove();
                }
    
            }
        }
    
    
    }
    

    其中实现IO多路复用的关键代码是以下三句:

    //创建selector
    Selector selector = Selector.open();
    
    //注册监听
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
    //阻塞等待事件发生
    selector.select();
    

​ 通过将所有的连接事件和IO事件注册到操作系统,操作系统收到相应事件后,进行回调,告知上层发生相应事件,实现了单线程非阻塞地处理多个连接请求。下面对底层实现做简要分析。

Linux系统函数

linux系统提供了三个系统函数,分别是:

//创建一个epoll文件描述符
int epoll_create(int size);

//向epoll中添加、删除或者修改事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

//等待事件发生,其中timeout表示该方法需阻塞的时长:若置为-1,则无限阻塞,直到有事件发生;若置为0,则立即返回,不管有没有事件发生;若置为大于0的值,则至多阻塞timeout时长后返回
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);


执行Selector.open()时会触发epoll_create函数;

而注册事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT)发生时,NIO会将该事件存到一个array或者map;

最后执行selector.select()时,会将上述array或者map中未注册的或者有修改的事件使用epoll_ctl函数进行注册,最后调用epoll_wait方法监听事件的发生。

关于epoll的原理,参见:epoll原理 epoll原理2

IO多路复用的理解:

其中第七部,需要解析包体,根据端口号找到对应的socket

posted @ 2022-03-09 19:58  南极星辰  阅读(196)  评论(0)    收藏  举报