[Programación de red] E/S avanzada

 

Directorio de artículos

  • 1. Conceptos básicos de cinco modelos IO
  • 2. Conceptos importantes de IO
    • 1. La comparación entre comunicación síncrona y comunicación asíncrona
    • 2. Bloqueo VS no bloqueo
  • 3. Demostración de código de E/S sin bloqueo
  • 4. Selección de multiplexación IO
  • Resumir


1. Conceptos básicos de cinco modelos IO

En primer lugar, IO está esperando + copia de datos. Recuerde que implementamos la interfaz de lectura/recepción para el servidor antes. Dijimos en ese momento que si hay datos en esta interfaz, la lectura/recepción volverá después de que se complete la copia. Si no hay datos, bloqueará la espera, y el propósito de la espera es esperar a que los recursos estén listos y copiar los datos una vez que haya recursos.

1. E/S de bloqueo

La llamada al sistema esperará hasta que el kernel prepare los datos.Todos los sockets están bloqueados por defecto.

 Cuando el proceso llama a recvfrom para hacer una llamada al sistema para leer los datos en el kernel, si los datos no están listos, recv bloqueará directamente y esperará a que los datos estén listos. el núcleo al espacio del usuario, y la copia devolverá instrucciones de éxito.

2. E/S sin bloqueo

Si el kernel aún no ha preparado los datos, la llamada al sistema aún regresará directamente y devolverá el código de error EWOULDBLOCK.
La E/S sin bloqueo a menudo requiere que los programadores intenten leer y escribir repetidamente los descriptores de archivos de manera cíclica . Este proceso se denomina sondeo . Este es un gran desperdicio para la CPU y, por lo general, solo se usa en escenarios específicos.

 Cuando el proceso llama a recvfrom para hacer una llamada al sistema para leer los datos en el kernel, si los datos no están listos, entonces recv devolverá un código de error, porque no bloquea, por lo que lleva un tiempo preguntar si el kernel los datos están listos y se pueden usar en otros momentos Deje que este proceso haga otras cosas, como imprimir registros o algo así, solo pregunte a intervalos si los datos están listos, si no están listos, envíe un código de error, copie los datos de el núcleo al espacio del usuario y devolver una indicación de éxito cuando esté listo.

3. E/S impulsada por señal

Cuando el kernel prepara los datos, utiliza la señal SIGIO para notificar a la aplicación que realice operaciones de E/S.

 Cuando los datos no están listos, podemos dejar que el proceso capture sigaction, y una vez que esté listo, capturamos esta señal para copiar los datos. Todavía puede hacer otras cosas cuando no esté listo.

4. Multiplexación IO

Porque la multiplexación de E/S puede esperar el estado de preparación de varios descriptores de archivos al mismo tiempo

 Nota: El principio de la transferencia multidireccional es esperar varios descriptores de archivos a la vez, por lo que no se puede usar la interfaz anterior y se debe usar la nueva llamada del sistema de selección. Y seleccionar, sondear y epoll son todos pasos intermedios de IO Una vez que tienen éxito, aún pueden llamar a recvfrom para copiar datos. Y recvfrom ya no se bloqueará durante la transferencia multidireccional, siempre que la selección espere el éxito, recvfrom copiará directamente los datos.

5. E/S asíncrona

El kernel notifica al programa de aplicación cuando se completa la copia de datos (y el controlador de señal le dice al programa de aplicación cuándo puede comenzar a copiar datos).

 El principio de E/S asíncrona es dejar que el sistema espere los datos. Cuando haya datos, se copiarán en el búfer que designé. Solo soy responsable de obtener los datos en el búfer. Esto es equivalente al hecho de que todos los IO anteriores se enfocan en cómo cocinar, mientras que el IO asíncrono solo se enfoca en cómo comer y no le importa cómo llega la comida.

En cualquier proceso de IO, hay dos pasos. El primero es esperar y el segundo es copiar. Y en los escenarios de aplicaciones reales, el tiempo que se dedica a esperar suele ser mucho IO sea más eficiente, lo más importante es enfoque central es minimizar el tiempo de espera.

2. Conceptos importantes de IO

