Wir sind uns des Werts des Informationsaustauschs bewusst. Wie kommunizieren Prozesse im Netzwerk? Wenn wir beispielsweise jeden Tag den Browser öffnen, um im Internet zu surfen, wie kommuniziert der Browserprozess mit dem Webserver? Wenn Sie QQ zum Chatten verwenden, wie kommuniziert der QQ-Prozess mit dem Server oder dem QQ-Prozess, auf dem sich Ihre Freunde befinden? Sind all diese auf Steckdosen angewiesen? Was ist also eine Steckdose? Welche Arten von Steckdosen gibt es? Es gibt auch grundlegende Funktionen von Sockets, die in diesem Artikel vorgestellt werden sollen. Der Hauptinhalt dieses Artikels ist wie folgt:
1. Wie kommuniziert man zwischen Prozessen im Netzwerk?
2. Was ist Socket?
3. Grundfunktionen von socket()
3.2. bind()-Funktion
3.3. ) Funktion
3.4, Funktion „accept()“
3.5, Funktionen „read()“, „write()“ usw.
3.6, Funktion „close()“
4 , Ausführliche Erläuterung des Drei-Wege-Handshakes von TCP zum Herstellen einer Verbindung im Socket
5. Ausführliche Erläuterung des Vier-Wege-Handshakes von TCP zum Freigeben der Verbindung im Socket
Ein Beispiel (Übung). it)
7. Hinterlassen Sie eine Frage und jeder ist willkommen, zu antworten! ! !
1. Wie kommuniziert man zwischen Prozessen im Netzwerk?
Es gibt viele Möglichkeiten der lokalen Interprozesskommunikation (IPC), aber sie können in den folgenden 4 Kategorien zusammengefasst werden:
Nachrichtenübermittlung (Pipeline, FIFO, Nachrichtenwarteschlange)
Synchronisation (Mutexe, Bedingungsvariablen, Lese-/Schreibsperren, Datei- und Schreibdatensatzsperren, Semaphoren)
Gemeinsamer Speicher (anonym und benannt)
Remote-Prozeduraufrufe (Solaris Gate und Sun RPC)
Aber das ist nicht das Thema dieses Artikels! Was wir diskutieren werden, ist die Frage, wie zwischen Prozessen im Netzwerk kommuniziert werden soll. Das erste zu lösende Problem besteht darin, einen Prozess eindeutig zu identifizieren, sonst ist eine Kommunikation unmöglich! Durch die Prozess-PID kann ein Prozess lokal eindeutig identifiziert werden, im Netzwerk funktioniert dies jedoch nicht. Tatsächlich hat uns die TCP/IP-Protokollsuite dabei geholfen, dieses Problem zu lösen. Die „IP-Adresse“ der Netzwerkschicht kann den Host im Netzwerk eindeutig identifizieren, während das „Protokoll + Port“ der Transportschicht die Anwendung eindeutig identifizieren kann (Prozess) im Host. 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 Socket?
Oben wissen wir bereits, dass Prozesse im Netzwerk über Sockets kommunizieren. Was ist also ein Socket? Sockets stammen aus Unix, und eine der Grundphilosophien von Unix/Linux ist, dass „alles eine Datei ist“ und im Modus „Öffnen -> Lesen und Schreiben, Schreiben/Lesen -> Schließen“ betrieben werden kann. Nach meinem Verständnis ist Socket eine spezielle Datei und einige Socket-Funktionen sind Operationen darauf (E/A lesen/schreiben, öffnen, schließen). Wir werden diese Funktionen später vorstellen.
Der Ursprung des Wortes Socket
Die erste Verwendung im Bereich Netzwerk wurde in dem am 12. Februar 1970 veröffentlichten Dokument IETF RFC33 gefunden, das von Stephen Carr, Steve Crocker und Vint Cerf verfasst wurde . Laut dem Computer History Museum schrieb Croker: „Elemente eines Namespace können als Socket-Schnittstellen bezeichnet werden. Eine Socket-Schnittstelle bildet ein Ende einer Verbindung, und eine Verbindung kann vollständig durch ein Paar von Socket-Schnittstellen spezifiziert werden.“ Das Computer History Museum hinzugefügt: „Dies ist etwa 12 Jahre früher als die Socket-Schnittstellendefinition von BSD Der Socket stellt funktionale Schnittstellen bereit, die diesen Vorgängen entsprechen. Im Folgenden wird TCP als Beispiel verwendet, um mehrere grundlegende Socket-Schnittstellenfunktionen vorzustellen.
3.1, socket()-Funktion
int socket(int domain, int type, int Protocol);
Die Socket-Funktion entspricht dem gewöhnlichen Öffnungsvorgang Dateien. 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:
Domäne: die Protokolldomäne, auch Protokollfamilie genannt. Zu den häufig verwendeten Protokollfamilien gehören AF_INET, AF_INET6, 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.
3.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; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */};/* Internet address. */struct in_addr { uint32_t s_addr; /* address in network byte order */}; ipv6对应的是: struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ };struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ }; Unix域对应的是: #define UNIX_PATH_MAX 108struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
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.
3.3. listen()- und connect()-Funktionen
Wenn Sie ein Server sind, wird nach dem Aufruf von socket() und 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 Parameter der Listen-Funktion 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.
3.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 accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
3.5、read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); #include <sys/types.h> #include <sys/socket.h> 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);
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。
3.6、close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include <unistd.h> int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
4、socket中TCP的三次握手建立连接详解
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
图1、Einführung in die Socket-Kommunikation
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
5、socket中TCP的四次握手释放连接详解
上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:
图2、socket中发送的TCP四次握手
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
6、一个例子(实践一下)
说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。
服务器端代码:
服务器端
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 4096
int main(int argc, char** argv) { int listenfd, connfd; struct sockaddr_in servaddr; char buff[4096]; int n; if( (listenfd = 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); servaddr.sin_port = htons(6666); if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)/n",strerror(errno),errno); exit(0); } if( listen(listenfd, 10) == -1){ printf("listen socket error: %s(errno: %d)/n",strerror(errno),errno); exit(0); } printf("======waiting for client's request======/n"); while(1){ if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } n = recv(connfd, buff, MAXLINE, 0); buff[n] = '/0'; printf("recv msg from client: %s/n", buff); close(connfd); } close(listenfd); }
客户端代码:
客户端
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 4096
int main(int argc, char** argv) { int sockfd, n; char recvline[4096], sendline[4096]; 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(6666); 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); } close(sockfd); exit(0); }
当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要fork()一个新的进程或者线程去处理请求等。
7、动动手
留下一个问题,欢迎大家回帖回答!!!是否熟悉Linux下网络编程?如熟悉,编写如下程序完成如下功能:
服务器端:
接收地址192.168.100.2的客户端信息,如信息为“Client Query”,则打印“Receive Query”
客户端:
向地址192.168.100.168的服务器端顺序发送信息“Client Query test”,“Cleint Query”,“Client Query Quit”,然后退出。
题目中出现的ip地址可以根据实际情况定。