本文由cxuan分享,原題“原來這才是 Socket”,有修訂。
1、引言
本系列文章前面那些主要講解的是計算機網絡的理論基礎,但對于即時通訊IM這方面的應用層開發者來說,跟計算機網絡打道的其實是各種API接口。
本篇文章就來聊一下網絡應用程序員最熟悉的Socket這個東西,拋開生澀的計算機網絡理論,從應用層的角度來理解到底什么是Socket。
對于 Socket 的認識,本文將從以下幾個方面著手介紹:
- 1)Socket 是什么;
- 2)Socket 是如何創建的;
- 3)Socket 是如何連接的;
- 4)Socket 是如何收發數據的;
- 5)Socket 是如何斷開連接的;
- 6)Socket 套接字的刪除等。
特別說明:本文中提到的“Socket”、“網絡套接字”、“套接字”,如無特殊指明,指的都是同一個東西哦。
2、Socket 是什么
一個數據包經由應用程序產生,進入到協議棧中進行各種報文頭的包裝,然后操作系統調用網卡驅動程序指揮硬件,把數據發送到對端主機。
整個過程的大體的圖示如下:

我們大家知道,協議棧其實是位于操作系統中的一些協議的堆疊,這些協議包括 TCP、UDP、ARP、ICMP、IP等。
通常某個協議的設計都是為了解決特定問題的,比如:
- 1)TCP 的設計就負責安全可靠的傳輸數據;
- 2)UDP 設計就是報文小,傳輸效率高;
- 3)ARP 的設計是能夠通過 IP 地址查詢物理(Mac)地址;
- 4)ICMP 的設計目的是返回錯誤報文給主機;
- 5)IP 設計的目的是為了實現大規模主機的互聯互通。
應用程序比如瀏覽器、電子郵件、文件傳輸服務器等產生的數據,會通過傳輸層協議進行傳輸。而應用程序是不會和傳輸層直接建立聯系的,而是有一個能夠連接應用層和傳輸層之間的套件,這個套件就是 Socket。
在上面這幅圖中,應用程序包含 Socket 和解析器,解析器的作用就是向 DNS 服務器發起查詢,查詢目標 IP 地址(關于DNS請見《理論聯系實際,全方位深入理解DNS》)。
應用程序的下面:就是操作系統內部,操作系統內部包括協議棧,協議棧是一系列協議的堆疊。
操作系統下面:就是網卡驅動程序,網卡驅動程序負責控制網卡硬件,驅動程序驅動網卡硬件完成收發工作。
在操作系統內部有一塊用于存放控制信息的存儲空間,這塊存儲空間記錄了用于控制通信的控制信息。其實這些控制信息就是 Socket 的實體,或者說存放控制信息的內存空間就是Socket的實體。
這里大家有可能不太清楚所以然,所以我用了一下 netstat 命令來給大伙看一下Socket是啥玩意。
我們在 Windows 的命令提示符中輸入:
netstat-ano
# netstat 用于顯示Socket內容 , -ano 是可選選項
# a 不僅顯示正在通信的Socket,還顯示包括尚未開始通信等狀態的所有Socket
# n 顯示 IP 地址和端口號
# o 顯示Socket的程序 PID
我的計算機會出現下面結果:

如上圖所示:
- 1)每一行都相當于一個Socket;
- 2)每一列也被稱為一個元組。
所以,一個Socket就是五元組:
- 1)協議;
- 2)本地地址;
- 3)外部地址;
- 4)狀態;
- 5)PID。
PS:有的時候也被叫做四元組,四元組不包括協議。
我們來解讀一下上圖中的數據,比如圖中的第一行:
1)它的協議就是 TCP,本地地址和遠程地址都是 0.0.0.0(這表示通信還沒有開始,IP 地址暫時還未確定)。
2)而本地端口已知是 135,但是遠程端口還未知,此時的狀態是 LISTENING(LISTENING 表示應用程序已經打開,正在等待與遠程主機建立連接。關于各種狀態之間的轉換,大家可以閱讀《通俗易懂-深入理解TCP協議(上):理論基礎》)。
3)最后一個元組是 PID,即進程標識符,PID 就像我們的身份證號碼,能夠精確定位唯一的進程。
3、Socket 是如何創建的
通過上節的講解,現在你可能對 Socket 有了一個基本的認識,先喝口水,休息一下,讓我們繼續探究 Socket。
現在我有個問題,Socket 是如何創建的呢?
Socket 是和應用程序一起創建的。
應用程序中有一個 socket 組件,在應用程序啟動時,會調用 socket 申請創建Socket,協議棧會根據應用程序的申請創建Socket:首先分配一個Socket所需的內存空間,這一步相當于是為控制信息準備一個容器,但只有容器并沒有實際作用,所以你還需要向容器中放入控制信息;如果你不申請創建Socket所需要的內存空間,你創建的控制信息也沒有地方存放,所以分配內存空間,放入控制信息缺一不可。至此Socket的創建就已經完成了。
Socket創建完成后,會返回一個Socket描述符給應用程序,這個描述符相當于是區分不同Socket的號碼牌。根據這個描述符,應用程序在委托協議棧收發數據時就需要提供這個描述符。
4、Socket 是如何連接的
Socket創建完成后,最終還是為數據收發服務的。但是,在數據收發之前,還需要進行一步“連接”(術語就是 connect),建立連接有一整套過程。
這個“連接”并不是真實的連接(用一根水管插在兩個電腦之間?不是你想的這樣。。。)。

