Webingenieure verwenden TCP / IP, unabhängig davon, ob sie sich dessen bewusst sind oder nicht. Es ist eines der unverzichtbaren Kommunikationsprotokolle für das Internet.
In den letzten Jahren, mit der Verbreitung von IoT usw., ist es zu einem unverzichtbaren Wissen in anderen Bereichen als der herkömmlichen Webtechnologie geworden.
Daher ist es zum De-facto-Standard für Netzwerk-APIs geworden. Ich möchte das Netzwerk anhand der BSD-Socket-Schnittstelle erneut untersuchen.
Es gibt viele Bücher und Dokumente über den Mechanismus des Netzwerks, insbesondere über TCP / IP, aber bis ich den Quellcode der C-Sprache mithilfe der Socket-API gelesen habe, konnte ich das Bild unabhängig von der Erklärung nicht verstehen, und der Mechanismus war nicht so sehr. Ich konnte es nicht verstehen, daher werde ich im Grunde dem Fluss des Quellcodes der C-Sprache folgen und die Operation in Teilen basierend auf den verwendeten Daten und der Verarbeitung lernen.
Natürlich denke ich nicht, dass es perfekt ist, also würde ich es begrüßen, wenn Sie auf Fehler hinweisen könnten.
Betrachten wir zunächst den Kommunikationsmechanismus zwischen verschiedenen Hosts über ein TCP-Kommunikationsprogramm unter Verwendung der bekannten verbindungsorientierten Socket-API.
Der Quellcode, den ich zum ersten Mal geschrieben habe, ist ein TCP-Serverprogramm.
Dies ist ein erschöpftes Legacy-Beispiel nur für IPv4. Beachten Sie jedoch, dass es besser ist, einfach zu verstehen, wie es funktioniert. Schließlich werden Beispiele mit der Struktur sockaddr_in6 angezeigt, die IPv6 unterstützt, sowie Beispiele mit APIs wie getaddrinfo, die nicht von der Adressfamilie abhängen.
Ein Beispiel für die Verwendung von getaddrinfo wurde in Einführung in die in C erlernte moderne Socket-API geschrieben.
Grob gesagt handelt es sich um CentOs 6.8 von x86_64. Der Compiler ist gcc ohne Optionen angegeben. Ich denke, es gibt einige Unterschiede in Abhängigkeit von CPU, Betriebssystem und Compiler, aber die Grundlagen sollten auf die gleiche Weise funktionieren.
tcpd.c
#include <stdio.h> //printf(), fprintf(), perror()
#include <sys/socket.h> //socket(), bind(), accept(), listen()
#include <arpa/inet.h> // struct sockaddr_in, struct sockaddr, inet_ntoa()
#include <stdlib.h> //atoi(), exit(), EXIT_FAILURE, EXIT_SUCCESS
#include <string.h> //memset()
#include <unistd.h> //close()
#define QUEUELIMIT 5
int main(int argc, char* argv[]) {
int servSock; //server socket descriptor
int clitSock; //client socket descriptor
struct sockaddr_in servSockAddr; //server internet socket address
struct sockaddr_in clitSockAddr; //client internet socket address
unsigned short servPort; //server port number
unsigned int clitLen; // client internet socket address length
if ( argc != 2) {
fprintf(stderr, "argument count mismatch error.\n");
exit(EXIT_FAILURE);
}
if ((servPort = (unsigned short) atoi(argv[1])) == 0) {
fprintf(stderr, "invalid port number.\n");
exit(EXIT_FAILURE);
}
if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 ){
perror("socket() failed.");
exit(EXIT_FAILURE);
}
memset(&servSockAddr, 0, sizeof(servSockAddr));
servSockAddr.sin_family = AF_INET;
servSockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servSockAddr.sin_port = htons(servPort);
if (bind(servSock, (struct sockaddr *) &servSockAddr, sizeof(servSockAddr) ) < 0 ) {
perror("bind() failed.");
exit(EXIT_FAILURE);
}
if (listen(servSock, QUEUELIMIT) < 0) {
perror("listen() failed.");
exit(EXIT_FAILURE);
}
while(1) {
clitLen = sizeof(clitSockAddr);
if ((clitSock = accept(servSock, (struct sockaddr *) &clitSockAddr, &clitLen)) < 0) {
perror("accept() failed.");
exit(EXIT_FAILURE);
}
printf("connected from %s.\n", inet_ntoa(clitSockAddr.sin_addr));
close(clitSock);
}
return EXIT_SUCCESS;
}
Laden der erforderlichen Header. Der Kommentar rechts beschreibt, was zur Verwendung gelesen wird. Einige von ihnen können von dort aufgenommen und verwendet werden, aber ich werde sie weglassen.
Ordnen Sie einen Speicherbereich für den gewünschten Datentyp zu. Wie üblich ist in der Sprache C die Größe des anderen Typs als des Zeichentyps undefiniert, sodass sie je nach Umgebung unterschiedlich sein kann. Int beträgt jedoch 4 Byte und short 2 Byte. Darüber hinaus beträgt das Byte im Allgemeinen 8 Bit. Da es sich jedoch nicht um eine strenge Definition handelt, wird es bei Bedarf als Oktett ausgedrückt, das eine Datendarstellung in der Kommunikation darstellt.
Argumentprüfung. Die dem Socket zugeordnete Portnummer kann zum Zeitpunkt der Ausführung durch eine beliebige Nummer angegeben werden.
Die Portnummer, bei der es sich um eine Zeichenfolgendarstellung handelt, wird von der Funktion atoi in eine Ganzzahldarstellung konvertiert. Atoi gibt 0 zurück, wenn die Ganzzahlkonvertierung fehlschlägt. In diesem Fall scheint es einfacher zu sein, zu verwenden, wenn beurteilt wird, dass der Dienstname durch die Zeichenfolge und die Verzweigungen angegeben wird. Diesmal akzeptieren wir jedoch nur Zahlen.
Die Socket-API ist endlich da.
socket () ist ein Systemaufruf, der das Betriebssystem auffordert, einen Socket zu erstellen. Da die Protokollfamilie im ersten Argument angegeben wird, geben Sie PF_INET an. Dies bedeutet, dass die TCP / IP-Protokollfamilie verwendet wird.
Gegenwärtig hat AF_INET, das später beschrieben wird, dieselbe Bedeutung, aber PF_INET wird in Bezug auf das Socket-API-Entwurfskonzept verwendet. (Ehrlich gesagt, ich kenne das Bild der Welt nicht, in der PF_INET benötigt werden sollte, daher wäre es hilfreich, wenn Sie mir sagen könnten, ob Sie verstehen, welche Art von Protokollimplementierung sein soll.)
Das zweite Argument gibt den Socket-Typ an. Dieses Mal verwenden wir TCP, ein Stream-Protokoll, das eine äußerst zuverlässige Kommunikation garantiert. Geben Sie daher SOCK_STREAM an.
Geben Sie für das dritte Argument IPPROTO_TCP an, dh TCP, das zu verwendende Protokoll. Wenn 0 angegeben wird, wird dies automatisch aus der zu verwendenden Protokollfamilie und dem Socket-Typ bestimmt. Es sollte jedoch explizit angegeben werden, falls die Anzahl der Protokolle in Zukunft zunimmt.
In meiner Umgebung ist das, was für jedes angegeben werden kann, in "/ usr / include / bits / socket.h" oder "/ usr / include / netinet / in.h" geschrieben.
Initialisierung der sockaddr_in-Struktur auf der Serverseite.
Die Struktur sockaddr_in ist ein Datentyp, mit dem Adressen wie IP-Adressen und Portnummern mit TCP / IP-Sockets verknüpft werden.
Die sockaddr_in-Struktur in meiner Umgebung sieht folgendermaßen aus:
python
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
python
__SOCKADDR_COMMON (sin_);
Mit dem Präprozessor in meiner Umgebung
python
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
Da es durch ersetzt wird, wird es zu sa_family_t sin_family.
Da wir nicht wissen, welcher Wert sich im zugewiesenen Speicherbereich befindet, werden wir memset verwenden, um ihn auf Null zu löschen und dann den erforderlichen Wert in jedem Feld zu speichern.
Geben Sie für sin_family AF_INET an, was darauf hinweist, dass es sich um eine Internetadressfamilie (IPv4) handelt.
Geben Sie für sin_addr die Struktur in_addr an.
Die in_addr-Struktur sieht in meiner Umgebung folgendermaßen aus:
python
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
Verwenden Sie INADDR_ANY für das Feld s_addr. Auf diese Weise können Verbindungen zu allen IP-Adressen abhängig von der Portnummer akzeptiert werden, auch wenn der Server-Netzwerkkarte mehrere IP-Adressen zugewiesen sind.
Da x_86 eine Little Endian-Architektur ist, erfordert das Senden von Multibyte-Daten an einen anderen Host die Konvertierung in die Bytereihenfolge in den Netzwerkstandard Big Endian. Verwenden Sie Funktionen wie htonl und htons, um in die Bytereihenfolge des Netzwerks zu konvertieren.
Endian ist besonders wichtig für die Netzwerkprogrammierung, aber vorerst diesmal [Wikipedia-Link](https://ja.wikipedia.org/wiki/%E3%82%A8%E3%83%B3% E3% 83% 87% E3% 82% A3% E3% 82% A2% E3% 83% B3) Ich werde mehr darüber schreiben, wenn ich anfange, das Bitfeld der Struktur zu verwenden.
Übrigens scheint INADDR_ANY in meiner Umgebung 0x00000000 zu bedeuten, daher scheint es nicht von der Konvertierung betroffen zu sein, aber ich verarbeite die htonl-Funktion, um die Codierung konsistent zu machen. Soweit dies angezeigt wird, ist es vorerst besser, dies anzugeben. Sieht gut aus.
htonl steht für Host to Network Long und konvertiert eine 4-Byte-Ganzzahl in Ihrer Umgebung in die Netzwerkbyte-Reihenfolge. Ebenso wird die Portnummer von der Funktion htons verarbeitet. htons ist eine Abkürzung für Host to Network Short, die eine 2-Byte-Ganzzahl in Ihrer Umgebung in die Netzwerkbyte-Reihenfolge konvertiert.
Die IP-Adresse und die Portnummer sind mit dem Socket verknüpft, der durch den Systemaufruf bind () erstellt wurde. Der Socket kann keine Nachrichten von Remote-Hosts akzeptieren, es sei denn, das Protokoll, die IP-Adresse und die Portnummer sind zugeordnet.
Da das zu verwendende Protokoll beim Erstellen des Sockets zugeordnet wird, übergeben Sie die zuvor erstellte Struktur sockaddr_in und ihre Länge an die Bindefunktion, um die IP-Adresse und die Portnummer dem Socket zuzuordnen.
Hierbei ist zu beachten, dass die Struktur sockaddr_in auf den Zeiger der Struktur namens sockaddr umgewandelt wird.
Die sockaddr-Struktur sieht in meiner Umgebung folgendermaßen aus:
python
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
};
Der Teil von __SOCKADDR_COMMON
wird durch den Präprozessor ersetzt, sodass er zu sa_family_t sa_family
wird.
Die verbleibenden Bits werden als Array vom Typ char deklariert, sodass Sie möglicherweise denken, dass etwas nicht stimmt. Kurz gesagt, die sockaddr-Struktur besteht aus Adressfamilienfeldern und einem Speicherbereich, der beliebige 14 Bits enthalten kann.
Während sockaddr ein universeller Datentyp für Socket-APIs ist, wird sockaddr_in als auf TCP / IP spezialisierter Datentyp positioniert.
Infolgedessen muss jede Socket-API nur den Zeiger der sockaddr-Struktur als Argument akzeptieren, und durch Untersuchen der sa_family kann die Datenfeldstruktur der Struktur bekannt sein, so dass die Verarbeitung entsprechend verzweigt werden kann. Ich werde. Man kann sagen, dass sich die Implementierung der Vielseitigkeit bewusst ist.
Ich persönlich denke, dass dies eines der konkreten Beispiele für das Verfahren zum Datenaustausch und das Protokoll ist, das den Inhalt der Daten aushandelt.
Wenn die Bindung fehlschlägt, wird wahrscheinlich eine entsprechende Meldung von perror angezeigt. Es empfiehlt sich jedoch zu überprüfen, ob der Port bereits einem anderen Socket zugeordnet ist.
Wenn es wiederholt ausgeführt und gestoppt wird, kann es erforderlich sein, eine Weile zu warten, um den Time-Wait-Status des Sockets zu berücksichtigen, nachdem die TCP-Verbindung getrennt wurde. Verwenden wir "netstat".
Der Systemaufruf listen () wird aufgerufen und der Verbindungsstatus vom Client wird zum ersten Mal akzeptiert. Alle Verbindungsanfragen von Clients, die früher eintreffen, werden abgelehnt. Wenn das ausführbare Programm beispielsweise a.out in der Befehlszeile ist,
./a.out 8080
Wenn Sie versuchen, die Ausführung auszuführen, werden Verbindungen von Clients durch Abhören akzeptiert. Wenn Sie also einen Befehl wie netstat -tln auf einem anderen Terminal ausführen
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN
Sie können sehen, dass es herauskommt.
Dies bedeutet, dass dieses Serverprogramm bereit ist, Verbindungsanforderungen von Clients anzunehmen.
Wenn in diesem Status eine Verbindungsanforderung (SYN) vom Client vorliegt, erstellt der Server eine neue Socket-Struktur, in der die Informationen vom Client basierend auf dem Paket gespeichert werden.
Basierend darauf führt das TCP-Modul auf der Serverseite einen 3-Wege-Handshake durch. Wenn der Handshake erfolgreich ist, befindet sich die neue Struktur im Status ESTABLISHED und wird in der Listenstruktur in die Warteschlange gestellt, bis sie von accept aufgerufen wird.
Geben Sie beim Ausführen des Serverprogramms als Test die Portnummer im Webbrowser des Clients als 8080 an, wiederholen Sie den Zugriff und führen Sie auf der Serverseite einen Befehl wie "netstat -tnc" aus, um den Fortschritt dieses Status zu überprüfen. Ich werde.
Abgesehen davon war ich zunächst wütend, dass ich nicht über den Browser auf die verdächtige Portnummer zugreifen würde, wenn ich es mit einer geeigneten Portnummer versuchen würde. Es scheint also, dass der Anbieter jedes Browsers die Portnummernverbindung des Remote-Hosts einschränkt. Natürlich muss der entsprechende Port auf der Serverseite geöffnet werden.
Um tatsächliche Daten an und von dem Client zu senden und zu empfangen, nimmt accept die ESTABLISHED-Socket-Struktur aus der Warteschlange, weist ihr einen Deskriptor zu und gibt sie an den Benutzerprozess zurück.
Der Socket-Deskriptor hat denselben ganzzahligen Wert wie der Dateideskriptor und entspricht dem Index des Arrays, das den Zeiger des Objekts enthält. Dieses Array ist ein Zeigerarray, das für im Kernel verwendete Eingabe- / Ausgabeobjekte verwendet wird, und wird verwendet, um eine abstrakte Schnittstelle für die Eingabe / Ausgabe zu verwenden. Ich habe darüber in Hacking Linux File Descriptors geschrieben. Wenn Sie also interessiert sind, lesen Sie bitte auch hier.
Ursprünglich möchte ich dies zum Senden und Empfangen von Daten zum und vom Client verwenden, aber dieses Mal zerstöre ich sie, sobald der Socket verfügbar wird.
Übrigens speichern die Variablen clitSockAddr und clitLen die Client-Informationen. Dieses Mal verwenden wir sie, um die IP-Adresse des Clients anzuzeigen. inet_ntoa generiert aus der in_addr-Struktur eine Punkt-Dezimalzeichenfolge und gibt die Startadresse ihres Speicherbereichs zurück.
Angenommen, die Serverdomäne ist hogehoge.com, wird die Verbindung geschlossen, sobald Sie mit einem Browser auf "http: //hogehoge.com: 8080" zugreifen. Chrome zeigt also nur ERR_EMPTY_RESPONSE an, jedoch auf dem Server Der Ausgang wird als verbunden von xxx.xxx.xxx.xxx.
.
Interessant war, dass in Chrome und Safari die obige Nachricht 3 Zeilen in einem Zugriff enthielt, in Firefox jedoch 13 Zeilen. Es kann interessant sein zu analysieren, welche Art von Austausch in einer Anfrage mit tcpdump erfolgt.
Dieses Mal habe ich einen Webbrowser als TCP-Client-Software verwendet, aber es ist einfacher, verschiedene Dinge zu überprüfen, wenn ich die Client-Software auch selbst erstellt habe, also 2nd Ich möchte TCP-Client-Software machen.
Recommended Posts