Implémentation basée sur l'assemblage de coroutines C/C++ (pour les serveurs)

php是最好的语言
Libérer: 2018-08-02 16:09:00
original
2571 Les gens l'ont consulté

Cet article est une implémentation de la coroutine C/C++. Nous devons atteindre ces deux objectifs :

  1. Avoir une idée séquentielle de programmation de serveur synchrone pour faciliter la conception fonctionnelle et le débogage de code - J'ai utilisé la partie coroutine dans libco

  2. a des performances d'E/S asynchrones - J'ai utilisé les E/S d'événement dans libevent apache php mysql

Structurellement, ce sont les fonctions de libco et libevent sont combinés, j'ai donc nommé mon projet libcoevent, qui signifie "cadre de programmation de serveur coroutine synchrone basé sur libevent". Le mot co dans le nom ne signifie pas libco, mais coroutine.

En ce qui concerne le langage de programmation, j'ai choisi C++, principalement parce que libco ne prend en charge que Linux basé sur une architecture x86 ou x64, et que ces architectures sont essentiellement des PC dotés de ressources suffisantes et de hautes performances. aucun problème pour apprendre le C++. Cet article explique comment le code est implémenté.

Si vous souhaitez utiliser ce projet, veuillez ajouter -lco -levent -lcoevent trois options aux options de lien.

Relation de classe et fonctions de base

Relation de classe

Relation d'héritage de classe

Le schéma de base de la relation d'héritage de la classe est le suivant :

Implémentation basée sur lassemblage de coroutines C/C++ (pour les serveurs)

Dans les appels réels, seules les classes sur les nœuds feuilles de l'arbre des relations d'héritage seront réellement utilisées, et les autres classes sont considérées comme des classes virtuelles.

Affiliation de classe

Les instances de différents types ont des affiliations pendant l'exécution du programme. En plus de la classe de base de niveau supérieur, d'autres classes feuilles doivent être attachées à d'autres classes. l’environnement opérationnel. Le diagramme de dépendances est le suivant : La classe

Implémentation basée sur lassemblage de coroutines C/C++ (pour les serveurs)

  • Base fournit l'environnement d'exploitation le plus basique et gère le Serveur Objet ;

  • Procédureobjet de gestion Client. Il ressort de la figure que les objets Serveur et Session gèrent les objets Client. L'objet

    • Serveur est créé par l'application et initialisé pour s'exécuter dans l'objet Base. Un objet Serveur peut être configuré pour être automatiquement détruit lorsque le serveur se termine ou lorsque son objet Base dépendant est détruit. L'objet

    • Session est automatiquement créé par l'objet Serveur en mode session, et est exécuté en appelant l'entrée de programme spécifiée par l'application ; L'objet returnServeur est automatiquement détruit à la fin de la session (appel de fonction ) ou à la fin de son service objet Serveur subordonné. L'objet

  • Client est créé par l'application appelant l'interface de l'objet Procédure pour interagir avec des services tiers. L'application peut appeler l'interface à l'avance pour demander la destruction de l'objet Client, ou elle peut le détruire automatiquement à la fin du service Procédure.

Classes Base et Event

Implémentation basée sur lassemblage de coroutines C/C++ (pour les serveurs)

Classes Base est utilisée pour exécuter divers services de libcoevent. Chaque instance de la classe Base doit correspondre à un thread, et tous les services s'exécutent dans l'instance Base de manière coroutine. Comme le montre la figure ci-dessus, la classe Base contient un objet event_base de la bibliothèque libevent et une série d'objets Event de cette bibliothèque coroutine. La classe

Implémentation basée sur lassemblage de coroutines C/C++ (pour les serveurs)

Event emprunte en fait le nom struct event à libevent, car chaque instance de la classe Event correspond à A event objet de libevent. Les points clés sur lesquels nous devons nous concentrer sont les classes Procédure et Client. La classe

Procedure

