Socket, auch als Socket bekannt, ist eine Art Linux-Interprozesskommunikationsmethode (IPC), die nicht nur eine prozessübergreifende Kommunikation innerhalb desselben Hosts erreichen kann, sondern auch eine prozessübergreifende Kommunikation zwischen verschiedenen Hosts.
Die Betriebsumgebung dieses Tutorials: Linux5.9.8-System, Dell G3-Computer.
Die ursprüngliche Bedeutung von Socket ist „Socket“. Im Bereich der Computerkommunikation wird Socket mit „Socket“ übersetzt. Es handelt sich um eine Konvention oder eine Methode zur Kommunikation zwischen Computern. Durch die Socket-Konvention kann ein Computer Daten von anderen Computern empfangen und auch Daten an andere Computer senden.
Socket in Linux
Socket ist eine Methode der prozessübergreifenden Linux-Kommunikation (IPC, Inter Process Communication, Einzelheiten finden Sie unter: Zusammenfassung der prozessübergreifenden Kommunikationsmethoden von Linux). Im Vergleich zu anderen IPC-Methoden besteht der Vorteil von Socket darin, dass nicht nur eine prozessübergreifende Kommunikation innerhalb desselben Hosts, sondern auch eine prozessübergreifende Kommunikation zwischen verschiedenen Hosts erreicht werden kann. Je nach Kommunikationsdomäne kann es in zwei Typen unterteilt werden: Unix-Domänen-Socket und Internet-Domänen-Socket.
1. Internet-Domänen-Socket
Internet-Domänen-Socket wird verwendet, um die Kommunikation zwischen Prozessen auf verschiedenen Hosts zu implementieren. In den meisten Fällen bezieht sich der Socket, auf den wir uns beziehen, auf den Internet-Domänen-Socket. (Sofern unten nicht anders angegeben, bezieht sich Socket auf den Internet-Domain-Socket.)
Um eine prozessübergreifende Kommunikation zwischen verschiedenen Hosts zu erreichen, muss zunächst das Problem gelöst werden, wie ein Prozess eindeutig identifiziert werden kann. Wir wissen, dass jeder Prozess auf dem Host eine eindeutige PID hat, und die PID kann das Problem der Identifizierung prozessübergreifender Kommunikationsprozesse auf demselben Host lösen. Wenn sich die beiden Prozesse jedoch nicht auf demselben Host befinden, wird die PID möglicherweise wiederholt und ist daher in diesem Szenario nicht anwendbar. Gibt es eine andere Möglichkeit? Wir wissen, dass der Host über die Host-IP eindeutig gesperrt werden kann und das Programm über den Port lokalisiert werden kann. Für die Kommunikation zwischen Prozessen müssen wir auch wissen, welches Protokoll für die Kommunikation verwendet wird. Auf diese Weise kann durch die Kombination „IP + Port + Protokoll“ ein Prozess auf einem Host im Netzwerk eindeutig identifiziert werden. Dies ist auch der Hauptparameter zum Generieren von Sockets.
Nachdem jeder Prozess eine eindeutige Kennung hat, ist der nächste Schritt die Kommunikation. Kommunikation ist eine Frage eines Schlags. Wo es ein Senderprogramm gibt, gibt es ein Empfängerprogramm, und Socket kann als Endpunkt in der Kommunikationsverbindung zwischen den beiden Enden betrachtet werden. Der Sender schreibt eine Information in den Sender-Socket. und der Absender-Socket Senden Sie diese Informationen an den Socket der empfangenden Seite, und schließlich werden diese Informationen an die empfangende Seite gesendet. Wie die Informationen vom sendenden Socket zum empfangenden Socket gelangen, ist etwas, worüber sich das Betriebssystem und der Netzwerk-Stack Gedanken machen sollten. Wir müssen die Details nicht kennen. Wie in der Abbildung unten gezeigt:
Um die Verbindung zwischen beiden Enden aufrechtzuerhalten, reicht es nicht aus, dass unser Socket eine eigene eindeutige Kennung hat. Er benötigt auch die eindeutige Kennung der anderen Partei, also des Absenders Die oben erwähnten Empfänger-Sockets sind eigentlich nur die Hälfte. Ein vollständiger Socket sollte aus einem fünfdimensionalen Array bestehend aus [Protokoll, lokale Adresse, lokaler Port, Remote-Adresse, Remote-Port] bestehen. Beispielsweise ist der Socket des sendenden Endes [TCP, IP des sendenden Endes, Port des sendenden Endes, IP des empfangenden Endes, Port des empfangenden Endes], dann ist der Socket des empfangenden Endes [TCP, IP des empfangenden Endes, Port des empfangenden Endes, Senden End-IP, sendender End-Port].
Lassen Sie uns eine Analogie verwenden, um unser Verständnis zu vertiefen. In dem Szenario, in dem ich Ihnen einen WeChat sende, um Sie zu kontaktieren, sind wir der Prozess, der WeChat-Client ist der Socket und die WeChat-ID ist unsere eindeutige Kennung für die Art und Weise, wie Tencent mich gesendet hat. Wir müssen uns nicht um die Details der WeChat-Nachrichten kümmern, die an Ihren WeChat gesendet werden. Um die Verbindung zwischen uns beiden aufrechtzuerhalten, müssen wir in unserem Socket nur Freunde hinzufügen, damit wir uns über die Freundesliste meines WeChat-Clients finden können Complete Socket. Und ich in der Freundesliste Ihres WeChat-Clients ist Ihr vollständiger Socket. Ich hoffe, ich habe dich nicht bewusstlos gemacht. . .
Sockets können je nach Kommunikationsprotokoll in drei Typen unterteilt werden: Stream-Socket (SOCK_STREAM), Datagramm-Socket (SOCK_DGRAM) und Raw-Socket.
Streaming-Socket (SOCK_STREAM): Der gebräuchlichste Socket, der das TCP-Protokoll verwendet, sorgt für einen zuverlässigen, verbindungsorientierten Kommunikationsfluss. Stellen Sie sicher, dass die Datenübertragung korrekt und sequenziell erfolgt. Wird in Telnet-Fernverbindungen, WWW-Diensten usw. verwendet.
Datagram Socket (SOCK_DGRAM): Verwendet das UDP-Protokoll, um verbindungslose Dienste bereitzustellen. Die Daten werden über unabhängige Nachrichten übertragen, was nicht in Ordnung ist und die Zuverlässigkeit nicht gewährleistet ist. Anwendungen, die UDP verwenden, müssen über eigene Protokolle zur Datenbestätigung verfügen.
Raw-Socket: Ermöglicht den direkten Zugriff auf Low-Level-Protokolle wie IP oder ICMP, wird hauptsächlich zum Testen neuer Netzwerkprotokollimplementierungen usw. verwendet. Raw-Sockets werden hauptsächlich für die Entwicklung einiger Protokolle verwendet und können Operationen auf relativ niedriger Ebene ausführen. Es ist leistungsstark, aber nicht so bequem zu verwenden wie die beiden oben vorgestellten Sockets, und normale Programme verwenden keine Original-Sockets.
Der Arbeitsprozess des Sockets ist in der folgenden Abbildung dargestellt (am Beispiel des Streaming-Sockets ist der Datagramm-Socket-Prozess anders, Sie können sich auf Folgendes beziehen: Was ist ein Socket (Socket)): Der Server startet zuerst. und übergibt Call socket(), um einen Socket einzurichten, dann ruft bind() auf, um den Socket mit der lokalen Netzwerkadresse zu verknüpfen, und ruft dann listen() auf, um den Socket für das Abhören vorzubereiten und seine Anforderungswarteschlangenlänge anzugeben, und ruft dann Accept auf (), um die Verbindung zu empfangen. Nach dem Einrichten des Sockets kann der Client connect() aufrufen, um eine Verbindung mit dem Server herzustellen. Sobald die Verbindung hergestellt ist, können Daten zwischen Client und Server gesendet und empfangen werden, indem read() und write() aufgerufen werden. Nachdem die Datenübertragung abgeschlossen ist, rufen beide Parteien schließlich close () auf, um den Socket zu schließen.
Der obige Prozess kann aus der Perspektive der TCP-Verbindung zusammengefasst werden, wie in der Abbildung gezeigt. Sie können sehen, dass der Drei-Wege-Handshake von TCP den Prozess des Herstellens einer Socket-Verbindung darstellt Durch Lesen und Schreiben aneinander übertragen, und die letzten vier Wellen werden getrennt und der Socket gelöscht. 2. Unix-Domain-Socket Socket wurde ursprünglich für die Netzwerkkommunikation entwickelt, später wurde jedoch ein IPC-Mechanismus basierend auf dem Socket-Framework entwickelt, bei dem es sich um einen UNIX-Domänen-Socket handelt. Obwohl Netzwerk-Sockets auch für die Kommunikation zwischen Prozessen auf demselben Host verwendet werden können (über die Loopback-Adresse 127.0.0.1), sind UNIX-Domänen-Sockets für IPC effizienter: Sie müssen nicht den Netzwerkprotokollstapel sowie das Packen und Entpacken durchlaufen. und Prüfsummenberechnungen, Pflege von Sequenznummern und Antworten usw., kopieren Sie einfach Daten der Anwendungsschicht von einem Prozess in einen anderen. Dies liegt daran, dass der IPC-Mechanismus von Natur aus eine zuverlässige Kommunikation darstellt, während Netzwerkprotokolle für eine unzuverlässige Kommunikation ausgelegt sind.
Der UNIX-Domänen-Socket ist Vollduplex und verfügt über eine umfangreiche API-Schnittstellensemantik. Er ist zum Beispiel der am weitesten verbreitete IPC-Mechanismus über UNIX-Domänen-Socket-Kommunikation. Der Unix-Domain-Socket ist eine Komponente des POSIX-Standards. Lassen Sie sich also nicht durch den Namen verwirren, denn auch Linux-Systeme unterstützen ihn.
Studenten, die Docker kennen, sollten wissen, dass der Docker-Daemon eine docker.sock-Datei überwacht. Der Standardpfad dieser docker.sock-Datei ist /var/run/docker.sock. Dieser Socket ist ein Unix-Domänen-Socket. Dies wird in den späteren Praxiseinheiten ausführlich vorgestellt.Socket-Übungen Um das Programmieren gut zu lernen, ist es am besten zu üben. Als nächstes verwenden wir tatsächlich die Socket-Kommunikation und beobachten die Socket-Dateien.
Jetzt verwenden wir Socket, um einen Server zu schreiben. Ich habe mich hier für die Verwendung von GoLang entschieden . . Die Funktion des Servers ist sehr einfach, das heißt, er lauscht dem 1208-Port, wenn er das Echo xxx empfängt, und schließt ihn Verbindung. Code-Referenzartikel für socket-server.go: Go für Socket-Programmierung verwenden |. Wie folgt: package main
import (
"fmt"
"net"
"strings"
)
func connHandler(c net.Conn) {
if c == nil {
return
}
buf := make([]byte, 4096)
for {
cnt, err := c.Read(buf)
if err != nil || cnt == 0 {
c.Close()
break
}
inStr := strings.TrimSpace(string(buf[0:cnt]))
inputs := strings.Split(inStr, " ")
switch inputs[0] {
case "ping":
c.Write([]byte("pong\n"))
case "echo":
echoStr := strings.Join(inputs[1:], " ") + "\n"
c.Write([]byte(echoStr))
case "quit":
c.Close()
break
default:
fmt.Printf("Unsupported command: %s\n", inputs[0])
}
}
fmt.Printf("Connection from %v closed. \n", c.RemoteAddr())
}
func main() {
server, err := net.Listen("tcp", ":1208")
if err != nil {
fmt.Printf("Fail to start server, %s\n", err)
}
fmt.Println("Server Started ...")
for {
conn, err := server.Accept()
if err != nil {
fmt.Printf("Fail to connect, %s\n", err)
break
}
go connHandler(conn)
}
}
# go run socket-server.go
Server Started ...
# lsof -i :1208 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME socket-se 20007 root 3u IPv6 470314 0t0 TCP *:1208 (LISTEN) # netstat -tupan | grep 1208 tcp6 0 0 :::1208 :::* LISTEN 20007/socket-server
Sie können sehen, dass unsere Server-PID 20007 ist Socket, auf den der Server lauscht:
# ls -l /proc/20007/fd total 0 lrwx------ 1 root root 64 Sep 11 07:15 0 -> /dev/pts/0 lrwx------ 1 root root 64 Sep 11 07:15 1 -> /dev/pts/0 lrwx------ 1 root root 64 Sep 11 07:15 2 -> /dev/pts/0 lrwx------ 1 root root 64 Sep 11 07:15 3 -> 'socket:[470314]' lrwx------ 1 root root 64 Sep 11 07:15 4 -> 'anon_inode:[eventpoll]'
Sie können sehen, dass /proc/20007/fd/3 eine Linkdatei ist, die auf socket:[470314] verweist, den Socket auf der Serverseite. Der Start des Socket-Servers hat drei Prozesse durchlaufen: socket() --> bind() --> Dieser LISTEN-Socket wird erstellt, um auf Verbindungsanfragen an Port 1208 zu warten. Wir wissen, dass für die Socket-Kommunikation ein Socket-Paar erforderlich ist: serverseitig und clientseitig. Öffnen wir nun ein weiteres Fenster und starten wir mit Telnet einen Client auf demselben Computer wie der Socket-Server. Schauen wir uns den Socket auf der Client-Seite an:
# telnet localhost 1208 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'.
Überprüfen Sie weiterhin den vom Server-Port geöffneten Dateideskriptor
# lsof -i :1208 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME socket-se 20007 root 3u IPv6 470314 0t0 TCP *:1208 (LISTEN) socket-se 20007 root 5u IPv6 473748 0t0 TCP localhost:1208->localhost:51090 (ESTABLISHED) telnet 20375 ubuntu 3u IPv4 473747 0t0 TCP localhost:51090->localhost:1208 (ESTABLISHED)
在/proc/pid/fd/
文件路径下可以看到server和client新建的socket,这里不做赘述。从第3条结果我们可以看出,前2条socket,LISTEN socket和新建的ESTABLISHED socket都属于server进程,对于每条链接server进程都会创建一个新的socket去链接client,这条socket的源IP和源端口为server的IP和端口,目的IP和目的端口是client的IP和端口。相应的client也创建一条新的socket,该socket的源IP和源端口与目的IP和目的端口恰好与server创建的socket相反,client的端口为一个主机随机分配的高位端口。
从上面的结果我们可以回答一个问题 “服务端socket.accept后,会产生新端口吗”? 答案是不会。server的监听端口不会变,server为client创建的新的socket的端口也不会变,在本例中都是1208。这难到不会出现端口冲突吗?当然不会,我们知道socket是通过5维数组[协议,本地IP,本地端口,远程IP,远程端口] 来唯一确定的。socket: *:1208 (LISTEN)和socket: localhost:1208->localhost:51090 (ESTABLISHED)是不同的socket 。那这个LISTEN socket有什么用呢?我的理解是当收到请求连接的数据包,比如TCP的SYN请求,那么这个连接会被LISTEN socket接收,进行accept处理。如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用ESTABLISHED套接字通过recv或者read函数到缓冲区里面去取指定的数据,这样就可以保证响应会发送到正确的客户端。
上面提到客户端主机会为发起连接的进程分配一个随机端口去创建一个socket,而server的进程则会为每个连接创建一个新的socket。因此对于客户端而言,由于端口最多只有65535个,其中还有1024个是不准用户程序用的,那么最多只能有64512个并发连接。对于服务端而言,并发连接的总量受到一个进程能够打开的文件句柄数的限制,因为socket也是文件的一种,每个socket都有一个文件描述符(FD,file descriptor),进程每创建一个socket都会打开一个文件句柄。该上限可以通过ulimt -n查看,通过增加ulimit可以增加server的并发连接上限。本例的server机器的ulimit为:
# ulimit -n 1024
上面讲了半天服务端与客户端的socket创建,现在我们来看看服务端与客户端的socket通信。还记得我们的server可以响应3个命令吗,分别是ping,echo和quit,我们来试试:
# telnet localhost 1208 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. ping pong echo Hello,socket Hello,socket quit Connection closed by foreign host.
我们可以看到client与server通过socket的通信。
到此为止,我们来总结下从telnet发起连接,到客户端发出ping,服务端响应pong,到最后客户端quit,连接断开的整个过程:
telnet发起向localhost:1208发起连接请求;
server通过socket: TCP *:1208 (LISTEN)收到请求数据包,进行accept处理;
server返回socket信息给客户端,客户端收到server socket信息,为客户端进程分配一个随机端口51090,然后创建socket: TCP localhost:51090->localhost:1208 来连接服务端;
服务端进程创建一个新的socket: TCP localhost:1208->localhost:51090来连接客户端;
客户端发出ping,ping数据包send到socket: TCP localhost:51090->localhost:1208 ;
服务端通过socket: TCP localhost:1208->localhost:51090收到ping数据包,返回pong,pong数据包又通过原路返回到客户端 ,完成一次通信。
客户端进程发起quit请求,通过上述相同的socket路径到达服务端后,服务端切断连接,服务端删除socket: TCP localhost:1208->localhost:51090释放文件句柄;客户端删除 socket: TCP localhost:51090->localhost:1208,释放端口 51090。
在上述过程中,socket到socket之间还要经过操作系统,网络栈等过程,这里就不做细致描述。
2. Unix domain socket实践
我们知道docker使用的是client-server架构,用户通过docker client输入命令,client将命令转达给docker daemon去执行。docker daemon会监听一个unix domain socket来与其他进程通信,默认路径为/var/run/docker.sock。我们来看看这个文件:
# ls -l /var/run/docker.sock srw-rw---- 1 root docker 0 Aug 31 01:19 /var/run/docker.sock
可以看到它的Linux文件类型是“s”,也就是socket。通过这个socket,我们可以直接调用docker daemon的API进行操作,接下来我们通过docker.sock调用API来运行一个nginx容器,相当于在docker client上执行:
# docker run nginx
与在docker client上一行命令搞定不同的是,通过API的形式运行容器需要2步:创建容器和启动容器。
1. 创建nginx容器,我们使用curl命令调用docker API,通过--unix-socket /var/run/docker.sock指定Unix domain socket。首先调用/containers/create,并传入参数指定镜像为nginx,如下:
# curl -XPOST --unix-socket /var/run/docker.sock -d '{"Image":"nginx"}' -H 'Content-Type: application/json' http://localhost/containers/create {"Id":"67bfc390d58f7ba9ac808d3fc948a5d4e29395e94288a7588ec3523af6806e1a","Warnings":[]}
2. 启动容器,通过上一步创建容器返回的容器id,我们来启动这个nginx:
# curl -XPOST --unix-socket /var/run/docker.sock http://localhost/containers/67bfc390d58f7ba9ac808d3fc948a5d4e29395e94288a7588ec3523af6806e1a/start
# docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 67bfc390d58f nginx "/docker-entrypoint.…" About a minute ago Up 7 seconds 80/tcp romantic_heisenberg
至此,通过Unix domain socket我们实现了客户端进程curl与服务端进程docker daemon间的通信,并成功地调用了docker API运行了一个nginx container。
值得注意的是,在连接服务端的Unix domain socket的时候,我们直接指定的是服务端的socket文件。而在使用Internet domain socket的时候,我们指定的是服务端的IP地址和端口号。
总结
Socket是Linux跨进程通信方式的一种。它不仅仅可以做到同一台主机内跨进程通信,它还可以做到不同主机间的跨进程通信。根据通信域的不同可以划分成2种:Unix domain socket 和 Internet domain socket。
Internet domain socket根据通信协议划分成3种:流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM)及原始套接字
一个完整的Socket的组成应该是由[协议,本地地址,本地端口,远程地址,远程端口]组成的一个5维数组。
相关推荐:《Linux视频教程》
Das obige ist der detaillierte Inhalt vonWas ist ein Linux-Socket?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!