Netty(三) 浅析Nio源码

     前面我们介绍了BIO,NIO还有AIO,这一篇我们来简单看一下Nio的底层是怎么实现的.要是不想看这一篇可以直接看结论:Nio底层实际调用的epoll的三个函数,分别是epoll_create(创建epoll对象),epoll_ctl(把channel关联到epoll对象),epoll_wait(等待事件就绪),下一篇我们将介绍select,poll和epoll的区别.

1.前置条件:看NIO的源码,必须下载openJdk源码,因为windos上面的实现和linux上面的实现不一样,都是调用操作系统内核方法来实现的,你在idea点进去看到的是windos上面的实现,如果看linux的实现需要自己去下载源码.

可以参考:OpenJdk源码下载 - 月下小魔王 - 博客园

小技巧:如何看navite(操作系统底层方法)方法的实现,openJdk,例如我当前的类是EpollArrayWrapper,我想看他的epollCreate方法,直接搜索EpollArrayWrapper_epollCreate,就能找到对应的C语言实现,我们看到epoll_create就是调用了linux系统的epoll_create返回一个int.

 

2.我们再来复习一下NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)

      1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组

      2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理

      3、NIO 的 Buffer 和 channel 都是既可以读也可以写

      NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,内核每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO(只读取有事件发生的channel)。

3.代码演示

    不要在windows或者mac点进去看selector的代码,因为不同平台会有不同的实现,linux下面是epoll

public class NioSelectorServer {
 
    public static void main(String[] args) throws IOException, InterruptedException {
 
        // 创建NIO ServerSocketChannel
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        //必须配置为非阻塞才能往selector上注册,否则会报错,selector模式本身就是非阻塞模式
        serverSocket.configureBlocking(false);
        // 打开Selector处理Channel,即创建epoll
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到selector上,并且selector对客户端accept连接操作感兴趣
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动成功");
 
        while (true) {
            // 阻塞等待需要处理的事件发生
            selector.select();
 
            // 获取selector中注册的全部事件的 SelectionKey 实例
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
 
            // 遍历SelectionKey对事件进行处理
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                // 如果是OP_ACCEPT事件,则进行连接获取和事件注册
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
            //NIO非阻塞体现:此处accept方法是阻塞的,但是这里因为是发生了连接事件,所以这个方法会马上执行完,不会阻塞
                    SocketChannel socketChannel = server.accept();
                 //设置为true会报错
                    socketChannel.configureBlocking(false);
                    // 这里只注册了读事件,如果需要给客户端发送数据可以注册写事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("客户端连接成功");
                } else if (key.isReadable()) {  // 如果是OP_READ事件,则进行读取和打印
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
          //NIO非阻塞体现:首先read方法不会阻塞,其次这种事件响应模型,当调用到read方法时肯定是发生了客户端发送数据的事件
                    int len = socketChannel.read(byteBuffer);
                    // 如果有数据,把数据打印出来
                    if (len > 0) {
                        System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    } else if (len == -1) { // 如果客户端断开连接,关闭Socket
                        System.out.println("客户端断开连接");
                        socketChannel.close();
                    }
                } else if (key.isWritable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                System.out.println("write事件");
                // NIO事件触发是水平触发
                // 使用Java的NIO编程的时候,在没有数据可以往外写的时候要取消写事件,
                // 在有数据往外写的时候再注册写事件
                key.interestOps(SelectionKey.OP_READ);
                //sc.close();
        }
                //从事件集合里删除本次处理的key,防止下次select重复处理
                iterator.remove();
            }
        }
    }
}

我们重点关注下面这几个方法:

Selector.open() 

创建多路复用器,底层new了一个数组EpollArrayWrapper,并且调用Liunx的内核方法epoll_create,创建了epoll实例.

socketChannel.register(selector, SelectionKey.OP_READ)

将channel注册到多路复用器上,这里实际做的只是把我们的socketChannel放进去一个集合里面,并没有进行真正的绑定,返回selectKey,根据这个key可以获取对应的channel.

selector.select() //阻塞等待需要处理的事件发生,这里底层的方法会真正调用epoll_ctl绑定我们的socketChannel

下图是看源码时候的主流程,可以根据下图自己尝试去跟一下

4.nio底层调用的这些都是linux系统内部方法,我们可以在Linux上用man 命令去查看这些方法的描述(例如 man epoll_create)

1.  accept:

                accept a connection on a socket 接收一个socket连接

2.  epoll_create:

                           open an epoll file descriptor.    打开一个epoll文件描述符(linux万物皆文件),说白了了就是创建一个linux的epoll对象

int epoll_create(int size);

创建一个epoll实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用,类似返回一个索引。参数size代表可能会容纳size个描述符,但size不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。

3.  epoll_ctl:

                  control interface for an epoll descriptor.    操作epoll文件描述符的接口,说白了就是将epoll和我们的目标fd(channel)关联起来.

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

使用文件描述符epfd引用的epoll实例,对目标文件描述符fd执行op操作。

参数epfd表示epoll对应的文件描述符,参数fd表示socket对应的文件描述符。

4.epoll_wait:

                      wait for an I/O event on an epoll file descriptor  在epoll文件描述符上等待I/O事件发生.

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待文件描述符epfd上的事件。

epfd是Epoll对应的文件描述符,events表示调用者所有可用事件的集合,maxevents表示最多等到多少个事件就返回,timeout是超时时间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值