socket又稱套接字,是linux跨進程通信(IPC)方式的一種,它不僅僅可以做到同一臺主機內(nèi)跨進程通信,還可以做到不同主機間的跨進程通信。
本教程操作環(huán)境:linux5.9.8系統(tǒng)、Dell G3電腦。
socket 的原意是“插座”,在計算機通信領域,socket 被翻譯為“套接字”,它是計算機之間進行通信的一種約定或一種方式。通過 socket 這種約定,一臺計算機可以接收其他計算機的數(shù)據(jù),也可以向其他計算機發(fā)送數(shù)據(jù)。
linux中的socket
Socket是Linux跨進程通信(IPC,Inter Process Communication,詳情參考:Linux進程間通信方式總結(jié))方式的一種。相比于其他IPC方式,Socket更牛的地方在于,它不僅僅可以做到同一臺主機內(nèi)跨進程通信,它還可以做到不同主機間的跨進程通信。根據(jù)通信域的不同可以劃分成2種:Unix domain socket 和 Internet domain socket。
1. Internet domain socket
Internet domain socket用于實現(xiàn)不同主機上的進程間通信,大部分情況下我們所說的socket都是指internet domain socket。(下文不特殊指代的情況下,socket就是指internet domain socket。)
要做到不同主機跨進程通信,第一個要解決的問題就是怎么唯一標識一個進程。我們知道主機上每個進程都有一個唯一的pid,通過pid可以解決同一臺主機上的跨進程通信進程的識別問題。但是如果2個進程不在一臺主機上的話,pid是有可能重復的,所以在這個場景下不適用,那有什么其他的方式嗎?我們知道通過主機IP可以唯一鎖定主機,而通過端口可以定位到程序,而進程間通信我們還需要知道通信用的什么協(xié)議。這樣一來“IP+端口+協(xié)議”的組合就可以唯一標識網(wǎng)絡中一臺主機上的一個進程。這也是生成socket的主要參數(shù)。
每個進程都有唯一標識之后,接下來就是通信了。通信這事一個巴掌拍不響,有發(fā)送端程序就有接收端程序,而Socket可以看成在兩端進行通訊連接中的一個端點,發(fā)送端將一段信息寫入發(fā)送端Socket中,發(fā)送端Socket將這段信息發(fā)送給接收端Socket,最后這段信息傳送到接收端。至于信息怎么從發(fā)送端Socket到接收端Socket就是操作系統(tǒng)和網(wǎng)絡棧該操心的事情,我們可以不用了解細節(jié)。如下圖所示:
為了維護兩端的連接,我們的Socket光有自己的唯一標識還不夠,還需要對方的唯一標識,所以一個上面說的發(fā)送端和接收端Socket其實都只有一半,一個完整的Socket的組成應該是由[協(xié)議,本地地址,本地端口,遠程地址,遠程端口] 組成的一個5維數(shù)組。比如發(fā)送端的Socket就是 [tcp,發(fā)送端IP,發(fā)送端port,接收端IP,接收端port],那么接收端的Socket就是 [tcp,接收端IP,接收端port,發(fā)送端IP,發(fā)送端port]。
打個比方加深下理解,就比如我給你發(fā)微信聯(lián)系你這個場景,我倆就是進程,微信客戶端就是Socket,微信號就是我倆的唯一標識,至于騰訊是怎么把我發(fā)的微信消息傳到你的微信上的細節(jié),我們都不需要關心。為了維持我倆的聯(lián)系,我們的Socket光有微信客戶端還不行,我倆還得加好友,這樣通過好友列表就能互相找到,我的微信客戶端的好友列表中的你就是我的完整Socket,而你的微信客戶端的好友列表中的我就是你的完整Socket。希望沒有把你們弄暈。。。
Socket根據(jù)通信協(xié)議的不同還可以分為3種:流式套接字(SOCK_STREAM),數(shù)據(jù)報套接字(SOCK_DGRAM)及原始套接字。
-
流式套接字(SOCK_STREAM):最常見的套接字,使用TCP協(xié)議,提供可靠的、面向連接的通信流。保證數(shù)據(jù)傳輸是正確的,并且是順序的。應用于Telnet遠程連接、WWW服務等。
-
數(shù)據(jù)報套接字(SOCK_DGRAM):使用UDP協(xié)議,提供無連接的服務,數(shù)據(jù)通過相互獨立的報文進行傳輸,是無序的,并且不保證可靠性。使用UDP的應用程序要有自己的對數(shù)據(jù)進行確認的協(xié)議。
-
原始套接字:允許對低層協(xié)議如IP或ICMP直接訪問,主要用于新的網(wǎng)絡協(xié)議實現(xiàn)的測試等。原始套接字主要用于一些協(xié)議的開發(fā),可以進行比較底層的操作。它功能強大,但是沒有上面介紹的兩種套接字使用方便,一般的程序也涉及不到原始套接字。
套接字工作過程如下圖所示(以流式套接字為例,數(shù)據(jù)報套接字流程有所不同,可以參考:什么是套接字(Socket)):服務器首先啟動,通過調(diào)用socket()建立一個套接字,然后調(diào)用bind()將該套接字和本地網(wǎng)絡地址聯(lián)系在一起,再調(diào)用listen()使套接字做好偵聽的準備,并規(guī)定它的請求隊列的長度,之后就調(diào)用accept()來接收連接。客戶端在建立套接字后就可調(diào)用connect()和服務器建立連接。連接一旦建立,客戶機和服務器之間就可以通過調(diào)用read()和write()來發(fā)送和接收數(shù)據(jù)。最后,待數(shù)據(jù)傳送結(jié)束后,雙方調(diào)用close()關閉套接字。
從TCP連接視角看待上述過程可以總結(jié)如圖,可以看到TCP的三次握手代表著Socket連接建立的過程,建立完連接后就可以通過read,wirte相互傳輸數(shù)據(jù),最后四次揮手斷開連接刪除Socket。
2. Unix domain socket
Unix domain socket 又叫 IPC(inter-process communication 進程間通信) socket,用于實現(xiàn)同一主機上的進程間通信。socket 原本是為網(wǎng)絡通訊設計的,但后來在 socket 的框架上發(fā)展出一種 IPC 機制,就是 UNIX domain socket。雖然網(wǎng)絡 socket 也可用于同一臺主機的進程間通訊(通過 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要經(jīng)過網(wǎng)絡協(xié)議棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數(shù)據(jù)從一個進程拷貝到另一個進程。這是因為,IPC 機制本質(zhì)上是可靠的通訊,而網(wǎng)絡協(xié)議是為不可靠的通訊設計的。
UNIX domain socket 是全雙工的,API 接口語義豐富,相比其它 IPC 機制有明顯的優(yōu)越性,目前已成為使用最廣泛的 IPC 機制,比如 X Window 服務器和 GUI 程序之間就是通過 UNIX domain socket 通訊的。Unix domain socket 是 POSIX 標準中的一個組件,所以不要被名字迷惑,linux 系統(tǒng)也是支持它的。
了解Docker的同學應該知道Docker daemon監(jiān)聽一個docker.sock文件,這個docker.sock文件的默認路徑是/var/run/docker.sock,這個Socket就是一個Unix domain socket。在后面的實踐環(huán)節(jié)會詳細介紹。
Socket實踐
要學好編程,最好的方式就是實踐。接下來我們來實際用下Socket通信,并且觀察Socket文件
1. Internet domain socket實踐
現(xiàn)在我們就用socket寫一個server,由于本人C語言經(jīng)驗較少,所以這里我選擇用GoLang實踐。server的功能很簡單,就是監(jiān)聽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("pongn")) case?"echo": echoStr?:=?strings.Join(inputs[1:],?"?")?+?"n" c.Write([]byte(echoStr)) case?"quit": c.Close() break default: fmt.Printf("Unsupported?command:?%sn",?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,?%sn",?err) } fmt.Println("Server?Started?...") for?{ conn,?err?:=?server.Accept() if?err?!=?nil?{ fmt.Printf("Fail?to?connect,?%sn",?err) break } go?connHandler(conn) } }
在一切皆文件的Unix-like系統(tǒng)中,進程生產(chǎn)的socket通過socket文件來表示,進程通過向socket文件讀寫內(nèi)容實現(xiàn)消息的傳遞。在Linux系統(tǒng)中,通常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監(jiān)聽的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啟動經(jīng)歷了socket() –> bind() –> listen()3個過程,創(chuàng)建了這個LISTEN socket用來監(jiān)聽對1208端口的連接請求。
我們知道socket通信需要一對socket:server端和client端。現(xiàn)在我們再開一個窗口,在socket-server的同一臺機器上用telnet啟動一個client ,來看看client端的socket:
#?telnet?localhost?1208 Trying?127.0.0.1... Connected?to?localhost. Escape?character?is?'^]'.
繼續(xù)查看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)
我們發(fā)現(xiàn),相對于之前的結(jié)果多了2條,這3條分別是:
-
*:1208 (LISTEN)是server到監(jiān)聽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條結(jié)果我們可以看出,前2條socket,LISTEN socket和新建的ESTABLISHED socket都屬于server進程,對于每條鏈接server進程都會創(chuàng)建一個新的socket去鏈接client,這條socket的源IP和源端口為server的IP和端口,目的IP和目的端口是client的IP和端口。相應的client也創(chuàng)建一條新的socket,該socket的源IP和源端口與目的IP和目的端口恰好與server創(chuàng)建的socket相反,client的端口為一個主機隨機分配的高位端口。
從上面的結(jié)果我們可以回答一個問題 “服務端socket.accept后,會產(chǎn)生新端口嗎”? 答案是不會。server的監(jiān)聽端口不會變,server為client創(chuàng)建的新的socket的端口也不會變,在本例中都是1208。這難到不會出現(xiàn)端口沖突嗎?當然不會,我們知道socket是通過5維數(shù)組[協(xié)議,本地IP,本地端口,遠程IP,遠程端口] 來唯一確定的。socket: *:1208 (LISTEN)和socket: localhost:1208->localhost:51090 (ESTABLISHED)是不同的socket 。那這個LISTEN socket有什么用呢?我的理解是當收到請求連接的數(shù)據(jù)包,比如TCP的SYN請求,那么這個連接會被LISTEN socket接收,進行accept處理。如果是已經(jīng)建立過連接后的客戶端數(shù)據(jù)包,則將數(shù)據(jù)放入接收緩沖區(qū)。這樣,當服務器端需要讀取指定客戶端的數(shù)據(jù)時,則可以利用ESTABLISHED套接字通過recv或者read函數(shù)到緩沖區(qū)里面去取指定的數(shù)據(jù),這樣就可以保證響應會發(fā)送到正確的客戶端。
上面提到客戶端主機會為發(fā)起連接的進程分配一個隨機端口去創(chuàng)建一個socket,而server的進程則會為每個連接創(chuàng)建一個新的socket。因此對于客戶端而言,由于端口最多只有65535個,其中還有1024個是不準用戶程序用的,那么最多只能有64512個并發(fā)連接。對于服務端而言,并發(fā)連接的總量受到一個進程能夠打開的文件句柄數(shù)的限制,因為socket也是文件的一種,每個socket都有一個文件描述符(FD,file descriptor),進程每創(chuàng)建一個socket都會打開一個文件句柄。該上限可以通過ulimt -n查看,通過增加ulimit可以增加server的并發(fā)連接上限。本例的server機器的ulimit為:
#?ulimit?-n 1024
上面講了半天服務端與客戶端的socket創(chuàng)建,現(xiàn)在我們來看看服務端與客戶端的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的通信。
到此為止,我們來總結(jié)下從telnet發(fā)起連接,到客戶端發(fā)出ping,服務端響應pong,到最后客戶端quit,連接斷開的整個過程:
-
telnet發(fā)起向localhost:1208發(fā)起連接請求;
-
server通過socket: TCP *:1208 (LISTEN)收到請求數(shù)據(jù)包,進行accept處理;
-
server返回socket信息給客戶端,客戶端收到server socket信息,為客戶端進程分配一個隨機端口51090,然后創(chuàng)建socket: TCP localhost:51090->localhost:1208 來連接服務端;
-
服務端進程創(chuàng)建一個新的socket: TCP localhost:1208->localhost:51090來連接客戶端;
-
客戶端發(fā)出ping,ping數(shù)據(jù)包send到socket: TCP localhost:51090->localhost:1208 ;
-
服務端通過socket: TCP localhost:1208->localhost:51090收到ping數(shù)據(jù)包,返回pong,pong數(shù)據(jù)包又通過原路返回到客戶端 ,完成一次通信。
-
客戶端進程發(fā)起quit請求,通過上述相同的socket路徑到達服務端后,服務端切斷連接,服務端刪除socket: TCP localhost:1208->localhost:51090釋放文件句柄;客戶端刪除 socket: TCP localhost:51090->localhost:1208,釋放端口 51090。
在上述過程中,socket到socket之間還要經(jīng)過操作系統(tǒng),網(wǎng)絡棧等過程,這里就不做細致描述。
2. Unix domain socket實踐
我們知道docker使用的是client-server架構(gòu),用戶通過docker client輸入命令,client將命令轉(zhuǎn)達給docker daemon去執(zhí)行。docker daemon會監(jiān)聽一個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,我們可以直接調(diào)用docker daemon的API進行操作,接下來我們通過docker.sock調(diào)用API來運行一個nginx容器,相當于在docker client上執(zhí)行:
#?docker?run?nginx
與在docker client上一行命令搞定不同的是,通過API的形式運行容器需要2步:創(chuàng)建容器和啟動容器。
1. 創(chuàng)建nginx容器,我們使用curl命令調(diào)用docker API,通過–unix-socket /var/run/docker.sock指定Unix domain socket。首先調(diào)用/containers/create,并傳入?yún)?shù)指定鏡像為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. 啟動容器,通過上一步創(chuàng)建容器返回的容器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我們實現(xiàn)了客戶端進程curl與服務端進程docker daemon間的通信,并成功地調(diào)用了docker API運行了一個nginx container。
值得注意的是,在連接服務端的Unix domain socket的時候,我們直接指定的是服務端的socket文件。而在使用Internet domain socket的時候,我們指定的是服務端的IP地址和端口號。
總結(jié)
Socket是Linux跨進程通信方式的一種。它不僅僅可以做到同一臺主機內(nèi)跨進程通信,它還可以做到不同主機間的跨進程通信。根據(jù)通信域的不同可以劃分成2種:Unix domain socket 和 Internet domain socket。
Internet domain socket根據(jù)通信協(xié)議劃分成3種:流式套接字(SOCK_STREAM),數(shù)據(jù)報套接字(SOCK_DGRAM)及原始套接字
一個完整的Socket的組成應該是由[協(xié)議,本地地址,本地端口,遠程地址,遠程端口]組成的一個5維數(shù)組。
相關推薦:《Linux視頻教程》