Heim  >  Artikel  >  Backend-Entwicklung  >  Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

php是最好的语言
php是最好的语言Original
2018-08-02 16:09:002568Durchsuche

Dieser Artikel ist eine Implementierung der C/C++-Coroutine. Wir müssen diese beiden Ziele erreichen:

  1. Eine sequentielle Vorstellung von synchroner Serverprogrammierung haben, um Funktionsdesign und Code-Debugging zu erleichtern – ich habe den Coroutine-Teil in libco verwendet

  2. hat asynchrone E/A-Leistung – ich habe Ereignis-E/A in Libevent Apache PHP MySQL verwendet

Strukturell handelt es sich um die Funktionen von libco und libevent sind kombiniert, daher habe ich mein Projekt libcoevent genannt, was „synchrones Coroutine-Server-Programmierframework basierend auf Libevent“ bedeutet. Das Wort co im Namen bedeutet nicht libco, sondern Coroutine.

Was die Programmiersprache betrifft, habe ich mich für C++ entschieden, hauptsächlich weil libco nur Linux basierend auf x86- oder x64-Architektur unterstützt und es sich bei solchen Architekturen grundsätzlich um PCs mit ausreichenden Ressourcen und hoher Leistung handelt Kein Problem, C++ zu lernen. In diesem Artikel wird erläutert, wie der Code implementiert wird.

Wenn Sie dieses Projekt verwenden möchten, fügen Sie bitte -lco -levent -lcoevent drei Optionen zu den Linkoptionen hinzu.

Klassenbeziehung und Grundfunktionen

Klassenbeziehung

Klassenvererbungsbeziehung

Das grundlegende Vererbungsbeziehungsdiagramm der Klasse lautet wie folgt:

Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

Bei tatsächlichen Aufrufen werden nur die Klassen auf den Blattknoten des Vererbungsbeziehungsbaums tatsächlich verwendet, und andere Klassen werden als virtuelle Klassen betrachtet.

Klassenzugehörigkeit

Instanzen verschiedener Typen haben während der Programmausführung Zugehörigkeiten. Zusätzlich zur Basisklasse der obersten Ebene müssen andere Blattklassen an andere Klassen angehängt werden die Betriebsumgebung. Das Abhängigkeitsdiagramm sieht wie folgt aus: Die Klasse

Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

  • Base stellt die grundlegendste Betriebsumgebung bereit und verwaltet Server Objekt;

  • Prozedur Objektverwaltung Client Objekt. Dies spiegelt sich in der Abbildung wider, da die Objekte Server und Sitzung beide Client-Objekte verwalten. Das

    • Server-Objekt wird von der Anwendung erstellt und für die Ausführung im Base-Objekt initialisiert. Ein Server-Objekt kann so konfiguriert werden, dass es automatisch zerstört wird, wenn der Server beendet wird oder wenn sein abhängiges Basis-Objekt zerstört wird. Das Objekt

    • Session wird automatisch vom Objekt Server im Sitzungsmodus erstellt und durch Aufrufen des von der Anwendung angegebenen Programmeintrags ausgeführt ; Das returnServer-Objekt wird automatisch zerstört, wenn die Sitzung endet (Funktionsaufruf ) oder wenn sein untergeordneter Server-Objektdienst endet. Das

  • Client-Objekt wird von der Anwendung erstellt, die die Schnittstelle des Procedure-Objekts für die Interaktion mit Drittanbieterdiensten aufruft. Die Anwendung kann die Schnittstelle im Voraus aufrufen, um die Zerstörung des Client-Objekts anzufordern, oder sie kann es automatisch zerstören, wenn der Prozedur-Dienst endet.

Basis- und Ereignisklassen

Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

Basis-Klasse wird zum Ausführen verschiedener Dienste von libcoevent verwendet. Jede Instanz der Base-Klasse sollte einem Thread entsprechen und alle Dienste sollten in der Base-Instanz auf Coroutine-Weise ausgeführt werden. Wie aus der obigen Abbildung ersichtlich ist, enthält die Klasse Base ein event_base-Objekt der Libevent-Bibliothek und eine Reihe von Event-Objekten dieser Coroutine-Bibliothek. Die Klasse

Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

Event entlehnt tatsächlich den Namen struct event von libevent, da jede Instanz der Klasse Event A entspricht event Objekt von libevent. Die wichtigsten Punkte, auf die wir uns konzentrieren müssen, sind die Klassen Procedure und Client.

Prozedurklasse

