[LINUX] Eine Einführung in die moderne Socket-API zum Erlernen in C.

Als wir mit Einführung in die Socket-API in C-Sprache, Teil 1 Server Edition begannen, begannen wir mit einer auf IPv4 beschränkten Legacy-API und entwickelten sie schrittweise weiter. Ich hatte vor, dies zu tun, aber in Zukunft möchte ich über Socket-Programmierung mit einer modernen API sprechen, egal was ich tue.

Daher werden wir uns diesmal den Details nähern, indem wir das Echo-Server-Programm neu schreiben, das die vom Client empfangene Zeichenfolge mithilfe einer modernen API zurückgibt. Der Vorteil moderner APIs bei der Socket-Programmierung gegenüber älteren Beispielen liegt nicht in der Verarbeitungsgeschwindigkeit, sondern in der Flexibilität, verschiedene Fälle jetzt und in Zukunft zu behandeln.

Im alten Beispiel war es nur möglich, einen Socket zu erstellen, der nur IPv4 unterstützt. Im Beispiel mit der modernen API ist es jedoch flexibel, sowohl IPv4- als auch IPv6-Sockets vorzubereiten und die Verarbeitung gemäß dem empfangenen Datenpaket zu verzweigen. Sie können sexuelle Funktionen implementieren.

Darüber hinaus ist es möglich, ein Programm zu erstellen, das nicht von einer bestimmten Adressfamilie ** abhängt, anstatt auf IPv4 und IPv6 beschränkt zu sein. Die moderne API ist in diesem Sinne implementiert.

Insbesondere für diejenigen, die iOS-Ingenieur werden möchten, hat Apple bereits ** verbotener Code, der von IPv4 abhängt **. Aus diesem Grund ist es eine Denkweise und Fähigkeit, die auch in der Welt, in der IPv4 noch alltäglich ist, beherrscht werden muss.

Da die Erklärung dessen, was ich versucht habe, groß zu treffen, kompliziert ist, werde ich sie diesmal auf IPv4 beschränken, aber es ist möglich, das Programm, das IPv6-Paketen entspricht, mit einer kleinen Änderung zu ändern. Es wird etwas geschrieben, das sich zu einem späteren Zeitpunkt auf diesen Prozess konzentriert.

python


#include <sys/socket.h> //socket(), bind(), accept(), listen()
#include <stdlib.h> // exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <netdb.h> // getaddrinfo, getnameinfo, gai_strerror, NI_MAXHOST NI_MAXSERV
#include <string.h> //memset()
#include <unistd.h> //close()
#include <stdio.h> // IO Library

#define MAX_BUF_SIZE 1024

int get_socket(const char*);
void do_service(int);
static inline void do_concrete_service(int);
void echo_back(int);
static inline void usage(int);

int main (int argc, char *argv[]) {

    int sock;

    if (argc != 2) {
        usage(EXIT_FAILURE);
    }   

    if ((sock = get_socket(argv[1])) == -1){
        fprintf(stderr, "get_socket() failure.\n");
        exit(EXIT_FAILURE);
    }

    do_service(sock);

    close(sock);

    return EXIT_SUCCESS;
}

Die Hauptfunktion sieht wie oben aus. Die Angabe ist so, dass die Portnummer oder der Dienstname als Argument angegeben wird. Wenn dies jedoch nicht angegeben wird oder wenn ein zusätzliches Argument als Argument verwendet wird, wird die folgende Funktion namens Verwendung ausgeführt und beendet. ..

python


static inline void usage (int status){
    fputs ("\
argument count mismatch error.\n\
please input a service name or port number.\n\
", stderr);
    exit (status);
}

Wenn das Argument gültig ist, erstellen Sie einen Socket mit der diesmal definierten Funktion get_socket und rufen Sie den Socket-Deskriptor auf, um auf die Steuerinformationen zu verweisen.

Der Socket-Deskriptor ist genau der gleiche wie der Dateideskriptor. Ich habe darüber ausführlich in Hacken eines Linux-Dateideskriptors geschrieben. Wenn Sie also interessiert sind, lesen Sie dies bitte.

Nachdem Sie den Socket-Deskriptor erhalten haben, wird in der diesmal definierten Funktion do_service zusammengefasst, welche Art von Dienst tatsächlich als Server bereitgestellt wird.

