哈嘍大家好,我是鹹魚 我相信大家在面試過程中或多或少都會被問到這樣一個問題:你能解釋一下什麼是 socket 嗎 我記得我當初的回答很是淺顯:socket 也叫套接字,用來負責不同主機程式之間的網路通信連接,socket 的表現方式由四元組(ip地址:埠)組成 那麼今天,鹹魚將跟大家打開 sock ...
哈嘍大家好,我是鹹魚
我相信大家在面試過程中或多或少都會被問到這樣一個問題:你能解釋一下什麼是 socket 嗎
我記得我當初的回答很是淺顯:socket 也叫套接字,用來負責不同主機程式之間的網路通信連接,socket 的表現方式由四元組(ip地址:埠)組成
那麼今天,鹹魚將跟大家打開 socket 的神秘大門,不但要搞清楚 socket 的概念,最好還能夠瞭解它的底層實現
我們首先查看一下 socket 的翻譯
我們看到,socket 可以翻譯成插座、插頭
那現在請想象這麼一個場景:給手機充電時,你將充電插頭插入電源插座裡面,是不是意味著插座與充電插頭連接起來了
在電腦世界中,socket 翻譯成套接字,通過 socket 我們可以與某台伺服器進行連接,而建立連接的過程,你可以腦補成將充電插頭插進插座的過程
socket 使用場景
假設我們想要將數據從 A 電腦的某個進程傳送到 B 電腦的某個進程(比如鹹魚用微信發信息給冰冰)
那麼在與對方聊天的過程中,其實就是這兩臺電腦中的微信進程相互傳輸數據的過程
在這個過程中,兩臺電腦各自調用 socket 方法,然後會得到一個 fd 句柄(socket_fd),這個 fd 句柄就相當於 socket 的身份證號
得到 fd 句柄之後:
-
服務端執行 bind()、listen()、accept() 方法等待客戶端建立連接的請求
-
客戶端執行 connect() 方法向服務端發起連接
-
連接建立起來之後,兩端都可以執行 send()、recv() 方法來互相傳遞數據
PS:對於不同的傳輸層協議,上面這個過程是不一樣的,詳情可以查看我之前的文章《Python 網路編程》
TCP 協議
UDP 協議
socket 底層設計
我們知道了 socket 是用來實現網路傳輸功能的,它負責不同主機進程之間的網路通信連接
我將上面的問題改一下,把 ”socket 是什麼“ 改成 ”如果讓你來實現一個網路傳輸功能,你會怎麼設計“
網路傳輸功能,簡單點來講就是兩端伺服器之間進行網路通信並互相收發數據,收發數據也就是讀寫數據
首先我們會遇到第一個問題:茫茫互聯網中你怎麼能找到那台夢中情機
聰明的你肯定會想到——ip地址!我們用 ip 地址來定位電腦
找到了你的夢中情機之後,你會發現,一臺電腦上面這麼多進程,我怎麼才能找到與我通信的那個進程(比如說微信)
聰明的你很快就想到了用埠號(port)
可以這麼理解,ip 地址是用來定位街區的,而埠號 port 對應這個街區中的門牌號,通過 ip +port 的組合,你可以在茫茫互聯網中找到屬於你的夢中情機並且與之通信
所以你在設計網路傳輸功能初期,定義了一個數據結構 sock,sock 裡面包含了 ip 和 port 欄位(假設用 C 語言實現)
在 Linux 中(以 CentOS 7舉例),在頭文件/usr/include/netinet/in.h
可以看到負責套接字地址的 sock 結構體
sin_family 欄位為 AF_INET,sin_port 表示埠號,sin_addr 表示 IPv4 地址,是一個
struct in_addr
類型的結構體
sin6_family 欄位為 AF_INET6,sin6_port 表示埠號,sin6_addr 表示 IPv6 地址,是一個 struct in6_addr 類型的結構體
解決了定位問題之後,我們知道在電腦網路中有很多協議,這些協議規定了電腦之間的通信方式
比如你是選用可靠的 TCP 協議去進行網路通信,還是相對不可靠的 UDP 協議
不同的網路協議還對應著不同的網路通信場景,如果你選擇了 TCP協議,你還得考慮例如滑動視窗、超時重傳這些場景
所以有了 ip 和 port 還不行,你還需要定義新的數據結構用來維護網路協議以及對應的網路場景
又因為不同的網路協議中有一些功能相似的方法(例如收發數據),於是你決定將不同協議中公共的部分提取出來,通過”繼承“的方式來實現功能復用
所以可以先定義一個名為 sock 的數據結構,然後定義”繼承“ sock 的各類 sock
PS:Linux 內核是用 C 語言實現的,在 C 語言中沒有繼承這個概念,你可以簡單將這個繼承理解成 xx_sock 基於 sock 進行了擴展,xx_sock 是 sock 的進階版
-
sock
:最基礎的結構,用來維護任何網路協議都會用到的收發數據緩衝區(公用部分) -
inet_sock
:負責網路傳輸功能的 sock,在 sock 基礎上加了 TTL(網路生存時間)、ip 和 port 這些跟網路傳輸相關的欄位信息 -
inet_connection_sock
:面向連接的 sock,在inet_sock
基礎上添加了面向連接的協議里相關欄位,比如 accept 隊列,數據包分片大小,握手失敗,重試次數等;雖然我們現在提到面向連接的協議就是指 TCP,但從設計上 Linux 需要支持擴展其他面向連接的新協議,比如 SCTP 協議,所以說tcp_sock
則是在這個基礎上實現的真正的 TCP 協議專用 sock 結構
上面例子中的這些 sock 都可以在系統上直接找到,以 CentOS 7 為例
現在你用代碼實現了這一堆數據結構——sock,不同的 sock 分別實現自己職責內的功能(負責面向連接的數據結構 inet_connection_sock
、負責 UDP 協議的數據結構 udp_sock
等等)
但是你需要這些 sock 去跟硬體網卡交互才能實現網路傳輸的功能,既然需要跟硬體交互,那就說明需要比較高的操作系統許可權
同時考慮到性能和安全,這套數據結構不能放在用戶態,需要給它放到系統內核裡面
既然這套數據結構在內核里,處在用戶態的程式想要用這套數據結構來實現網路傳輸功能該怎麼辦呢?
除此之外,處在用戶態的程式並不關心也不知道你這套數據結構在底層內核是怎麼操作的,功能是怎麼實現的,它只關心結果
於是你想到了用介面調用的方式——你將一個個功能抽象一個個介面,以後別人只需要調用這些介面,就可以讓內核中這一大堆複雜的數據結構去實現指定功能
又因為在 Linux 中一切皆文件,你索性將這些 sock 封裝成文件,當用戶態的程式去調用你提供的介面時,需要先創建一個 sock 文件
這個新生成的 sock 文件有一個文件句柄 fd,用戶態的程式只需要拿著這個 fd 就可以對內核中的 sock 進行操作
上面有說到,你將不同的數據結構(inet_sock
、tcp_sock
等等)抽象成一個個 API 介面,以後別人只需要調用這些 API 介面就可以驅動我們寫好的這一大堆複雜的數據結構去進行網路傳輸
下麵列出了一些常見的介面:
-
send
-
recv
-
bind
-
listen
-
connect
到這裡,整個網路傳輸功能就已經基本實現了。上面列舉出來的這些方法,其實就是 socket 提供出來的介面
到這裡,我們對 socket 有了一個更深地瞭解——socket 其實相當於一個介面層,它處在內核態和用戶態之間:
-
向上用戶態
-
為處在用戶態的程式提供 API 介面,方便用戶態程式實現網路傳輸功能
-
向下內核態
-
對網卡進行操作,負責網路傳輸工作
或者你也可以這麼理解,處在用戶態的程式通過 socket 提供的介面,將網路傳輸的這部分工作外包給了 Linux 內核
我們以 tcp 協議為例子來看下 python 中是如何操作 socket 的
在客戶端中,程式首先調用 socket 提供的 socket 方法創建一個 socket 文件來獲得 socket 句柄,然後調用 connect 方法,這時候內核會根據 socket_fd 找到對應的 sock 文件
再根據文件里的信息找到處在內核的 sock 結構,通過 sock 結構與服務端進行三次握手建立連接
連接建立好之後,客戶端調用 send 方法來進行數據傳輸,sock 中定義了一個發送緩衝區和接收緩衝區,其實就是一個鏈表,鏈表上面放著一個個等待發送或接收的數據
總結
我們再次回到那個問題——socket 是什麼?
sock(或 socket)是操作系統內核提供的一種數據結構,用於實現網路傳輸功能
基於不同的網路協議以及應用場景,衍生了各種類型的 sock
每個網路層協議都有相應的 sock 結構體來管理該層協議的連接狀態和數據傳輸。各類 sock 操作硬體網卡,就實現了網路傳輸的功能
為了將這些功能讓處在用戶態的應用程式使用,不但引入了 socket 層,還將各類功能的實現方式抽象成了 API 介面,供應用程式調用
同時將 sock 封裝成文件,應用程式就可以在用戶層通過文件句柄(socket fd)來操作內核中 sock 的網路傳輸功能
這個 socket fd 是一個 int 類型的數字,而 socket 中文翻譯叫做套接字,結合這個 socket fd,你是不是可以將其理解成:一套用於連接的數字
而 socket 分 Internet socket 和 UNIX Domain socket,兩者都可以用於不同主機進程間的通信和本機進程間的通信
只是前者採用的是基於 IP 協議的網路通信方式,而後者採用的是基於本地文件系統的通信方式
關於 UNIX Domain socket,可以通過 netstat -x
查看