[Linux] Análisis detallado de la práctica del proyecto C ++ de servidores de alta concurrencia

color naranja

Implementación multiproceso de servidor concurrente

El contenido del archivo server_process.c es el siguiente:

Preste atención al if (errno == EINTR) en la línea 70. Sin este juicio if, cuando varios clientes se conectan al mismo tiempo, detiene un cliente y luego inicia otro cliente, encontrará que no puede conectarse. reportar un error. Debido a que un cliente se detiene, es equivalente a que un proceso hijo finalice la ejecución en el lado del servidor y se emitirá la señal SIGCHLD, que es capturada por la función de captura de señal. En este momento, el programa se bloquea al aceptar, esperando que próxima conexión del cliente. Cuando la función de captura de señal vuelve a aceptarse después del procesamiento, se informará un error, que es EINTR. Para ello, también puede leer la introducción de la función de aceptación, que tiene instrucciones (como se muestra a continuación). Entonces hay un proceso por hacer aquí. Si errno es EINTR, el informe de error será ignorado.
Insertar descripción de la imagen aquí


La línea 101 strlen(recvBuf) + 1 es muy necesaria. Al contar, strlen llega hasta el terminador '\0', pero no incluye el terminador. La cadena escrita en el descriptor de archivo después de +1 tendrá un terminador. Si no hay un terminador, aparecerá fácilmente un símbolo extraño al final de los datos cuando el otro extremo los lea a través del descriptor del archivo.

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {
    
    
    while(1) {
    
    
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
    
    
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
    
    
            // 还有子进程活着
            break;
        } else if(ret > 0){
    
    
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {
    
    

    struct sigaction act;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    act.sa_handler = recyleChild;
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    
    
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {
    
    

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
    
    
            if(errno == EINTR) {
    
    
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {
    
    
            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
    
    
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
    
    
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
    
    
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }

    }
    close(lfd);
    return 0;
}

El contenido del archivo client.c es el siguiente:

// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    
    

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.193.128", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
    
    
        perror("connect");
        exit(-1);
    }
    
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
    
    
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
    
    
            perror("read");
            exit(-1);
        } else if(len > 0) {
    
    
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
    
    
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}

Subprocesos múltiples para implementar un servidor concurrente

El contenido del archivo del cliente es el mismo que el anterior.

El contenido del archivo del lado del servidor es el siguiente:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

struct sockInfo {
    
    
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];

void * working(void * arg) {
    
    
    // 子线程和客户端通信   需要cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
    
    
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
    
    
            perror("read");
            exit(-1);
        }else if(len > 0) {
    
    
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
    
    
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
    
    
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
    
    
        perror("bind");
        exit(-1);
    }

    // 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
    
    
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
    
    
        bzero(&sockinfos[i], sizeof(sockinfos[i]));//将结构体里面所有的成员都初始化为0
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {
    
    

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
    
    
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
    
    
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
    
    
                sleep(1);
                i=-1;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);//拷贝数据

        // 创建子线程,因为线程号仅仅在线程创建后才有,所以直接在这里传入pinfo->tid,就很方便
        pthread_create(&pinfo->tid, NULL, working, pinfo);

        //这里不能使用pthread_join,因为它是阻塞函数,那么一个子线程没结束主线程就只能阻塞在这里,没办法创建新的线程
        pthread_detach(pinfo->tid);
    }

    close(lfd);
    return 0;
}

modelo BIO

Espera de bloqueo: no ocupa valiosos intervalos de tiempo de CPU, pero solo puede procesar una operación a la vez.
Insertar descripción de la imagen aquí

Cuando la otra parte no envía datos por el momento, el programa se bloqueará durante la lectura.


Modelo BIO: resuelva el problema de procesar solo una operación a la vez mediante subprocesos múltiples / procesos múltiples. Pero el subproceso/proceso en sí necesita consumir recursos del sistema, y ​​la programación de subprocesos y procesos consume CPU.
Insertar descripción de la imagen aquí

modelo nio

Sondeo ocupado y sin bloqueo: recuerde continuamente o verifique cada dos veces para ver si hay alguna operación

Mejora la eficiencia de ejecución del programa, pero consume muchos recursos de la CPU y del sistema (suponiendo que hay 10.000 clientes vinculados, el servidor leerá el contenido de un determinado cliente tan lentamente como la 10.000 vez, porque debe leerse en secuencia Encuesta a estos 10,000 clientes, pero tal vez en estas 1,000 encuestas, solo hayan llegado los datos de un cliente, entonces los 9999 recorridos restantes se desperdiciarán)

