Network programming: 2. The principle and implementation of reactor

Network programming: the principle and implementation of reactor

1. What is epoll

epoll is a poll improved by the Linux kernel to handle a large number of file descriptors (FD). It is an enhanced version of the multiplexed IO interface select/poll under Linux. It can significantly improve the performance of programs that are only active in a large number of concurrent connections. The CPU utilization of the system under the circumstances.

2. Features of epoll

When getting an event, it does not need to traverse the entire listened descriptor set, just traverse those descriptor sets that are asynchronously awakened by the kernel IO event and added to the Ready queue.

In fact, it was after epoll that Linux started to catch fire

3. What are the interfaces of epoll

  1. epoll_create(int)
    • It is equivalent to creating a Fengchao box
  2. epoll_ctl(epfd, OPERA, fd, ev)
    • It is equivalent to the management fee paid by each person, and the addition and deletion of managers
    • The first is for each epoll, the second is for operations, the third is for each fd, and the fourth is for each event
  3. epoll_wait(epfd, events, evlength, timeout)
    • Compared with the courier to pick up the courier, how often does the timeout go, once in the morning and once in the afternoon
    • events is the bag, and evlength is how big the bag is
    • The second is how many events are returned
    • The third is the length of events
    • The fourth is time

4. epoll code example

#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. What is Reactor

Reactor mode is a relatively common mode for processing concurrent I/O. It is used for synchronous I/O. The central idea is to register all I/O events to be processed to a central I/O multiplexer, and at the same time the main Thread/process blocked on multiplexer

Once an I/O event arrives or is ready (file descriptor or socket can be read or written), the multiplexer returns and distributes the corresponding I/O event registered in advance to the corresponding processor.

(If multiple fds share buffers, there will be a phenomenon of sticking packets. This is where reactors are created, and the events corresponding to each fd should be their own, making an isolation)

6. Advantages of Reactor

  1. Fast response, no need to be blocked by a single synchronization time, although Reactor itself is still synchronous
  2. The programming is relatively simple, which can avoid complex multi-threading and synchronization problems to the greatest extent, and avoid the switching overhead of multi-thread/process
  3. Scalability, you can easily make full use of CPU resources by increasing the number of Reactor instances
  4. Reusability, the reactor framework itself has nothing to do with specific event processing logic and has high reusability

7. Level trigger and edge trigger

level-triggered

As long as the read kernel buffer associated with the file descriptor is not empty and there is data to read, it will always send a readable signal to notify. When the kernel write buffer associated with the file descriptor is not full (as long as there is space to write), will always emit a writable signal notification

  1. epoll is triggered horizontally by default
  2. When the data is small, I tend to LT, and read it all at once.
  3. Level trigger can be used to block IO

Edge-triggered

When the read kernel buffer associated with the file descriptor changes from empty to non-empty , a readable signal is sent to notify

When the write kernel buffer associated with the file descriptor changes from full to full , a writable signal is sent to notify

  1. Trigger once, read cyclically, until there is no data to read
  2. data will be stored in the buffer
  3. When using big data, ET is actually no different from LT
  4. The edge trigger is a loop to recv, and cannot fall into blocking IO

Recommend a free open course of Zero Sound Academy. I personally think the teacher taught it well, so I would like to share it with you:

Linux, Nginx, ZeroMQ, MySQL, Redis, fastdfs, MongoDB, ZK, streaming media, CDN, P2P, K8S, Docker, TCP/IP, coroutines, DPDK and other technical content, learn now

Guess you like

Origin blog.csdn.net/weixin_44839362/article/details/128990675