1. Comunicación síncrona vs comunicación asíncrona (comunicación síncrona/ comunicación asíncrona)

Enfoque síncrono y asíncrono en el mecanismo de comunicación de mensajes .
La llamada sincronización significa que cuando se realiza una llamada , la llamada no regresa hasta que se obtiene el resultado , pero una vez que la llamada regresa, se obtiene el valor de retorno, es decir, la persona que llama espera activamente el resultado de la llamada . ;
Asíncrono es lo contrario. Después de que se emite la llamada , la llamada regresa directamente, por lo que no hay resultado devuelto ; en otras palabras, cuando se emite una llamada de procedimiento asíncrono, la persona que llama no obtendrá el resultado inmediatamente ; notifica a la persona que llama a través de estado, notificación o maneja la llamada a través de una función de devolución de llamada.
Además , recordamos que al hablar de multiproceso y multihilo , también mencionamos sincronización y exclusión mutua.Aquí , la comunicación síncrona y la sincronización entre procesos son conceptos que no queremos hacer en absoluto.
La sincronización de procesos / subprocesos también es una relación de restricción directa entre procesos/subprocesos. Son dos o más subprocesos establecidos para completar una determinada tarea. Este subproceso necesita coordinar su orden de trabajo en algunas posiciones para esperar y transmitir información . Especialmente al acceder a recursos críticos .
Cuando vea la palabra " sincronización " , primero debe averiguar cuál es el fondo . Esta sincronización es la sincronización de la comunicación sincrónica y la comunicación asincrónica , o la sincronización de la sincronización y la exclusión mutua.

2. Bloqueo vs no bloqueo

El bloqueo y el no bloqueo se centran en el estado del programa mientras se espera el resultado de la llamada (mensaje, valor devuelto) .
Llamada de bloqueo significa que el subproceso actual se suspenderá antes de que se devuelva el resultado de la llamada. El subproceso de llamada no volverá hasta que se obtenga el resultado.
Una llamada sin bloqueo significa que la llamada no bloqueará el subproceso actual hasta que no se pueda obtener el resultado inmediatamente.

3. Demostración de código de E/S sin bloqueo

Primero, conozcamos la interfaz fcntl:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
El valor de cmd pasado es diferente , y los parámetros agregados más tarde también son diferentes .
La función fcntl tiene 5 funciones :
Duplicar un descriptor existente (cmd=F_DUPFD).
Obtener/establecer indicadores de descriptor de archivo (cmd=F_GETFD o F_SETFD).
Obtener/establecer indicadores de estado del archivo (cmd=F_GETFL o F_SETFL).
Obtenga/establezca la propiedad de E/S asíncrona (cmd=F_GETOWN o F_SETOWN).
Obtener/establecer bloqueo de registros (cmd=F_GETLK, F_SETLK o F_SETLKW).
Solo usamos la tercera función aquí , obtener / establecer el indicador de estado del archivo , puede configurar un descriptor de archivo como no bloqueante
void setNonBlock(int fd)
{
    int n = fcntl(fd,F_GETFL);
    if (n<0)
    {
        std::cerr<<"fcntl: "<<strerror(errno)<<std::endl;
        return;
    }
    fcntl(fd, F_SETFL, n | O_NONBLOCK);
}

F_GETFD obtiene el indicador de estado del descriptor de archivo y la función devuelve -1 para indicar que la configuración falla

F_SETFL puede configurar el indicador de estado del descriptor de archivo, como configurar la lectura o configurar la escritura, como se muestra en la siguiente figura:

El último O_NONBLOCK es la opción configurada para no bloquear. Una vez que hayamos escrito la función que establece que el descriptor de archivo no bloquee, primero demuestre el resultado del estado de bloqueo y luego demuestre el resultado del estado de no bloqueo:

int main()
{
    char buffer[1024];
    while (true)
    {
        printf(">>> ");
        fflush(stdout);
        ssize_t s = read(0,buffer,sizeof(buffer)-1);
        if (s>0)
        {
            buffer[s] = 0;
            std::cout<<"echo# "<<buffer<<std::endl;
        }
        else if (s == 0)
        {
            std::cout<<"read end"<<std::endl;
            break;
        }
        else 
        {

        }
    }
    return 0;
}

 Leemos directamente en un bucle infinito, primero creamos un búfer y luego leemos los datos en el descriptor de archivo de entrada estándar 0 a nuestro propio búfer, cuando la lectura es exitosa, ponemos \0 al final del archivo e imprimimos. Al ver el resultado, podemos saber que se trata de una lectura de bloqueo, porque una vez que no imprimimos el contenido en el descriptor de archivo de entrada estándar, se bloqueará en la función de lectura. Echemos un vistazo al resultado de no bloqueo:

int main()
{
    char buffer[1024];
    setNonBlock(0);
    while (true)
    {
        printf(">>> ");
        fflush(stdout);
        ssize_t s = read(0,buffer,sizeof(buffer)-1);
        if (s>0)
        {
            buffer[s] = 0;
            std::cout<<"echo# "<<buffer<<std::endl;
        }
        else if (s == 0)
        {
            std::cout<<"read end"<<std::endl;
            break;
        }
        else 
        {

        }
        sleep(1);
    }
    return 0;
}

Primero, configure el descriptor 0 como no bloqueante, porque es demasiado rápido para imprimir >>> durante la prueba. Para demostrarlo, dormimos durante 1 segundo:

 Se puede ver que incluso si no ingresamos la función en el descriptor de archivo número 0, aún se ejecutará en un bucle sin fin. El resultado reflejado es que si no ingresamos, el símbolo >>> seguirá siendo impreso, y el símbolo >>> también se imprimirá durante nuestro proceso de entrada. , ¡que no bloquea! Ya no necesitamos bloquear la interfaz de lectura para esperar la entrada de datos.

Nota: Podemos escribir algunas funciones simples, como imprimir registros para ejecutar en el ciclo, el efecto es el mismo que en la imagen de arriba, como se muestra en la figura a continuación:

Recuerde que al principio dijimos que el IO sin bloqueo devolverá un código de error si los datos no están listos. Sabemos que la interfaz de lectura devuelve -1 si no se lee. Verifiquémoslo a continuación:

 A partir de los resultados, podemos ver que efectivamente se devuelve el código de error -1, e imprimiremos la causa del error a continuación:

 ​​​​​​

 Se puede ver que aunque se devuelve -1 no es un error sino que el recurso no está listo. De hecho, el sistema operativo nos ha preparado algunos códigos de error:

 Por ejemplo, EAGAIN significa que el recurso no está listo y EINTR significa que los datos no se han leído y están interrumpidos, lo que no es un error:

 Entonces de hecho la forma correcta de escribir es la anterior, porque de esta forma podemos saber que no hay error en este momento pero los recursos no están listos.

Lo anterior es la demostración del código de E/S sin bloqueo. A continuación, presentamos la interfaz de selección de la transferencia multicanal de E/S.

 4. Selección de multiplexación IO

El sistema proporciona la función de selección para implementar el modelo de entrada / salida multiplexada .
La llamada al sistema select se usa para permitir que nuestro programa controle los cambios de estado de varios descriptores de archivos;
El programa se detendrá en la selección y esperará hasta que uno o más de los descriptores de archivos monitoreados cambien de estado;
int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout);

Debido a que select puede esperar múltiples descriptores de archivo a la vez, y la esencia de los descriptores de archivo es un subíndice de matriz, por lo que el primer parámetro es el descriptor de archivo más grande que debe verificarse + 1, +1 es porque el descriptor de archivo subyacente será atravesado

readfds, writefds y exceptfds son respectivamente el conjunto de descriptores de archivo de lectura, el conjunto de descriptores de archivo de escritura y el conjunto de descriptores de archivo de excepción.

timeout es una estructura que se utiliza para establecer el tiempo de espera de select. Veamos qué es timeval:

Qué significa eso. Por ejemplo, si pasamos timeout={0,0} para indicar un descriptor de archivo de control sin bloqueo, timeout=nullptr indica un descriptor de archivo de control con bloqueo, timeout={5,0} indica un descriptor de archivo de control con bloqueo en 5 s, superando Retorno sin bloqueo de 5 segundos y el tiempo de espera subsiguiente {5,0} se convierte en {0,0}. 

