socket又稱套接字,是Linux跨進程通信(IPC)方式的一種,它不僅可以做到同一台主機內跨進程通信,還可以做到不同主機間的跨進程通信。
本教學操作環境:linux5.9.8系統、Dell G3電腦。
socket 的原意是“插座”,在電腦通訊領域,socket 被翻譯為“套接字”,它是電腦之間進行通訊的一種約定或一種方式。透過 socket 這種約定,一台計算機可以接收其他計算機的數據,也可以向其他計算機發送數據。
linux中的socket
Socket是Linux跨進程通訊(IPC,Inter Process Communication,詳情參考:Linux進程間通訊方式總結)方式的一種。相較於其他IPC方式,Socket更牛的地方在於,它不僅可以做到同一台主機內跨進程通信,它還可以做到不同主機間的跨進程通信。根據通訊域的差異可以分割成2種:Unix domain socket 和 Internet domain socket。
1. Internet domain socket
Internet domain socket用於實現不同主機上的進程間通信,大部分情況下我們所說的socket都是指internet domain socket。 (下文不特殊指涉的情況下,socket就是指internet domain socket。)
要做到不同主機跨進程通信,第一個要解決的問題就是怎麼唯一標識一個進程。我們知道主機上每個進程都有一個唯一的pid,透過pid可以解決同一台主機上的跨進程通訊進程的識別問題。但是如果2個進程不在一台主機上的話,pid是有可能重複的,所以在這個場景下不適用,那有什麼其他的方式嗎?我們知道透過主機IP可以唯一鎖定主機,而透過連接埠可以定位到程序,而進程間通訊我們還需要知道通訊用的什麼協定。這樣一來「IP 連接埠 協定」的組合就可以唯一標識網路中一台主機上的一個進程。這也是產生socket的主要參數。
每個行程都有唯一識別之後,接下來就是通訊了。通訊這事一個巴掌拍不響,有發送端程式就有接收端程序,而Socket可以看成在兩端進行通訊連接中的一個端點,發送端將一段訊息寫入發送端Socket中,發送端Socket將這段訊息傳送給接收端Socket,最後這段訊息傳送到接收端。至於訊息怎麼從發送端Socket到接收端Socket就是作業系統和網路堆疊該操心的事情,我們可以不用了解細節。如下圖所示:
為了維護兩端的連接,我們的Socket光有自己的唯一標識還不夠,還需要對方的唯一標識,所以一個上面說的發送端和接收端Socket其實都只有一半,一個完整的Socket的組成應該是由[協議,本地地址,本地端口,遠端地址,遠端端口] 組成的5維數組。例如發送端的Socket就是 [tcp,發送端IP,發送端port,接收端IP,接收端port],那麼接收端的Socket就是 [tcp,接收端IP,接收端port,發送端IP,發送端port]。
打個比方加深下理解,就例如我給你發微信聯絡你這個場景,我倆就是進程,微信客戶端就是Socket,微訊號就是我倆的唯一標識,至於騰訊是怎麼把我寄的微信訊息傳到你的微信上的細節,我們都不需要關心。為了維持我倆的聯繫,我們的Socket光有微信客戶端還不行,我倆還得加好友,這樣透過好友列表就能互相找到,我的微信客戶端的好友列表中的你就是我的完整Socket,而你的微信客戶端的好友清單中的我就是你的完整Socket。希望沒有把你們弄暈。 。 。
Socket依通訊協定的差異也可以分為3種:串流套接字(SOCK_STREAM),資料封包套接字(SOCK_DGRAM)及原始套接字。
串流套接字(SOCK_STREAM):最常見的套接字,使用TCP協議,提供可靠的、面向連接的通訊流。保證資料傳輸是正確的,並且是順序的。應用於Telnet遠端連線、WWW服務等。
資料報套接字(SOCK_DGRAM):使用UDP協議,提供無連接的服務,資料透過相互獨立的報文進行傳輸,是無序的,並且不保證可靠性。使用UDP的應用程式要有自己的對資料進行確認的協定。
原始套接字:允許對低層協定如IP或ICMP直接訪問,主要用於新的網路協定實現的測試等。原始套接字主要用於一些協定的開發,可以進行比較底層的操作。它功能強大,但是沒有上面介紹的兩種套接字使用方便,一般的程式也涉及不到原始套接字。
套接字工作流程如下圖所示(以串流套接字為例,資料報套接字流程有所不同,可以參考:什麼是套接字(Socket)) :伺服器先啟動,透過呼叫socket()建立一個套接字,然後呼叫bind()將該套接字和本地網路位址連結在一起,再呼叫listen()使套接字做好偵聽的準備,並規定它的請求佇列的長度,之後就呼叫accept()來接收連線。客戶端在建立套接字後就可呼叫connect()和伺服器建立連線。連線一旦建立,客戶機和伺服器之間就可以透過呼叫read()和write()來傳送和接收資料。最後,待資料傳送結束後,雙方呼叫close()關閉套接字。
從TCP連接視角看待上述過程可以總結如圖,可以看到TCP的三次握手代表著Socket連接建立的過程,建立完連接後就可以透過read ,wirte相互傳輸數據,最後四次揮手斷開連接刪除Socket。
2. Unix domain socket
Unix domain socket 又叫IPC(inter-process communication 進程間通訊) socket,用於實現同一主機上的進程間通訊。 socket 原本是為網路通訊設計的,但後來在 socket 的框架上發展出一種 IPC 機制,就是 UNIX domain socket。雖然網路socket 也可用於同一台主機的進程間通訊(透過loopback 位址127.0.0.1),但是UNIX domain socket 用於IPC 更有效率:不需要經過網路協定棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層資料從一個進程拷貝到另一個進程。這是因為,IPC 機製本質上是可靠的通訊,而網路協定是為不可靠的通訊設計的。
UNIX domain socket 是全雙工的,API 介面語意豐富,相較於其它IPC 機制有明顯的優越性,目前已成為使用最廣泛的IPC 機制,例如X Window 伺服器和GUI 程式之間就是透過UNIX domain socket 通訊的。 Unix domain socket 是 POSIX 標準中的一個元件,所以不要被名字迷惑,linux 系統也是支援它的。
了解Docker的同學應該知道Docker daemon監聽一個docker.sock文件,這個docker.sock文件的預設路徑是/var/run/docker.sock,這個Socket就是一個Unix domain socket。在後面的實作環節會詳細介紹。
Socket實踐
要學好編程,最好的方式就是實作。接下來我們來實際用下Socket通信,並且觀察Socket文件
1. Internet domain socket實踐
現在我們就用socket寫一個server,由於本人C語言經驗較少,所以這裡我選擇用GoLang實踐。 server的功能很簡單,就是監聽1208端口,當收到輸入ping時就返回pong,收到echo xxx就返回xxx,收到quit就關閉連接。 socket-server.go的程式碼參考文章:使用 Go 進行 Socket 程式設計 | 始於珞塵。如下:
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) } }
在一切皆檔案的Unix-like系統中,進程生產的socket透過socket檔案來表示,進程透過向socket檔案讀寫內容實現訊息的傳遞。在Linux系統中,通常是socket檔案在/proc/pid/fd/檔案路徑下。啟動我們的socket-server,我們來窺探一下對應的socket檔。先啟動server:
# go run socket-server.go Server Started ...
再開一個窗口,我們先查看server進程的pid,可以使用lsof或netstat指令:
# 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
可以看到我們的server pid為20007,接下來我們來查看下server監聽的socket:
# 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]'
可以看到/proc/20007/fd/3是一個連結文件,指向socket:[470314],這個便是server端的socket。 socket-server啟動經歷了socket() --> bind() --> listen()3個過程,創建了這個LISTEN socket用來監聽對1208埠的連線請求。
我們知道socket通訊需要一對socket:server端和client端。現在我們再開一個窗口,在socket-server的同一台機器上用telnet啟動一個client ,來看看client端的socket:
# telnet localhost 1208 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'.
繼續查看server端口打開的文件描述符;
# 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)
我們發現,相對於之前的結果多了2條,這3條分別是:
*:1208 (LISTEN)是server到監聽socket檔名,所屬進程pid是20007
localhost:1208->localhost:51090 (ESTABLISHED)是server端為client端建立的新的socket,負責和client通信,所屬進程pid是20007
localhost:51090->localhost:1208 (ESTABLISHED)是client端為server端建立的新的socket,負責和server通信,所屬進程pid是20375
在/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视频教程》
以上是linux socket是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!