Ich habe den Socket am Ende geschlossen, aber tatsächlich befindet sich do_service in einer Endlosschleife, sodass dieses Programm hier nicht ankommt.

Wenn Sie ein solches Programm in einen Daemon verwandeln, implementieren Sie es so, dass close zu einem geeigneten Zeitpunkt aufgerufen werden kann, indem Sie die Operation anpassen, wenn am Ende ein bestimmtes Signal empfangen wird. Wir werden auch diskutieren, wie der Daemon-Prozess separat implementiert wird.

Nachdem wir den Ablauf kennen, schauen wir uns die Verarbeitung der Funktion get_socket an. Diese Funktion ist für die Erstellung des Sockets und die Rückgabe des Socket-Deskriptors verantwortlich.

python


int get_socket (const char *port) {

    struct addrinfo hints, *res;
    int ecode, sock;
    char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

    memset(&hints, 0, sizeof(hints));

    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags    = AI_PASSIVE;

    if ((ecode = getaddrinfo(NULL, port, &hints, &res) != 0)) {
        fprintf(stderr, "failed getaddrinfo() %s\n", gai_strerror(ecode));
        goto failure_1;
    }

    if ((ecode = getnameinfo(res->ai_addr, res->ai_addrlen, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV)) != 0){
        fprintf(stderr, "failed getnameinfo() %s\n", gai_strerror(ecode));
        goto failure_2;

    }

    fprintf(stdout, "port is %s\n", sbuf);
    fprintf(stdout, "host is %s\n", hbuf);

    if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) {
        perror("socket() failed.");
        goto failure_2;
    }

    if (bind(sock, res->ai_addr, res->ai_addrlen) < 0) {
        perror("bind() failed.");
        goto failure_3;
    }

    if (listen(sock, SOMAXCONN) < 0) {
        perror("listen() failed.");
        goto failure_3;
    }

    return sock;

failure_3:
    close(sock);
failure_2:
    freeaddrinfo(res);
failure_1:
    return -1;
}

Es unterscheidet sich ein wenig von dem alten IPv4-Beispiel. Im Legacy-Beispiel mussten wir eine sockaddr_in-Adressstruktur definieren und die entsprechenden Werte angeben.

Die moderne Methode besteht darin, eine Addrinfo-Struktur zu übergeben, in der nur die Informationen gespeichert werden, die der Schlüssel zur Funktion getaddrinfo sind, und alle fehlenden Informationen basierend auf dem Hinweis zu speichern.

Ich denke, dass Sie die Atmosphäre anhand des alten Beispiels spüren können, aber die verschiedenen von der Socket-API angegebenen Daten hängen zusammen, und wenn eine Daten angegeben werden, werden die anderen erforderlichen Daten automatisch ermittelt. Weil es so ist, ist es ein Algorithmus, der seine Eigenschaften ausnutzt.

Die addrinfo-Struktur ist eine Struktur, in der die Informationen gespeichert werden, die erforderlich sind, damit die Socket-API auf die Adressinformationen verweist. Im Legacy-Beispiel enthält sie auch die direkt verwendete sockaddr-Struktur.

python


/* Structure to contain information about address of a service provider.  */
struct addrinfo
{
  int ai_flags;         /* Input flags.  */
  int ai_family;        /* Protocol family for socket.  */
  int ai_socktype;      /* Socket type.  */
  int ai_protocol;      /* Protocol for socket.  */
  socklen_t ai_addrlen;     /* Length of socket address.  */
  struct sockaddr *ai_addr; /* Socket address for socket.  */
  char *ai_canonname;       /* Canonical name for service location.  */
  struct addrinfo *ai_next; /* Pointer to next in list.  */
};

Ein Mitglied des Zeigers auf addrinfo namens ai_next wird in Zukunft eine wichtige Rolle spielen. Wenn ein Mitglied einer Struktur einen Zeiger auf die Struktur enthält, handelt es sich normalerweise um eine verkettete Listendatenstruktur. Wie in diesem Fall kann die versierte Person zu dem Schluss gekommen sein, dass diese Tatsache mit der familienunabhängigen Programmierung zusammenhängt.

Da nun die Informationen der Mitglieder dieser Addrinfo-Struktur als Argument an die Socket-API wie Binden und Socket übergeben werden können, wird eine einfache und flexible Implementierung möglich.

