關於對 Socket 的認識,大致分為下面幾個主題,Socket 是什麼,Socket 是如何建立的,Socket 是如何連線並收發資料的,Socket 套接字的刪除等。
Socket 是什麼以及建立過程
一個資料包經由應用程式產生,進入到協議棧中進行各種報文頭的包裝,然後作業系統呼叫網路卡驅動程式指揮硬體,把資料傳送到對端主機。整個過程的大體的圖示如下。
我們大家知道,協議棧其實是位於作業系統中的一些協議的堆疊,這些協議包括 TCP、UDP、ARP、ICMP、IP等。通常某個協議的設計都是為了解決某些問題,比如 TCP 的設計就負責安全可靠的傳輸資料,UDP 設計就是報文小,傳輸效率高,ARP 的設計是能夠通過 IP 地址查詢物理(Mac)地址,ICMP 的設計目的是返回錯誤報文給主機,IP 設計的目的是為了實現大規模主機的互聯互通。
應用程式比如瀏覽器、電子郵件、檔案傳輸伺服器等產生的資料,會通過傳輸層協議進行傳輸,而應用程式是不會和傳輸層直接建立聯絡的,而是有一個能夠連線應用層和傳輸層之間的套件,這個套件就是 Socket
。
在上面這幅圖中,應用程式包含 Socket 和解析器,解析器的作用就是向 DNS 伺服器發起查詢,查詢目標 IP 地址。
應用程式的下面就是作業系統內部,作業系統內部包括協議棧,協議棧是一系列協議的堆疊。作業系統下面就是網路卡驅動程式,網路卡驅動程式負責控制網路卡硬體,驅動程式驅動網路卡硬體完成收發工作。
在作業系統內部有一塊用於存放控制資訊的儲存空間,這塊儲存空間記錄了用於控制通訊的控制資訊。其實這些控制資訊就是 Socket 的實體,或者說存放控制資訊的記憶體空間就是套接字的實體。
這裡大家有可能不太清楚所以然,所以我用了一下 netstat 命令來給大夥看一下套接字是啥玩意。
我們在 Windows 的命令提示符中輸入
netstat -ano
# netstat 用於顯示套接字內容 , -ano 是可選選項
# a 不僅顯示正在通訊的套接字,還顯示包括尚未開始通訊等狀態的所有套接字
# n 顯示 IP 地址和埠號
# o 顯示套接字的程式 PID
我的計算機會出現下面結果。
圖中的每一行都相當於一個套接字,每一列也被稱為一個元組,所以一個套接字就是五元組(協議、本地地址、外部地址、狀態、PID)。有的時候也被叫做四元組,四元組不包括協議。
比如圖中的第一行,它的協議就是 TCP,本地地址和遠端地址都是 0.0.0.0,這表示通訊還沒有開始,IP 地址暫時還未確定,而本地埠已知是 135,但是遠端埠還未知,此時的狀態是 LISTENING
,LISTENING 表示應用程式已經開啟,正在等待與遠端主機建立連線(關於各種狀態之間的轉換,大家可以閱讀筆者的這篇文章 TCP ,丫的終於來了!!)最後一個元組是 PID,即程式識別符號,PID 就像我們的身份證號碼,能夠精確定位唯一的程式。
現在你可能對 Socket 有了一個基本的認識,現在喝口水,休息一下,讓我們繼續探究 Socket。
現在我有個問題,Socket 是如何建立的呢?
Socket 是和應用程式一起建立的。應用程式中有一個 socket 元件,在應用程式啟動時,會呼叫 socket 申請建立套接字,協議棧會根據應用程式的申請建立套接字:首先分配一個套接字所需的記憶體空間,這一步相當於是為控制資訊準備一個容器,但只有容器並沒有實際作用,所以你還需要向容器中放入控制資訊;如果你不申請建立套接字所需要的記憶體空間,你建立的控制資訊也沒有地方存放,所以分配記憶體空間,放入控制資訊缺一不可。至此套接字的建立就已經完成了。
套接字建立完成後,會返回一個套接字描述符給應用程式,這個描述符相當於是區分不同套接字的號碼牌。根據這個描述符,應用程式在委託協議棧收發資料時就需要提供這個描述符。
套接字連線
套接字建立完成後,最終還是為資料收發服務的,在資料收發之前,還需要進行一步 connect
,也就是建立連線的過程。這個連線並不是真實的連線:用一根水管插在兩個電腦之間。
而是應用程式通過 TCP/IP 協議標準從一個主機通過網路介質傳輸到另一個主機的過程。
套接字剛剛建立完成後,還沒有資料,也不知道通訊物件。在這種狀態下,即使你讓客戶端應用程式委託協議棧傳送資料,它也不知道傳送到哪裡。所以瀏覽器需要根據網址來查詢伺服器的 IP 地址,做這項工作的協議是 DNS,查詢到目標主機後,再把目標主機的 IP 告訴協議棧,至此,客戶端這邊就準備好了。
在伺服器上,與客戶端一樣也需要建立套接字,但是同樣的它也不知道通訊物件是誰,所以我們需要讓客戶端向伺服器告知客戶端的必要資訊:IP 地址和埠號。
現在通訊雙方建立連線的必要資訊已經具備,只欠一股東南風了。通訊雙方收到資料之後,還需要一塊位置
來存放,這個位置就是緩衝區,它是記憶體的一部分,有了緩衝區,就能夠進行資料的收發操作了。
OK,現在客戶端想要給伺服器傳送一條資料,該進行哪些操作呢?
首先,客戶端應用程式需要呼叫 Socket
庫中的 connect 方法,提供 socket 描述符和伺服器 IP 地址、埠號。
connect(<描述符>、<伺服器IP地址和埠號>)
這些資訊會傳遞給協議棧中的 TCP 模組,TCP 模組會對請求報文進行封裝,再傳遞給 IP 模組,進行 IP 報文頭的封裝,然後傳遞給物理層,進行幀頭封裝,之後通過網路介質傳遞給伺服器,伺服器上會對幀頭、IP 模組、TCP 模組的報文頭進行解析,從而找到對應的套接字,套接字收到請求後,會寫入相應的資訊,並且把狀態改為正在連線。請求過程完成後,伺服器的 TCP 模組會返回響應,這個過程和客戶端是一樣的(如果大家不太清楚報文頭的封裝過程,可以閱讀筆者的這篇文章 TCP/IP 基礎知識總結)
在一個完整的請求和響應過程中,控制資訊起到非常關鍵的作用(具體的作用我們後面會說)。
- SYN 就是同步的縮寫,客戶端會首先傳送 SYN 資料包,請求服務端建立連線。
- ACK 就是相應的意思,它是對傳送 SYN 資料包的響應。
- FIN 是終止的意思,它表示客戶端/伺服器想要終止連線。
由於網路環境的複雜多變,經常會存在資料包丟失的情況,所以雙方通訊時需要相互確認對方的資料包是否已經到達,而判斷的標準就是 ACK 的值。
(通訊雙方連線的建立會經過三次握手流程,對三次握手詳細的介紹可以閱讀筆者的這篇文章 TCP 基礎知識)
當所有建立連線的報文都能夠正常收發之後,此時套接字就已經進入可收發狀態了,此時可以認為用一根管理把兩個套接字連線了起來。當然,實際上並不存在這個管子。建立連線之後,協議棧的連線操作就結束了,也就是說 connect 已經執行完畢,控制流程被交回給應用程式。
收發資料
當控制流程從 connect 回到應用程式之後,接下來就會直接進入資料收發階段,資料收發操作是從應用程式呼叫 write 將要傳送的資料交給協議棧開始的,協議棧收到資料之後執行傳送操作。
協議棧不會關心應用程式傳輸過來的是什麼資料,因為這些資料最終都會轉換為二進位制序列,協議棧在收到資料之後並不會馬上把資料傳送出去,而是會將資料放在傳送緩衝區,再等待應用程式傳送下一條資料。
為什麼收到資料包不會直接傳送出去,而是放在緩衝區中呢?
因為只要一旦收到資料就會傳送,就有可能傳送大量的小資料包,導致網路效率下降。所以協議棧需要將資料積攢到一定數量才能將其傳送出去。至於協議棧會向緩衝區放多少資料,這個不同版本和種類的作業系統有不同的說法,不過,所有的作業系統和種類都會遵循下面這幾個標準:
- 第一個判斷要素是每個網路包能夠容納的資料長度,判斷的標準是
MTU
,它表示的是一個網路包的最大長度。最大長度包含頭部,所以如果單論資料區的話,就會用 MTU - 包頭長度,由此的出來的最大資料長度被稱為MSS
。
- 另一個判斷標準是時間,當應用程式產生的資料比較少,協議棧向緩衝區放置資料效率不高時,如果每次都等到 MSS 再傳送的話,可能因為等待時間太長造成延遲,在這種情況下,即使資料長度沒有到達 MSS,也應該把資料傳送出去。
協議棧並沒有告訴我們怎樣平衡這兩個因素,如果資料長度優先,那麼效率有可能比較低;如果時間優先,那又會降低網路的效率。
經過了一段時間。。。。。。
<img src="https://tva1.sinaimg.cn/large/008i3skNly1gxdjgvl9iqg30m80godky.gif" alt="img" style="zoom: 25%;" />
假設我們使用的是長度有限法則,此時緩衝區已滿,協議棧要傳送資料了,協議棧剛要把資料傳送出去,卻發現無法一次性傳輸這麼大資料量(相對的)的資料,那怎麼辦呢?
在這種情況下,傳送緩衝區中的資料就會超過 MSS 的長度,傳送緩衝區中的資料會以 MSS 大小為一個資料包進行拆分,拆分出來的每塊資料都會加上 TCP,IP,乙太網頭部,然後被放進單獨的網路包中。
到現在,網路包已經準備好發往伺服器了,但是資料傳送操作還沒有結束,因為伺服器還未確認是否已經收到網路包。因此在客戶端傳送資料包之後,還需要伺服器進行確認。
TCP 模組在拆分資料時,會計算出網路包偏移量,這個偏移量就是相對於資料從頭開始計算的第幾個位元組,並將算好的位元組數寫在 TCP 頭部,TCP 模組還會生成一個網路包的序號(SYN),這個序號是唯一的,這個序號就是用來讓伺服器進行確認的。
伺服器會對客戶端傳送過來的資料包進行確認,確認無誤之後,伺服器會生成一個序號和確認號(ACK)並一起傳送給客戶端,客戶端確認之後再傳送確認號給伺服器。
我們來看一下實際的工作過程。
首先,客戶端在連線時需要計算出序號初始值,並將這個值傳送給伺服器。接下來,伺服器通過這個初始值計算出 確認號並返回給客戶端。初始值在通訊過程中有可能會丟棄,因此當伺服器收到初始值後需要返回確認號用於確認。同時,伺服器也需要計算出從伺服器到客戶端方向的序號初始值,並將這個值傳送給客戶端。然後,客戶端也需要根據伺服器發來的初始值計算出確認號傳送給伺服器,至此,連線建立完成,接下來就可以進入資料收發階段了。
資料收發階段中,通訊雙方可以同時傳送請求和響應,雙方也可以同時對請求進行確認。
請求 - 確認機制非常強大,通過這一機制,我們可以確認接收方有沒有收到某個包,如果沒有收到則重新傳送,這樣一來,但凡網路中出現的任何錯誤,我們都可以即使發現並補救。
網路卡、集線器、路由器都沒有錯誤補救機制,一旦檢測到錯誤就會直接丟棄資料包,應用程式也沒有這種機制,起作用的只是 TCP/IP 模組。
由於網路環境複雜多變,所以資料包會存在丟失情況,因此傳送序號和確認號也存在一定規則,TCP 會通過視窗管理確認號,我們這篇文章不再贅述,大家可以閱讀筆者的這篇文章 TCP 基礎知識 來尋找答案。
斷開連線
當通訊雙方不再需要收發資料時,需要斷開連線。不同的應用程式斷開連線的時機不同。以 Web 為例,瀏覽器向 Web 伺服器傳送請求訊息,Web 伺服器再返回響應訊息,這時收發資料就全部結束了,伺服器可能會首先發起斷開響應,當然客戶端也有可能會首先發起(誰先斷開連線是應用程式做出的判斷),與協議棧無關。
無論哪一方發起斷開連線的請求,都會呼叫 Socket 庫的 close 程式。我們以伺服器斷開連線為例,伺服器發起斷開連線請求,協議棧會生成斷開連線的 TCP 頭部,其實就是設定 FIN 位,然後委託 IP 模組向客戶端傳送資料,與此同時,伺服器的套接字會記錄下斷開連線的相關資訊。
收到伺服器發來 FIN 請求後,客戶端協議棧會將套接字標記為斷開連線狀態,然後,客戶端會向伺服器返回一個確認號,這是斷開連線的第一步,在這一步之後,應用程式還會呼叫 read 來讀取資料。等到伺服器資料傳送完成後,協議棧會通知客戶端應用程式資料已經接收完畢。
只要收到伺服器返回的所有資料,客戶端就會呼叫 close 程式來結束收發操作,這時客戶端會生成一個 FIN 傳送給伺服器,一段時間後伺服器返回 ACK 號,至此,客戶端和伺服器的通訊就結束了。
刪除套接字
通訊完成後,用來通訊的套接字就不再會使用了,此時我們就可以刪除這個套接字了。不過,這時候套接字不會馬上刪除,而是等過一段時間再刪除。
等待這段時間是為了防止誤操作,最常見的誤操作就是客戶端返回的確認號丟失,至於等待多長時間,和資料包重傳的方式有關。
原文連結:原來這才是 Socket!