實際上這個“連接”是應用程序通過 TCP/IP 協議標準從一個主機通過網絡介質傳輸到另一個主機的過程。
Socket剛剛創建完成后,還沒有數據,也不知道通信對象。
在這種狀態下:即使你讓客戶端應用程序委托協議棧發送數據,它也不知道發送到哪里。所以瀏覽器需要根據網址來查詢服務器的 IP 地址(做這項工作的協議是 DNS),查詢到目標主機后,再把目標主機的 IP 告訴協議棧。至此,客戶端這邊就準備好了。
在服務器上:與客戶端一樣也需要創建Socket,但是同樣的它也不知道通信對象是誰,所以我們需要讓客戶端向服務器告知客戶端的必要信息:IP 地址和端口號。
現在通信雙方建立連接的必要信息已經具備,可以開始“連接”過程了。
首先:客戶端應用程序需要調用 Socket 庫中的 connect 方法,提供 socket 描述符和服務器 IP 地址、端口號。
以下是connect的偽碼調用:
connect(<描述符>、<服務器IP地址和端口號>)
這些信息會傳遞給協議棧中的 TCP 模塊,TCP 模塊會對請求報文進行封裝,再傳遞給 IP 模塊,進行 IP 報文頭的封裝,然后傳遞給物理層,進行幀頭封裝。
之后通過網絡介質傳遞給服務器,服務器上會對幀頭、IP 模塊、TCP 模塊的報文頭進行解析,從而找到對應的Socket。
Socket收到請求后,會寫入相應的信息,并且把狀態改為正在連接。
請求過程完成后:服務器的 TCP 模塊會返回響應,這個過程和客戶端是一樣的(如果大家不太清楚報文頭的封裝過程,可以閱讀《快速理解TCP協議一篇就夠》)。
在一個完整的請求和響應過程中,控制信息起到非常關鍵的作用:
- 1)SYN 就是同步的縮寫,客戶端會首先發送 SYN 數據包,請求服務端建立連接;
- 2)ACK 就是相應的意思,它是對發送 SYN 數據包的響應;
- 3)FIN 是終止的意思,它表示客戶端/服務器想要終止連接。
由于網絡環境的復雜多變,經常會存在數據包丟失的情況,所以雙方通信時需要相互確認對方的數據包是否已經到達,而判斷的標準就是 ACK 的值。
上面的文字不夠生動,動畫可以更好的說明這個過程:
(PS:這個“連接”的詳細理論知識,可以閱讀《理論經典:TCP協議的3次握手與4次揮手過程詳解》、《跟著動畫來學TCP三次握手和四次揮手》,這里不再贅述。)
當所有建立連接的報文都能夠正常收發之后,此時套接字就已經進入可收發狀態了,此時可以認為用一根管理把兩個套接字連接了起來。當然,實際上并不存在這個管子。建立連接之后,協議棧的連接操作就結束了,也就是說 connect 已經執行完畢,控制流程被交回給應用程序。
另外:如果你對Socket代碼更熟悉的話,可以先讀讀這篇《手把手教你寫基于TCP的Socket長連接》。
5、Socket 是如何收發數據的
當控制流程上節中的連接過程回到應用程序之后,接下來就會直接進入數據收發階段。
數據收發操作是從應用程序調用 write 將要發送的數據交給協議棧開始的,協議棧收到數據之后執行發送操作。
協議棧不會關心應用程序傳輸過來的是什么數據,因為這些數據最終都會轉換為二進制序列,協議棧在收到數據之后并不會馬上把數據發送出去,而是會將數據放在發送緩沖區,再等待應用程序發送下一條數據。
為什么收到數據包不會直接發送出去,而是放在緩沖區中呢?
因為只要一旦收到數據就會發送,就有可能發送大量的小數據包,導致網絡效率下降(所以協議棧需要將數據積攢到一定數量才能將其發送出去)。
至于協議棧會向緩沖區放多少數據,這個不同版本和種類的操作系統有不同的說法。
不過,所有的操作系統都會遵循下面這幾個標準:
1)第一個判斷要素:是每個網絡包能夠容納的數據長度,判斷的標準是 MTU,它表示的是一個網絡包的最大長度。最大長度包含頭部,所以如果單論數據區的話,就會用 MTU - 包頭長度,由此的出來的最大數據長度被稱為 MSS。