Geben Sie in der Funktion getaddrinfo im ersten Argument einen Zeiger auf den Hostnamen oder die IP-Adresszeichenfolge an. Geben Sie diesmal NULL an, der Grund wird jedoch später beschrieben. Beim Erstellen eines Client-Programms können Sie einen Zeiger auf ein Zeichenfolgenobjekt angeben, das auf einen Domänennamen wie "tajima-taso.jp" verweist.

Geben Sie im zweiten Argument einen Zeiger auf die Portnummer oder die Dienstnamenzeichenfolge an. Unter Linux wird der Dienstname über den NIS-Server oder aus der Datei / etc / services auf dem lokalen Host referenziert. In meiner Umgebung ist die Portnummer 8080 geöffnet, sodass Sie "./a.out 8080" angeben können. Wenn Sie jedoch / etc / services überprüfen, wird 8080 als Dienstname dem Webcache zugeordnet. Selbst wenn Sie "./a.out webcache" ausführen, wird daher dieselbe Spezifikation erhalten.

Wenn AI_PASSIVE im 3. Argument angegeben ist, entspricht die NULL-Angabe im 1. Argument INADDR_ANY (IN6ADDR_ANY_INIT im Fall von IPv6), das wie im Legacy-Beispiel Verbindungen von allen NICs empfangen kann. AI_PASSIVE wird später beschrieben.

Auch wenn es auf glibc beschränkt sein mag, ist es so implementiert, dass es, selbst wenn Sie die Zeichenfolge "\ *" übergeben, als NULL beurteilt wird. Selbst wenn Sie "\ *" angeben, funktioniert es auf die gleiche Weise. Obwohl dies im Quellcode implementiert ist, ist es möglicherweise nicht vielseitig, da es auf Dokumentationsebene nicht gefunden wurde.

Die AF_INET- und SOCK_STREAM-Spezifikationen sind dieselben wie im Legacy-Beispiel. Dies bedeutet IPv4-Adressensystem und Stream-Typ-Protokoll = TCP.

Ich denke, dass die Spezifikation von AI_PASSIVE oft in Kombination mit dem ersten Argument NULL verwendet wird. Was ist das? Geben Sie an, wenn Sie die addrinfo-Struktur verwenden, die in bind vervollständigt werden soll.

Da bind ein Systemaufruf ist, der verwendet wird, wenn ein Socket mit expliziten Adressinformationen verknüpft wird, fungiert AI_PASSIVE als Flag zum Anfordern der Funktion getaddrinfo, um die Addrinfo-Struktur für den Server zu vervollständigen. .. Ein Socket, der auf eine solche Verbindung wartet, wird als passiver Socket bezeichnet.

Übergeben Sie als viertes Argument einen Zeiger auf den Zeiger der addrinfo-Struktur. Von hier aus können Sie durch Dereferenzierung auf den Zeiger auf die fertige Addrinfo-Struktur verweisen. Da die Bibliothek zum Zuweisen von dynamischem Speicher intern zum Zuweisen des Speicherbereichs der Addrinfo-Struktur verwendet wird, tritt außerdem ein Speicherverlust auf, es sei denn, der Bereich wird von der Funktion freeaddrinfo freigegeben, wenn er nicht mehr benötigt wird. Ich werde am Ende.

Wenn ein Fehler auftritt und Sie die Ganzzahl als Rückgabewert an die Funktion gai_strerror übergeben, wird ein Zeiger auf die Zeichenfolge zurückgegeben, in der der entsprechende Fehlerinhalt gespeichert ist, sodass der Inhalt an die Standardfehlerausgabe ausgegeben wird.

Dann rufen wir die Funktion getnameinfo auf, die für den Socket-Kommunikationsprozess selbst nicht erforderlich ist. Diese Funktion wird verwendet, um auf den Hostnamen (IP-Adresse) und den Dienstnamen (Portnummer) aus der Adressstruktur zu verweisen.

Das erste Argument ist ein Zeiger auf die Socket-Adressstruktur, das zweite Argument ist die Größe des Objekts, das dritte Argument ist ein Zeiger auf den Puffer, in dem der Hostname gespeichert werden soll, das vierte Argument ist seine Größe und das fünfte Argument ist service. Geben Sie einen Zeiger auf den Puffer an, in dem der Name gespeichert ist, seine Größe im 6. Argument und verschiedene Flags im 7. Argument.

