Heim >php教程 >PHP开发 >Detaillierte Erklärung der SOCKET-Programmierung unter Linux

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

高洛峰
高洛峰Original
2016-12-13 10:30:181598Durchsuche

1. Wie man zwischen Prozessen im Netzwerk kommuniziert

Das Konzept der Prozesskommunikation stammt ursprünglich von eigenständigen Systemen. Da jeder Prozess innerhalb seines eigenen Adressbereichs abläuft, stellt das Betriebssystem entsprechende Möglichkeiten zur Prozesskommunikation bereit, um sicherzustellen, dass sich zwei kommunizierende Prozesse nicht gegenseitig stören und koordiniert arbeiten, beispielsweise

UNIX BSD verfügt über: Pipe (Pipe), Named Pipe (Benannte Pipe), Soft-Interrupt-Signal (Signal)

UNIX-System V verfügt über: Nachricht (Nachricht), gemeinsam genutzten Speicherbereich (gemeinsam genutzter Speicher) und Semaphor usw.

Sie beschränken sich auf die Kommunikation zwischen lokalen Prozessen. Ziel der Internet-Prozesskommunikation ist es, das Problem der gegenseitigen Kommunikation zwischen verschiedenen Hostprozessen zu lösen (die Prozesskommunikation auf derselben Maschine kann als Sonderfall angesehen werden). Zu diesem Zweck muss zunächst das Problem der Prozessidentifikation zwischen Netzwerken gelöst werden. Auf demselben Host können verschiedene Prozesse durch Prozess-IDs eindeutig identifiziert werden. In einer Netzwerkumgebung kann die von jedem Host unabhängig zugewiesene Prozessnummer den Prozess jedoch nicht eindeutig identifizieren. Beispielsweise weist Host A einem bestimmten Prozess die Prozessnummer 5 zu, und Prozessnummer 5 kann auch in Host B vorhanden sein. Daher ist der Satz „Prozessnummer 5“ bedeutungslos. Zweitens unterstützt das Betriebssystem viele Netzwerkprotokolle und verschiedene Protokolle funktionieren auf unterschiedliche Weise und haben unterschiedliche Adressformate. Daher muss die Prozesskommunikation zwischen Netzwerken auch das Problem der Identifizierung mehrerer Protokolle lösen.

Tatsächlich hat uns die TCP/IP-Protokollsuite geholfen, dieses Problem zu lösen. Die „IP-Adresse“ der Netzwerkschicht kann den Host im Netzwerk eindeutig identifizieren, während das „Protokoll + Port“ des Transports Die Schicht kann die Host-Anwendung (den Prozess) eindeutig identifizieren. Auf diese Weise kann das Triplett (IP-Adresse, Protokoll, Port) zur Identifizierung des Netzwerkprozesses verwendet werden, und die Prozesskommunikation im Netzwerk kann diese Markierung verwenden, um mit anderen Prozessen zu interagieren.

Anwendungen, die das TCP/IP-Protokoll verwenden, verwenden normalerweise Anwendungsprogrammierschnittstellen: Sockets von UNIX BSD und TLI von UNIX System V (bereits veraltet), um die Kommunikation zwischen Netzwerkprozessen zu erreichen. Derzeit verwenden fast alle Anwendungen Sockets, und jetzt ist die Prozesskommunikation im Netzwerk allgegenwärtig. Deshalb sage ich: „Alles ist Socket“.


2. Was ist TCP/IP, UDP

TCP/ IP (Transmission Control Protocol/Internet Protocol) ist ein industrieller Standardprotokollsatz, der für Weitverkehrsnetze (WANs) entwickelt wurde.

Das TCP/IP-Protokoll ist im Betriebssystem vorhanden und Netzwerkdienste werden über das Betriebssystem bereitgestellt. Systemaufrufe, die TCP/IP unterstützen, werden dem Betriebssystem hinzugefügt – Berkeley-Sockets, wie Socket, Connect, Send, Recv usw.

UDP (User Data Protocol) ist ein Protokoll, das TCP entspricht. Es ist Mitglied der TCP/IP-Protokollsuite. Wie im Bild gezeigt:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux Die TCP/IP-Protokollfamilie umfasst die Transportschicht, die Netzwerkschicht und die Verbindungsschicht. Die Position des Sockets ist wie im Bild gezeigt . Socket ist die Anwendungsschicht und das TCP/IP-Protokoll. Zwischensoftware-Abstraktionsschicht für die Familienkommunikation.

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

3. Was ist Socket?

1. Socket entstand von Unix, und eine der Grundphilosophien von Unix/Linux ist, dass „alles eine Datei ist“, und sie kann im Modus „Öffnen -> Lesen und Schreiben, Schreiben/Lesen -> Schließen“ betrieben werden. Socket ist eine Implementierung dieses Modus, und einige Socket-Funktionen sind Operationen darauf (E/A lesen/schreiben, öffnen, schließen).

Kurz gesagt, Socket ist die Anwendungsschicht und TCP/IP Eine zwischengeschaltete Software-Abstraktionsschicht für die Protokollfamilienkommunikation, bei der es sich um eine Reihe von Schnittstellen handelt. Im Entwurfsmodus ist Socket eigentlich ein Fassadenmodus, der die komplexe TCP/IP-Protokollfamilie hinter der Socket-Schnittstelle verbirgt. Für Benutzer ist alles eine Reihe einfacher Schnittstellen, die es Socket ermöglichen, Daten so zu organisieren, dass sie dem angegebenen Protokoll entsprechen.

Hinweis: Tatsächlich verfügt Socket nicht über das Konzept von Ebenen. Es handelt sich lediglich um eine Anwendung des Fassadendesignmusters, was die Programmierung erleichtert. Es handelt sich um eine Software-Abstraktionsschicht. Bei der Netzwerkprogrammierung verwenden wir viele Sockets.

2. Socket-Deskriptor

Es handelt sich tatsächlich um eine Ganzzahl. Die drei Handles, mit denen wir am besten vertraut sind, sind 0, 1 und 2. 0 ist die Standardeingabe, 1 ist die Standardausgabe und 2 ist die Standardfehlerausgabe. 0, 1 und 2 werden durch Ganzzahlen dargestellt, und die entsprechenden FILE *-Strukturen werden durch stdin, stdout, stderr dargestellt


Die Socket-API war ursprünglich Teil des UNIX-Betriebssystems System Es wurde so entwickelt, dass die Socket-API in die anderen E/A-Geräte des Systems integriert ist. Insbesondere wenn eine Anwendung einen Socket für die Internetkommunikation erstellt, gibt das Betriebssystem eine kleine Ganzzahl als Deskriptor zur Identifizierung des Sockets zurück. Die Anwendung übergibt dann den Deskriptor als Parameter und ruft eine Funktion auf, um einen Vorgang abzuschließen (z. B. das Übertragen von Daten über das Netzwerk oder den Empfang eingehender Daten).

In vielen Betriebssystemen sind Socket-Deskriptoren und andere I/O-Deskriptoren integriert, sodass Anwendungen Socket-I/O oder I/O-Lesen/Schreiben in Dateien ausführen können.

Wenn eine Anwendung einen Socket erstellen möchte, gibt das Betriebssystem eine kleine Ganzzahl als Deskriptor zurück, und die Anwendung verwendet diesen Deskriptor, um auf die Anwendungsanforderung zu verweisen, die E/A-Anforderungen für den Socket erfordert Das System öffnet eine Datei. Das Betriebssystem erstellt einen Dateideskriptor für den Zugriff der Anwendung auf die Datei. Aus Sicht einer Anwendung ist ein Dateideskriptor eine Ganzzahl, die eine Anwendung zum Lesen und Schreiben von Dateien verwenden kann. Die folgende Abbildung zeigt, wie das Betriebssystem einen Dateideskriptor als Array von Zeigern implementiert, die auf interne Datenstrukturen verweisen.

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

Für jedes Programmsystem gibt es eine eigene Tabelle. Genauer gesagt verwaltet das System für jeden laufenden Prozess eine separate Dateideskriptortabelle. Wenn ein Prozess eine Datei öffnet, schreibt das System einen Zeiger auf die interne Datenstruktur der Datei in die Dateideskriptortabelle und gibt den Indexwert der Tabelle an den Aufrufer zurück. Die Anwendung muss sich diesen Deskriptor nur merken und ihn bei der zukünftigen Bearbeitung der Datei verwenden. Das Betriebssystem verwendet diesen Deskriptor als Index für den Zugriff auf die Prozessdeskriptortabelle und verwendet den Zeiger, um die Datenstruktur zu finden, die alle Informationen über die Datei enthält.

Systemdatenstruktur für Sockets:

1) In der Socket-API gibt es einen Funktions-Socket, der zum Erstellen eines Sockets verwendet wird. Die allgemeine Idee des Socket-Designs besteht darin, dass ein einzelner Systemaufruf jeden Socket erstellen kann, da Sockets recht allgemein sind. Sobald der Socket erstellt ist, muss die Anwendung andere Funktionen aufrufen, um die spezifischen Details anzugeben. Wenn Sie beispielsweise den Socket aufrufen, wird ein neuer Deskriptoreintrag erstellt:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

2). Wortfelder bleiben unbefüllt. Nachdem die Anwendung den Socket erstellt hat, muss sie andere Prozeduren aufrufen, um diese Felder zu füllen, bevor der Socket verwendet werden kann.

3. Der Unterschied zwischen Dateideskriptoren und Dateizeigern:

Dateideskriptor: Wenn Sie eine Datei in einem Linux-System öffnen, erhalten Sie einen Dateideskriptor, der ist eine kleine positive ganze Zahl. Jeder Prozess speichert eine Dateideskriptortabelle im PCB (Prozesskontrollblock). Der Dateideskriptor ist der Index dieser Tabelle. Jeder Tabelleneintrag hat einen Zeiger auf eine geöffnete Datei.

Dateizeiger: Der Dateizeiger wird als I/O-Handle in der C-Sprache verwendet. Der Dateizeiger zeigt auf eine Datenstruktur namens FILE-Struktur im Benutzerbereich des Prozesses. Die FILE-Struktur umfasst einen Puffer und einen Dateideskriptor. Der Dateideskriptor ist ein Index in der Dateideskriptortabelle, sodass der Dateizeiger gewissermaßen das Handle des Handles ist (auf Windows-Systemen wird der Dateideskriptor als Dateihandle bezeichnet).

4. Grundlegende SOCKET-Schnittstellenfunktionen

Im Leben möchte A B anrufen, A wählt und B hört das Telefon klingeln Sie erwähnen das Telefon, A und B sind verbunden und A und B können sprechen. Wenn die Kommunikation beendet ist, legen Sie den Hörer auf, um das Gespräch zu beenden. Der Aufruf erklärte auf einfache Weise, wie das funktioniert: „Open-Write/Read-Close“-Modus.

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

Der Server initialisiert zuerst den Socket, bindet dann an den Port, lauscht auf den Port, ruft „Accept“ zum „Blockieren“ auf und wartet darauf, dass der Client eine Verbindung herstellt. Wenn zu diesem Zeitpunkt ein Client einen Socket initialisiert und dann eine Verbindung zum Server herstellt und die Verbindung erfolgreich ist, wird die Verbindung zwischen dem Client und dem Server hergestellt. Der Client sendet eine Datenanforderung, der Server empfängt die Anforderung und verarbeitet sie, sendet dann die Antwortdaten an den Client, der Client liest die Daten und schließt schließlich die Verbindung und die Interaktion endet.

Die Implementierung dieser Schnittstellen wird durch den Kernel abgeschlossen. Einzelheiten zur Implementierung finden Sie im Linux-Kernel


4.1, socket() function

int socket(int protofamily, int type , int Protokoll); //Sockfd zurückgeben

sockfd ist der Deskriptor.

Die Socket-Funktion entspricht dem Öffnungsvorgang einer gewöhnlichen Datei. Der normale Vorgang zum Öffnen einer Datei gibt einen Dateideskriptor zurück, und socket () wird zum Erstellen eines Socket-Deskriptors (Socket-Deskriptor) verwendet, der einen Socket eindeutig identifiziert. Dieser Socket-Deskriptor ist derselbe wie der Dateideskriptor. Er wird in nachfolgenden Vorgängen verwendet. Er wird als Parameter zum Ausführen einiger Lese- und Schreibvorgänge verwendet.

Genauso wie Sie verschiedene Parameterwerte an fopen übergeben können, um verschiedene Dateien zu öffnen. Beim Erstellen eines Sockets können Sie auch verschiedene Parameter angeben, um unterschiedliche Socket-Deskriptoren zu erstellen. Die drei Parameter der Socket-Funktion sind:

Protofamilie: Dies ist die Protokolldomäne, die auch als Protokollfamilie (Familie) bezeichnet wird. . Zu den häufig verwendeten Protokollfamilien gehören AF_INET (IPV4), AF_INET6 (IPV6), AF_LOCAL (oder AF_UNIX, Unix-Domänen-Socket), AF_ROUTE usw. Die Protokollfamilie bestimmt den Adresstyp des Sockets und die entsprechende Adresse muss bei der Kommunikation verwendet werden. AF_INET bestimmt beispielsweise die Verwendung einer Kombination aus IPv4-Adresse (32-Bit) und Portnummer (16-Bit) und AF_UNIX bestimmt um einen absoluten Namen als Adresse zu verwenden.

Typ: Geben Sie den Socket-Typ an. Zu den häufig verwendeten Socket-Typen gehören SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, SOCK_PACKET, SOCK_SEQPACKET usw. (Welche Socket-Typen gibt es?).

Protokoll: Daher der Name, der die Angabe des Protokolls bedeutet. Zu den häufig verwendeten Protokollen gehören IPPROTO_TCP, IPPTOTO_UDP, IPPROTO_SCTP, IPPROTO_TIPC usw., die jeweils dem TCP-Übertragungsprotokoll, dem UDP-Übertragungsprotokoll, dem STCP-Übertragungsprotokoll und dem TIPC-Übertragungsprotokoll entsprechen (ich werde dieses Protokoll separat besprechen!).

Hinweis: Der oben genannte Typ und das Protokoll können nicht beliebig kombiniert werden. Beispielsweise kann SOCK_STREAM nicht mit IPPROTO_UDP kombiniert werden. Wenn das Protokoll 0 ist, wird automatisch das dem Typ entsprechende Standardprotokoll ausgewählt.

Wenn wir Socket aufrufen, um einen Socket zu erstellen, ist der zurückgegebene Socket-Deskriptor im Raum der Protokollfamilie (Adressfamilie, AF_XXX) vorhanden, hat jedoch keine spezifische Adresse. Wenn Sie ihm eine Adresse zuweisen möchten, müssen Sie die Funktion bind() aufrufen, andernfalls weist das System beim Aufruf von connect() oder listen() automatisch einen Port zufällig zu.

4.2. bind()-Funktion

Wie oben erwähnt, weist die bind()-Funktion dem Socket eine bestimmte Adresse in einer Adressfamilie zu. Beispielsweise wird dem Socket entsprechend AF_INET und AF_INET6 eine Kombination aus IPv4- oder IPv6-Adresse und Portnummer zugewiesen.

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Die drei Parameter der Funktion sind:

sockfd: das heißt, das Socket-Beschreibungswort, das durch die Funktion socket() erstellt wird und einen Socket eindeutig identifiziert. Die Funktion bind() bindet einen Namen an diesen Deskriptor.

addr: ein const struct sockaddr * Zeiger, der auf die Protokolladresse zeigt, die an sockfd gebunden werden soll. Diese Adressstruktur variiert je nach Adressprotokollfamilie, wenn die Adresse den Socket erstellt. Beispielsweise entspricht ipv4:

struct sockaddr_in {
sa_family_t sin_family; /* Adressfamilie: AF_INET */.
in_port_t sin_port; /* Port in Netzwerk-Byte-Reihenfolge */
struct in_addr sin_addr; /* Internetadresse */
};

/* Internetadresse */
struct in_addr {
uint32_t s_addr; /* Adresse in Netzwerk-Byte-Reihenfolge */
};

ipv6 entspricht:

struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* Portnummer */
uint32_t sin6_flowinfo; /* IPv6-Flussinformationen */
struct in6_addr sin6_addr ; /* IPv6-Adresse */
uint32_t sin6_scope_id; /* Scope-ID (neu in 2.4) */
};

struct in6_addr {
unsigned char s6_addr[16]; /* IPv6-Adresse */
};

Unix-Domäne entspricht:

#define UNIX_PATH_MAX 108

struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char path sun_path[UNIX_PATH_MAX]; /* name */
};

addrlen: Entspricht der Länge der Adresse.

Normalerweise bindet der Server beim Start eine bekannte Adresse (z. B. IP-Adresse + Portnummer), um Dienste bereitzustellen, und der Client kann über diese keine Verbindung zum Server herstellen Geben Sie es an. Das System weist automatisch eine Portnummer und eine eigene IP-Adresskombination zu. Aus diesem Grund ruft der Server normalerweise bind() auf, bevor er lauscht, aber der Client ruft es nicht auf. Stattdessen generiert das System zufällig eines, wenn connect().

Netzwerk-Byte-Reihenfolge und Host-Byte-Reihenfolge

Host-Byte-Reihenfolge ist das, was wir normalerweise Big-Endian- und Little-Endian-Modi nennen: Verschiedene CPUs haben unterschiedliche Byte-Reihenfolge-Typen, diese Wörter Abschnittsreihenfolge beziehen sich auf die Reihenfolge in dem ganze Zahlen im Speicher gespeichert werden. Dies wird als Host-Reihenfolge bezeichnet. Die Standarddefinitionen von Big-Endian und Little-Endian werden wie folgt zitiert:

a) Little-Endian bedeutet, dass die niederwertigen Bytes am unteren Adressende des Speichers und die höherwertigen Bytes angeordnet sind Bytes sind am oberen Adressende des Speichers angeordnet.

b) Big-Endian bedeutet, dass die höherwertigen Bytes am unteren Adressende des Speichers und die niederwertigen Bytes am oberen Adressende des Speichers angeordnet sind.

Netzwerk-Byte-Reihenfolge: 4-Byte-32-Bit-Werte werden in der folgenden Reihenfolge übertragen: zuerst 0 ~ 7 Bit, dann 8 ~ 15 Bit, dann 16 ~ 23 Bit und schließlich 24 ~ 31 Bit. Dieser Übertragungsauftrag wird Big-Endian genannt. Da alle binären Ganzzahlen im TCP/IP-Header bei der Übertragung über das Netzwerk in dieser Reihenfolge vorliegen müssen, wird sie auch als Netzwerk-Byte-Reihenfolge bezeichnet. Die Bytereihenfolge ist, wie der Name schon sagt, die Reihenfolge, in der Daten, die größer als ein Byte sind, im Speicher gespeichert werden. Bei Daten mit einem Byte gibt es kein Problem mit der Reihenfolge.

Also: Wenn Sie eine Adresse an einen Socket binden, konvertieren Sie bitte zuerst die Host-Byte-Reihenfolge in die Netzwerk-Byte-Reihenfolge und gehen Sie nicht davon aus, dass die Host-Byte-Reihenfolge „Big“ verwendet, was mit der Netzwerk-Byte-Reihenfolge identisch ist. -Endian. Es gab Morde, die durch dieses Problem verursacht wurden! Dieses Problem hat viele unerklärliche Probleme im Projektcode des Unternehmens verursacht. Denken Sie daher bitte daran, keine Annahmen über die Host-Byte-Reihenfolge zu treffen und diese unbedingt in die Netzwerk-Byte-Reihenfolge umzuwandeln, bevor Sie sie dem Socket zuweisen.

4.3, listen(), connect()-Funktionen

Wenn Sie ein Server sind, werden nach dem Aufruf von socket(), bind(), listen() aufgerufen, um den Socket abzuhören. Wenn der Client Der Client ruft dann connect() auf, um eine Verbindungsanforderung auszugeben, und der Server empfängt die Anforderung.

int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Der erste des Listen Funktion Der Parameter ist der zu überwachende Socket-Deskriptor, und der zweite Parameter ist die maximale Anzahl von Verbindungen, die für den entsprechenden Socket in die Warteschlange gestellt werden können. Der von der Funktion socket() erstellte Socket ist standardmäßig ein aktiver Typ, und die Listen-Funktion ändert den Socket in einen passiven Typ und wartet auf die Verbindungsanforderung des Clients.

Der erste Parameter der Verbindungsfunktion ist der Socket-Deskriptor des Clients, der zweite Parameter ist die Socket-Adresse des Servers und der dritte Parameter ist die Länge der Socket-Adresse. Der Client stellt eine Verbindung mit dem TCP-Server her, indem er die Verbindungsfunktion aufruft.

4.4. Funktion „accept()“

Nachdem der TCP-Server nacheinander socket(), bind() und listen() aufgerufen hat, lauscht er auf die angegebene Socket-Adresse. Nachdem er socket() und connect() nacheinander aufgerufen hat, sendet der TCP-Client eine Verbindungsanforderung an den TCP-Server. Nachdem der TCP-Server diese Anforderung überwacht hat, ruft er die Funktion „accept()“ auf, um die Anforderung zu empfangen, sodass die Verbindung hergestellt wird. Anschließend können Sie Netzwerk-E/A-Vorgänge starten, die gewöhnlichen E/A-Vorgängen zum Lesen und Schreiben von Dateien ähneln.

int akzeptieren(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //Verbindung zurückgeben connect_fd

Parameter sockfd

Der Parameter sockfd ist der oben erläuterte Listening-Socket. Wenn ein Client eine Verbindung zum Server herstellt, verwendet er genau diese Portnummer. Natürlich kennt der Client nicht die Details des Sockets, er kennt nur eine Adresse und eine Portnummer.

Parameter addr

Dies ist ein Ergebnisparameter, der zur Annahme eines Rückgabewerts verwendet wird. Diese Adresse wird natürlich durch eine Adressstruktur beschrieben . Der Benutzer sollte wissen, um welche Art von Adressstruktur es sich handelt. Wenn Sie an der Adresse des Kunden nicht interessiert sind, können Sie diesen Wert auf NULL setzen.

Parameter len

Wie jeder denkt, ist es auch ein Parameter des Ergebnisses. Es wird verwendet, um die Größe der obigen Addr-Struktur zu akzeptieren. Es gibt die Anzahl der von der Addr belegten Bytes an Struktur. Ebenso kann es auch auf NULL gesetzt werden.


Wenn die Annahme erfolgreich ist, haben der Server und der Client korrekt eine Verbindung hergestellt. Zu diesem Zeitpunkt schließt der Server die Kommunikation mit dem Client über den von der Annahme zurückgegebenen Socket ab.

Hinweis:

Standardmäßig blockiert Accept den Prozess, bis eine Client-Verbindung hergestellt wird, und gibt einen neu verfügbaren Socket zurück, der der Verbindungs-Socket ist.

An dieser Stelle müssen wir zwischen zwei Arten von Steckdosen unterscheiden,

Listening-Socket: Der Listening-Socket ist genau wie der Parameter sockfd von Accept. Nach dem Aufruf der Listen-Funktion wird er vom Server generiert, der mit dem Aufruf der Funktion socket() beginnt Socket-Deskriptor (Listening Socket)

Verbindungs-Socket: Ein Socket wird von einem aktiv verbundenen Socket in einen Listening-Socket umgewandelt und die Accept-Funktion gibt den verbundenen Socket-Deskriptor (einen Verbindungs-Socket) zurück, der einen vorhandenen Punkt darstellt. Punktverbindung in einem Netzwerk.

Ein Server erstellt normalerweise nur einen Listening-Socket-Deskriptor, der während des Lebenszyklus des Servers immer vorhanden ist. Der Kernel erstellt einen verbundenen Socket-Deskriptor für jede vom Serverprozess akzeptierte Client-Verbindung. Wenn der Server die Bedienung eines Clients abschließt, wird der entsprechende verbundene Socket-Deskriptor geschlossen.

Die natürliche Frage lautet: Warum gibt es zwei Arten von Steckdosen? Der Grund ist sehr einfach. Wenn Sie einen Deskriptor verwenden, hat er zu viele Funktionen, was seine Verwendung sehr unintuitiv macht. Gleichzeitig wird ein solcher neuer Deskriptor tatsächlich im Kernel generiert.

Der Verbindungs-Socket socketfd_new belegt keinen neuen Port für die Kommunikation mit dem Client. Er verwendet weiterhin dieselbe Portnummer wie der Listening-Socket socketfd

4.5, read() , write () und andere Funktionen

Alles ist bereit, aber der Ostwind wird benötigt. Zu diesem Zeitpunkt ist die Verbindung zwischen dem Server und dem Client hergestellt. Netzwerk-E/A kann für Lese- und Schreibvorgänge aufgerufen werden, wodurch die Kommunikation zwischen verschiedenen Prozessen im Netzwerk realisiert wird! Netzwerk-E/A-Vorgänge haben die folgenden Gruppen:

read()/write()

recv()/send()

readv()/writev()

recvmsg()/sendmsg()

recvfrom()/sendto()

Ich empfehle die Verwendung der Funktion recvmsg()/sendmsg(), diese beiden Funktionen sind die meisten Dank der vielseitigen I/O-Funktionen können Sie die anderen oben genannten Funktionen tatsächlich durch diese beiden Funktionen ersetzen. Ihre Deklarationen lauten wie folgt:

#include

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

            #include
            #include

        ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t sendto(int sockfd, const void *buf, size_t len , int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

Lesefunktion Ist für das Lesen des Inhalts von fd verantwortlich. Wenn der zurückgegebene Wert 0 ist, bedeutet dies, dass das Ende der Datei kleiner als 0 ist bedeutet, dass ein Fehler aufgetreten ist. Wenn der Fehler EINTR lautet, bedeutet dies, dass der Lesevorgang durch einen Interrupt verursacht wurde. Wenn es sich um ECONNREST handelt, liegt ein Problem mit der Netzwerkverbindung vor.

Die Schreibfunktion schreibt den Inhalt von nbytes Bytes in buf in den Dateideskriptor fd. Gibt bei Erfolg die Anzahl der geschriebenen Bytes zurück. Bei einem Fehler wird -1 zurückgegeben und die Variable errno gesetzt. In Netzwerkprogrammen gibt es zwei Möglichkeiten, wenn wir in den Socket-Dateideskriptor schreiben. 1) Der Rückgabewert von write ist größer als 0, was darauf hinweist, dass ein Teil oder alle Daten geschrieben wurden. 2) Der zurückgegebene Wert ist kleiner als 0 und es ist ein Fehler aufgetreten. Wir müssen uns entsprechend der Fehlerart damit befassen. Wenn der Fehler EINTR ist, bedeutet dies, dass beim Schreiben ein Interrupt-Fehler aufgetreten ist. Wenn es EPIPE ist, bedeutet dies, dass ein Problem mit der Netzwerkverbindung vorliegt (die andere Partei hat die Verbindung geschlossen).

Ich werde diese Paare von E/A-Funktionen nicht einzeln vorstellen. Weitere Informationen finden Sie im Man-Dokument. Im folgenden Beispiel wird Baidu oder Google verwendet.

4.6. close()-Funktion

Nachdem der Server eine Verbindung mit dem Client hergestellt hat, müssen einige Lese- und Schreibvorgänge ausgeführt werden geschlossen werden, z. B. der Vorgang Rufen Sie nach dem Öffnen der Datei fclose auf, um die geöffnete Datei zu schließen.

#include
int close(int fd);

Das Standardverhalten von close besteht darin, den Socket beim Schließen eines TCP-Sockets als geschlossen zu markieren . Kehrt dann sofort zum aufrufenden Prozess zurück. Dieser Deskriptor kann vom aufrufenden Prozess nicht mehr verwendet werden, dh er kann nicht mehr als erster Parameter beim Lesen oder Schreiben verwendet werden.

Hinweis: Der Schließvorgang reduziert nur den Referenzzähler des entsprechenden Socket-Deskriptors um -1. Nur wenn der Referenzzähler 0 ist, wird der TCP-Client dazu veranlasst, eine Beendigungsanforderung an den Server zu senden.



5. Einrichtung von TCP im Socket (Drei-Wege-Handshake)

Das TCP-Protokoll schließt den Verbindungsaufbau über drei Nachrichtensegmente ab. Dieser Vorgang wird als Drei-Wege-Handshake bezeichnet.


Erster Handshake: Beim Verbindungsaufbau sendet der Client ein Syn-Paket (syn=j) an den Server und wechselt in den SYN_SEND-Status und wartet auf die Bestätigung vom Server. SYN: Sequenznummern synchronisieren.

Zweiter Handshake: Der Server empfängt das Syn-Paket und muss das SYN des Clients bestätigen (ack=j+1). Gleichzeitig sendet er auch ein SYN-Paket (syn=k). ist, SYN+ACK-Paket, zu diesem Zeitpunkt wechselt der Server in den SYN_RECV-Status
Der dritte Handshake: Der Client empfängt das SYN+ACK-Paket vom Server und sendet ein Bestätigungspaket ACK (ack=k+1) an den Nachdem das Paket gesendet wurde, wechselt der Client zum Server in den ESTABLISHED-Status und schließt den Drei-Wege-Handshake ab.
Ein vollständiger Drei-Wege-Handshake ist: Anfrage – Antwort – erneut bestätigen.

Entsprechende Funktionsschnittstelle:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

Wie aus der Abbildung ersichtlich ist, wird beim Aufrufen von connect durch den Client eine Verbindungsanforderung ausgelöst und ein SYN gesendet Dann wechselt der Server in den Blockierungsstatus, empfängt das SYN-J-Paket, ruft die Akzeptanzfunktion auf und sendet SYN K, ACK J+1 an den Client Wenn der Client SYN K und ACK J+1 vom Server empfängt, kehrt Accept zurück und bestätigt, dass der Server ACK K+1 empfängt. Der Handshake ist abgeschlossen und die Verbindung ist hergestellt.


Wir können den spezifischen Prozess durch Netzwerkpaketerfassung anzeigen:

Zum Beispiel öffnet unser Server Port 9502. Verwenden Sie tcpdump, um Pakete zu erfassen:


tcpdump -iany tcp port 9502


Dann verwenden wir Telnet 127.0 .0.1 9502 offene Verbindung.:

telnet 127.0.0.1 9502

14:12:45.104687 IP localhost.39870 > localhost.9502: Flags [S], seq 2927179378, win 32792, Optionen [mss 16396, sackOK, TS val 255474104 ecr 0, nop, wscale 3], Länge 0 (1)
14:12:45.104701 IP localhost.9502 > localhost.39870: Flags [S.] , seq 1721825043, ack 2927179379, win 32768, Optionen [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], Länge 0 (2)
14:12:45.104711 localhost.39 870 > . 9502: Flags [.], ack 1, win 4099, Optionen [nop,nop,TS val 255474104 ecr 255474104], Länge 0 (3)


14:13:01.415407 IP localhost.39870 > ; localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, Optionen [nop,nop,TS val 255478182 ecr 255474104], Länge 7
14:13:01.415432 IP localhost.9502 > ; localhost.39870: Flags [.], ack 8, win 4096, Optionen [nop,nop,TS val 255478182 ecr 255478182], Länge 0
14:13:01.415747 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, Optionen [nop,nop,TS val 255478182 ecr 255478182], Länge 18
14:13:01.415757 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, Optionen [nop,nop,TS val 255478182 ecr 255478182], Länge 0


114 :12:45.104687 Die Zeit ist genau

localhost.39870 > localhost.9502 gibt den Kommunikationsfluss an, 39870 ist der Client, 9502 ist der Server

[S] zeigt an, dass es sich um eine SYN-Anfrage handelt.

[S.] bedeutet, dass es sich um ein SYN+ACK-Bestätigungspaket handelt:

[.] bedeutet, dass es sich um ein ACT-Bestätigungspaket handelt, (Client)SYN -> (server)SYN->(client)ACT ist ein 3-Wege-Handshake-Prozess

[P] bedeutet, dass es sich um einen Daten-Push handelt, der vom Server zum Client oder vom Client erfolgen kann Das Drücken von

[F] zeigt an, dass es sich um ein FIN-Paket handelt. Dabei handelt es sich um einen Vorgang zum Schließen der Verbindung.

[R] zeigt an, dass es sich um ein RST-Paket handelt , was den gleichen Effekt hat wie das F-Paket, aber RST bedeutet, dass beim Schließen der Verbindung noch Daten vorhanden sind, die nicht verarbeitet wurden. Dies kann als gewaltsames Unterbrechen der Verbindung verstanden werden

Win 4099 bezieht sich auf die Größe des Schiebefensters

Länge 18 bezieht sich auf die Größe des Datenpakets


Sehen wir uns die drei Schritte zu (1) (2) (3) an, um TCP einzurichten:

Erster Handshake:

14:12:45.104687 IP localhost.39870 > ; localhost.9502: Flags [ S], seq 2927179378

Client-IP localhost.39870 (der Port des Clients wird normalerweise automatisch zugewiesen) Syn-Paket (syn=j) an den Server senden. localhost.9502 an den Server》

syn-Paket (syn=j): syn's seq= 2927179378 (j=2927179378)

Second Handshake:

14:12:45.104701 IP localhost .9502 > ; localhost.39870: Flags [S.], seq 1721825043, ack 2927179379,

Anfrage empfangen und bestätigt: Der Server empfängt das Syn-Paket und muss die SYN des Clients bestätigen (ack=j+1) Gleichzeitig wird auch ein SYN-Paket (syn = k) gesendet, d.
ACK ist j+1 = (ack=j+1) =ack 2927179379



Dritter Handshake:

14:12:45.104711 IP localhost .39870 > localhost.9502: Flags [.], ack 1,


Der Client empfängt das SYN+ACK-Paket vom Server und sendet ein Bestätigungspaket ACK an den Server (ack=k+ 1 )


Nachdem Client und Server in den Status ESTABLISHED eingetreten sind, können Kommunikationsdaten ausgetauscht werden. Dieses Mal hat es nichts mit der Akzeptanzschnittstelle zu tun. Auch wenn kein Accepte vorhanden ist, ist der Drei-Wege-Handshake abgeschlossen.

连接出现连接不上的问题,一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。

紫色背景的部分:

IP localhost.39870 > localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7

客户端向服务器发送长度为7个字节的数据,


IP localhost.9502 > localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0

服务器向客户确认已经收到数据


 IP localhost.9502 > localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18

然后服务器同时向客户端写入数据。


 IP localhost.39870 > localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0

客户端向服务器确认已经收到数据

这个就是tcp可靠的连接,每次通信都需要对方来确认。


6. Detaillierte Erklärung der SOCKET-Programmierung unter Linux

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的,如图:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

(1)客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。

(2)服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。

(3)服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。

(4)客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

Detaillierte Erklärung der SOCKET-Programmierung unter Linux如图:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

过程如下:

某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

1.为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。


2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。


7. Socket编程实例

服务器端:一直监听本机的8000号端口,如果收到连接请求,将接收请求并接收客户端发来的消息,并向客户端返回消息。

/* File Name: server.c */  
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
#define DEFAULT_PORT 8000  
#define MAXLINE 4096  
int main(int argc, char** argv)  
{  
    int    socket_fd, connect_fd;  
    struct sockaddr_in     servaddr;  
    char    buff[4096];  
    int     n;  
    //初始化Socket  
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){  
    printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    //初始化  
    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
    servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT  
  
    //将本地地址绑定到所创建的套接字上  
    if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){  
    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    //开始监听是否有客户端连接  
    if( listen(socket_fd, 10) == -1){  
    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
    printf("======waiting for client&#39;s request======\n");  
    while(1){  
//阻塞直到有客户端连接,不然多浪费CPU资源。  
        if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -1){  
        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);  
        continue;  
    }  
//接受客户端传过来的数据  
    n = recv(connect_fd, buff, MAXLINE, 0);  
//向客户端发送回应数据  
    if(!fork()){ /*紫禁城*/  
        if(send(connect_fd, "Hello,you are connected!\n", 26,0) == -1)  
        perror("send error");  
        close(connect_fd);  
        exit(0);  
    }  
    buff[n] = &#39;\0&#39;;  
    printf("recv msg from client: %s\n", buff);  
    close(connect_fd);  
    }  
    close(socket_fd);  
}

客户端:

/* File Name: client.c */  
  
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
  
#define MAXLINE 4096  
  
  
int main(int argc, char** argv)  
{  
    int    sockfd, n,rec_len;  
    char    recvline[4096], sendline[4096];  
    char    buf[MAXLINE];  
    struct sockaddr_in    servaddr;  
  
  
    if( argc != 2){  
    printf("usage: ./client <ipaddress>\n");  
    exit(0);  
    }  
  
  
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){  
    printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);  
    exit(0);  
    }  
  
  
    memset(&servaddr, 0, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(8000);  
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){  
    printf("inet_pton error for %s\n",argv[1]);  
    exit(0);  
    }  
  
  
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){  
    printf("connect error: %s(errno: %d)\n",strerror(errno),errno);  
    exit(0);  
    }  
  
  
    printf("send msg to server: \n");  
    fgets(sendline, 4096, stdin);  
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)  
    {  
    printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);  
    exit(0);  
    }  
    if((rec_len = recv(sockfd, buf, MAXLINE,0)) == -1) {  
       perror("recv error");  
       exit(1);  
    }  
    buf[rec_len]  = &#39;\0&#39;;  
    printf("Received : %s ",buf);  
    close(sockfd);  
    exit(0);  
}

inet_pton 是Linux下IP地址转换函数,可以在将IP地址在“点分十进制”和“整数”之间转换 ,是inet_addr的扩展。

int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址:

第一个参数af是地址族,转换后存在dst中
    af = AF_INET:src为指向字符型的地址,即ASCII的地址的首地址(ddd.ddd.ddd.ddd格式的),函数将该地址转换为in_addr的结构体,并复制在*dst中
  af =AF_INET6:src为指向IPV6的地址,函数将该地址转换为in6_addr的结构体,并复制在*dst中
如果函数出错将返回一个负值,并将errno设置为EAFNOSUPPORT,如果参数af指定的地址族和src格式不对,函数将返回0。


测试:

编译server.c

gcc -o server server.c

启动进程:

./server

显示结果:

======waiting for client's request======

并等待客户端连接。

编译 client.c

gcc -o client server.c

客户端去连接server:

./client 127.0.0.1 

等待输入消息

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

发送一条消息,输入:c++

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

此时服务器端看到:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

客户端收到消息:

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

其实可以不用client,可以使用telnet来测试:

telnet 127.0.0.1 8000

Detaillierte Erklärung der SOCKET-Programmierung unter Linux

注意:

在ubuntu 编译源代码的时候,头文件types.h可能找不到。
使用dpkg -L libc6-dev | grep types.h 查看。
如果没有,可以使用
apt-get install libc6-dev安装。
如果有了,但不在/usr/include/sys/目录下,手动把这个文件添加到这个目录下就可以了。


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