Insertar descripción de la imagen aquí

Multiplexación de E/S (multiplexación de E/S)

Se ingresa la escritura de los datos del archivo en la memoria y se genera la escritura de los datos del archivo en la memoria.

       I/O多路复用使得程序能够同时监听多个文件描述符,能够提高程序的性能, las llamadas al sistema para implementar la multiplexación de E / S en Linux son: select, pool y epoll

seleccionar

Idea principal
  1. Primero, construya una lista de descriptores de archivos y agregue los descriptores de archivos que se monitorearán a la lista.
  2. Llame a una función del sistema que escuche los descriptores de archivos en esta lista y no regrese hasta que uno o más de estos descriptores se someta a una operación de E/S.
           a. Esta función está bloqueando
           b. La detección de descriptores de archivos por parte de la función la completa el kernel
  3. Al regresar, le dice al proceso cuántos descriptores hay para las operaciones de E/S en
Principio esquemático

Insertar descripción de la imagen aquí
Los tres primeros están fijos y ya ocupados por el sistema.

Análisis de funciones
//sizeof(fd_set)=128字节   也就是1024位
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/select.h>

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timerval *timeval);
	- 参数:
		- nfds:委托内核检测的最大的文件描述符的值+1
        - readfds:要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性
        	- 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区
        	- 是一个传入传出参数(比如我想看第5个文件描述符是否可以读,那我把它置为1,传入函数,函数会把这个列表指针交给内核,内核来检查,如果该文件描述符确实可以读,那么内核会把它置为1,不可读,内核就会把它置为0- writefds:要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性
        	- 委托内核检测写缓冲区是不是还可以写数据〈不满的就可以写,也就是置为1)
        - exceptfds:检测发生异常的文件描述符的集合(一般不用)
        - timeout:设置的超时时间
        	struct timeval {
    
    
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

			- NULL:永远等待,直到检测到了文件描述符有变化
			- tv_sec=0 tv_usec=0, 不阻塞
			- tv_sec>0 tv_usec>0,阻塞对应的时间

		- 返回值:
			- -1:失败
			- >0(n):检测的集合中有n个文件描述符发生了变化		

//将参数文件描述符fd对应的标志位设为0
void FD_CLR(int fd, fd_set *set);
//判断fd对应的标志位是0还是1,返回值:fa对应的标志位的值是0,返回0,是1,返回1
int  FD_ISSET(int fd, fd_set *set);
//将参数文件描述符fd对应的标志位设为1
void FD_SET(int fd, fd_set *set);
//fd_set一共有1024位,全部初始化为0
void FD_ZERO(fd_set *set);   
Ejemplo de código

Programa cliente:

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    
    

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    
    
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
    
    
        char sendBuf[1024] = {
    
    0};
        sprintf(sendBuf, "send data %d", num++);
        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
    
    
            perror("read");
            return -1;
        }else if(len > 0) {
    
    
            printf("read buf = %s\n", sendBuf);
        } else {
    
    
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
        
    }

    close(fd);

    return 0;
}

Programa del lado del servidor:

Todavía hay muchos detalles a los que se debe prestar atención en este programa del lado del servidor.

Realicé un análisis en el primer bucle y primero configuré el descriptor de escucha lfd en 1 en rdset. Luego ingresa al bucle infinito while(1).

Para evitar que la función de selección en el bucle cambie el rdset al pasar el rdset (debido a que el descriptor de archivo que necesito detectar está registrado en el rdset, siempre debe ser 1, pero si el rdset se pasa al select, debe ser Si no se pasan datos al descriptor de archivo detectado, el kernel lo establecerá en 0, por lo que se necesita tmp), por lo que rdset se copia a tmp al comienzo del ciclo.

Luego, cuando ret>0, significa que el descriptor del archivo debe haber cambiado. Luego mire lfd primero para ver si hay un nuevo cliente conectándose. Si es así, agréguelo al conjunto rdset (puede haber algo aquí). confundido, ¿por qué no agregar una línea de FD_SET(cfd, &rdset); después de FD_SET(cfd, &rdset);? De esta manera, los datos entrantes desde este nuevo puerto se pueden leer en el bucle infinito. Pero considerando la posibilidad de esto El nuevo puerto es solo una conexión y no se pasan datos. Si la lectura no puede leer los datos, se bloquearán aquí, por lo que no se agregarán. Es más seguro dejar que se lean nuevamente en el siguiente ciclo)