2)另一個判斷標準:是時間,當應用程序產生的數據比較少,協議棧向緩沖區放置數據效率不高時,如果每次都等到 MSS 再發送的話,可能因為等待時間太長造成延遲。在這種情況下,即使數據長度沒有到達 MSS,也應該把數據發送出去。
但協議棧并沒有告訴我們怎樣平衡這兩個因素,如果數據長度優先,那么效率有可能比較低;如果時間優先,那又會降低網絡的效率。
經過了一段時間。。。。。。

假設我們使用的是長度有限法則:此時緩沖區已滿,協議棧要發送數據了,協議棧剛要把數據發送出去,卻發現無法一次性傳輸這么大數據量(相對的)的數據,那怎么辦呢?
在這種情況下,發送緩沖區中的數據就會超過 MSS 的長度,發送緩沖區中的數據會以 MSS 大小為一個數據包進行拆分,拆分出來的每塊數據都會加上 TCP,IP,以太網頭部,然后被放進單獨的網絡包中。
到現在,網絡包已經準備好發往服務器了,但是數據發送操作還沒有結束,因為服務器還未確認是否已經收到網絡包。因此在客戶端發送數據包之后,還需要服務器進行確認。
TCP 模塊在拆分數據時,會計算出網絡包偏移量,這個偏移量就是相對于數據從頭開始計算的第幾個字節,并將算好的字節數寫在 TCP 頭部,TCP 模塊還會生成一個網絡包的序號(SYN),這個序號是唯一的,這個序號就是用來讓服務器進行確認的。
服務器會對客戶端發送過來的數據包進行確認,確認無誤之后,服務器會生成一個序號和確認號(ACK)并一起發送給客戶端,客戶端確認之后再發送確認號給服務器。
我們來看一下實際的工作過程:

首先:客戶端在連接時需要計算出序號初始值,并將這個值發送給服務器。
接下來:服務器通過這個初始值計算出確認號并返回給客戶端(初始值在通信過程中有可能會丟棄,因此當服務器收到初始值后需要返回確認號用于確認)。
同時:服務器也需要計算出從服務器到客戶端方向的序號初始值,并將這個值發送給客戶端。然后,客戶端也需要根據服務器發來的初始值計算出確認號發送給服務器。
至此:連接建立完成,接下來就可以進入數據收發階段了。
數據收發階段中,通信雙方可以同時發送請求和響應,雙方也可以同時對請求進行確認。
請求 - 確認機制非常強大:通過這一機制,我們可以確認接收方有沒有收到某個包,如果沒有收到則重新發送,這樣一來,但凡網絡中出現的任何錯誤,我們都可以即使發現并補救。
上面的文字不夠生動,動畫可以更好的理解請求 - 確認機制:
網卡、集線器、路由器(見《史上最通俗的集線器、交換機、路由器功能原理入門》)都沒有錯誤補救機制,一旦檢測到錯誤就會直接丟棄數據包,應用程序也沒有這種機制,起作用的只是 TCP/IP 模塊。
由于網絡環境復雜多變,所以數據包會存在丟失情況,因此發送序號和確認號也存在一定規則,TCP 會通過窗口管理確認號,我們這篇文章不再贅述,大家可以閱讀《通俗易懂-深入理解TCP協議(下):RTT、滑動窗口、擁塞處理》來尋找答案。
PS:另一篇《我們在讀寫Socket時,究竟在讀寫什么?》中用動畫詳細說明了這個過程,有興趣可以讀一讀。
6、Socket 是如何斷開連接的
當通信雙方不再需要收發數據時,需要斷開連接。不同的應用程序斷開連接的時機不同。
以 Web 為例:瀏覽器向 Web 服務器發送請求消息,Web 服務器再返回響應消息,這時收發數據就全部結束了,服務器可能會首先發起斷開響應,當然客戶端也有可能會首先發起(誰先斷開連接是應用程序做出的判斷),與協議棧無關。

