久久精品人人爽,华人av在线,亚洲性视频网站,欧美专区一二三

linux socket怎么使用

150次閱讀
沒有評論

共計 9109 個字符,預計需要花費 23 分鐘才能閱讀完成。

本篇內容介紹了“linux socket 怎么使用”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓丸趣 TV 小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

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  ^] .
echo Hello,socket
Hello,socket
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 地址和端口號。

“linux socket 怎么使用”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注丸趣 TV 網站,丸趣 TV 小編將為大家輸出更多高質量的實用文章!

正文完
 
丸趣
版權聲明:本站原創文章,由 丸趣 2023-08-03發表,共計9109字。
轉載說明:除特殊說明外本站除技術相關以外文章皆由網絡搜集發布,轉載請注明出處。
評論(沒有評論)
主站蜘蛛池模板: 汽车| 苍山县| 同心县| 开阳县| 崇文区| 景德镇市| 甘孜县| 文化| 禄丰县| 宜昌市| 上栗县| 遵义县| 阿尔山市| 宜黄县| 昌黎县| 潼关县| 夏津县| 历史| 梁平县| 孝义市| 乡城县| 礼泉县| 保德县| 余姚市| 远安县| 通山县| 宁明县| 谢通门县| 梨树县| 松江区| 开封县| 竹溪县| 富民县| 洛隆县| 黄石市| 武平县| 大同县| 定结县| 沂源县| 农安县| 贵德县|