Cuando se ejecutan estos dos programas, no importa cuántos clientes haya, el servidor puede ejecutarse, no depende de subprocesos ni procesos múltiples, sino de la función de selección.

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 创建一个fd_set的集合,存放的是需要检测的文件描述符
    fd_set rdset, tmp;
    FD_ZERO(&rdset);
    FD_SET(lfd, &rdset);
    int maxfd = lfd;

    while(1) {
    
    

        tmp = rdset;

        // 调用select系统函数,让内核帮检测哪些文件描述符有数据
        int ret = select(maxfd + 1, &tmp, NULL, NULL, NULL);
        if(ret == -1) {
    
    
            perror("select");
            exit(-1);
        } else if(ret == 0) {
    
      //不可能为0,因为上面select设置的是阻塞,只有当文件描述符有变化时才会到这里
            continue;
        } else if(ret > 0) {
    
    
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            //为什么要检测lfd是否为1呢?因为第一次发生了改变肯定是lfd,但后面发生改变就可能是其他的文件描述符,而不是lfd(也就是说不是有新的文件描述符加进来)
            if(FD_ISSET(lfd, &tmp)) {
    
    
                // 表示有新的客户端连接进来了
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 将新的文件描述符加入到集合中
                FD_SET(cfd, &rdset);

                // 更新最大的文件描述符
                maxfd = maxfd > cfd ? maxfd : cfd;
            }
            //要检测的是连接描述符的数据有没有变化,所以不需要检测监听文件描述符,循环从lfd+1开始
            for(int i = lfd + 1; i <= maxfd; i++) {
    
    
                if(FD_ISSET(i, &tmp)) {
    
    
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {
    
    0};
                    int len = read(i, buf, sizeof(buf));
                    if(len == -1) {
    
    
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
    
    
                        printf("client closed...\n");
                        close(i);
                        FD_CLR(i, &rdset);
                    } else if(len > 0) {
    
    
                        printf("read buf = %s\n", buf);
                        write(i, buf, strlen(buf) + 1);
                    }
                }
            }

        }

    }
    close(lfd);
    return 0;
}
Desventajas de seleccionar
  1. Cada vez que se llama a select, la colección fd debe copiarse del modo de usuario al modo kernel. Esta sobrecarga será muy grande cuando haya muchos fds.

  2. Al mismo tiempo, cada vez que llama a select, debe recorrer todos los fd pasados ​​en el kernel. Esta sobrecarga también es muy grande cuando hay muchos fd.

  3. La cantidad de descriptores de archivos admitidos por select es demasiado pequeña, el valor predeterminado es 1024

  4. El conjunto fds no se puede reutilizar y debe restablecerse cada vez (en realidad, el programa del lado del servidor anterior define dos fd_sets. Si solo se pasa uno al kernel, el puerto a detectar no tiene datos que lleguen en este momento, entonces Será El kernel lo establece en 0 y luego lo pasa, entonces el puerto no se detectará la próxima vez que se pase, y esto obviamente no es posible)

encuesta

La encuesta solo es válida para Linux. El modelo de encuesta se propone en función del límite máximo de descriptores de archivos de select. Al igual que select, simplemente encapsula los tres descriptores de archivos basados ​​en bits (readfds/writefds/exceptfds) utilizados por select en una estructura. Luego use la forma de una matriz para romper el límite del descriptor de archivo máximo.

Análisis de funciones
#include <poll.h>
struct pollfd{
    
    
	int fd;                  //委托内核检测的文件描述符
	short  events;           //委托内核检测文件描述符的什么事件
	short  revents;          //文件描述符实际发生的事件
}; 

//既要检测读也要检测写该怎么写?
struct po11fd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT;

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
	- 参数:
		- fds:数组的首地址
		- nfds:这个是第一个参数数组中最后一个有效元素的下标+1
		- timeout:阻塞时长
			0:不阻塞
			-1:阻塞,当检测到需要检测的文件描述符有变化,解除阻塞
			>0:阻塞时长(单位是毫秒)
	- 返回值:
		-1:失败
		>0(n):成功, n表示检测到集合中有n个文件描述符发生变化
		

Insertar descripción de la imagen aquí

ejemplo de código

El programa cliente es el mismo que en select,
el programa servidor es el siguiente:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>