Por ejemplo, en el código de lectura de bloqueo que se mostró al principio, si configura 5 y 0 con seleccionar, solo >>> se mostrará en 5 segundos, esperando la entrada del usuario, y se devolverá un código de error después de 5 segundos. regresando, continuará imprimiendo como sin bloqueo >>>

Si el valor de retorno de select es mayor que 0, significa que varios descriptores de archivo están listos. Si el valor de retorno es igual a 0, significa que se ha agotado el tiempo de retorno. Si el valor de retorno es menor que 0, significa que hay un error en la llamada selecta.

De hecho, nuestro tipo fd_set es un mapa de bits. Cuando un evento de lectura del descriptor de archivo está listo, la posición del descriptor de archivo en el mapa de bits se establece en 1. Los eventos de escritura y los eventos de excepción son los mismos, como se muestra en la siguiente figura:

 Cuando lo llamamos, el usuario le dice al kernel qué descriptores de archivo deben tener en cuenta.

 Cuando se ejecuta la función, qué bit en el mapa de bits se establece en 1 representa qué evento del descriptor de archivo está listo.

La siguiente es la interfaz para manipular mapas de bits:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
 int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
 void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
 void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

Después de conocer la interfaz anterior, implementaremos el servidor seleccionado:

Primero, encapsulamos los cuatro pasos de crear un socket, enlazar, escuchar y obtener una nueva conexión en funciones:

enum
{
    SOCKET_ERR = 2,
    USE_ERR,
    BIND_ERR,
    LISTEN_ERR
};
const uint16_t gport = 8080;
class Sock
{
private:

public:
    const static int gbacklog = 32;
    static int createSock()
    {
        // 1.创建文件套接字对象
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock == -1)
        {
            logMessage(FATAL, "create socket error");
            exit(SOCKET_ERR);
        }
        logMessage(NORMAL, "socket success %d",sock);

        int opt = 1;
        setsockopt(sock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&opt,sizeof(opt));
        return sock;
    }
    static void Bind(int sock,uint16_t port)
    {
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY绑定任意地址IP
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind socket error");
            exit(BIND_ERR);
        }
        logMessage(NORMAL, "bind socket success");
    }
    static void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen socket error");
            exit(LISTEN_ERR);
        }
        logMessage(NORMAL, "listen socket success");
    }
    static int Accept(int listensock,std::string *clientip,uint16_t& clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        //  sock是和client通信的fd
        int sock = accept(listensock, (struct sockaddr *)&peer, &len);
        // accept失败也无所谓,继续让accept去获取新链接
        if (sock < 0)
        {
            logMessage(ERROR, "accept error,next");
        }
        else 
        {
            logMessage(NORMAL, "accept a new link success");
            *clientip = inet_ntoa(peer.sin_addr);
            clientport = ntohs(peer.sin_port);
        }
        return sock;
    }
};

Hemos hablado de todas las interfaces de función del servidor arriba al implementar el servidor TCP, si no lo entiendes, puedes ir y ver:

namespace select_ns
{
    static const int defaultport = 8080;
    class SelectServer
    {
    private:
        int _port;
        int _listensock;
    public:
        SelectServer(int port = defaultport)
        :_port(port)
        ,_listensock(-1)
        {

        }
        void initServer()
        {
            _listensock = Sock::createSock();
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
        }
        void start()
        {
            for (;;)
            {
                fd_set rfds;
                FD_ZERO(&rfds);
                // 把lsock添加到读文件描述符集中
                FD_SET(_listensock, &rfds);
                struct timeval timeout = {1, 0};
                int n = select(_listensock+1,&rfds,nullptr,nullptr,&timeout);
                switch (n)
                {
                    case 0:
                        logMessage(NORMAL,"time out.....");
                        break;
                    case -1:
                        logMessage(WARNING,"select error,code: %d,err string: %s",errno,strerror(errno));
                        break;
                    default:
                        //说明有事件就绪了
                        logMessage(NORMAL,"get a new link");
                        break;
                }
                sleep(1);
                /* std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock,&clientip,clientport);
                if (sock<0)
                {
                    continue;
                }
                //开始进行服务器的处理逻辑 */
            }
        }
        ~SelectServer()
        {
            if (_listensock != -1)
            {
                close(_listensock);
            }
        }
    };
}

