哈嘍大家好,我是鹹魚
我相信大家在面試過程中或多或少都會被問到這樣一個問題:你能解釋一下什麼是 socket 嗎
我記得我當初的回答很是淺顯:socket 也叫通訊端,用來負責不同主機程式之間的網路通訊連線,socket 的表現方式由四元組(ip地址:埠)組成
那麼今天,鹹魚將跟大家開啟 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 是什麼「 改成 」如果讓你來實現一個網路傳輸功能,你會怎麼設計「
網路傳輸功能,簡單點來講就是兩端伺服器之間進行網路通訊並互相收發資料,收發資料也就是讀寫資料
首先我們會遇到第一個問題:茫茫網際網路中你怎麼能找到那臺夢中情機
聰明的你肯定會想到——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
檢視