int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 初始化检测的文件描述符数组
    struct pollfd fds[1024];
    for(int i = 0; i < 1024; i++) {
    
    
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }
    fds[0].fd = lfd;
    int nfds = 0;
    int i;
    while(1) {
    
    

        // 调用poll系统函数,让内核帮检测哪些文件描述符有数据
        int ret = poll(fds, nfds + 1, -1);
        if(ret == -1) {
    
    
            perror("poll");
            exit(-1);
        } else if(ret == 0) {
    
    
            continue;
        } else if(ret > 0) {
    
    
            // 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变
            if(fds[0].revents & POLLIN) {
    
    
                // 表示有新的客户端连接进来了
                //先看结构体数组中是否有空位,没空位的话就等下次再accept新的客户端,有的话就直接accept
                for(i = 1; i < 1024; i++) {
    
    
                    if(fds[i].fd == -1) {
    
                          
                        struct sockaddr_in cliaddr;
                        int len = sizeof(cliaddr);
                        int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                        // 将新的文件描述符加入到集合中
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                    
                        // 更新最大的文件描述符的索引
                        nfds = nfds > i ? nfds : i;
                        break;
                    }
                }   
            }

            for(int i = 1; i <= nfds; i++) {
    
    
                if(fds[i].revents & POLLIN) {
    
    
                    // 说明这个文件描述符对应的客户端发来了数据
                    char buf[1024] = {
    
    0};
                    int len = read(fds[i].fd, buf, sizeof(buf));
                    if(len == -1) {
    
    
                        perror("read");
                        exit(-1);
                    } else if(len == 0) {
    
    
                        printf("client closed...\n");
                        close(fds[i].fd);
                        fds[i].fd = -1;
                    } else if(len > 0) {
    
    
                        printf("read buf = %s\n", buf);
                        write(fds[i].fd, buf, strlen(buf) + 1);
                    }
                }
            }
        }
    }
    close(lfd);
    return 0;
}

epoll (el más importante, concéntrate en dominar)

Insertar descripción de la imagen aquí

Análisis de funciones

#include <sys/epoll.h>
//创建一个新的epoll示例。在内核中创建了一个数据。这个数据中有两个比较重要的数据,一个是需要检测的文件描述符的信息(红黑树〉,还有一个是就绪列表,存放检测到数据发送改变的文件描述符信息(双向链表〉。
int epoll_create(int size); 
 	 - 参数: size : 目前没有意义了。随便写一个数,必须大于0
 	 - 返回值: -1 : 失败, > 0 : 文件描述符,操作epoll实例的




//对epo11实例进行管理:添加文件描述符信息,删除信息,修改信息
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
	- 参数:
		- epfd:epoll实例对应的文件描述符
		- op:要进行什么操作
			EPOLL_CTL_ADD:添加
			EPOLL_CTL_MOD:修改
			EPOLL_CTL_DEL:删除
		- fd:要检测的文件描述符
		- event:检测文件描述符什么事情(如果是删除操作的话直接NULL就行)
		  struct epoll_event{
    
    
		  	  _uint32_t         events;                // Epoll events
	 		  epoll_data       data;                    //user data variable
		  };
		  typedef union epoll_data {
    
    
			  void *ptr;                                        //回调函数
			  int fd;
			  uint32_t u32;
			  uint64_t u64;
		  } epoll_data_t;	

常见的Epoll检测事件(events):
- EPOLLIN
- EPOLLOUT
- EPOLLERR
- EPOLLET(边沿模式)如果想要使用边沿模式并检测是否可以读,events可以这么写:EPOLLIN | EPOLLET

//检测函数,检测内核中的eventpoll是否有文件描述符改变了,注意events是一个struct epoll_event数组的指针
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  
	- 参数:
		- epfd:epo11实例对应的文件描述符
		- events:传出参数,是一个struct epoll_event数组的指针,保存了发送了变化的文件描述符的信息,
		- maxevent:第二个参数结构体数组的大小
		- timeout:阻塞时间
			- 0:不阻塞
			- -1:阻塞,直到检测到fd数据发生变化,解除阻塞
			- >0:阻塞的时长(毫秒)
	- 返回值:
		- 成功,返回发送变化的文件描述符的个数>0
		- 失败 -1 

Ejemplo de código

Terminal de servicio:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        //ret代表的是发生改变的文件描述符的数量
        for(int i = 0; i < ret; i++) {
    
    

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {
    
    0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
    
    
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
    
    
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }
    close(lfd);
    close(epfd);
    return 0;
}