Die Größe von hbuf ist NI_MAXHOST und die Größe von sbuf ist NI_MAXSERV, und der Bereich ist gesichert. Es ist in .h definiert.

In meiner Umgebung

netdb.h


#  define NI_MAXHOST      1025
#  define NI_MAXSERV      32

Es war.

Das 7. Argument setzt die Flags von NI_NUMERICHOST und NI_NUMERICSERV, aber sie sind die Flags, um den Hostnamen im numerischen Format und den Dienstnamen (Portnummer) im numerischen Format im Puffer zu speichern.

Wenn Sie es als "./a.out 8080" ausführen,

port is 8080
host is 0.0.0.0

Wird angezeigt.

Führen Sie dann einen Socket-Systemaufruf durch, um den Socket zu erstellen. Im Legacy-Beispiel haben die drei als Argumente übergebenen Werte konstante Literale angegeben. Im modernen Beispiel werden die erforderlichen Werte in den Elementen der erfassten Addrinfo-Struktur gespeichert. Verwenden Sie sie daher. Erschaffen. Auf diese Weise können Sie Programme schreiben, die unabhängig von der Adressfamilie sind.

Übrigens, erstellen Sie als Socket-Verarbeitung eine Socket-Struktur, die intern Protokollinformationen angibt, speichern Sie den Zeiger auf das erstellte Dateiobjekt in dem Element, in dem der Zeiger auf das Dateiobjekt gespeichert ist, und fd_installieren Sie schließlich das Dateiobjekt. Es wird der Fluss des Tuns sein.

Wie im Legacy-Beispiel erläutert, ist bind ein Systemaufruf zum Verknüpfen von Sockets und Adressinformationen. Der Socket kann keine Nachrichten von Remote-Hosts akzeptieren, es sei denn, die Adressinformationen sind zusätzlich zum Protokoll verknüpft. Dies ist daher ein notwendiger Prozess. Es sollte kein Problem mit der IP-Adresse geben. Wenn jedoch eine Portnummer bereits verwendet wird, kann der Socket nicht verknüpft werden. Geben Sie daher unbedingt eine freie Portnummer oder einen Dienstnamen an.

Das Beispiel für den Aufruf von listen ist dasselbe wie das Legacy-Beispiel, außer dass eine Adressstruktur verwendet wird. Zur Überprüfung ist das TCP-Socket-Programm so konzipiert, dass für jede Client-Verbindung ein Socket erstellt wird, um eine Kommunikation zu realisieren, die eine Verbindung herstellt. Dieses Mal wird die maximale Warteschlangengröße mithilfe einer Makrokonstante namens SOMAXCONN angegeben, die in "bits / socket.h" definiert ist.

SOMAXCONN bedeutet die Anzahl der Warteschlangen, die der Kernel als Standardwert für das Abhören definiert. 128 in meiner Umgebung.

Ist es nicht möglich, dass einige Infrastrukturingenieure zu dem Schluss kommen, wenn sie diesen Wert sehen?

Im Fall eines Servers, auf dem eine Anwendung wie eine Datenbank mit vielen Verbindungsanforderungen ausgeführt wird, kann diese Nummer die Warteschlange überlaufen und die Verbindung trennen. Ich denke, dass es viele Fälle gibt, in denen dieser Wert dynamisch erhöht wird. ..

Nach der obigen Verarbeitung wird der Socket-Deskriptor, der auf die Verbindung wartet, an die Hauptfunktion zurückgegeben. Übergeben Sie dann den Socket-Deskriptor an die Funktion do_service, um den eigentlichen Service auszuführen.

python


void do_service (int sock) {

    char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
    struct sockaddr_storage from_sock_addr;
    int acc_sock;
    socklen_t addr_len;

    for (;;) {
        addr_len = sizeof(from_sock_addr);
        if ((acc_sock = accept(sock, (struct sockaddr *) &from_sock_addr, &addr_len)) == -1) {
            perror("accept() failed.");
            continue;
        } else {
            getnameinfo((struct sockaddr *) &from_sock_addr, addr_len, hbuf, sizeof(hbuf), sbuf, sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV);

            fprintf(stderr, "port is %s\n", sbuf);
            fprintf(stderr, "host is %s\n", hbuf);

            do_concrete_service(acc_sock);

            close(acc_sock);
        }
    }
}

