Lorsque vous utilisez le modèle Socket pour implémenter la communication réseau, vous devez passer par plusieurs étapes telles que la création d'un Socket, l'écoute des ports, le traitement des connexions et les requêtes de lecture et d'écriture. Examinons maintenant de plus près les opérations clés de celles-ci. étapes pour nous aider à analyser les lacunes du modèle Socket.
Tout d'abord, lorsque nous avons besoin de communiquer entre le serveur et le client, nous pouvons créer une socket d'écoute (Listening Socket) qui écoute la connexion client à travers les trois étapes suivantes côté serveur :
Appelez le fonction socket, Créer une socket. Généralement, nous appelons ce socket un socket actif
Appelez la fonction bind pour lier le socket actif à l'IP et au port d'écoute du serveur actuel
Appelez la fonction d'écoute pour lier Le socket actif est converti en un socket actif. socket d'écoute et commence à écouter les connexions client.
Après avoir terminé les trois étapes ci-dessus, le serveur peut recevoir la demande de connexion du client. Afin de recevoir la demande de connexion du client en temps opportun, nous pouvons exécuter un processus en boucle dans lequel la fonction d'acceptation est appelée pour recevoir la demande de connexion du client.
Ce que vous devez noter ici, c'est que la fonction d'acceptation est une fonction de blocage. C'est-à-dire que s'il n'y a pas de demande de connexion client à ce moment-là, alors le processus d'exécution côté serveur sera toujours bloqué dans la fonction d'acceptation. Une fois qu'une demande de connexion client arrive, accept ne bloquera plus, mais traitera la demande de connexion, établira une connexion avec le client et renverra le Socket connecté.
Enfin, le serveur peut recevoir et traiter des requêtes de lecture et d'écriture sur le socket connecté qui vient d'être renvoyé en appelant la fonction recv ou send, ou envoyer des données au client.
Code :
listenSocket = socket(); //调用socket系统调用创建一个主动套接字 bind(listenSocket); //绑定地址和端口 listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字 while(1) { //循环监听是否有客户端连接请求到来 connSocket = accept(listenSocket);//接受客户端连接 recv(connSocket);//从客户端读取数据,只能同时处理一个客户端 send(connSocket);//给客户端返回数据,只能同时处理一个客户端 }
Cependant, à partir du code ci-dessus, vous constaterez peut-être que bien qu'il puisse établir la communication entre le serveur et le client, le programme ne peut gérer qu'une seule connexion client à chaque fois que la fonction d'acceptation est appelée. Par conséquent, si nous souhaitons gérer plusieurs requêtes client simultanées, nous devons utiliser le multithreading pour gérer les requêtes sur plusieurs connexions client établies via la fonction accept.
Après avoir utilisé cette méthode, nous devons créer un thread après que la fonction accept ait renvoyé le socket connecté et transmettre le socket connecté au thread créé. Ce thread sera responsable du traitement ultérieur sur cette socket connectée. Dans le même temps, le processus d'exécution côté serveur appellera à nouveau la fonction d'acceptation et attendra la prochaine connexion client.
Multi-threading :
listenSocket = socket(); //调用socket系统调用创建一个主动套接字 bind(listenSocket); //绑定地址和端口 listen(listenSocket); //将默认的主动套接字转换为服务器使用的被动套接字,也就是监听套接字 while(1) { //循环监听是否有客户端连接请求到来 connSocket = accept(listenSocket);//接受客户端连接 pthread_create(processData, connSocket);//创建新线程对已连接套接字进行处理 } processData(connSocket){ recv(connSocket);//从客户端读取数据,只能同时处理一个客户端 send(connSocket);//给客户端返回数据,只能同时处理一个客户端 }
Bien que cette méthode puisse améliorer la capacité de traitement simultané du serveur, le processus d'exécution principal de Redis est exécuté par un seul thread et le multi-threading ne peut pas être utilisé pour améliorer la capacité de traitement simultané. Par conséquent, cette méthode ne fonctionne pas pour Redis.
Existe-t-il d'autres méthodes qui peuvent aider Redis à améliorer les capacités de traitement des clients simultanés ? Cela nécessite l'utilisation de la fonction de multiplexage IO fournie par le système d'exploitation. Dans le modèle de programmation Socket de base, la fonction accept ne peut écouter que les connexions client sur un socket d'écoute, et la fonction recv ne peut attendre que les requêtes envoyées par le client sur un socket connecté.
Étant donné que le système d'exploitation Linux est largement utilisé dans les applications pratiques, dans cette leçon, nous étudions principalement le mécanisme de multiplexage des E/S sous Linux. Select, poll et epoll sont les trois principales formes de mécanisme de multiplexage d'E/S fournies par Linux. Ensuite, nous apprendrons respectivement les idées de mise en œuvre et les méthodes d’utilisation de ces trois mécanismes. Voyons ensuite pourquoi Redis choisit souvent d'utiliser le mécanisme epoll pour implémenter la communication réseau.
Tout d'abord, comprenons le modèle de programmation du mécanisme de sélection.
Mais avant d'apprendre en détail, nous devons savoir quels points clés nous devons maîtriser pour un mécanisme de multiplexage d'E/S. Cela peut nous aider à comprendre rapidement les connexions et les différences entre les différents mécanismes. En fait, lorsque nous apprenons le mécanisme de multiplexage des E/S, nous devons être capables de répondre aux questions suivantes : Premièrement, quels événements sur le socket le mécanisme de multiplexage écoutera-t-il ? Deuxièmement, combien de sockets le mécanisme de multiplexage peut-il écouter ? Troisièmement, lorsqu’un socket est prêt, comment le mécanisme de multiplexage trouve-t-il le socket prêt ?
Une fonction importante dans le mécanisme de sélection est la fonction de sélection. Pour la fonction select, ses paramètres incluent le nombre de descripteurs de fichiers surveillés __nfds, les trois collections de descripteurs surveillés readfds, writefds, exceptfds et le délai d'attente pour bloquer l'attente pendant la surveillance. select function prototype :
int select(int __nfds, fd_set *__readfds, fd_set *__writefds, fd_set *__exceptfds, struct timeval *__timeout)
Ce que vous devez noter ici, c'est que Linux aura un descripteur de fichier pour chaque socket, qui est un entier non négatif, utilisé pour identifier de manière unique le socket. Il est courant sous Linux d'utiliser des descripteurs de fichiers comme arguments dans les fonctions du mécanisme de multiplexage. La fonction trouve le socket correspondant via le descripteur de fichier pour implémenter des opérations telles que la surveillance, la lecture et l'écriture.
Les trois paramètres de la fonction select spécifient l'ensemble de descripteurs de fichiers qui doivent être surveillés, qui représente en fait l'ensemble des sockets qui doivent être surveillés. Alors pourquoi y a-t-il trois ensembles ?
关于刚才提到的第一个问题,即多路复用机制监听的套接字事件有哪些。select 函数使用三个集合,表示监听的三类事件,分别是读数据事件,写数据事件,异常事件。
我们进一步可以看到,参数 readfds、writefds 和 exceptfds 的类型是 fd_set 结构体,它主要定义部分如下所示。其中,fd_mask类型是 long int 类型的别名,__FD_SETSIZE 和 __NFDBITS 这两个宏定义的大小默认为 1024 和 32。
所以,fd_set 结构体的定义,其实就是一个 long int 类型的数组,该数组中一共有 32 个元素(1024/32=32),每个元素是 32 位(long int 类型的大小),而每一位可以用来表示一个文件描述符的状态。了解了 fd_set 结构体的定义,我们就可以回答刚才提出的第二个问题了。每个描述符集合都可以被 select 函数监听 1024 个描述符。
首先,我们在调用 select 函数前,可以先创建好传递给 select 函数的描述符集合,然后再创建监听套接字。而为了让创建的监听套接字能被 select 函数监控,我们需要把这个套接字的描述符加入到创建好的描述符集合中。
接下来,我们可以使用 select 函数并传入已创建的描述符集合作为参数。程序在调用 select 函数后,会发生阻塞。一旦 select 函数检测到有就绪的描述符,会立即终止阻塞并返回已就绪的文件描述符数。
那么此时,我们就可以在描述符集合中查找哪些描述符就绪了。然后,我们对已就绪描述符对应的套接字进行处理。比如,如果是 readfds 集合中有描述符就绪,这就表明这些就绪描述符对应的套接字上,有读事件发生,此时,我们就在该套接字上读取数据。
而因为 select 函数一次可以监听 1024 个文件描述符的状态,所以 select 函数在返回时,也可能会一次返回多个就绪的文件描述符。我们可以使用循环处理流程,对每个就绪描述符对应的套接字依次进行读写或异常处理操作。
select函数有两个不足
首先,select 函数对单个进程能监听的文件描述符数量是有限制的,它能监听的文件描述符个数由 __FD_SETSIZE 决定,默认值是 1024。
其次,当 select 函数返回后,我们需要遍历描述符集合,才能找到具体是哪些描述符就绪了。这个遍历过程会产生一定开销,从而降低程序的性能。
poll 机制的主要函数是 poll 函数,我们先来看下它的原型定义,如下所示:
int poll(struct pollfd *__fds, nfds_t __nfds, int __timeout)
其中,参数 *__fds 是 pollfd 结构体数组,参数 __nfds 表示的是 *__fds 数组的元素个数,而 __timeout 表示 poll 函数阻塞的超时时间。
pollfd 结构体里包含了要监听的描述符,以及该描述符上要监听的事件类型。从 pollfd 结构体的定义中,我们可以看出来这一点,具体如下所示。pollfd 结构体中包含了三个成员变量 fd、events 和 revents,分别表示要监听的文件描述符、要监听的事件类型和实际发生的事件类型。
pollfd 结构体中要监听和实际发生的事件类型,是通过以下三个宏定义来表示的,分别是 POLLRDNORM、POLLWRNORM 和 POLLERR,它们分别表示可读、可写和错误事件。
了解了 poll 函数的参数后,我们来看下如何使用 poll 函数完成网络通信。这个流程主要可以分成三步:
第一步,创建 pollfd 数组和监听套接字,并进行绑定;
第二步,将监听套接字加入 pollfd 数组,并设置其监听读事件,也就是客户端的连接请求;
第三步,循环调用 poll 函数,检测 pollfd 数组中是否有就绪的文件描述符。
而在第三步的循环过程中,其处理逻辑又分成了两种情况:
如果是连接套接字就绪,这表明是有客户端连接,我们可以调用 accept 接受连接,并创建已连接套接字,并将其加入 pollfd 数组,并监听读事件;
如果是已连接套接字就绪,这表明客户端有读写请求,我们可以调用 recv/send 函数处理读写请求。
其实,和 select 函数相比,poll 函数的改进之处主要就在于,它允许一次监听超过 1024 个文件描述符。但是当调用了 poll 函数后,我们仍然需要遍历每个文件描述符,检测该描述符是否就绪,然后再进行处理。
Tout d'abord, le mécanisme epoll utilise la structure epoll_event pour enregistrer les descripteurs de fichiers à surveiller et les types d'événements à surveiller. Ceci est similaire à la structure pollfd utilisée dans le mécanisme de sondage.
Donc, pour la structure epoll_event, elle contient la variable union epoll_data_t et la variable events de type entier. Il existe une variable membre fd dans l'union epoll_data_t qui enregistre les descripteurs de fichiers, et la variable d'événements prendra différentes valeurs de définition de macro pour représenter les types d'événements qui concernent les descripteurs de fichiers dans la variable epoll_data_t, par exemple certains courants. Les types d'événements incluent les types suivants.
EPOLLIN : Événement de lecture, indiquant que le socket correspondant au descripteur de fichier a des données à lire.
EPOLLOUT : Evénement d'écriture, indiquant que le socket correspondant au descripteur de fichier a des données à écrire.
EPOLLERR : Événement d'erreur, indiquant que le descripteur de fichier est erroné pour le socket.
Lorsque vous utilisez la fonction de sélection ou d'interrogation, après avoir créé l'ensemble de descripteurs de fichiers ou le tableau pollfd, vous pouvez ajouter les descripteurs de fichiers que nous devons surveiller au tableau.
Mais pour le mécanisme epoll, nous devons d'abord appeler la fonction epoll_create pour créer une instance epoll. Cette instance epoll maintient deux structures en interne, qui enregistrent les descripteurs de fichiers à surveiller et les descripteurs de fichiers prêts. Pour les descripteurs de fichiers prêts, ils seront renvoyés au programme utilisateur pour traitement.
Ainsi, lorsque nous utilisons le mécanisme epoll, nous n'avons pas besoin de parcourir et de demander quels descripteurs de fichiers sont prêts comme en utilisant select et poll. Par conséquent, epoll est plus efficace que select et poll.
Après avoir créé l'instance epoll, nous devons utiliser la fonction epoll_ctl pour ajouter le type d'événement d'écoute au descripteur de fichier surveillé, et utiliser la fonction epoll_wait pour obtenir le descripteur de fichier prêt.
Nous comprenons maintenant comment utiliser la fonction epoll. En fait, c'est précisément parce qu'epoll peut personnaliser le nombre de descripteurs surveillés et renvoyer directement des descripteurs prêts que lorsque Redis conçoit et implémente le cadre de communication réseau, il est basé sur des fonctions telles que epoll_create, epoll_ctl et epoll_wait dans le mécanisme Read et epoll. Les événements d'écriture ont été encapsulés et développés pour implémenter un cadre de communication réseau basé sur les événements, de sorte que même si Redis s'exécute dans un seul thread, il peut toujours gérer efficacement l'accès client à haute concurrence.
Le modèle Reactor est un modèle de programmation utilisé par le serveur réseau pour traiter les demandes d'E/S réseau à haute concurrence Caractéristiques du modèle :
Trois types d'événements de traitement, à savoir les événements de connexion, l'écriture. événements et lecture d'événements ;
Trois rôles clés, à savoir le réacteur, l'accepteur et le gestionnaire.
Le modèle Reactor traite du processus d'interaction entre le client et le serveur, et ces trois types d'événements correspondent aux événements en attente déclenchés par différents types de requêtes côté serveur lors de l'interaction entre le client et le serveur :
Lorsqu'un client souhaite interagir avec le serveur, le client enverra une demande de connexion au serveur pour établir une connexion, ce qui correspond à un événement de lien sur le serveur. Une fois la connexion établie, le client enverra un. demande au serveur. Envoyez une demande de lecture pour lire les données. Lorsque le serveur traite une demande de lecture, il doit réécrire les données au client, ce qui correspond à l'événement d'écriture sur le serveur. Peu importe que le client envoie une demande de lecture ou d'écriture au serveur, le serveur doit lire le contenu de la demande. du client. , donc ici, la requête de lecture ou d'écriture correspond à l'événement de lecture côté serveur
Trois rôles clés :
Premièrement, l'événement de connexion est géré par l'accepteur, qui se charge de la réception la connexion ; l'accepteur après avoir reçu la connexion , un gestionnaire sera créé pour traiter les événements de lecture et d'écriture ultérieurs sur la connexion réseau
Deuxièmement, les événements de lecture et d'écriture sont gérés par le gestionnaire
; les scénarios à forte concurrence, les événements de connexion et les événements de lecture et d'écriture se produiront en même temps, nous avons donc besoin d'un rôle pour écouter et distribuer spécifiquement les événements, ce qui est le rôle du réacteur. Lorsqu'il y a une demande de connexion, le réacteur transmettra l'événement de connexion généré à l'accepteur pour traitement ; lorsqu'il y a une demande de lecture ou d'écriture, le réacteur transmettra les événements de lecture et d'écriture au gestionnaire pour traitement.
Alors, maintenant que nous savons que ces trois rôles interagissent autour de la surveillance, de la transmission et du traitement des événements, comment réaliser l'interaction de ces trois lors de la programmation ? Ceci est indissociable de la conduite événementielle.
L'initialisation de l'événement est exécutée au démarrage du programme serveur. Sa fonction principale est de créer le type d'événement qui doit être surveillé et le gestionnaire correspondant à ce type d'événement. Une fois que le serveur a terminé l'initialisation, l'initialisation des événements est terminée en conséquence et le programme serveur doit entrer dans la boucle principale de capture, de distribution et de traitement des événements.
Utilisez une boucle while comme boucle principale. Ensuite, dans cette boucle principale, nous devons capturer l'événement qui s'est produit, déterminer le type d'événement et, en fonction du type d'événement, appeler le gestionnaire d'événements créé lors de l'initialisation pour gérer réellement l'événement.
Par exemple, lorsqu'un événement de connexion se produit, le programme serveur doit appeler la fonction de traitement de l'accepteur pour créer une connexion avec le client. Lorsqu'un événement de lecture se produit, cela indique qu'une demande de lecture ou d'écriture a été envoyée au serveur. Le programme serveur appellera une fonction de traitement de demande spécifique pour lire le contenu de la demande à partir de la connexion client, complétant ainsi le traitement de l'événement de lecture.
Le mécanisme de fonctionnement de base du modèle Reactor : différents types de requêtes du client déclencheront trois types d'événements : la connexion, la lecture et l'écriture côté serveur. La surveillance, la distribution et le traitement de ces trois types d'événements sont. exécutés par les trois types de rôles : réacteur, accepteur et gestionnaire. Une fois terminés, ces trois types de rôles mettront en œuvre l'interaction et le traitement des événements via le cadre piloté par les événements.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!