El if(epevs[i].events & EPOLLOUT) en la línea 59 es para evitar una situación: cuando detecto la lectura y escritura del descriptor de archivo al mismo tiempo, debido a que el siguiente código maneja la lectura, entonces Si el epevs[i]. ].events del descriptor de archivo se escribe, continúa y omite el cambio del descriptor de archivo


: se pasa cuando se llama inicialmente a epoll_ctl para colocar el descriptor de archivo monitoreado en el árbol rojo-negro. &epev, que es el puntero de epev ¿Por qué se puede reutilizar este epev cuando se pasa un nuevo descriptor de archivo más adelante? Si epev se reutiliza de esta manera, ¿no se cambiará el descriptor de escucha pasado anteriormente? ¿O es realmente que después de llamar a esta función y pasar el árbol rojo-negro, se copiaron los datos en epev?
Respuesta: Cuando se llama a la función epoll_ctl para agregar un descriptor de archivo al objeto epoll, epoll copiará los datos en la estructura epoll_event, los almacenará en su propio espacio de memoria e insertará la estructura copiada como un nodo en Entre árboles rojos y negros. .
La ventaja de esto es que cuando ocurre un evento en el descriptor de archivo, epoll puede obtener directamente la información del evento correspondiente de su propio espacio de memoria, sin tener que acceder a la estructura epoll_event en el espacio del usuario cada vez. Esto puede mejorar la eficiencia y reducir la cantidad de llamadas al sistema.

Dos modos de trabajo de epoll.

  • Activado por nivel (LT): LT (activado por nivel) es el modo de trabajo predeterminado (predeterminado significa predeterminado) y admite enchufes con y sin bloqueo. En este enfoque, el kernel le indica si un descriptor de archivo está listo y luego puede realizar operaciones de IO en el fd listo. Si no hace nada, el kernel continuará notificándole.
假设委托内核检测读事件>检测fd的读缓冲区
    读缓冲区有数据- > epoll检测到了会给用户通知
        a.用户不读数据,数据一直在缓冲区,epoll会一直通知
        b.用户只读了一部分数据,epoll会通知
        c.缓冲区的数据读完了
  • Edge Triggred (ET) Activado por flanco: ET (activado por flanco) es un modo de trabajo de alta velocidad y solo admite enchufes sin bloqueo. En este modo, el kernel le avisa mediante epoll cuando un descriptor cambia de no listo a listo. Luego asumirá que usted sabe que el descriptor de archivo está listo y no enviará más notificaciones de preparación para ese descriptor de archivo hasta que haga algo que cause que ese descriptor de archivo ya no esté listo. Pero tenga en cuenta que si no hay ninguna operación IO en este fd (lo que hace que no esté listo nuevamente), el kernel no enviará más notificaciones (solo una vez).

El modo ET reduce en gran medida la cantidad de veces que el evento epoll se activa repetidamente, por lo que es más eficiente que el modo LT. Cuando epoll funciona en modo ET, debe usar un socket sin bloqueo para evitar privar a la tarea de procesar múltiples descriptores de archivos debido al bloqueo de las operaciones de lectura/escritura de un identificador de archivo.

假设委托内核检测读事件->检测fd的读缓冲区
    读缓冲区有数据- > epoll检测到了会给用户通知
        a.用户不读数据,数据一致在缓冲区中,epoll下次检测的时候就不通知了
		b.用户只读了一部分数据,epoll不通知
		c.缓冲区的数据读完了,不通知

Ejemplo de código:

El programa cliente es el siguiente:

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
    
    

    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
    
    
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);

    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));

    if(ret == -1){
    
    
        perror("connect");
        return -1;
    }

    int num = 0;
    while(1) {
    
    
        char sendBuf[1024] = {
    
    0};
        // sprintf(sendBuf, "send data %d", num++);
        fgets(sendBuf, sizeof(sendBuf), stdin);

        write(fd, sendBuf, strlen(sendBuf) + 1);

        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
    
    
            perror("read");
            return -1;
        }else if(len > 0) {
    
    
            printf("read buf = %s\n", sendBuf);
        } else {
    
    
            printf("服务器已经断开连接...\n");
            break;
        }
    }

    close(fd);

    return 0;
}