Lo anterior es un marco para que implementemos un servidor seleccionado utilizando la interfaz encapsulada. En la función de inicio del servidor, necesitamos crear un objeto de lectura de mapa de bits de descriptor de archivo y luego inicializarlo a 0 con FD_ZERO. Tenga en cuenta que solo demostramos cómo leer como una demostración De hecho, la escritura y las excepciones son lo mismo que la lectura. Establezca la lectura de bloqueo dentro de 1 segundo, dividimos el valor de retorno de seleccionar en 3 casos, 1. seleccionar tiempo de espera 2. seleccionar error 3. detectar que hay un evento listo, una vez que haya un evento listo, lo imprimiremos. Ahora vamos a ejecutarlo:

 Cuando no hay conexión, debe imprimir time_out, y cuando hay conexión, imprimirá get new:

 Entonces, ¿por qué tantos reciben una nueva impresión? Esto se debe a que no procesamos el descriptor de archivo obtenido por esta selección, por lo que el valor del descriptor de archivo en el mapa de bits siempre es 1, por lo que sigue imprimiendo. A continuación, escribimos una función de procesamiento para tratar con el descriptor de archivo listo:

void HanderEvent(fd_set &rfds)
        {
            if (FD_ISSET(_listensock, &rfds))
            {
                //listensock必然就绪
                std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock, &clientip, clientport);
                if (sock < 0)
                {
                    return;
                }
                logMessage(NORMAL,"accept success [%s:%d]",clientip.c_str(),clientport);
            }
        }

Cuando el evento de lectura del descriptor del archivo de escucha está listo, obtenemos una nueva conexión e imprimimos la IP y el número de puerto del cliente:

Ahora vamos a ejecutarlo:

 

 Podemos ver que una vez que la nueva conexión se obtiene con éxito, esta vez no imprimirá repetidamente y obtendrá la nueva conexión como antes, sino que continuará esperando la nueva conexión, esto se debe a que el evento de lectura está listo y hemos procesado este evento. .

Después de tratar este punto, pensemos en cómo dejar que select maneje otros descriptores de archivo. Por ejemplo, ahora necesitamos usar el descriptor de archivo devuelto por accept para comunicarnos. Cuando el cliente envía datos, nuestro servidor puede mostrar estos datos. De hecho , por lo general, el uso de select requiere que los programadores mantengan una matriz que almacene todos los archivos legales. Vamos a implementarlo a continuación:

Primero creamos una matriz y un valor predeterminado, que se utiliza para inicializar todos los elementos de la matriz:

 fd_num representa el número máximo de descriptores de archivos que se pueden almacenar en esta matriz, y este número es tan grande como fd_set*8.

void initServer()
        {
            _listensock = Sock::createSock();
            if (_listensock == -1)
            {
                logMessage(NORMAL,"createSock error");
                return;
            }
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
            fdarray = new int[fd_num];
            for (int i = 0;i<fd_num;i++)
            {
                fdarray[i] = defaultfd;
            }
            fdarray[0] = _listensock;
        }

Cuando inicializamos, necesitamos abrir espacio e inicializar todos los valores a -1 (¿por qué es un número negativo? Debido a que el descriptor de archivo comienza desde 0, si es un número positivo, puede afectar a un determinado descriptor de archivo), como el espacio está abierto, no es necesario. Debe ser destruido, por lo que hay un destructor. Por supuesto, nuestro socket de escucha debe administrarse en una matriz durante la inicialización:

 ~SelectServer()
        {
            if (_listensock != -1)
            {
                close(_listensock);
            }
            if (fdarray)
            {
                delete[] fdarray;
                fdarray = nullptr;
            }
        }

En la función de inicio, cuando un evento está listo, ejecutamos la función hander, porque ahora estamos usando una matriz para administrar todos los descriptores de archivos, por lo que el método hander queda de la siguiente manera:

void HanderEvent(fd_set &rfds)
        {
            if (FD_ISSET(_listensock, &rfds))
            {
                //listensock必然就绪
                std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock, &clientip, clientport);
                if (sock < 0)
                {
                    return;
                }
                logMessage(NORMAL,"accept success [%s:%d]",clientip.c_str(),clientport);
                // 开始进行服务器的处理逻辑
                // 将accept返回的文件描述符放到自己管理的数组中,本质就是放到了select管理的位图中
                int i = 0;
                for (i = 0; i < fd_num; i++)
                {
                    if (fdarray[i] != defaultfd)
                    {
                        continue;
                    }
                    else
                    {
                        break;
                    }
                }
                if (i == fd_num)
                {
                    logMessage(WARNING, "server is full ,please wait");
                    close(sock);
                }
                else
                {
                    fdarray[i] = sock;
                }
                print();
            }
        }

 El primer paso es juzgar si el evento de lectura del socket de escucha está listo, y solo cuando esté listo, haremos las siguientes operaciones. Cuando obtenemos el socket de comunicación devuelto por la nueva conexión, debemos colocar este socket en el mapa de bits en seleccionar para administrar, así que primero recorra la matriz para encontrar el descriptor de archivo legal (si se usa el valor predeterminado, entonces es ilegal) , después de encontrar un descriptor legal, primero juzgamos si se ha alcanzado el final de la matriz en el proceso de recorrido. Si llega al final de la matriz, significa que todos los descriptores de archivos en la matriz son legales. En este momento , necesitamos registrar que la matriz de registros está llena y debemos esperar . Si no se alcanza el final de la matriz, simplemente coloque el nuevo descriptor de archivo devuelto por accept justo en la posición especificada de la matriz. Posteriormente agregamos una función de impresión para la comodidad de ver el resultado:

void print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < fd_num; i++)
            {
                if (fdarray[i] != defaultfd)
                {
                    std::cout << fdarray[i] << " ";
                }
            }
            std::cout << std::endl;
        }

Esta función solo imprimirá descriptores de archivos legales. Por supuesto, hay un lugar que no ha sido modificado. Recuerde el primer parámetro de selección. Este parámetro es el descriptor de archivo más grande + 1, por lo que la modificación es la siguiente:

 Primero suponga que el descriptor de archivo más grande es el conector de escucha, luego recorra la matriz, encuentre un descriptor de archivo legal, agregue el descriptor de archivo legal al mapa de bits de lectura y luego determine si es mayor que maxfd.Veamos el efecto:

 Se puede ver que no hay problema, cada vez que llega una nueva conexión, agregaremos el descriptor de archivo de la nueva conexión a la matriz, y finalmente la matriz colocará estos descriptores de archivos legales en selección para monitorear.

Sigamos modificando el código para que nuestro servidor seleccionado admita la comunicación IO normal:

Debido a que necesitamos manejar todos los descriptores de archivos, encapsulamos la parte de aceptación en la función de manejador y luego implementamos las funciones correspondientes de acuerdo con los diferentes descriptores de archivos:

void HanderEvent(fd_set &rfds)
        {
            for (int i = 0;i<fd_num;i++)
            {
                //过滤掉非法的文件描述符
                if (fdarray[i] == defaultfd) 
                    continue;
                //如果是listensock事件就绪,就去监听新连接获取文件描述符,如果不是listensock事件,那么就是普通的IO事件就绪了 
                if (FD_ISSET(fdarray[i], &rfds) && fdarray[i] == _listensock)
                {
                    Accepter(_listensock);
                }
                else if (FD_ISSET(fdarray[i], &rfds))
                {
                    Recver(fdarray[i],i);
                }
                else 
                {

                }
            }
        }

Cuando el descriptor de archivo listensock está listo, llamamos a la función de aceptación para manejar la escucha de nuevas conexiones.Si el descriptor de archivo normal está listo, ejecute la función de lectura de datos:

void Accepter(int listensock)
        {
            // listensock必然就绪
            std::string clientip;
            uint16_t clientport = 0;
            int sock = Sock::Accept(listensock, &clientip, clientport);
            if (sock < 0)
            {
                return;
            }
            logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
            // 开始进行服务器的处理逻辑
            // 将accept返回的文件描述符放到自己管理的数组中,本质就是放到了select管理的位图中
            int i = 0;
            for (i = 0; i < fd_num; i++)
            {
                if (fdarray[i] != defaultfd)
                {
                    continue;
                }
                else
                {
                    break;
                }
            }
            if (i == fd_num)
            {
                logMessage(WARNING, "server is full ,please wait");
                close(sock);
            }
            else
            {
                fdarray[i] = sock;
            }
            print();
        }

accept es el código en la función hander justo ahora, explicaremos directamente cómo procesar los datos:

 void Recver(int sock,int pos)
        {
            //注意:这样的读取有问题,由于没有定协议所以我们不能确定是否能读取一个完整的报文,并且还有序列化反序列化操作...
            //由于我们只做演示所以不再定协议,在TCP服务器定制的协议大家可以看看
            char buffer[1024];
            ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
            if (s>0)
            {
                buffer[s] = 0;
                logMessage(NORMAL,"client# %s",buffer);
            }
            else if (s == 0)
            {
                //对方关闭文件描述符,我们也要关闭并且下次不让select关心这个文件描述符了
                close(sock);
                fdarray[pos] = defaultfd;
                logMessage(NORMAL,"client quit");
            }
            else 
            {
                //读取失败,关闭文件描述符
                close(sock);
                fdarray[pos] = defaultfd;
                logMessage(ERROR,"client quit: %s",strerror(errno));
            }
            //2.处理 request
            std::string response = func(buffer);

            //3.返回response
            write(sock,response.c_str(),response.size());
        }

En primer lugar, hay un problema con nuestro procesamiento de datos, porque en circunstancias normales, se requiere un protocolo personalizado para garantizar que se lea un mensaje completo y se requiere serialización y deserialización. Hoy, no haremos estas tareas con fines de demostración. . Después de leer los datos, realizamos una impresión de eco en el servidor.Si la lectura falla o el cliente cierra el descriptor de archivo, nuestro servidor también debe cerrar el descriptor de archivo correspondiente en este momento, y queremos describir el archivo en la matriz. el carácter se establece en un estado ilegal, por lo que la siguiente selección ya no supervisará este descriptor de archivo. Después de recibir el mensaje del cliente, llamamos directamente a una función func para procesarlo. func es una función recién agregada para demostración, como se muestra en la siguiente figura:

 

 Se puede ver que simplemente devolvemos el mensaje del cliente, pero de hecho la función de esta función es procesar la solicitud del cliente y enviar una respuesta al cliente después de la serialización y deserialización.

Después de obtener la respuesta, la escribimos directamente en el descriptor de archivo utilizado para la comunicación. De esta forma, hemos modificado el código, vamos a ejecutarlo y vemos:

 Se puede ver que el programa se ejecuta sin problemas.


Resumir

Resumamos las características del servidor seleccionado:

1. Hay un límite superior en los descriptores de archivo que seleccionar puede esperar al mismo tiempo. Cambiar el kernel solo puede aumentar un poco el límite superior, pero no puede resolverlo por completo.

2. El servidor seleccionado debe usar una matriz de terceros para mantener los descriptores de archivos legales.

3. La mayoría de los parámetros de selección son tipos de entrada y salida. Antes de llamar a seleccionar, todos los descriptores de archivo deben restablecerse. Después de llamar, también debemos verificar y actualizar todos los descriptores de archivo, lo que trae el costo de atravesar.

4. ¿Por qué el primer parámetro de seleccionar el descriptor de archivo más grande es +1? Esto se debe a que los descriptores de archivos también deben recorrerse en el nivel del kernel.

5. Select usa mapas de bits, por lo que con frecuencia cambiará del modo kernel al modo usuario, y luego cambiará del modo usuario al modo kernel para copiar datos de un lado a otro, lo que tiene el problema del costo de copia.

Entonces, ¿cómo resolver el problema anterior? Los siguientes servidores de sondeo y epoll resolverán este problema.

Supongo que te gusta

Origin blog.csdn.net/Sxy_wspsby/article/details/132045534
Recomendado
Clasificación