网络编程:2. reactor的原理与实现

网络编程:reactor的原理与实现

1. epoll是什么

epoll是Linux内核为处理大批量文件描述符(FD)而做了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

2. epoll的特点

获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了

其实是有了epoll之后,Linux才开始火了起来

3. epoll的接口有哪些

  1. epoll_create(int)
    • 相当于创建了一个丰巢的盒子
  2. epoll_ctl(epfd, OPERA, fd, ev)
    • 相当于每个人要交管理费,管理人员的添加和删除
    • 第一个是每个epoll,第二个是操作,第三个每个fd,第四个是每个事件
  3. epoll_wait(epfd, events, evlength, timeout)
    • 相对于快递员去取快递,timeout是多久去一次,上午一次下午一次
    • events就是袋子,evlength就是袋子有多大
    • 第二个是返回的事件有多少
    • 第三个是events的长度
    • 第四个是时间

4. epoll代码示例

#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<pthread.h>
#include<unistd.h>
#include<sys/epoll.h>
#include<string.h>

#define BUFFER_LENGTH 128
#define EVENTS_LENGTH 128

char rbuffer[BUFFER_LENGTH] = {
    
    0};
char wbuffer[BUFFER_LENGTH] = {
    
    0};

int main(){
    
    
    // fd创建的时候默认是阻塞的
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1) return -1;

    // listenfd绑定一个固定的地址
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))){
    
    
        // 返回-1 失败
        return -2;
    }

    listen(listenfd, 10); // 相当于是迎宾的人

		// 传的参数只要大于0就可以了
    // 这个函数的参数一开始是设置就绪的最大数量的, 但其实用链表来串就绪集合就可以了
    int epfd = epoll_create(1); 
    struct epoll_event ev, events[EVENTS_LENGTH];
    // 为什么一开始要添加事件, 不应该是内核有事件通知我吗
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    // 这个关注一个可读的事件,后面有事件了才会通知我们
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
    // printf("fd : %d \n", epfd);

    // 服务器 7 * 24 运行就是因为这个函数
    // 所有的循环都是这样操作的
    while(1){
    
     
        // 如果是-1,直到有事件才返回,没事件就会阻塞
        // 0代表立即返回, timeout时间是毫秒
        int nready = epoll_wait(epfd, events, EVENTS_LENGTH, 0);
        // nready 有数据的时候返回是大于0的数
        // printf("-----nready -> %d\n", nready);
        int i = 0;
        for(i = 0; i < nready; i++){
    
    
            int clientfd = events->data.fd;
            // 用accept处理
            if(listenfd == clientfd){
    
    
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                // 新增加的fd, 也加到可读的fd集合中
                int connfd = accept(listenfd, (struct sockaddr*)&client, &len);
                printf("accept: %d\n", connfd);
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
            }else if(events[i].events & EPOLLIN) {
    
     //clientfd
                // char rbuffer[BUFFER_LENGTH] = {0};
                int n = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
                if(n > 0){
    
    
                    rbuffer[n] = '\0';
                    printf("recv: %s, n: %d\n", rbuffer, n);
                    memcpy(wbuffer, rbuffer, BUFFER_LENGTH);
                    int j = 0;
                    for(j = 0; j < BUFFER_LENGTH; j++){
    
    
                        rbuffer[j] = 'A' + (j % 26);
                    }
                    // send
                    ev.events = EPOLLOUT;
                    ev.data.fd = clientfd;
                    epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
                }
            }else if(events[i].events & EPOLLOUT){
    
    
                int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0);
                printf("sent: %d\n", sent);
            }
        }
    }
}

5. Reactor是什么

Reactor模式是处理并发I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的 I/O 事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上

一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中。

(如果多个fd公用缓冲区,就会出现沾包的现象。这就有了reactor,每个fd对应的事件都应该是自己的,做一个隔离)

6. Reactor的优点

  1. 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的
  2. 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
  3. 可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源
  4. 可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性

7. 水平触发和边沿触发

水平触发(level-triggered)

只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,当文件描述符关联的内核写缓冲区不满时(只要有空间就能写入),会一直发出可写信号通知

  1. epoll默认是水平触发
  2. 小数据的时候倾向LT,一次性读完
  3. 水平触发是可以用阻塞IO的

边沿触发(edge-triggered)

当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知

当文件描述符关联的写内核缓冲区满转化为不满的时候,则发出可写信号进行通知

  1. 一次触发,循环读,读到里面没数据可读
  2. 数据会存放在缓冲区
  3. 大数据的时候用ET,其实与LT没什么区别
  4. 边沿触发是循环去recv,不能陷入阻塞IO

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:

Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习

猜你喜欢

转载自blog.csdn.net/weixin_44839362/article/details/128990675