無論哪一方發起斷開連接的請求,都會調用 Socket 庫的 close 程序。
我們以服務器斷開連接為例:服務器發起斷開連接請求,協議棧會生成斷開連接的 TCP 頭部,其實就是設置 FIN 位,然后委托 IP 模塊向客戶端發送數據,與此同時,服務器的Socket會記錄下斷開連接的相關信息。
收到服務器發來 FIN 請求后:客戶端協議棧會將Socket標記為斷開連接狀態,然后,客戶端會向服務器返回一個確認號,這是斷開連接的第一步,在這一步之后,應用程序還會調用 read 來讀取數據。等到服務器數據發送完成后,協議棧會通知客戶端應用程序數據已經接收完畢。
只要收到服務器返回的所有數據,客戶端就會調用 close 程序來結束收發操作,這時客戶端會生成一個 FIN 發送給服務器,一段時間后服務器返回 ACK 號。至此,客戶端和服務器的通信就結束了。
上面的文字不夠生動,動畫可以更好的說明這個過程:
PS:斷開連接的詳細理論知識,可以閱讀《理論經典:TCP協議的3次握手與4次揮手過程詳解》、《跟著動畫來學TCP三次握手和四次揮手》,這里不再贅述。
7、Socket的刪除
上述通信過程完成后,用來通信的Socket就不再會使用了,此時我們就可以刪除這個Socket了。
不過,這時候Socket不會馬上刪除,而是等過一段時間再刪除。
等待這段時間是為了防止誤操作,最常見的誤操作就是客戶端返回的確認號丟失,至于等待多長時間,和數據包重傳的方式有關,這里我們就深入展開討論了。
關于Socket操作的全過程,如果從系統的角度來看,可能會更深入一些,建議可以深入閱讀張彥飛的《深入操作系統,從內核理解網絡包的接收過程(Linux篇)》一文。
8、系列文章
本文是系列文章中的第14篇,本系列文章的大綱如下:
[1] 網絡編程懶人入門(一):快速理解網絡通信協議(上篇)
[2] 網絡編程懶人入門(二):快速理解網絡通信協議(下篇)
[3] 網絡編程懶人入門(三):快速理解TCP協議一篇就夠
[4] 網絡編程懶人入門(四):快速理解TCP和UDP的差異
[5] 網絡編程懶人入門(五):快速理解為什么說UDP有時比TCP更有優勢
[6] 網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門
[7] 網絡編程懶人入門(七):深入淺出,全面理解HTTP協議
[8] 網絡編程懶人入門(八):手把手教你寫基于TCP的Socket長連接
[9] 網絡編程懶人入門(九):通俗講解,有了IP地址,為何還要用MAC地址?
[10] 網絡編程懶人入門(十):一泡尿的時間,快速讀懂QUIC協議
[11] 網絡編程懶人入門(十一):一文讀懂什么是IPv6
[12] 網絡編程懶人入門(十二):快速讀懂Http/3協議,一篇就夠!
[13] 網絡編程懶人入門(十三):一泡尿的時間,快速搞懂TCP和UDP的區別
[14] 網絡編程懶人入門(十四):到底什么是Socket?一文即懂!(* 本文)
9、參考資料
[1] TCP/IP詳解 - 第17章·TCP:傳輸控制協議
[2] TCP/IP詳解 - 第18章·TCP連接的建立與終止
[3] TCP/IP詳解 - 第21章·TCP的超時與重傳
[4] 快速理解網絡通信協議(上篇)
[5] 快速理解網絡通信協議(下篇)
[6] 面視必備,史上最通俗計算機網絡分層詳解
[7] 假如你來設計網絡,會怎么做?
[8] 假如你來設計TCP協議,會怎么做?
[10] 淺析TCP協議中的疑難雜癥(下篇)
[11] 關閉TCP連接時為什么會TIME_WAIT、CLOSE_WAIT
[12] 從底層入手,深度分析TCP連接耗時的秘密
(本文已同步發布于:http://www.52im.net/thread-3821-1-1.html)