El código del modo de disparo horizontal de epoll es el siguiente:

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
    
    

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[5] = {
    
    0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
    
    
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
    
    
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
    
    
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

El código del modo de activación del borde de epoll es el siguiente:

El código del modo de disparo de borde es diferente del código del modo de disparo horizontal. En el concepto anterior, hemos aprendido que el modo de disparo de borde solo notifica al descriptor de archivo una vez desde no listo a listo (es decir, han llegado datos). . Entonces, en el modo activado por borde, debemos leer todo lo enviado por el remitente a la vez. Si la lectura no se completa, el estado del descriptor del archivo aún estará listo, epoll no nos notificará en el siguiente ciclo y los datos restantes no leídos enviados por el remitente se perderán.


(Suponga char buf [2] y luego ingrese lllhh en el descriptor de archivo de dirección de envío fd, luego en modo LT, primero epoll_wait detectará que el evento de lectura en fd está listo. Luego comience a leer, porque la capacidad de buf es 2, entonces ll se lee primero y luego regresa a epoll_wait al comienzo del ciclo. Debido a que no ha terminado de leer, el evento de lectura en fd todavía está en el estado listo y luego se lee nuevamente hasta que finaliza la lectura. En ET Modo, ll se lee primero y luego regresa a epoll_wait al comienzo del ciclo. Incluso si el evento de lectura en fd no finaliza, todavía está en el estado listo, pero se ignora. Los datos posteriores equivalen a perderse. Vale la pena señalar que el requisito previo para la pérdida es: si el remitente no envía más datos, los datos restantes no leídos en el búfer se perderán. Si el remitente vuelve a enviar datos, como rr, se leerá lh y rr todavía está leyendo el búfer (no se leerá)


, por lo que debe leerse de una sola vez en modo ET. Entonces, ¿cómo leerlo todo de una vez? Naturalmente, se necesita un bucle while, pero hay un problema: después de leer los datos, la lectura ya no puede leer los datos, pero el remitente no se ha desconectado, por lo que la lectura se bloqueará aquí, lo que provocará que el programa no pueda continuar. abajo. Por lo tanto, necesitamos configurar la función de lectura para que no sea bloqueante, lo que en realidad significa configurar el socket para que no sea bloqueante, usando la función fcntl.
Al configurar el socket en sin bloqueo para que la lectura no sea bloqueante, esto causará otro problema. Después de que un cierto recorrido haya leído todos los datos en el búfer del descriptor de archivo, la próxima vez que lea, la lectura no se bloqueará, pero habrá No hay datos en el descriptor del archivo y la conexión del remitente no está cerrada, se informará un error EAGAIN. Esa es la línea 81 del programa. En este caso, no se debe salir del bucle while, por lo que se utiliza if para juzgar.
En la línea 74, printf no puede imprimir después de leer todos los datos, así que cambie printf en la línea 74 para escribir en la línea 75. La línea 75 escribe directamente el contenido de buf en la salida estándar y escribe los datos en la terminal. interfaz de línea para mostrar. La línea 76 es escribir el contenido de buf en el socket curfd, el propósito es completar la retrorreflexión para que el cliente pueda leer el contenido enviado por el cliente.

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    
    

    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 监听
    listen(lfd, 8);

    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);

    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);

    struct epoll_event epevs[1024];

    while(1) {
    
    

        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
    
    
            perror("epoll_wait");
            exit(-1);
        }

        printf("ret = %d\n", ret);

        for(int i = 0; i < ret; i++) {
    
    

            int curfd = epevs[i].data.fd;

            if(curfd == lfd) {
    
    
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);

                // 设置cfd属性非阻塞
                int flag = fcntl(cfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(cfd, F_SETFL, flag);

                epev.events = EPOLLIN | EPOLLET;    // 设置边沿触发
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
    
    
                if(epevs[i].events & EPOLLOUT) {
    
    
                    continue;
                }  

                // 循环读取出所有数据
                char buf[5];
                int len = 0;
                while( (len = read(curfd, buf, sizeof(buf))) > 0) {
    
    
                    // 打印数据
                    // printf("recv data : %s\n", buf);
                    write(STDOUT_FILENO, buf, len);
                    write(curfd, buf, len);
                }
                if(len == 0) {
    
    
                    printf("client closed....");
                }else if(len == -1) {
    
    
                    if(errno == EAGAIN) {
    
    
                        write(STDOUT_FILENO, "over.\n", strlen("over.\n") + 1);
                    }else {
    
    
                        perror("read");
                        exit(-1);
                    }
                    
                }

            }

        }
    }

    close(lfd);
    close(epfd);
    return 0;
}

Supongo que te gusta

Origin blog.csdn.net/mhyasadj/article/details/131317011
Recomendado
Clasificación