Prozedur-Klasse hat zwei Hauptmerkmale:

  1. Jedes Objekt hat eine Libco-Coroutine, das heißt, es verfügt über eigene unabhängige Kontextinformationen und kann zum Schreiben eines unabhängigen Serverprozesses (Prozedur) verwendet werden;

  2. Prozedurunterklassen können Client-Objekte und Serverkommunikation mit Dritten erstellen Interaktion. Die Klasse

Procedure hat zwei Unterklassen, nämlich Server und Session.

Serverklasse

Die Serverklasse wird von der Anwendung erstellt und initialisiert, um im Basisobjekt ausgeführt zu werden. Die Server-Klasse hat drei Unterklassen:

  • SubRoutine: Es dient eigentlich nicht als Serverprogramm, bietet aber die grundlegendste sleep() Funktion und unterstützt die Funktion der Erstellung von Client-Objekten der Procedure-Klasse, also Die Anwendung kann als temporär erstelltes oder residentes internes Programm verwendet werden.

  • UDPServer: Nachdem die Anwendung das UDPServer-Objekt erstellt und initialisiert hat, bindet das Programm automatisch an eine Datagramm-Socket-Schnittstelle. Anwendungen können Netzwerkdienste implementieren, indem sie Datenpakete über die Netzwerkschnittstelle senden und empfangen. UDPServer bietet sowohl den Normalmodus als auch den Sitzungsmodus.

  • TCPServer: Nachdem die Anwendung das TCPPServer-Objekt erstellt und initialisiert hat, bindet das Programm automatisch den Stream-Socket und lauscht darauf. TCPServer unterstützt nur den Sitzungsmodus.

Der sogenannte „Normalmodus“ ist das Verhalten, bei dem die Anwendung die Eingabefunktion des Serverobjekts registriert und die Anwendung das Serverobjekt betreibt.

Der sogenannte „Sitzungsmodus“ bezieht sich auf das UDPServer- oder TCPServer-Objekt. Nach dem Empfang eingehender Daten erkennt es automatisch den Client und erstellt ein separates Sitzungsobjekt verarbeitet wird. Jedes Sitzungsobjekt bedient nur einen Client.

Sitzungsklasse

Sitzungsobjekte können nicht aktiv von der Anwendung erstellt werden, sondern werden bei Bedarf automatisch von der Serverklasse im Sitzungsmodus erstellt. Das Merkmal des Session-Objekts besteht darin, dass es nur mit einem einzigen Client kommunizieren kann (im Vergleich zum UDPServer-Objekt), daher gibt es keine send()-Funktion, sondern nur reply() .

Die coevent.hSession-Klasse und ihre in der Header-Datei deklarierten Unterklassen sind alle reine virtuelle Klassen. Der Zweck besteht darin, Anwendungen daran zu hindern, explizit Session-Objekte zu erstellen Implementierungsdetails ausblenden.

Client-Klasse

Client-Objekte werden von Procedure-Objekten erstellt und von Procedure-Objekten recycelt. Die Rolle des Client-Objekts besteht darin, aktiv die Kommunikation mit dem Remote-Server zu initiieren. Da diese Aktion aus Sicht der Client-Service-Struktur zum Client gehört, wird sie Client genannt.

DNSClient

Client Die speziellste Unterklasse ist die DNSClient-Klasse. Diese Klasse existiert, um das getaddrinfo()-Blockierungsproblem bei asynchroner E/A zu lösen. Informationen zum Implementierungsprinzip von DNSClient finden Sie im Code und in meinem vorherigen Artikel „DNS-Nachrichtenstruktur und persönliche DNS-Analysecode-Implementierung“.

Was die DNSClient-Klasse betrifft, besteht das spezifische Implementierungsprinzip darin, ein UDPClient-Objekt zu kapseln, dieses Objekt zum Abschließen des Sendens und Empfangens von DNS-Nachrichten zu verwenden und das Parsen der Nachrichten in der Klasse zu implementieren.

UDPServer – Coroutine-Implementierung basierend auf Libevent

UDPServer Das Prinzip des Normalmodus ist ein sehr typisches synchrones Coroutine-Server-Framework basierend auf Libevent. In seiner Code-Implementierung sind die Kernfunktionen die folgenden Funktionen:

  • _libco_routine(), die Eingabefunktion der Coroutine. Mithilfe dieser Funktion wird sie in die einheitliche Service-Eingabefunktion von umgewandelt liboevent

  • _libevent_callback(), libevent Zeitrückruffunktion, in dieser Funktion wird der Coroutine-Kontext wiederhergestellt.

  • UDPServer::recv_in_timeval(), Datenempfangsfunktion, in dieser Funktion wird die Schlüsseldaten-Wartefunktion implementiert und das Speichern des Coroutine-Kontexts wird ebenfalls realisiert

