muduo网络库

2024/12/01 c++ 共 2952 字,约 9 分钟
闷骚的程序员

muduo网络库

一 I/O复用之-select/poll/epoll

1 select

int listenfd=socket(PF_INET,SOCK_STREAM,0);
bind(listenfd,(struct sockaddr*)&address,sizeof(address));
listen(listenfd,5);
fd_set read_fds;
for(int i=0;i<5;i++){
//fd[i]是文件描述信息
    fd[i]=accept(listenfd,(struct sockaddr*)&client,&addr_len);
    if(fd[i]>max){
            max=fd[i];
    }
}
while(1){
    FD_ZERO(&read_fds);
    for(int i=0;i<5;i++){
        FD_SET(fd[i],&read_fds);
    }
}
//第一个参数为最大的文件描述符+1,限定了在内核态循环遍历时的大小范围
//第二个参数为读文件描述符的集合,事实上就是一个bitmap
//第三个参数为写文件描述符的集合
//第四个参数为监听的异常事件的文件描述集合
//第五个参数代表超时时间
//返回值为就绪的文件描述符的个数
ret=select(max+1,&read_fds,NULL,NULL,NULL);
for(int i=0;i<5;i++){
    //用FD_ISSET()方法去判断哪两个读事件已就绪
    if(FD_ISSET(fd[i],&read_fds)){
        ret=recv(fd[i],buff,sizeof(buff)-1,0);
    }
}

流程:

select进程A去调用select函数时,将bitmap放到内核态下(效率较高,如果在用户态下面,每次都要去内核态询问该文件描述信息是否就绪)。如果没有信息到达,进程A就会让出CPU的执行权限,进入阻塞状态。等读事件就绪时,会返回就绪事件的个数。每次使用bitmap时都要重新创建并初始化。

缺点:

  1. 文件描述符表为bitmap数据类型,且有长度为1024的限制。
  2. fdset无法做到重用,每次循环必须重新创建。
  3. 频繁的用户态和内核态拷贝,性能开销比较大。
  4. 需要对文件描述符表进行遍历,O(n)的时间复杂度

2 poll模型

//pollfd结构体原型
struct pollfd{
    int fd;         //文件描述符
    short events;   //注册事件
    short revents;  //实际发生的事件,由内核填充
};
int sscfd=socket(PF_INET,SOCK_STREAM,0);
bind(sscfd,(struct sockaddr*)&address,sizeof(address));
listen(sscfd,5);
struct pollfd fds[5];
for(int i=0;i<5;i++){
    fds[i].fd=accept(sscfd,(struct sockaddr*)&client,&addr_len);
    fds[i].event=POLLIN;
}
sleep(1);
while(1){
    //第一个参数为文件描述符结构体
    //第二个参数为结构体数组的最大长度
    //第三个参数为等待阻塞时间
    ret=poll(fds,5,4000);
    for(int i=0;i<5;i++){
        if(fds[i].revents&POLLIN){
            //判断完之后,将revents字段置为0
            fds[i].revents=0;
            ret=recv(fds[i].fd,buff,sizeof(buff)-1,0);
        }
    }
}

流程:

也和select一样通过accept()创建5个文件描述符,在调用poll时,也是一个阻塞调用,poll()方法和select()方法一样,也是一次性的将一批文件描述符发送到内核态,在内核态循环遍历哪些事件已就绪。已就绪的文件描述符对应结构体的revents字段置为1。返回ret后将文件描述符结构体拷贝到用户态,再由用户态去遍历哪些文件描述符已就绪。

优点和不足:

  1. poll模型采用了pollfd结构数组,解决了select的1024个文件描述符的限制。
  2. 仍存在频繁的用户态和内核态拷贝,性能开销较大。
  3. 需要对文件描述符表进行遍历,时间复杂度为O(n)。

3 epoll模型

struct epoll_event{
    _uint32_t events;
    epoll_data_t data;
};
typedef union epoll_data{
    void *ptr;
    int fd;
    _uint32_t u32;
    _uint64_t u64;
}epoll_data_t;

struct epoll_event events[5];
//创建一个eventpoll结构体
int epoll_fd=epoll_create(5);

流程:

  1. 在epoll_ctl()函数中,为每个文件描述符都指定了回调函数,基于回调函数把就绪事件放到就绪队列中,因此,把时间复杂度从O(n)降到了O(1)。
  2. 只需要在epoll_ctl()时传递一次文件描述符,epoll_wait()不需要再次传递文件描述符。
  3. epoll基于红黑树+双链表存储数据,没有最大连接数的限制,不存在clock问题。

同步:调用调用应用层或第三方库时,等待数据的到来,数据来时数据的搬运也得自己来做。

异步:把参数都填好,数据来时系统帮我做好工作,给我一个信号即可

名词解释:阻塞、非阻塞、同步、异步

描述的都是IO的状态,一个典型的网络IO包含两个阶段:数据就绪和数据读写。比如revc(sockfd,buffer,sizeof(buffer),数据的就绪指的是在内核时相应的sockfd对应的tcp缓冲区是否有数据可读,当工作在阻塞模式下,调用revc时,如果数据没有就绪,revc会阻塞当前线程;如果工作在非阻塞状态时,会立即返回。是根据返回值判断是否就绪。如果数据准备好,可以进行读写。同步是revc会继续把这些数据从内存缓冲区花自己的事件拷贝到用户自定义的缓冲区。就是从操作系统内核的缓冲区搬到应用程序的缓冲区之后,应用程序才继续执行。如果是异步的话,不会像同步那样智穿一个sockfd,buffer。还要传一个通知方式,这是异步的一个重要特点,可以通过信号或回调。帮我把数据拷贝到应用程序缓冲区,结束之后通知我,这就是异步。

epoll的两种模式:

  1. LT模式:内核数据没有被读完,就会一直上报数据。
  2. ET模式:内核数据只上报一次。

二 Linux系统的IO模型

1. 阻塞blocking

image.png

2.非阻塞non-blocking

image.png

3.IO复用(IO multiplexing)一个线程可以监听好多个socket

image.png

4.信号驱动(signal-driven)

image.png

内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

5.异步(asynchronous)

image.png

三 Reactor模型

重要组件:Event事件、Reactor反应堆、Demultiplex事件分发器、Evanthandler事件处理器

image.png

四 muduo网络库

文档信息

Search

    Table of Contents