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