Die Gesamtmenge des Codes für die oben genannten drei Funktionen, einschließlich der Leerzeilen, überschreitet meiner Meinung nach immer noch nicht die 200 Zeilen. Das Implementierungsprinzip wird im Folgenden ausführlich erläutert:

libco-Coroutine-Schnittstelle

Wie bereits erwähnt, verwende ich libco als Coroutine-Bibliothek. Coroutinen sind für die Anwendung transparent, für die Bibliotheksimplementierung ist dies jedoch der Kern.

Im Folgenden werden mehrere Schnittstellen erläutert, die von der Coroutine-Funktion von libco bereitgestellt werden (die Anzahl der Dokumente von libco ist einfach „ergreifend“, was im Internet oft beklagt wird...):

Erstellung und Zerstörung von Coroutine

Libco verwendet die Struktur struct stCoRoutine_t *, um die Coroutine zu speichern. Sie können das Coroutine-Objekt erstellen, indem Sie co_create() aufrufen; verwenden Sie co_release(), um die Coroutine-Ressource zu zerstören.

Geben Sie die Coroutine ein

Nachdem Sie die Coroutine erstellt haben, rufen Sie co_resume() auf, um die Ausführung der Coroutine vom Anfang der Coroutine-Funktion an zu starten.

Coroutine anhalten

Wenn die Coroutine die CPU-Nutzungsrechte übergeben muss, können Sie co_yield() aufrufen, um die Coroutine freizugeben und den Kontext zu wechseln. Nach dem Aufruf wird der Kontext auf die letzte Coroutine zurückgesetzt, die co_resume() aufgerufen hat. Der Ort, an dem co_yield() aufgerufen wird, kann als „Haltepunkt“ betrachtet werden.

Wiederherstellen der Coroutine

Die zum Wiederherstellen der Coroutine und zum Erstellen der Coroutine verwendeten Funktionen sind beide co_resume(). Rufen Sie diese Funktion auf, um den aktuellen Stapel auf den Kontext der angegebenen Coroutine umzustellen beginnt am oben erwähnten „Haltepunkt“ und setzt die Ausführung fort.

Coroutine-Scheduling-Implementierung

Wie Sie im vorherigen Abschnitt sehen können, enthält die von uns verwendete libco-Coroutine-Funktion eine Coroutine-Umschaltfunktion, aber wann umgeschaltet werden muss und was mit der CPU nach dem Umschalten passiert Verteilung, das ist was Wir müssen implementieren und kapseln.

Der Zeitpunkt zum Erstellen und Zerstören von Coroutinen ist natürlich, wenn die Klasse UDPServer initialisiert und zerstört wird. Im Folgenden liegt der Schwerpunkt auf der Analyse der Vorgänge zum Betreten, Anhalten und Fortsetzen der Coroutine:

Betreten der Coroutine

Der Code zum Betreten/Fortsetzen der Coroutine befindet sich in _libevent_callback(), mit dieser Zeile:

// handle control to user application
co_resume(arg->coroutine);

Wenn die aktuelle Coroutine nicht ausgeführt wurde, wechselt das Programm nach der Ausführung dieses Codes zu der beim Erstellen der libco-Coroutine angegebenen Coroutine-Funktion und startet die Ausführung. Für UDPServer ist das die Funktion _libco_routine(). Diese Funktion ist sehr einfach und besteht aus nur drei Zeilen:

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;
}

Durch die Übergabe von Parametern wird die libco-Rückruffunktion in eine von der Anwendung angegebene Serverfunktion zur Ausführung umgewandelt.

Aber wie implementiert man den ersten Libevent-Rückruf? Dies ist immer noch sehr einfach. Setzen Sie einfach das Timeout auf 0, wenn Sie event_add() von libevent aufrufen, wodurch das libevent-Ereignis sofort abläuft. Durch diesen Mechanismus erreichen wir auch den Zweck, jede Procedure-Dienstfunktion unmittelbar nach der Ausführung von Base auszuführen.

Die Coroutine anhalten und fortsetzen