Procedure a deux fonctionnalités clés :

  1. Chaque objet a une coroutine libco, c'est-à-dire qu'il possède ses propres informations de contexte indépendantes et peut être utilisée pour écrire un processus serveur indépendant (procédure

  2. Les sous-classes de procédure peuvent créer des objets Client et des communications serveur tierces ; interaction. La classe

Procédure comporte deux sous-classes, à savoir Serveur et Session.

Classe serveur

La classe serveur est créée et initialisée par l'application pour s'exécuter dans l'objet Base. La classe Server comporte trois sous-classes :

  • SubRoutine : Il ne sert en fait pas de programme serveur, mais fournit la fonction sleep() la plus basique et prend en charge la fonction de création d'objets Client de la classe Procedure, donc l'application Il peut être utilisé comme programme interne créé temporairement ou résident.

  • UDPServer : Une fois que l'application a créé et initialisé l'objet UDPServer, le programme se liera automatiquement à une interface de socket datagramme. Les applications peuvent implémenter des services réseau en envoyant et en recevant des paquets de données dans l'interface réseau. UDPServer fournit à la fois le mode normal et le mode session.

  • TCPServer : Une fois que l'application a créé et initialisé l'objet TCPPServer, le programme se liera et écoutera automatiquement le socket de flux. TCPServer ne prend en charge que le mode session.

Le soi-disant "Mode normal" est le comportement dans lequel l'application enregistre la fonction d'entrée de l'objet Serveur et l'application exploite l'objet Serveur.

Le soi-disant « Mode Session » fait référence à l'objet UDPServer ou TCPServer. Après avoir reçu les données entrantes, il distingue automatiquement le client et crée un objet Session distinct. est traité. Chaque objet Session ne sert qu'un seul client.

Classe Session

Les objets Session ne peuvent pas être créés activement par l'application, mais sont automatiquement créés à la demande par la classe Serveur en mode session. La caractéristique de l'objet Session est qu'il ne peut communiquer qu'avec un seul client (par rapport à l'objet UDPServer), il n'y a donc pas de fonction send(), seulement reply() .

La classe coevent.hSession et ses sous-classes déclarées dans le fichier d'en-tête sont toutes des classes virtuelles pures. Le but est d'empêcher les applications de construire explicitement des objets Session et. Masquer les détails de mise en œuvre. Les objets

Client class

Client sont créés par les objets Procedure et recyclés par les objets Procedure. Le rôle de l'objet Client est d'initier activement la communication avec le serveur distant. Puisque cette action appartient au client du point de vue de la structure client-service, elle est nommée Client.

DNSClient

Client La sous-classe la plus spéciale est la classe DNSClient Cette classe existe pour résoudre le problème de blocage getaddrinfo() dans les E/S asynchrones. Pour le principe d'implémentation de DNSClient, veuillez vous référer au code et à mon article précédent "DNS structure des messages et implémentation du code d'analyse DNS personnel".

Quant à la classe DNSClient, le principe d'implémentation spécifique est d'encapsuler un objet UDPClient, d'utiliser cet objet pour finaliser l'envoi et la réception des messages DNS, et d'implémenter l'analyse des messages dans la classe.

UDPServer - Implémentation de coroutine basée sur libevent

UDPServer Le principe du mode normal est un framework de serveur coroutine synchrone très typique basé sur libevent. Dans son implémentation de code, les fonctions principales sont les fonctions suivantes :

  • _libco_routine(), la fonction d'entrée de la coroutine. À l'aide de cette fonction, elle est transformée en fonction d'entrée de service unifiée de. liboevent

  • _libevent_callback(), libevent fonction de rappel horaire, dans cette fonction, restaure le contexte de la coroutine.

  • UDPServer::recv_in_timeval(), fonction de réception de données, dans cette fonction, la fonction d'attente des données clés est implémentée, et la sauvegarde du contexte coroutine est également réalisée

La quantité totale de code pour les trois fonctions ci-dessus, y compris les lignes vides, ne dépasse pas 200 lignes. Je pense que c'est toujours facile à comprendre. Le principe d'implémentation est expliqué en détail ci-dessous :

Interface coroutine libco

Comme mentionné précédemment, j'utilise libco comme bibliothèque coroutine. Les coroutines sont transparentes pour l'application, mais pour l'implémentation de la bibliothèque, c'est essentiel.

Ce qui suit explique plusieurs interfaces fournies par la fonction coroutine de libco (le nombre de documents de libco est simplement "touchant", ce dont on se plaint souvent sur Internet...) :

Création et destruction de Coroutine

Libco utilise la structure struct stCoRoutine_t * pour enregistrer la coroutine. Vous pouvez créer des objets coroutine en appelant co_create() ; co_release()

Entrez la coroutine

Après avoir créé la coroutine, appelez

pour commencer à exécuter la coroutine depuis le début de la fonction coroutine. co_resume()

Suspendre la coroutine

Lorsque la coroutine doit remettre les droits d'utilisation du processeur, vous pouvez appeler

pour libérer la coroutine et changer de contexte. Après l'appel, le contexte est restauré à la dernière coroutine qui a appelé co_yield(). L'emplacement où co_resume() est appelé peut être considéré comme un "co_yield()point d'arrêt".

Restauration de la coroutine

Les fonctions utilisées pour restaurer la coroutine et créer la coroutine sont toutes deux

Appelez cette fonction pour basculer la pile actuelle vers le contexte de la coroutine spécifiée. partira du "co_resume()Breakpoint" mentionné ci-dessus reprend l'exécution.

Implémentation de la planification des coroutines

Comme vous pouvez le voir dans la section précédente, la fonction de coroutine de la libco que nous utilisons inclut une fonction de commutation de coroutine, mais quand changer et qu'arrive-t-il au processeur après le changement de distribution, voici ce que nous devons mettre en œuvre et encapsuler.

Le moment de créer et de détruire les coroutines est naturellement lorsque la classe UDPServer est initialisée et détruite. Ce qui suit se concentre sur l'analyse des opérations de saisie, de suspension et de reprise de la coroutine :

Saisie de la coroutine

Le code de saisie/reprise de la coroutine est en _libevent_callback(), avec cette ligne :

// handle control to user application
co_resume(arg->coroutine);
Copier après la connexion

Si la coroutine actuelle n'a pas encore été exécutée, après avoir exécuté ce code, le programme passera à la fonction coroutine spécifiée lors de la création de la coroutine libco et lancera l'exécution. Pour UDPServer, c'est la fonction _libco_routine(). Cette fonction est très simple, avec seulement trois lignes :

static void *_libco_routine(void *libco_arg)
{
    struct _EventArg *arg = (struct _EventArg *)libco_arg;
    (arg->worker_func)(arg->fd, arg->event, arg->user_arg);
    return NULL;
}
Copier après la connexion

En passant des paramètres, la fonction de rappel libco est convertie en une fonction serveur spécifiée par l'application pour exécution.

Mais comment implémenter le premier rappel libevent ? C'est toujours très simple, il suffit de définir le délai d'attente sur 0 lors de l'appel de event_add() de libevent, ce qui entraînera l'expiration immédiate de l'événement libevent. Grâce à ce mécanisme, nous atteignons également l'objectif d'exécuter chaque fonction de service Procédure immédiatement après l'exécution de Base.

Mettre en pause et reprendre la coroutine

Quand appeler co_yield est au centre de cette implémentation de coroutine. L'emplacement où co_yield est appelé est un endroit qui peut provoquer un changement de contexte, et cela. C'est également des points techniques clés dans la conversion d'un framework de programmation asynchrone en un framework synchrone. Vous pouvez vous référer à la fonction de UDPServerrecv_in_timeval() ici. La logique de base de la fonction est la suivante :

Implémentation basée sur lassemblage de coroutines C/C++ (pour les serveurs)

La branche la plus importante est le jugement du drapeau d'événement libevent et la logique la plus importante est le event_add() et < ; 🎜>appel de fonctions. Le fragment de fonction est le suivant : co_yield()

struct timeval timeout_copy;
timeout_copy.tv_sec = timeout.tv_sec;
timeout_copy.tv_usec = timeout.tv_usec;
    ...
event_add(_event, &timeout_copy);
co_yield(arg->coroutine);
Copier après la connexion
Ici, nous comprenons la fonction

comme un point d'arrêt. Lorsque le programme s'exécute ici, le droit d'utiliser le CPU sera remis et le programme le fera. revenez à l'appel co_yield() entre les mains de la fonction de niveau précédent. Où se trouve exactement cette « fonction de niveau supérieur » ? En fait, il s'agit de la fonction co_resume() mentionnée ci-dessus. _libevent_callback()

Du point de vue de

, le programme reviendra de la fonction _libevent_callback() et poursuivra son exécution. A ce stade on peut comprendre ceci : l'ordonnancement des coroutines s'effectue en réalité par emprunt co_resume(). Ici, nous devons faire attention aux phrases ci-dessus libevent : co_resume()

// switch into the coroutine
if (arg->libevent_what_ptr) {
    *(arg->libevent_what_ptr) = (uint32_t)what;
}
Copier après la connexion
Ici, la valeur du drapeau d'événement

libevent est transmise à la coroutine, ce qui est une base importante pour l'événement précédent jugement. Le moment venu, appellera _libevent_callback() ci-dessous et rendra les droits d'utilisation du CPU à la coroutine. co_resume()

Détruire la coroutine

En plus de

, l'appel de fonction coroutine ci_yield() provoquera également le retour de return, donc dans co_resume(), nous devons également déterminer la coroutine Si le processus est terminé. Si la coroutine se termine, les ressources de coroutine associées doivent être détruites. Voir le code à l'intérieur du corps conditionnel _libevent_callback(). if (is_coroutine_end(arg->coroutine)) {...}

Mode Session

Dans la mise en œuvre de ce projet, un modèle de conception de serveur appelé « Mode Session » est fourni. Le mode Session fait référence à l'objet UDPServer ou TCPServer. Après avoir reçu les données entrantes, il distingue automatiquement le client et crée un objet

Session distinct pour le traitement. Chaque objet Session ne sert qu'un seul client.

Pour

TCPServer, il est relativement simple d'implémenter la fonction ci-dessus, car après avoir surveillé un socket TCP, lorsqu'il y a une connexion entrante, il suffit d'appeler pour obtenir un nouveau descripteur de fichier , créez simplement une nouvelle sous-classe de accept()Server pour ce descripteur de fichier - c'est la classe TCPSession.

Mais

UDPServer est plus gênant car UDP ne peut pas faire cela. Nous ne pouvons mettre en œuvre la soi-disant session que par nous-mêmes.

UDPSession atteint les

objectifs de conception

Nous devons obtenir les effets suivants de la classe

UDPSession :

  • classe Lors de l'appel de la fonction

    recv, seules les données envoyées par le client distant correspondant seront reçues. La classe

  • appelle la classe

    send fonction (l'implémentation réelle est ), vous pouvez utiliser le port de UDPServerreply() pour répondre

recv()

Dans le projet, UDPSession est une classe abstraite, et l'implémentation réelle est UDPItnlSession. Mais pour être précis, l'implémentation de UDPItnlSession dépend étroitement de UDPServer. Pour cette partie, vous pouvez vous référer au code du corps de la boucle dans la fonction de _session_mode_worker()UDPServerdo-while(). L'idée du programme est la suivante :

  • UDPServer maintient un dictionnaire UDPSession, avec la combinaison IP distante + nom de port comme clé.

  • Lorsque les données arrivent, déterminez si la combinaison IP + port distant est dans le dictionnaire. Si c'est le cas, copiez les données dans la session correspondante si elle n'existe pas, créez le. session

Pour le code permettant de copier les données, voir l'implémentation de la fonction de la classe UDPItnlSessionforward_incoming_data().

reply()

L'envoi de données est en fait très simple, il suffit d'effectuer directement sur le fd de UDPServersendto().

quitter

Pour l'objet Serveur en mode session, le code fournit une fonction qui peut être appelée par sa session et demande au serveur de quitter et de détruire les ressources : quit_session_mode_server() . Le principe de mise en œuvre est de déclencher un événement EV_SIGNAL vers le serveur. Pour les événements d'E/S ordinaires, cela ne devrait pas se produire et nous l'utilisons ici comme signal de sortie. Si le serveur détecte ce signal, la logique de sortie est déclenchée.

Exemple d'application

L'exemple de code de ce projet est divisé en deux parties : serveur et client. Le serveur utilise libcoevent, tandis que le client utilise simplement Python. Un programme simple écrit. Cet article n'expliquera pas la partie client du code.

Le code de Server fournit des exemples d'application pour les trois sous-classes de la classe Server. En utilisant une logique comprenant des lignes vides, des instructions de débogage, des jugements d'erreurs, etc., un processus et deux services ont été implémentés en moins de 300 lignes. Il faut dire que la logique est quand même très claire et que beaucoup de code est économisé.

SubRoutine

démontre une logique de réseau linéaire unique via la fonction _simple_test_routine(). Dans le programme, la routine crée d'abord un objet DNSClient, demande un nom de domaine au serveur de noms de domaine par défaut, puis le connect() port 80 du serveur. Après succès, revenez directement.

Cette fonction montre le scénario d'utilisation de SubRoutine et l'utilisation de l'objet Client, en particulier l'utilisation simple de DNSClient. La fonction d'entrée de

UDPServer

UDPServer est _udp_session_routine(), et sa fonction est de fournir des services de requête de nom de domaine aux clients. Les clients envoient une chaîne comme nom de domaine à interroger, puis le serveur renvoie les résultats de la requête au client après l'avoir demandé via l'objet DNSClient.

Cette fonction démontre l'utilisation (plus complexe et complète) de l'objet UDPSession et du DNSClient.

TCPServer

La fonction d'entrée est _tcp_session_routine(), la logique est relativement simple, principalement pour montrer l'utilisation de TCPSession.

Postscript

En principe, libcoevent a été développé, a implémenté les fonctions nécessaires et peut être utilisé pour écrire des programmes serveur. Bien sûr, puisqu’il s’agit de la première version, une grande partie du code semble encore un peu brouillon. L'importance de cette bibliothèque est qu'elle peut expliquer soigneusement les principes d'implémentation les plus originaux des coroutines C/C++ d'un point de vue pédagogique, et qu'elle peut également être utilisée comme bibliothèque de serveur de coroutines utilisable.

Les lecteurs sont invités à critiquer cette bibliothèque, et les lecteurs sont également invités à proposer de nouvelles exigences - par exemple, j'ai décidé d'ajouter quelques exigences, qui sont considérées comme TODO :

  1. ImplémentationHTTPServer, en tant que sous-classe de TCPServer, fournit le service HTTP fcgi

  2. implémente la classe SSLClient pour gérer les demandes SSL externes.

Articles connexes :

Série d'articles sur la programmation réseau C# (8) UdpClient implémente un serveur UDP synchronisé

Implémentation d'un serveur php en langage C

Vidéos associées :

Tutoriel C#

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!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal