Directorio de artículos
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
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
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 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
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 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.
2. Conceptos importantes de IO
1. Comunicación síncrona vs comunicación asíncrona (comunicación síncrona/ comunicación asíncrona)
2. Bloqueo vs no bloqueo
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 */ );
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.
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
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.