Detaillierte Erklärung der Socket (TCP)-Bindung aus dem Linux-Quellcode

Detaillierte Erklärung der Socket (TCP)-Bindung aus dem Linux-Quellcode

1. Ein einfachstes serverseitiges Beispiel

Wie wir alle wissen, erfordert die Einrichtung eines serverseitigen Sockets vier Schritte: Socket, Binden, Listen und Akzeptieren.

Der Code lautet wie folgt:

void start_server(){
    // Server-FD
    int sockfd_server;
    // FD akzeptieren 
    int sockfd;
    int call_err;
    Struktur sockaddr_in sock_addr;

    sockfd_server = socket(AF_INET,SOCK_STREAM,0);
    memset(&sock_addr,0,Größevon(sock_addr));
    sock_addr.sin_family = AF_INET;
    sock_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    sock_addr.sin_port = htons(SERVER_PORT);
    // Das ist unser heutiger Fokus, binden
    call_err = binden(sockfd_server, (Struktur sockaddr*)(&sock_addr), Größe von(sock_addr));
    wenn(call_err == -1){
        fprintf(stdout,"Bindungsfehler!\n");
        Ausgang (1);
    }
    // Hören
    call_err = abhören(sockfd_server,MAX_BACK_LOG);
    wenn(call_err == -1){
        fprintf(stdout,"Abhörfehler!\n");
        Ausgang (1);
    }
}

Zuerst erstellen wir einen Socket durch den Socket-Systemaufruf, in dem SOCK_STREAM angegeben ist und der letzte Parameter 0 ist, was bedeutet, dass ein normaler TCP-Socket eingerichtet wird. Hier geben wir direkt die dem TCP-Socket entsprechenden Operationen an, also die Operationsfunktion.

2. Systemaufruf binden

Bind weist einem Socket eine lokale Protokolladresse (Protokoll:IP:Port) zu. Beispielsweise eine 32-Bit-IPv4-Adresse oder eine 128-Bit-IPv6-Adresse + eine 16-Bit-TCP- oder UDP-Portnummer.

#include <sys/socket.h>
// Gibt 0 zurück, wenn erfolgreich, -1, wenn ein Fehler auftritt
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

Okay, gehen wir direkt zum Aufrufstapel des Linux-Quellcodes.

binden

// Der Rückgabewert des Systemaufrufs wird von glibcs ​​INLINE_SYSCALL umschlossen

// Wenn ein Fehler auftritt, setzen Sie den Rückgabewert auf -1 und den absoluten Wert des Systemaufruf-Rückgabewerts auf errno

|->INLINE_SYSCALL (binden......);

|->SYSCALL_DEFINE3(binden......);

