前言
最近在整理網路抓包分析相關的資料,同時又在閱讀《網路是怎樣連線的》。上一篇從網路協議層對裝置連網的過程和傳送資料的過程進行了探討。本篇討論的是TCP協議的資料收發的過程。
在討論本篇文章時,假設讀者對TCP協議有一定了解。
建立Socket
由於TCP協議是需要建立連線的,在建立連線之前,需要先初始化Socket。在初始化Socket時,作業系統分配一片記憶體空間儲存該Socket的必要資訊。這些資訊除了傳輸資料時需要TCP頭部的資訊以外,還包括作業系統TCP協議需要用的SO_REUSEADDR
、TCP_NODELAY
等TCP控制引數。
建立完成後,就會返回一個Socket描述符。這個描述符相當於一個唯一的Socket識別號,通過這個描述符就能從作業系統獲取到Socket實體物件。
建立連線
我們知道TCP建立連線三次握手實際是為了交換客戶端與服務端必要的資訊,或者可以稱之為協商。主要協商的資訊包括雙方的IP和埠號、接收緩衝區大小、以及其他TCP控制引數(如ECN:顯式擁塞通告,SACK:選擇確認選項等)。
在建立Socket時,還不知道需要和誰傳輸資料,僅初始化了一個Socket結構。在建立連線時,我們需要告訴底層作業系統,需要連線的目標地址。就好比寄快遞時,我們需要在快遞單寫上收件人的地址和以及收件人資訊。
初始化緩衝區
在執行收發資料前,我們需要分配一個空間用於臨時存放資料,這個空間被稱為緩衝區。接收資料有接收資料緩衝區,傳送資料有傳送資料緩衝區。這兩個緩衝區也是在建立連線的時候分配的。
DNS解析器和ARP協議
作業系統支援我們直接傳遞一個明確的IP地址或者傳入一個域名。如果傳入的是域名,作業系統會呼叫DNS解析器。DNS解析器會通過DNS協議去查詢域名對應的IP,這個過程就是我們上一篇文章將的DNS協議部分。
當然如果我們還需要知道DNS的MAC地址,如果作業系統底層的ARP快取沒有DNS的IP所對應的MAC地址記錄,還需要通過ARP協議進行廣播獲取DNS伺服器的IP所對應的MAC地址。當獲取到該MAC地址後,作業系統就會將DNS的IP和MAC地址儲存到ARP快取中,下一次再呼叫DNS解析器時,就可以直接與DNS伺服器進行通訊了。
這整個過程就像一個黑盒一樣,應用層和開發人員完全無需干預。TCP協議的分層及作業系統提供的介面大大降低了開發人員的工作量,甚至無需深入瞭解TCP協議就能開發出一套網路通訊程式。就好比快遞員只要會開車,無需瞭解汽車內部結構一樣。
三次握手
當我們獲取到了需要連線的目的地址的真實IP時,就可以建立TCP連線了。建立連線需要三次握手,過程如下。
上圖看著是非常簡單,互動3次成功建立連線。實際這個過程中還有許多細節。在傳送第一個SYN包以前(客戶端傳送到服務端的第一個請求,TCP協議的Flags欄位中將SYC設定成了1)。作業系統需要獲取到TCP頭部所需要的控制資訊。
在建立連線時,作業系統需要從動態埠池中獲取一個可用的埠,將埠號資訊儲存到Socket物件的記憶體中。然後將頭部的控制位SYN設定為1,表示建立連線。接著還需要獲取作業系統的接收緩衝區大小,將其儲存到TCP頭部的視窗大小欄位中,當然傳送序號和確認序號目前都為0。
當TCP頭部資訊填寫完成後,作業系統就將TCP協議的資料傳遞給IP模組。IP模組執行網路包傳送之前會對資料包填充IP頭部,若有需要則對資料包進行分片,這個過程後面在討論。
獲取傳送方IP
若客戶端只有一個網路卡,傳送方IP就是這個網路卡的IP。但是如果有多個網路卡,那麼作業系統就需要選擇一個合適的網路卡作為傳送網路卡。IP模組通過路由表來判斷哪條路由能夠匹配,從而選擇一條合適的路由,最後就可以知道應該選擇哪個網路卡。獲取該網路卡的IP儲存到IP頭部的源IP地址中,將目標IP儲存到IP頭部的目標IP地址中。IP模組新增IP頭部完成之後,繼續新增MAC頭部(MAC頭部實際也是IP協議新增的)。
獲取目的MAC地址
IP模組需要獲取到目的裝置的MAC地址。和前面DNS討論的一樣,若ARP快取中有記錄,無需查詢,若沒有記錄則會通過ARP協議查詢目的裝置的MAC地址,如果目的裝置與當前裝置在同一個區域網內,則會獲取到目的裝置的MAC地址。如果目的裝置與當前裝置不在一個區域網,則會查詢到下一個路由器的MAC地址。具體MAC地址是誰的IP模組並不關心,他的任務就是獲取到目的MAC地址並將其填入到MAC頭部。
從理論上來說MAC地址是屬於乙太網的範疇,不應該有IP模組負責,但實際上,網路卡只負責傳送和接收而不參與拼包拆包,這樣就能夠相容各種型別的包。
傳送方的MAC地址就是網路卡的MAC地址,這個值是在網路卡生產時寫入到網路卡的ROM中的,只要將其讀取出來填入到MAC頭部的傳送端MAC地址欄位中即可。
經過以上步驟之後,一個完整的建立連線的SYN包就建立完成了。接下來就要將這個包傳送到對端。傳送SYN的過程和傳送資料的過程一樣,具體的傳送邏輯在傳送資料一節在做具體說明。
客戶端傳送SYN包給服務端時,服務端會返回SYN+ACK包,在這個包中也會將自己的接收緩衝區的大小填寫到TCP頭部的視窗大小欄位中。這樣客戶端傳送資料時就可以儘量避免傳送的在途包超過接收方的接收緩衝區導致丟包重傳。
當連線建立完成時,接下來雙方就可以收發資料了。
傳送資料
在討論網路卡傳送資料之前,我們先看看應用層呼叫傳送資料時發生了什麼。
應用程式呼叫作業系統傳送資料的介面時,並不是直接將資料傳送到出去,而是先將資料儲存到socket的傳送緩衝區中。具體何時傳送資料TCP協議棧決定的。
當傳送緩衝區有資料要傳送時,首先TCP協議棧會根據MSS對需要傳送的資料進行分段。分段的目的是避免資料超過MTU大小導致資料被IP層分片。
MSS(Maximum Segment Size)表示最大段長度,MTU(Maximum Transmission Unit)表示最大傳輸單元。MTU是乙太網是一個網路包的最大長度,乙太網中預設是1500。當資料包超過MTU時,IP模組就會對資料進行分片,每一片都會有自己的TCP頭部和IP頭部。
MTU=MSS+IP頭部+TCP頭部
Nagle演算法
當Socket配置了Nagle演算法時,當Socket的傳送緩衝區短時間內有大量的小包(資料長度小於MSS),則作業系統不會立即為每個包新增TCP頭部和IP頭部,而是會嘗試等待一段時間以便一次性將多個資料進行合併成一個大包傳送。這樣能夠提升頻寬利用率,但是也可能會造成資料的短暫延遲傳送。因此大多數情況下,我們通常會禁用Nagle演算法。
TCP VS UDP
眾所周知UDP效能比TCP效能高得多,主要原因就是TCP為了保證可靠性做了大量的工作,如建立連線、重傳、擁塞避免、慢啟動等。但是為什麼許多協議仍然選擇TCP而不是UDP呢?一個重要的原因就是傳輸小包資料適合使用UDP,比如DNS協議、DHCP協議。這些協議能夠保證資料在乙太網傳輸時小於MTU避免被IP層分片。在複雜的網路環境中,無法得知中間某個裝置的配置,若資料超過裝置MTU而裝置又設定了DF標記(Don't Frgment,不分片)時就會將資料包丟棄。大多數應用層協議的資料很可能會超過MTU,使用TCP協議就可以通過分段來避免IP層分片。TCP分段資料丟失時TCP有超時重傳、快速重傳等機制保證資料的可靠性。而IP層沒有這個機制,同時IP層分片後即使一個片丟失也必須重傳所有分片。而TCP層資料丟失僅需重傳丟失的資料即可(SACK機制)。
網路卡傳送資料
現在需要傳送的資料包已經加上了TCP頭部、IP頭部以及MAC頭部。接下來就可以將資料交給網路卡將資料轉換為電訊號或光訊號在網線上傳輸了。IP模組執行完成之後就會呼叫網路卡驅動執行傳送資料,網路卡驅動從IP模組獲取到資料包後,會將其複製到網路卡的緩衝區中,然後會在包的開頭和結尾新增報頭和起始幀分界符,在包尾新增用於檢測錯誤的幀校驗序列(FCS)。
報頭是由共計56個位元的01
,起始幀分界符是10101011
,網路卡通過報頭和起始幀分界符來判斷幀的位置。具體如何判斷這裡不做具體討論。
ACK
當傳送到對端時,並沒有結束,TCP協議通過ACK確認機制保證資料可靠。當接收端收到資料時,需要返回一個ACK包給客戶端來通知自己收到的資料包長度。傳送資料時TCP首部有幾個關鍵引數,SEQ表示傳送的資料起始偏移量,ACK表示確認收到對端的資料長度。當客戶端傳送的SEQ=1461,資料長度是1460時,服務端若確認已收到SEQ小於1461以前所有的包和當前包時,就需要返回一個ACK包,該包的TCP頭部的ACK值為SEQ+資料長度,也就是2921。這樣客戶端就能確認服務端是否收到了資料,如果沒有收到資料,可通過重傳和快速重傳等機制重傳資料。具體重傳邏輯這裡不進行詳細討論。
接收資料
接下來我們看接收資料時發生了什麼。當網路卡接收到網路上的資料時,會將資料從電訊號或光訊號轉換為數字訊號(也就是0和1),當網路卡判斷到包尾時,會通過CRC演算法計算出錯誤校驗碼和包尾的FCS進行對比,若不一致,則說明資料傳輸時發生了錯誤,則丟棄,客戶端最終通過重傳機制重發。若FCS校驗通過,還會校驗一下資料包的接收方MAC地址和當前網路卡的MAC地址是否一致,若不一致則會丟棄,如果一致則會將資料包存放到網路卡的接收緩衝區中。然後就會通過計算機的中斷機制通知計算機收到了資料。
中斷
首先,網路卡向擴充套件匯流排中的中斷訊號線傳送訊號,該訊號線通過計算機中的中斷控制器連線到CPU。當產生中斷訊號時,CPU會暫時掛起正在處理的任務,切換到作業系統中的中斷處理程式。(由於中斷例程的優先順序較高,如果不先處理網路卡緩衝區的資料,網路卡緩衝區的資料很快就會滿,從而造成丟包)然後,中斷處理程式會呼叫網路卡驅動,控制網路卡執行相應的接收操作。中斷是有編號的,網路卡在安裝的時候就在硬體中設定了中斷號,在中斷處理程式中則將硬體的中斷號和相應的驅動程式繫結。因此哪個網路卡收到了資料就會呼叫哪個中斷處理程式從而呼叫到對應的網路卡驅動。
網路卡驅動被中斷處理程式呼叫後,就會從網路卡的緩衝區中獲取到接收到的資料包,並通過MAC頭部判斷網路層的協議棧(目前通常是TCP/IP協議),網路卡驅動就會把包交給對應的協議棧處理。從這裡可以看到網路卡並不關心資料包的內容是什麼,它只需要校驗資料是發給自己的,而且資料傳輸過程中沒有錯誤,就可以放入到緩衝區中。
值得一提的是,從網路卡驅動緩衝區獲取接收的資料包是一個I/O操作,現代作業系統通常不會讓CPU直接從網路卡緩衝區拷貝資料(這樣會佔用大量的CPU執行I/O操作),而是向網路卡的DMA控制器傳送一個讀指令,DMA控制器會從網路卡緩衝區讀取資料到記憶體中,當DMA控制器讀取資料完成時也會通過中斷通知CPU執行後續操作,這樣就能極大的提升CPU的使用效率。
對於I/O中斷可以看下我另一篇部落格I/O中斷原理
IP模組接收資料
當IP模組獲取到接收的資料時,首先要判斷接收者是不是自己。因此會獲取到目的IP地址和當前IP地址比較是否一致。如果不一致,若伺服器配置了路由功能則會轉發到目標IP,否則IP模組就會通過ICMP訊息將錯誤告知對方並丟棄資料包。如果收到的IP和當前IP一致,則需要進行一個分片重組過程。首先判斷資料包是否發生了分片。如果發生了分片,則需要將分片包還原。分片包的IP頭部的唯一識別符號是一樣的,每個分片包會有自己的分片偏移量。
當接收到所有分片後就可以進行重組還原成一個完整包,IP模組的任務就結束了,接下來就將資料包交給TCP模組。TCP模組會根據收方和傳送方的IP和埠查詢到對應的套接字,並根據套接字的狀態執行響應的操作。
TCP模組接收資料
若接收到的是應用程式資料,則返回ACK,然後將資料放入socket的接收緩衝區中,等待應用來讀取(如果是非同步IO則會傳送一個完成通知到完成佇列,從而觸發應用程式讀取資料);如果是連線或關閉連線的控制包,則會返回對應的響應控制包(如SYN或FIN等),並告知應用程式的連線和關閉連線的操作狀態。
順便提一下,若作業系統開啟了延遲確認功能,則會延遲一段時間(windows下時200ms)返回ACK。
斷開連線
TCP斷開連線通過四次揮手,由於TCP協議是全雙工的,因此雙方互相傳送FIN包以及返回ACK包,斷開雙向的連線。TCP協議也支援只斷開單向連線,被稱為半連線。Windows和Linux許多管道機制都是通過socket的半連線實現的。
客戶端再關閉連線後並不會立即釋放socket資源,而是進入TIME_WAIT狀態等待2MSL時間以後再釋放客戶端的Socket資源。
1個MSL是2分鐘,所以也就是4分鐘。
主要原因是客戶端的最後一個ACK可能丟包,此時服務端會重傳FIN包,若客戶端不等待一段時間才釋放資源而是立即釋放。新建立的Socket可能恰好又使用了相同的斷開,這時候接收到了服務端的FIN包就會立即錯誤的關閉了新的連線。
結語
通過兩篇文章分別從網路協議和作業系統的協議棧2個方面的處理過程對網路資料傳輸的過程進行了討論。一些更細節的東西本篇並沒有說明,比如路由器、交換機等網路傳輸的具體過程。網路卡內部各模組的處理細節等。網路卡更細的內容大部分開發者無需瞭解,但是作業系統協議棧的處理過程能夠幫助開發者理解自己所開發的網路應用到底是如何處理的。
參考文獻
- 《網路是怎樣連線的》
- 《Wireshark資料包分析實戰詳解》
- I/O中斷原理
微信掃一掃二維碼關注訂閱號傑哥技術分享
出處:https://www.cnblogs.com/Jack-Blog/p/13426728.html
作者:傑哥很忙
本文使用「CC BY 4.0」創作共享協議。歡迎轉載,請在明顯位置給出出處及連結。