Der Zeitpunkt des Aufrufs von co_yield steht im Mittelpunkt dieser Coroutine-Implementierung. Der Ort, an dem co_yield aufgerufen wird, ist ein Ort, der einen Kontextwechsel verursachen kann Es sind auch wichtige technische Punkte bei der Umwandlung eines asynchronen Programmierframeworks in ein synchrones Framework. Sie können sich hier auf die -Funktion von UDPServerrecv_in_timeval() beziehen. Die Grundlogik der Funktion ist wie folgt:

Assemblybasierte Implementierung von C/C++-Coroutinen (für Server)

Der wichtigste Zweig ist die Beurteilung des Libevent-Ereignisflags und die wichtigste Logik sind event_add() und co_yield() Funktionsaufruf. Das Funktionsfragment lautet wie folgt:

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);

Hier verstehen wir die Funktion co_yield() als Haltepunkt. Wenn das Programm hier ausgeführt wird, wird das Recht zur Nutzung der CPU übergeben und das Programm kehrt zu zurück Der Aufrufpunkt von co_resume() In den Händen der Funktion der vorherigen Ebene. Wo genau ist diese „Funktion der oberen Ebene“? Tatsächlich handelt es sich um die zuvor erwähnte _libevent_callback()-Funktion.

Aus der Perspektive von _libevent_callback() kehrt das Programm von der Funktion co_resume() zurück und setzt die Ausführung fort. An diesem Punkt können wir Folgendes verstehen: Die Planung von Coroutinen erfolgt tatsächlich durch Ausleihen libevent. Hier möchten wir auf die obigen Sätze co_resume() achten:

// switch into the coroutine
if (arg->libevent_what_ptr) {
    *(arg->libevent_what_ptr) = (uint32_t)what;
}

Hier wird der Ereignisflagwert libevent an die Coroutine übergeben, was eine wichtige Grundlage für die Beurteilung des vorherigen Ereignisses darstellt. Wenn es soweit ist, ruft _libevent_callback() unten co_resume() auf und gibt die CPU-Nutzungsrechte an die Coroutine zurück.

Zerstöre die Coroutine

Zusätzlich zu ci_yield() bewirkt der Aufruf der Coroutine-Funktion return auch die Rückkehr von co_resume(), also müssen wir in _libevent_callback() auch bestimmen die Coroutine Ob der Prozess beendet ist. Wenn die Coroutine endet, sollten die zugehörigen Coroutine-Ressourcen zerstört werden. Sehen Sie sich den Code im Bedingungskörper if (is_coroutine_end(arg->coroutine)) {...} an.

Sitzungsmodus

Bei der Implementierung dieses Projekts wird ein Serverentwurfsmuster namens „Sitzungsmodus“ bereitgestellt. Der Sitzungsmodus bezieht sich auf das UDPServer- oder TCPServer-Objekt. Nach dem Empfang eingehender Daten erkennt es automatisch den Client und erstellt ein separates Session-Objekt zur Verarbeitung. Jedes Sitzungsobjekt bedient nur einen Client.

Für TCPServer ist es relativ einfach, die obige Funktion zu implementieren, da nach der Überwachung eines TCP-Sockets bei einer eingehenden Verbindung einfach accept() aufgerufen wird, um einen neuen Dateideskriptor zu erhalten , erstellen Sie einfach eine neue Unterklasse von Server für diesen Dateideskriptor – dies ist die Klasse TCPSession.

Aber UDPServer ist problematischer, da UDP dies nicht kann. Wir können die sogenannte Sitzung nur selbst durchführen.

UDPSession erreicht

Designziele

Wir müssen die folgenden Effekte der UDPSession-Klasse erreichen:

  • -Klasse Beim Aufruf der Funktion recv werden nur die vom entsprechenden Remote-Client gesendeten Daten empfangen. Die Klasse

  • ruft die Funktion send Funktion (die eigentliche Implementierung ist ), können Sie den Port von reply()UDPServer verwenden, um zu antworten

recv()

Im Projekt ist UDPSession eine abstrakte Klasse und die eigentliche Implementierung ist UDPItnlSession. Aber um genau zu sein, ist die Implementierung von UDPItnlSession eng abhängig von UDPServer. Für diesen Teil können Sie sich auf den -Schleifenkörpercode in der -Funktion von _session_mode_worker()UDPServerdo-while() beziehen. Die Programmidee ist wie folgt:

  • UDPServer verwaltet ein UDPSession-Wörterbuch mit der Kombination aus Remote-IP + Portname als Schlüssel.

  • Wenn die Daten eintreffen, stellen Sie fest, ob die Remote-IP- und Port-Kombination im Wörterbuch enthalten ist. Wenn dies der Fall ist, kopieren Sie die Daten in die entsprechende Sitzung session