/* Prüfen, ob der entsprechende Deskriptor fd existiert, wenn nicht, -BADF zurückgeben

|->sockfd_lookup_light

|->sock->ops->binden(inet_stream_ops)

|->inet_bind

|->AF_INET-Kompatibilitätsprüfung

|-><1024 Portberechtigungsprüfung

/* Überprüfung oder Auswahl der Bind-Portnummer (wenn Bind 0 ist)

|->sk->sk_prot->get_port(inet_csk_get_port)

2.1, inet_bind

Die Funktion inet_bind führt hauptsächlich zwei Vorgänge aus: Der eine besteht darin, festzustellen, ob Binden zulässig ist, und der andere darin, die verfügbare Portnummer abzurufen. Dies ist hier erwähnenswert. Wenn wir die zu bindende Portnummer auf 0 setzen, hilft uns der Kernel, zufällig eine verfügbare Portnummer für die Bindung auszuwählen!

// Das System wählt zufällig eine verfügbare Portnummer aus sock_addr.sin_port = 0;
call_err = binden(sockfd_server, (Struktur sockaddr*)(&sock_addr), Größe von(sock_addr));

Schauen wir uns den Prozess von inet_bind an

Es ist erwähnenswert, dass wir den Root-Benutzer verwenden oder der ausführbaren Datei die Berechtigung CAP_NET_BIND_SERVICE erteilen müssen, wenn auf Port 80 lauscht wird (z. B. beim Starten von nginx), da CAP_NET_BIND_SERVICE für Portnummern < 1024 erforderlich ist.

root verwenden

oder

setcap cap_net_bind_service=+eip ./nginx

Unser Bind ermöglicht die Bindung an die Adresse 0.0.0.0, die INADDR_ANY ist (normalerweise verwendet), was bedeutet, dass der Kernel die IP-Adresse auswählt. Die unmittelbarsten Auswirkungen auf uns werden in der folgenden Abbildung dargestellt:

Als nächstes betrachten wir die nächste komplexere Funktion, nämlich den Prozess der Auswahl der verfügbaren Portnummer, inet_csk_get_port
(sk->sk_prot->Port abrufen)

2.2, inet_csk_get_port

Wenn im ersten Abschnitt der Bind-Port 0 ist, wird nach dem Zufallsprinzip nach einer verfügbaren Portnummer gesucht

Direkt im Quellcode ist der erste Abschnitt des Codes der Suchvorgang für Portnummer 0

// Wenn snum als 0 angegeben ist, wird ein Port zufällig ausgewählt inet_csk_get_port(struct sock *sk, unsigned short snum)
{
	......
	// Hier verwendet net_random() prandom_u32, eine Pseudozufallszahl smallest_rover = rover = net_random() % remainder + low;
	kleinste_Größe = -1;
	// snum=0, wähle den Zweig des Ports zufällig aus, wenn (!sum) {
		// Den vom Kernel festgelegten Portnummernbereich abrufen, der dem Kernelparameter /proc/sys/net/ipv4/ip_local_port_range entspricht 
		inet_get_local_port_range(&niedrig,&hoch);
		......
		Tun{
			wenn (inet_ist_reservierter_lokaler_Port(rover)
				goto next_nonlock; // Wählen Sie nicht die reservierte Portnummer aus......
			inet_bind_bucket_for_each(tb, &Kopf->Kette)
				// Derselbe Port wie der Port Rover, den Sie auswählen möchten, existiert im selben Netzwerk-Namespace
				wenn (net_eq(ib_net(tb), net) und tb->port == Rover) {
					// Sowohl beim bestehenden als auch beim neuen Sock ist SO_REUSEADDR aktiviert und der aktuelle Sock-Status lautet „Nicht zuhören“
					// oder // Sowohl der vorhandene Sock als auch der neue Sock haben SO_REUSEPORT aktiviert und beide sind derselbe Benutzer, wenn (((tb->fastreuse > 0 &&
					      sk->sk_reuse &&
					      sk->sk_state != TCP_LISTEN) ||
					     (tb->Fastreueport > 0 &&
					      sk->sk_reuseport &&
					      uid_eq(tb->fastuid, uid))) &&
					    (tb->Anzahl_Besitzer < kleinste_Größe || kleinste_Größe == -1)) {
					   // Hier wählen wir einen Port mit den kleinsten num_owners aus, also einen Port mit der geringsten Anzahl gleichzeitiger Bind- oder Listen-Anfragen
					   // Weil eine Portnummer (Port) von mehreren Prozessen gleichzeitig verwendet werden kann, nachdem so_reuseaddr/so_reuseport aktiviert wurde, smallest_size = tb->num_owners;
						kleinster_rover = Rover;
						wenn (atomic_read(&hashinfo->bsockets) > (hoch - niedrig) + 1 &&
						    !inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
						    // Das Betreten dieses Zweigs zeigt an, dass die verfügbare Portnummer nicht ausreicht. Gleichzeitig steht die aktuelle Portnummer nicht im Konflikt mit dem zuvor verwendeten Port, daher wählen wir diese Portnummer (die kleinste).
							snum = kleinster_rover;
							gehe zu tb_found;
						}
					}
					// Wenn die Portnummer keinen Konflikt verursacht, wähle diesen Port aus, if (!inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, false)) {
						snum = Rover;
						gehe zu tb_found;
					}
					gehe zum nächsten;
				}
			brechen;
			// Bis alle verfügbaren Ports durchlaufen sind
		} während (--restlich > 0);
	}
	.......
}

Da wir bei der Verwendung von Bind selten zufällige Portnummern verwenden (insbesondere für TCP-Server), werde ich diesen Code kommentieren. Im Allgemeinen verwenden nur einige spezielle Remote Procedure Calls (RPCs) zufällige serverseitige Portnummern.

Der zweite Abschnitt findet die Portnummer oder wurde bereits angegeben

habe_snum:
	inet_bind_bucket_for_each(tb, &Kopf->Kette)
			wenn (net_eq(ib_net(tb), net) und tb->port == snum)
				gehe zu tb_found;
	}
	tb = NULL;
	gehe zu tb_not_found
tb_gefunden:
	// Wenn dieser Port gebunden wurde
	wenn (!hlist_empty(&tb->owners)) {
		// Wenn auf erzwungene Wiederverwendung eingestellt, ist es direkt erfolgreich, wenn (sk->sk_reuse == SK_FORCE_REUSE)
			gehe zum Erfolg;
	}
	wenn (((tb->fastreuse > 0 &&
		      sk->sk_reuse und sk->sk_state != TCP_LISTEN) ||
		     (tb->Fastreueport > 0 &&
		      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
		    kleinste_Größe == -1) {
		    // Dieser Zweig zeigt an, dass der zuvor gebundene Port und der aktuelle Sock beide auf Wiederverwendung eingestellt sind und der aktuelle Sock-Status nicht abhört
			// Oder sowohl Reuseport als auch UID werden gleichzeitig festgelegt (beachten Sie, dass Sie nach dem Festlegen von Reuseport gleichzeitig auf demselben Port lauschen können).
			gehe zum Erfolg;
	} anders {
			ret = 1;
			// Überprüfen Sie, ob ein Portkonflikt vorliegt, if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb, true)) {
				wenn (((sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
				     (tb->Fastreueport > 0 &&
				      sk->sk_reuseport && uid_eq(tb->fastuid, uid))) &&
				    kleinste_Größe != -1 und --Versuche >= 0) {
				    // Wenn ein Konflikt vorliegt, aber der Reuse-Nicht-Listen-Status gesetzt ist oder der Reuse-Port gesetzt ist und es sich um denselben Benutzer handelt, // können Sie spin_unlock(&head->lock); erneut versuchen.
					gehe nochmal zu;
				}

				gehe zu fail_unlock;
			}
			// Kein Konflikt, folgen Sie der folgenden Logik }
tb_nicht_gefunden:
	wenn (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
					net, head, snum)) == NULL)
			gehe zu fail_unlock;
	// Fastreuse einrichten
	//Fastreuseport festlegen
Erfolg:
	......
	// Verknüpfen Sie den aktuellen Sock mit tb->owner und tb->num_owners++
	inet_bind_hash(sk, tb, snum);
	ret = 0;
	// Gib Bind (Bindung) erfolgreich zurück return ret;

3. Stellen Sie fest, ob die Portnummer in Konflikt steht

Im obigen Quellcode lautet der Code zur Ermittlung, ob ein Portnummernkonflikt vorliegt:

inet_csk(sk)->icsk_af_ops->bind_conflict, auch bekannt als inet_csk_bind_conflict
int inet_csk_bind_conflict(const struct sock *sk,
			   const struct inet_bind_bucket *tb, bool relax){
	......
	sk_for_each_bound(sk2, &tb->Besitzer) {
			// Diese Beurteilung zeigt, dass für den nächsten internen Zweig dieselbe Schnittstelle (dev_if) verwendet werden muss, d. h. Ports, die sich nicht auf derselben Schnittstelle befinden, geraten nicht in Konflikt, wenn (sk != sk2 &&
		    !inet_v6_ipv6only(sk2) &&
		    (!sk->sk_bound_dev_if ||
		     !sk2->sk_bound_dev_if ||
		     sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) 
		     {
		     	wenn ((!wiederverwenden || !sk2->sk_wiederverwenden ||
			    sk2->sk_state == TCP_LISTEN) &&
			    (!reuseport || !sk2->sk_reuseport ||
			    (sk2->sk_state != TCP_TIME_WAIT &&
			     !uid_eq(uid, sock_i_uid(sk2))))) {
			   // Wenn eine Partei die Wiederverwendung nicht festlegt und Sock2 sich im Listenzustand befindet // Gleichzeitig legt eine Partei den Wiederverwendungsport nicht fest und Sock2 befindet sich nicht im Time_Wait-Zustand und die UIDs der beiden sind unterschiedlich const __be32 sk2_rcv_saddr = sk_rcv_saddr(sk2);
				wenn (!sk2_rcv_saddr || !sk_rcv_saddr(sk) ||
				 	 // Die IP-Adressen sind gleich, was als Konflikt betrachtet wird sk2_rcv_saddr == sk_rcv_saddr(sk))
					brechen;
			}
			// Im nicht entspannten Modus wird es nur dann als Konflikt betrachtet, wenn die IP-Adressen gleich sind …
		  	gibt sk2 zurück != NULL;
	}
	......
}

Die Logik des obigen Codes ist in der folgenden Abbildung dargestellt:

4. SO_REUSEADDR und SO_REUSEPORT

Der obige Code ist etwas verwirrend, daher möchte ich kurz darauf eingehen, worauf wir bei unserer täglichen Entwicklung achten sollten.

Im obigen Bind sehen wir häufig die beiden Socket-Flags sk_reuse und sk_reuseport. Diese beiden Flags können bestimmen, ob die Bindung erfolgreich sein kann. Die Einstellungen dieser beiden Flags werden im folgenden Code in der Sprache C angezeigt:

 setockopt(sockfd_server, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int));
 setsockopt(sockfd_server, SOL_SOCKET, SO_REUSEPORT, &(int){ 1 }, sizeof(int));

In nativem JAVA

 // In Java 8 unterstützen native Sockets so_reuseport nicht
 ServerSocket-Server = neuer ServerSocket(Port);
 server.setReuseAddress(true);

In Netty (Netty-Version >= 4.0.16 und Linux-Kernel-Version >= 3.9 oder höher) kann SO_REUSEPORT verwendet werden.

SO_REUSEADDR

Im vorherigen Quellcode haben wir gesehen, dass bei der Beurteilung, ob ein Bindungskonflikt besteht, ein solcher Zweig vorhanden ist

(!wiederverwenden || !sk2->sk_wiederverwenden ||
			    sk2->sk_state == TCP_LISTEN) /* Reuseport vorübergehend ignorieren */){
	//Eine Partei hat es nicht festgelegt}

Wenn sich sk2 (d. h. der gebundene Socket) im Status TCP_LISTEN befindet oder sowohl für sk2 als auch für den neuen SK _REUSEADDR nicht festgelegt ist, kann dies als Konflikt betrachtet werden.

Wir können daraus schließen, dass, wenn sowohl der ursprüngliche Sock als auch der neue Sock mit SO_REUSEADDR festgelegt sind, sie erfolgreich gebunden werden können, solange sich der ursprüngliche Sock nicht im Listen-Status befindet, sogar im ESTABLISHED-Status!

In unserer täglichen Arbeit ist die häufigste Situation, dass sich der ursprüngliche Sock im Status TIME_WAIT befindet, was normalerweise auftritt, wenn wir den Server herunterfahren. Wenn SO_REUSEADDR nicht festgelegt ist, schlägt die Bindung fehl und der Dienst wird nicht gestartet. Allerdings ist SO_REUSEADDR festgelegt und der Vorgang ist erfolgreich, da es sich nicht um TCP_LISTEN handelt.

Diese Funktion ist sehr nützlich für einen Notfallneustart und das Offline-Debugging und es wird empfohlen, sie zu aktivieren.

6. SO_REUSEPORT

SO_REUSEPORT ist eine neue Funktion, die in Linux Version 3.9 eingeführt wurde.

1. Beim Erstellen massiver und hochgradig gleichzeitiger Verbindungen besteht das normale Modell aus einer Single-Thread-Listener-Verteilung, die die Vorteile mehrerer Kerne nicht nutzen kann und somit zu einem Engpass wird.

2. CPU-Cache-Zeile fehlerhaft

Schauen wir uns das allgemeine Reactor-Thread-Modell an.

Offensichtlich wird es bei seinem einfädigen Listen/Accept zu einem Engpass kommen (wenn ein mehrfädiges Epoll-Accept verwendet wird, führt dies zu einer Gruppenpanik, und das Hinzufügen von WQ_FLAG_EXCLUSIVE kann einen Teil des Problems lösen), insbesondere bei der Verwendung kurzer Links.
Vor diesem Hintergrund hat Linux SO_REUSEPORT hinzugefügt. Der folgende Code in Bind, der ermittelt, ob ein Konflikt vorliegt, ist gleichzeitig die für diesen Parameter hinzugefügte Logik:

wenn(!reuseport || !sk2->sk_reuseport ||
			    (sk2->sk_state != TCP_TIME_WAIT &&
			     !uid_eq(uid, sock_i_uid(sk2))

Dieser Code ermöglicht uns mehrere Bindungen ohne Fehler, wenn SO_REUSEPORT gesetzt ist. Das bedeutet, dass wir die Möglichkeit haben, mehrere Threads (Prozesse) zu binden/abzuhören. Wie in der folgenden Abbildung dargestellt:

Nachdem SO_REUSEPORT aktiviert wurde, sieht der Codestapel wie folgt aus:

tcp_v4_rcv
	|->__inet_lookup_skb 
		|->__inet_lookup
			|->__inet_lookup_listener
 /* Verwende Punkte und Pseudozufallszahlen, um einen Listen-Sock auszuwählen */
Struktur sock *__inet_lookup_listener(......)
{
	......
	wenn (Punktzahl > höchster Punktestand) {
			Ergebnis = sk;
			hiscore = Punktzahl;
			Wiederverwendungsport = sk->sk_wiederverwendungsport;
			if (Wiederverwendungsport) {
				phash = inet_ehashfn(net, daddr, hnum,
						     saddr, Sport);
				Übereinstimmungen = 1;
			}
		} sonst wenn (Punktzahl == Höchstpunktzahl && Wiederverwendungsbericht) {
			Streichhölzer++;
			wenn (((u64)phash * übereinstimmt) >> 32 == 0)
				Ergebnis = sk;
			phash = nächster_pseudo_random32(phash);
		}
	......
}

Führen Sie den Lastenausgleich direkt auf Kernelebene durch und verteilen Sie die Akzeptanzaufgaben auf verschiedene Sockets verschiedener Threads (Sharding). Dadurch werden zweifellos Multi-Core-Funktionen genutzt und die Socket-Verteilungsfunktionen nach einer erfolgreichen Verbindung erheblich verbessert.

Nginx verwendet bereits SO_REUSEPORT

Nginx hat SO_REUSEPORT in Version 1.9.1 eingeführt und die Konfiguration ist wie folgt:

http {
     Server {
          hören Sie 80 Wiederverwendungsport;
          Servername localhost;
          # ...
     }
}

Strom {
     Server {
          hören Sie 12345 Wiederverwendungsport;
          # ...
     }
} 

VII. Fazit

Der Quellcode des Linux-Kernels ist umfangreich und tiefgründig. Ein scheinbar einfacher Bind-Systemaufruf enthält tatsächlich so viele Details, dass Sie daraus etwas herauslesen können. Ich teile dies hier in der Hoffnung, dass es für die Leser hilfreich sein wird.

Oben finden Sie eine ausführliche Erklärung der Socket (TCP)-Bindung aus dem Linux-Quellcode. Weitere Informationen zur Linux Socket (TCP)-Bindung finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM!

Das könnte Sie auch interessieren:
  • Unterschied zwischen sockaddr und sockaddr_in in Linux C
  • Apache-Startfehler: httpd: apr_sockaddr_info_get() fehlgeschlagen
  • Der perfekte Prozess des Netty-Frameworks zur Realisierung der TCP/IP-Kommunikation
  • Nettys Lösung zum Entpacken von TCP-Paketen
  • Java-Netzwerkprogrammierung TCP zur Realisierung der Datei-Upload-Funktion
  • Java-Netzwerkprogrammierung TCP zur Realisierung der Chat-Funktion
  • Implementierung einer TCP-Chatroom-Funktion basierend auf C++
  • Detaillierte Erklärung der Beispiele sockaddr und sockaddr_in in der Sprache C

<<:  Textmodus im IE! Einführung in die Rolle von DOCTYPE

>>:  Einige Indikatoren für exzellentes Web-Frontend-Design

Artikel empfehlen

Eine kurze Erläuterung der Schriftarteinstellungen in Webseiten

Das Festlegen der Schriftart für die gesamte Site...

10 Fähigkeiten, die Frontend-Entwickler millionenschwer machen

Die Fähigkeiten, die Front-End-Entwickler beherrs...

Die perfekte Lösung für das AutoFill-Problem in Google Chrome

In Google Chrome werden Sie nach der erfolgreiche...

Beispiele für dl-, dt- und dd-Listenbezeichnungen

Die Tags dd und dt werden für Listen verwendet. N...

Lösen Sie das Problem der Kombination von AND und OR in MySQL

Wie unten dargestellt: Wählen Sie Produktname, Pr...

Vue implementiert einfache Rechnerfunktion

In diesem Artikelbeispiel wird der spezifische Co...

So implementieren Sie die King of Glory-Personal-Ladeseite mit CSS3

Wer King of Glory gespielt hat, sollte mit der Wi...

Mobile Frontend-Anpassungslösung (Zusammenfassung)

Ich habe online gesucht und festgestellt, dass in...

Webseiten-Erlebnis: Planung und Design

1. Klären Sie die Designrichtung <br />Zuers...

JavaScript zum Erreichen eines einfachen Message Board-Falls

Verwenden Sie Javascript, um ein Message Board-Be...

10 Tipps für das User Interface Design mobiler Apps

Tipp 1: Konzentriert bleiben Die besten mobilen A...

Interpretieren von MySQL-Client- und Serverprotokollen

Inhaltsverzeichnis MySQL-Client/Server-Protokoll ...

So umbrechen Sie das HTML-Titelattribut

Als ich vor ein paar Tagen ein Programm schrieb, w...

So legen Sie MySQL-Berechtigungen mit phpmyadmin fest

Inhaltsverzeichnis Schritt 1: Melden Sie sich als...