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是什麼的詳細內容,更多請關注TW511.COM其它相關文章!