In der Funktion do_service wird durch Ausführen des Systemaufrufs accept ein neuer Socket erstellt, um die eigentliche Kommunikationsverarbeitung mit dem Client durchzuführen, und der Socket-Deskriptor wird an die Funktion do_concrete_service übergeben, die für die spezifische Dienstverarbeitung verantwortlich ist.

Akzeptieren ist auch wie im Legacy-Beispiel erläutert. Der Socket-Deskriptor ist dem Mitglied zugeordnet, das das Dateiobjekt der Socket-Struktur speichert, die beim 3-Wege-Handshake (Funktion fd_install) erfolgreich war, und der Socket-Deskriptor wird zurückgegeben.

In diesem Fall werden die Adressinformationen des Clients in der Variablen from_sock_addr gespeichert, sodass getnameinfo basierend darauf ausgeführt wird, um die Adressinformationen des Clients anzuzeigen.

Im alten Beispiel wurde die Struktur sockaddr_in zum Speichern von Informationen vom Client verwendet, diese Struktur wurde jedoch nur zum Speichern von IPv4-Adressinformationen verwendet. Um eine Adressfamilie zu unterstützen, wechseln wir daher zu einer Methode zum Definieren von sockaddr_storage, die über genügend Speicherplatz verfügt, um Adressinformationen zu speichern und nach Bedarf umzuwandeln. ..

static inline void do_concrete_service (int sock) {
    echo_back(sock);
}

In der Funktion do_concrete_service wird die Funktion ausgeführt, die sich auf die Dienstverarbeitung konzentriert. Führen Sie in diesem Fall eine Funktion namens echo_back aus, die die Nachricht vom Client unverändert zurücksendet. Dieser Vorgang ist getrennt, so dass der Inhalt leicht geändert werden kann.

Übrigens, in meiner Umgebung hat sich gcc, als ich auf -O2 optimiert habe, inline erweitert.

python


void echo_back (int sock) {

    char buf[MAX_BUF_SIZE];
    ssize_t len;

    for (;;) {
        if ((len = recv(sock, buf, sizeof(buf), 0)) == -1) {
            perror("recv() failed.");
            break;
        } else if (len == 0) {
            fprintf(stderr, "connection closed by remote host.\n");
            break;
        }

        if (send(sock, buf, (size_t) len, 0) != len) {
            perror("send() failed.");
            break;
        }
    }
}

Der Sende- / Empfangsprozess ist ursprünglich eine sehr abstrakte Implementierung, daher entspricht er zu diesem Zeitpunkt fast dem alten Beispiel.

Diesmal ist der eigentliche Verarbeitungsinhalt fast derselbe wie im alten Beispiel, aber wir konnten ein Grundformular für die Programmierung erstellen, das nicht vom Protokoll oder der Adressfamilie abhängt.

Wenn ich danach ein Beispiel für eine Socket-API vorstelle, werde ich es mit dieser API erstellen.

** "Ich verstehe, wie man die Funktion getaddrinfo benutzt! Ich möchte nur wissen, wie man IPv4 oder IPv6 benutzt!" **

Für die Methode beschreibt RFC6555 die von Google Chrome und Mozilla Firefox verwendete Methode. Wenn Sie also interessiert sind, lesen Sie bitte die folgenden Informationen.

Happy Eyeballs: Success with Dual-Stack Hosts

In Bezug auf die Funktion getaddrinfo wurde vor einiger Zeit (derzeit) die Sicherheitsanfälligkeit bezüglich Stapelüberlauf entdeckt. Ist behoben), daher ist es wichtig, nicht nur bei getaddrinfo, sondern auch bei Bibliotheken wie verwendetem glibc und Software-Updates vorsichtig zu sein.

Referenzierter Quellcode

Linux-Kernel: 2.6.11 glibc:glibc-2.12.1 CPU:x86_64

OS CentOS release 6.8

Compiler

gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-17)

Recommended Posts