Den Code zum Kopieren von Daten finden Sie in der Funktionsimplementierung der Klasse UDPItnlSessionforward_incoming_data().

reply()

Das Senden von Daten ist eigentlich sehr einfach, führen Sie es einfach direkt auf dem FD von UDPServersendto() aus.

quit

Für das Server-Objekt im Sitzungsmodus stellt der Code eine Funktion bereit, die von seiner Sitzung aufgerufen werden kann und erfordert, dass der Server beendet und Ressourcen zerstört: quit_session_mode_server() . Das Implementierungsprinzip besteht darin, ein EV_SIGNAL-Ereignis auf dem Server auszulösen. Bei gewöhnlichen E/A-Ereignissen sollte dies nicht auftreten und wir verwenden es hier als Exit-Signal. Wenn der Server dieses Signal erkennt, wird die Exit-Logik ausgelöst.

Anwendungsbeispiel

Der Beispielcode dieses Projekts ist in zwei Teile unterteilt: Server und Client. Der Server verwendet libcoevent, während der Client nur Python verwendet Ein einfaches Programm geschrieben. In diesem Artikel wird der Client-Teil des Codes nicht erläutert.

Der Code von Server bietet Anwendungsbeispiele für die drei Unterklassen der Server-Klasse. Unter Verwendung von Logik einschließlich Leerzeilen, Debugging-Anweisungen, Fehlerbeurteilungen usw. wurden ein Prozess und zwei Dienste in weniger als 300 Zeilen implementiert. Man muss sagen, dass die Logik immer noch sehr klar ist und viel Code eingespart wird.

SubRoutine

demonstriert eine einmalige lineare Netzwerklogik durch die Funktion _simple_test_routine(). Im Programm erstellt die Routine zunächst ein DNSClient-Objekt, fordert einen Domänennamen vom Standard-Domänennamenserver an und dann connect() Port 80 des Servers. Nach Erfolg kehren Sie direkt zurück.

Diese Funktion zeigt das Verwendungsszenario von SubRoutine und die Verwendung des Client-Objekts, insbesondere die einfache Verwendung von DNSClient. Die Eintragsfunktion von

UDPServer

UDPServer ist _udp_session_routine() und ihre Funktion besteht darin, Domänennamen-Abfragedienste für Clients bereitzustellen. Clients senden eine Zeichenfolge als abzufragenden Domänennamen, und dann gibt der Server die Abfrageergebnisse an den Client zurück, nachdem er sie über das Objekt DNSClient angefordert hat.

Diese Funktion demonstriert die (komplexere und vollständigere) Verwendung des UDPSession-Objekts und des DNSClient.

TCPServer

Die Eingabefunktion ist _tcp_session_routine(), die Logik ist relativ einfach, hauptsächlich um die Verwendung von TCPSession anzuzeigen.

Postscript

Grundsätzlich ist libcoevent entwickelt, hat die notwendigen Funktionen implementiert und kann zum Schreiben von Serverprogrammen verwendet werden. Da es sich natürlich um die erste Version handelt, sieht ein Großteil des Codes natürlich noch etwas chaotisch aus. Die Bedeutung dieser Bibliothek besteht darin, dass sie die originelleren Implementierungsprinzipien von C/C++-Coroutinen aus pädagogischer Sicht sorgfältig erklären kann und auch als verwendbare Coroutine-Serverbibliothek verwendet werden kann.

Leser sind herzlich eingeladen, diese Bibliothek zu kritisieren, und Leser können auch gerne neue Anforderungen vorschlagen – zum Beispiel habe ich beschlossen, ein paar Anforderungen hinzuzufügen, die als TODO gelten:

  1. ImplementierungHTTPServer stellt als Unterklasse von TCPServer den HTTP-fcgi-Dienst bereit;

  2. implementiert die Klasse SSLClient Behandeln Sie externe SSL-Anfragen.

Verwandte Artikel:

Artikelserie zur C#-Netzwerkprogrammierung (8) UdpClient implementiert synchronisierten UDP-Server

Implementierung eines PHP-Servers in der Sprache C

Ähnliche Videos:

C#-Tutorial

Das obige ist der detaillierte Inhalt vonAssemblybasierte Implementierung von C/C++-Coroutinen (für Server). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn