网络编程之多路复用器及Epoll

IO模型

只关注IO,不关注IO读写完成后的事情。
同步:程序(APP)自己进行读/写操作
异步:由Kernel完成读/写,程序跑起来感觉像没有访问IO,访问的是buffer
阻塞:BLOCKING,一直等待着方法有效的返回结果
非阻塞:NONBLOCKING,调用方法的时候就返回是否读取到,(java中要么返回null,要么返回具体的对象)
所以IO模型有:
同步阻塞:程序(APP)自己读取,调用了方法后一直等待着有效的返回结果
同步非阻塞:程序(APP)自己读取,调用方法的瞬间就给出是否读取到的返回结果,这个时候程序要考虑下一次再去读取的问题(比如用while循环)
那么异步呢?异步的只有非阻塞的,(异步阻塞无意义)。其实异步的问题暂时不需要讨论,因为IO模型下,目前Linux没有通用内核的异步处理方案。

在这里插入图片描述

NIO和多路复用器

nio 需要全部遍历内核fd(比如处于listen状态的文件描述符),用户态内核态需要切换(一次切换就是一次系统调用)才能实现
多路复用器:多条路(指IO)只通过一个系统调用,获得所有IO(fd)的状态,然后由程序自己对有状态的IO进行R/W操作。只要是程序自己读写,就说明IO模型是同步的
多路复用一般有SELECT,POSIX,POLL,EPOLL,KQUEUE

NIO

在这里插入图片描述

多路复用

在这里插入图片描述

select, poll, epoll 都是多路复用器,都属于同步状态下非阻塞的模型。
select 是在不同操作系统中很容易实现的,不依赖特定的软硬件的一个系统调用。
epoll 要求内核当中要求一定的实现,在linux上是epoll,在unix上是kqueue。技术是随着问题的产生一步步发展起来的。
这两者都是基于IO事件的一种通知行为。
异步阻塞是没有意义的。异步都是用非阻塞。

linux内核多路复用器select,poll,epoll

来看一下底层关于select的描述及api。这里借助于man select指令。

man select

在这里插入图片描述

再来几个描述:
An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in
undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor
大致意思是:fd_set是固定大小的缓冲区。如果fd值为负或大于等于FD_SETSIZE,则执行FD_CLR()或FD_SET(),这样会导致不确定的行为。而且,POSIX要求fd是有效的文件描述符。
这里提到了FD_SETSIZE,这是一个固定的数值(1024,2048之类的),这是select要求的,而poll则没有。这也是select和poll的区别。

在这里插入图片描述

其实到这里,我们可以得出结论:
无论nio、select、poll都是要遍历所有的IO,询问状态,
但是,
NIO的遍历是需要很多次系统调用的,成本在用户态与内核态的切换上;
而多路复用器select/poll,这个过程触发了一次系统调用,在这次用户态内核态且换的过程中,把fds传递到内核,内核根据这次用户传递过来的fds进行遍历,修改状态。

多路复用器select/poll的弊端:

  • 每次都要重新重复传递fds(内核开辟空间)
  • 每次内核被调用了之后,针对这次调用,触发了一个遍历fds全量的复杂度
    由此,引入epoll这个牛逼的东西。

这里插入一个概念

在内存中,有 kernel,有 app 等等的这些程序
软中断: trap int 80 等
硬中断:时钟中断(晶振)
IO中断:网卡、硬盘、鼠标

关于网络IO中断
最开始的时候,网卡来了IO数据包的时候,是可以产生中断的,这时候就会打断CPU,将输入的数据存到内存中。
后来经过改进,网卡是有buffer的,在内存中开辟一个DMA区域,专门给网卡用,网卡可以收集很多数据之后积攒起来,积攒到一定量之后一起发给DMA。

中断会产生callback回调函数
event有事件,就要去处理
在epoll之前的callback,只是完成了将网卡发来的数据,走一下内核的网络协议栈(2链路,3网络,4传输层),最终关联到fd的buffer里面
所以你在某一时间,如果从application询问内核某一个或者某些fd是否可读/可写,会有状态返回。

如果内核在回调的处理中,再加入(?红黑树、list),就有了多selector

在这里插入图片描述

还是直接看linux对epoll的描述:
man epoll:
在这里插入图片描述

epoll - I/O event notification facility 一看到这个大致知道是和事件通知相关的
epoll API执行与poll类似的任务:监视多个文件描述符以查看其中的任何文件是否可以进行I / O。
epoll_create 创建一个epoll实例并返回引用该实例的文件描述符。
epoll_ctl 通过epoll_ctl注册指定文件描述符集合。
epoll_wait 等待I / O事件,如果当前没有可用的事件,则阻塞调用线程。

画个图和select/poll类比一下
在这里插入图片描述

实际结合理论

Java中是如何使用多路复用的?我们用一段程序解释一下。
初始化Server

private ServerSocketChannel server = null;
private Selector selector = null;
int port = 9090;

public void initServer() {
    try {
        server = ServerSocketChannel.open();
        server.configureBlocking(false);
        server.bind(new InetSocketAddress(port));

        selector = Selector.open();

        server.register(selector, SelectionKey.OP_ACCEPT);
    } catch (IOException e) {
        e.printStackTrace();
    }
}


Selector 类似于linux下的多路复用器(select poll epoll kqueue),nginx,event{}
server.configureBlocking(false) 设置非阻塞
selector = Selector.open() 如果在epoll模型下,open()相当于前面提到的epoll_create方法,返回一个fd实例—假设是fd3
server.register(selector, SelectionKey.OP_ACCEPT) server约等于处于listen状态的fd—设fd4,对于register:若是select/poll模型,则jvm里开辟一个数组把这个fd放进去;若是epoll,则epoll_ctl(fd3,ADD,fd4,EPOLLIN

Start启动Server,并处理事件

public void start() {
    initServer();
    System.out.println("服务器启动了。。。。。");
    try {
        while (true) {
            while (selector.select() > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys(); 
                Iterator<SelectionKey> iter = selectionKeys.iterator();
                
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    iter.remove(); //set  不移除会重复循环处理
                    if (key.isAcceptable()) {
                        acceptHandler(key);
                    } else if (key.isReadable()) {
                        readHandler(key);
                    }
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}


这段代码除了启动server,重要的逻辑是调用多路复用器。
selector.select():
select/poll模型下,select()等于内核的select(fd4) poll(fd4)
epoll等于内核的 epoll_wait(),这个可以带时间参数。若没有时间或者参数为0表示阻塞,若有时间则设置一个超时。selector.wakeup() 结果返回0
while (iter.hasNext()):
这段代码表示,管你是什么多路复用器,你只能给我状态,我的程序还得一个一个的去处理他们的R/W。这就是同步—真的好辛苦!!!!!!!!
if (key.isAcceptable()) :
这里是重点,如果要去接受一个新的连接,语义上,accept接受连接且返回新连接的FD,那么这个新的FD如何处理?
select/poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
epoll: 通过epoll_ctl把新的客户端fd注册到内核空间
这个具体看下面acceptHandler这个方法
public void acceptHandler(SelectionKey key) {
    try {
        ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
        SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端  fd7
        client.configureBlocking(false);

        ByteBuffer buffer = ByteBuffer.allocate(8192); 

        // 0.0  我类个去
        //你看,调用了register
        /*
        select,poll:jvm里开辟一个数组 fd7 放进去
        epoll:  epoll_ctl(fd3,ADD,fd7,EPOLLIN
         */
        client.register(selector, SelectionKey.OP_READ, buffer);
        System.out.println("-------------------------------------------");
        System.out.println("新客户端:" + client.getRemoteAddress());
        System.out.println("-------------------------------------------");

    } catch (IOException e) {
        e.printStackTrace();
    }
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值