Eine Einführung in die moderne Socket-API zum Erlernen in C.
Einführung in die Socket-API in C Language 2nd Client Edition
Einführung in die Socket-API in C-Sprache 3. TCP-Server / Client Nr. 1
Einführung in die in C Language Part 1 Server Edition erlernte Socket-API
Einführung in die in C-Sprache gelernte Socket-API 4. UDP Server / Client Edition Nr. 1
Eine einfache Möglichkeit, die Amazon-Produkt-API in Python aufzurufen
Eine Einführung in Python für C-Sprachprogrammierer
Verwendung der C-Bibliothek in Python
So senden Sie automatisch E-Mails mit Anhängen mithilfe der Google Mail-API in Python
Erstellen Sie einen Filter, um ein Zugriffstoken mit der Graph-API (Flask) zu erhalten.
C-API in Python 3
Eine Einführung in das maschinelle Lernen
Eine Einführung in die Objektorientierung: Ändern Sie den internen Status eines Objekts
So manipulieren Sie das DOM im Iframe mit Selen
Ich habe versucht, die Zeit und die Zeit der C-Sprache zu veranschaulichen
Eine Einführung in die funktionale Programmierung zur Verbesserung der Debugging-Effizienz in 1 Minute
Eine Einführung in die Python-Programmierung
Einführung in die Bayes'sche Optimierung
So entfernen Sie die "Tags müssen ein Array von Hashes sein." Fehler in Qiita API
So geben Sie beim Testen absichtlich einen Fehler in der Shell aus
Wie Sie die interne Struktur eines Objekts in Python kennen
Erstellen Sie mit der AWS-API einen Alias für Route53 zu CloudFront
[Python] PCA-Scratch im Beispiel "Einführung in die multivariate Analysemethode"
[Einführung in Python] Ich habe die Namenskonventionen von C # und Python verglichen.
[Einführung in Python] Wie verwende ich den Operator in in der for-Anweisung?
Klicken Sie in Python auf die New Relic-API, um den Status des Servers abzurufen
Eine Einführung in Pandas, um zu lernen, während man leidet [Teil 1]
Programmierung, um in der Welt zu kämpfen ~ 5-5,5-6
Programmieren, um in der Welt zu kämpfen 5-3
Klicken Sie in Python auf die Sesami-API
Programmierung für den Kampf in der Welt - Kapitel 4
Im Python-Befehl zeigt Python auf Python3.8
Einführung in Protobuf-c (C-Sprache ⇔ Python)
Klicken Sie auf die Web-API in Python
So verpacken Sie C in Python
Probieren Sie Cython in kürzester Zeit aus
Greifen Sie mit Python auf die Twitter-API zu
Erste Schritte mit Python für Nicht-Ingenieure
Eine Alternative zu "Pause" in Python
Programmieren, um in der Welt zu kämpfen ~ 5-2
[Python Tutorial] Eine einfache Einführung in Python
Von der Einführung der GoogleCloudPlatform Natural Language API bis zur Verwendung
[C-Sprache] Ich möchte Zufallszahlen im angegebenen Bereich generieren
Codebeispiel zum Abrufen von oauth_token und oauth_token_secret der Twitter-API in Python 2.7
Ich möchte einen beliebigen Befehl im Befehlsverlauf von Shell belassen
[Einführung in Python] Eine ausführliche Erklärung der in Python verwendeten Zeichenkettentypen!
Ein Beispiel für die Antwort auf die Referenzfrage der Studiensitzung. Mit Python.
Ich möchte eine API erstellen, die ein Modell mit einer rekursiven Beziehung im Django REST Framework zurückgibt
Ich habe versucht zu erklären, wie der Artikelinhalt mit der MediaWiki-API auf leicht verständliche Weise anhand von Beispielen abgerufen werden kann (Python 3).
Lernen Sie das Entwurfsmuster "Prototype" mit Python
Lernen Sie das Entwurfsmuster "Builder" mit Python
[Einführung in Python] Wie verwende ich eine Klasse in Python?
Versuchen Sie es mit der Wunderlist-API in Python
Die Geschichte eines Fehlers in PyOCR
Lernen Sie das Designmuster "Flyweight" in Python
Versuchen Sie, die Kraken-API mit Python zu verwenden
Lernen Sie das Entwurfsmuster "Observer" in Python
Lernen Sie das Entwurfsmuster "Memento" mit Python