1. 程式人生 > >收發資料的原理(下)

收發資料的原理(下)

前言:網路知識非常的重要,如果你不是做程式的,那麼一些網路常識還是得知道的;而做程式的,就更不用說了,不僅需要了解一些網路知識,還是知道其原理,如果不瞭解原理,不敢說他不是程式設計師,但是總缺了點意思,就像去北京沒去過長城一樣。

網路原理系列文章:

一、五分鐘瞭解網路連線(已完成)

二、收發資料的原理(上)(已完成)

三、收發資料的原理(下)(已完成)

四、收發資料的番外篇(未完成)

因為網路原理不是三言兩語可以講完,如果讀者很忙,可以直接拉到最底下,看總結,知道個大概,再回頭細讀此文章。感謝關注。廢話不多說,直接進入主題。在上篇我們已經講了TCP收發資料的前兩步,接下來是最後兩步。

將HTTP訊息傳給協議棧

上篇講到控制流程從 connect 回到應用程式之後,就到了資料收發階段。 資料收發資料是從應用程式呼叫write將要傳送的資料交給協議棧開始的,協議棧收到資料後執行傳送操作,這一操作包含如下要點。

首先,協議棧並不關心應用程式傳來的資料是什麼內容。應用程式呼叫write時會指定傳送資料的長度,在協議棧看來,要傳送的資料資料就是一定長度的二進位制位元組序列而已。

其次並不是一收到資料就馬上傳送出去,而是會將資料存放在內部的傳送緩衝區中,並且繼續等下一段資料。不過應用程式交給協議棧傳送的資料長度是由應用程式本身決定,有些應用程式會一次性傳遞所有的資料,有些程式則會逐位元組或者逐行傳遞資料。

總之,一次將多少資料交給協議棧是由應用程式決定的,協議棧沒有這個控制行為。

協議棧之所以不一收到資料就發出去,是因為那樣可能會發送大量的小包,導致網路效率下降。至於積累多少資料才傳送,有以下兩個要素判斷。

第一,每個網路包能容納的資料長度。協議棧會根據一個叫做MTU的引數來進行判斷。MTU表示一個網路包的最大長度,在乙太網中一般是1500位元組。MTU包含了頭部的總長度,所以MTU減去頭部長度才是一個網路包所能容納的最大資料長度,這一長度叫做MSS。當協議棧收到的長度大於或者接近MSS時傳送出去,就很好的解決大量小包的問題。

MTU表示一個網路包的最大長度,在乙太網中一般是1500位元組。MTU包含了頭部的總長度,MTU = MSS + 頭部,所以MSS是一個網路包所能容納的最大資料長度。

第二,等待時間。當應用程式傳送資料頻率不高的時候,協議棧收到的資料要接近MSS,可能要等非常久,而造成傳送延遲,所以在這種情況下,即時緩衝區的資料沒接到MSS,都發送出去。協議棧內部裡面有計時器,經過一定時間,就會把網路包傳送出去。

協議棧內部裡面有計時器,經過一定時間,就會把網路包傳送出去。

讀者可以發現,其實這兩個判斷要素是相互矛盾的。如果長度優先,網路效率會提高,但可能因為等待而產生髮送延遲;相反,時間優先,則會降低網路效率,但延遲時間減少。所以這兩個要素要綜合考慮,以達到平衡。這個平衡由協議棧的開發者來決定,所以不同種類和版本的作業系統在相關操作上也就存在差異。當然應用程式在傳送資料時,可以指定傳送選項,比如說讓網路包直接傳送,不用存在緩衝區了。

對較大資料進行拆分

HTTP請求訊息一般不會很長,一個網路就可裝下,但如果要傳送一張圖片或者傳送一篇長文呢,傳送緩衝區的資料肯定超過MSS的長度。這時,我們除了不等到後面的資料,還要對現有資料進行拆分,拆分的每塊資料會放進每個單獨的網路包。

上一篇也講過,傳送資料前,要在每一塊資料新增TCP頭部,並根據套接字中包含的通訊物件的資訊(傳送方和接收方的埠號),然後交給IP模組處理髮送操作,IP模組會在每個網路包前面新增IP頭部和乙太網頭部,具體操作,後面再講。

網路錯誤檢測和補償機制

網路以及其他環境很複雜,收發資料時,難免會在傳送中出現錯誤,所以需要檢測和補償機制。

網路包發往伺服器,需要確認對方是否收到網路包,對方沒收到時及時重發。那麼確認原理是什麼?

TCP模組在拆分資料時,會算好每一塊資料相當於從頭開始的第幾個位元組,接下來在傳送此塊資料,會將算好的位元組數寫在TCP頭部中,上一篇中說到的seq作用就在這裡。然後告知接收方資料長度,但是資料長度不是通過TCP頭部傳輸,因為接收方可以通過整個網路包的長度減去頭部長度得出。所以,我們可以知道傳送的資料是從第幾個位元組開始,長度是多少。

通過上面兩個數值,接收方還可以檢查收到的網路包有沒遺漏。比如:上次接收到第1120位元組,如果接下來收到序號是第1121的包,則表示沒有遺漏。收到第2200位元組,則有包遺漏了。如果確認沒有遺漏,接收方會將到目前為止接收到的資料長度加起來,計算出一共已經收到了多少個位元組,然後將這個數值寫入TCP頭部的ACK號中傳送給傳送方,同時將控制位中的ACK位元設為1,這代表ACK號欄位有效。返回ACK號這一操作稱作確認響應。

有個需要注意的是,seq序號不是從1開始,因為從1開始,很容易被猜到,被攻擊者發動攻擊。所以seq序號初始值是用隨機數算出來,開始收發資料前需要告知通訊物件序號初始值。上文講到連線過程中,有一個將SYN控制位設為1併發送給伺服器的操作,就是在這一步將序號的初始值告知對方的。實際上,在將SYN設為1的同時,還需要同時設定序號欄位的值,而這裡的值就是初始值。

傳送方和接收方在開始收發資料前需要告知物件序號初始值

通過seq序號和ACK號可以確認資料,我們前面只考慮了單向傳輸,但TCP資料收發是雙向的,所以客戶端向伺服器傳送資料,伺服器也會向客戶端傳送。所以收發雙方都需要計算序號,並且在連線過程中相互告訴對方自己計算的序號初始值。

將HTTP訊息傳給協議棧

上篇講到控制流程從 connect 回到應用程式之後,就到了資料收發階段。 資料收發資料是從應用程式呼叫write將要傳送的資料交給協議棧開始的,協議棧收到資料後執行傳送操作,這一操作包含如下要點。

首先,協議棧並不關心應用程式傳來的資料是什麼內容。應用程式呼叫write時會指定傳送資料的長度,在協議棧看來,要傳送的資料資料就是一定長度的二進位制位元組序列而已。

其次並不是一收到資料就馬上傳送出去,而是會將資料存放在內部的傳送緩衝區中,並且繼續等下一段資料。不過應用程式交給協議棧傳送的資料長度是由應用程式本身決定,有些應用程式會一次性傳遞所有的資料,有些程式則會逐位元組或者逐行傳遞資料。

總之,一次將多少資料交給協議棧是由應用程式決定的,協議棧沒有這個控制行為。

協議棧之所以不一收到資料就發出去,是因為那樣可能會發送大量的小包,導致網路效率下降。至於積累多少資料才傳送,有以下兩個要素判斷。

第一,每個網路包能容納的資料長度。協議棧會根據一個叫做MTU的引數來進行判斷。MTU表示一個網路包的最大長度,在乙太網中一般是1500位元組。MTU包含了頭部的總長度,所以MTU減去頭部長度才是一個網路包所能容納的最大資料長度,這一長度叫做MSS。當協議棧收到的長度大於或者接近MSS時傳送出去,就很好的解決大量小包的問題。

MTU表示一個網路包的最大長度,在乙太網中一般是1500位元組。MTU包含了頭部的總長度,MTU = MSS + 頭部,所以MSS是一個網路包所能容納的最大資料長度。

第二,等待時間。當應用程式傳送資料頻率不高的時候,協議棧收到的資料要接近MSS,可能要等非常久,而造成傳送延遲,所以在這種情況下,即時緩衝區的資料沒接到MSS,都發送出去。協議棧內部裡面有計時器,經過一定時間,就會把網路包傳送出去。

協議棧內部裡面有計時器,經過一定時間,就會把網路包傳送出去。

讀者可以發現,其實這兩個判斷要素是相互矛盾的。如果長度優先,網路效率會提高,但可能因為等待而產生髮送延遲;相反,時間優先,則會降低網路效率,但延遲時間減少。所以這兩個要素要綜合考慮,以達到平衡。這個平衡由協議棧的開發者來決定,所以不同種類和版本的作業系統在相關操作上也就存在差異。當然應用程式在傳送資料時,可以指定傳送選項,比如說讓網路包直接傳送,不用存在緩衝區了。

對較大資料進行拆分

HTTP請求訊息一般不會很長,一個網路就可裝下,但如果要傳送一張圖片或者傳送一篇長文呢,傳送緩衝區的資料肯定超過MSS的長度。這時,我們除了不等到後面的資料,還要對現有資料進行拆分,拆分的每塊資料會放進每個單獨的網路包。

上一篇也講過,傳送資料前,要在每一塊資料新增TCP頭部,並根據套接字中包含的通訊物件的資訊(傳送方和接收方的埠號),然後交給IP模組處理髮送操作,IP模組會在每個網路包前面新增IP頭部和乙太網頭部,具體操作,後面再講。

網路錯誤檢測和補償機制

網路以及其他環境很複雜,收發資料時,難免會在傳送中出現錯誤,所以需要檢測和補償機制。

網路包發往伺服器,需要確認對方是否收到網路包,對方沒收到時及時重發。那麼確認原理是什麼?

TCP模組在拆分資料時,會算好每一塊資料相當於從頭開始的第幾個位元組,接下來在傳送此塊資料,會將算好的位元組數寫在TCP頭部中,上一篇中說到的seq作用就在這裡。然後告知接收方資料長度,但是資料長度不是通過TCP頭部傳輸,因為接收方可以通過整個網路包的長度減去頭部長度得出。所以,我們可以知道傳送的資料是從第幾個位元組開始,長度是多少。

通過上面兩個數值,接收方還可以檢查收到的網路包有沒遺漏。比如:上次接收到第1120位元組,如果接下來收到序號是第1121的包,則表示沒有遺漏。收到第2200位元組,則有包遺漏了。如果確認沒有遺漏,接收方會將到目前為止接收到的資料長度加起來,計算出一共已經收到了多少個位元組,然後將這個數值寫入TCP頭部的ACK號中傳送給傳送方,同時將控制位中的ACK位元設為1,這代表ACK號欄位有效。返回ACK號這一操作稱作確認響應。

有個需要注意的是,seq序號不是從1開始,因為從1開始,很容易被猜到,被攻擊者發動攻擊。所以seq序號初始值是用隨機數算出來,開始收發資料前需要告知通訊物件序號初始值。上文講到連線過程中,有一個將SYN控制位設為1併發送給伺服器的操作,就是在這一步將序號的初始值告知對方的。實際上,在將SYN設為1的同時,還需要同時設定序號欄位的值,而這裡的值就是初始值。

通過seq序號和ACK號可以確認資料,我們前面只考慮了單向傳輸,但TCP資料收發是雙向的,所以客戶端向伺服器傳送資料,伺服器也會向客戶端傳送。所以收發雙方都需要計算序號,並且在連線過程中相互告訴對方自己計算的序號初始值。

工作過程

上圖表示了實際的工作過程。首先,客戶端在連線時需要計算出序號初始值並告知伺服器(①)。接下來,伺服器會通過初始值計算出ACK號並返回給客戶端(②)。初始值有可能在通訊中丟失,所以伺服器需要返回ACK號給客戶端作為確認。因為資料傳輸是雙向,伺服器也需要告知客戶端它計算出來的序號初始值,並將其發給客戶端(②)。接下來,客戶端也會計算出ACK號告知伺服器,已經收到了其發來的初始值(③)。到此,連線操作工作完成。接下來到收發操作工作,資料收發工作可以雙向同時進行。客戶端向伺服器傳送請求,序號也會跟隨資料一起傳送(④),伺服器收到資料返回ACK號(⑤)。同理,伺服器向客戶端傳送資料(⑥⑦)。

在得到對方確認之前,傳送過的網路包都會儲存在緩衝區中,如果出現丟包現象,也就是通訊物件沒有返回ACK,協議棧中的TCP模組重新發送這些包。

通過“seq”和“ACK”可以確認對方是否收到網路包。

返回ACK號的等待時間(也叫超時時間),當網路繁忙時會發生擁塞,這時需要把等待時間設定長點,否則重發包了,上次需要返回的ACK號才來,這樣會導致本來就擁塞的網路更加要命。如果設定等待時間過長,也不行,重傳包會有很大延遲。這又要找一個時間平衡,真難!所以TCP採用了動態調整等待時間的方法。這個等待時間根據ACK號返回所需的時間來判斷的。具體來說,TCP會在傳送資料的過程中,不斷的測量ACK號的返回時間,如果ACK號返回很慢,則延長等待時間,相反,如果返回很快,則縮短等待時間。

採用滑動視窗來管理資料傳送和ACK操作

每傳送一個網路包,就等到一個ACK號返回,這個很容易理解,但是在等待ACK返回這段時間,如果什麼都不做,就非常浪費。為了減少浪費,TCP採用滑動視窗管理資料傳送和ACK號的操作。所謂滑動視窗,就是在傳送一個包,不等待ACK號返回,直接傳送後續的一系列包。

但是這樣有可能出現以下問題,在不返回ACK號的時候,就連續傳送包,可能導致傳送包的頻率超過接收方處理能力的情況。具體來說,接收方TCP接收到包,會先將資料存放到接收緩衝區中。然後,接收方需要計算ACK號,將資料塊組裝起來還原成原本的資料並傳遞給應用程式,如果該操作未完成,又有下一個包到來,同樣是存入接收緩衝區中,如果包到來速率比將資料塊組裝資料並傳給應用程式速率快,緩衝區資料就會越積越多,最後溢位,接收方就收不到後面的包了。所以,接收方需要告訴傳送方自己最多能接收多少資料,然後傳送方根據這個值對資料傳送進行控制,這個最大值稱為視窗大小。這就是滑動視窗方式的基本思路。

能夠接收的最大資料量稱為視窗大小,它屬於TCP調優的一個重要引數

ACK與視窗包的合併

前面說過視窗大小就是最大接收量,當接收的資料存入緩衝區中,沒必要馬上向傳送方更新視窗大小,更新視窗大小時機應該是接收方從緩衝區中取出資料傳遞給應用程式的時候,因為這時,緩衝區中資料減少,剩餘的空間變大,理應告訴傳送方。

接收方收到資料,確認內容沒有問題,就應該向傳送方返回ACK號。假設ACK包是一個包,而更新視窗大小又是另外一個包,這樣可能會收到一個包的情況下,接收方需要向傳送方返回兩個包。這樣一來,接收方發給傳送方的包就太多了,導致網路效率下降。

所以,如果在等待發送ACK的時候,剛好也要更新視窗大小,就可以把這兩個包合併成一個包傳送,從而減少的包的數量。當需要連續傳送多個ACK號,也可以減少包的數量,這是因為ACK號表示的是已經收到的資料量,也就是說,它是告訴傳送方目前已接收的資料最後位置在哪裡,因為當需要連續傳送ACK號時,只要傳送最後一個ACK號就可以了。同理,當需要連續傳送多個視窗更新也可以減少包的數量。

接收HTTP響應訊息

客戶端委託協議棧傳送請求後,等待服務端返回的訊息,呼叫read程式來獲取響應訊息。和傳送資料一樣,接收資料也需要將資料暫存到接收緩衝區中。具體操作如下,協議棧嘗試從接收緩衝區取出資料並傳遞給應用程式,但這個時候可能響應訊息還沒返回,所以接收操作就沒法繼續。那麼,協議棧會將應用程式的委託,也就是從緩衝區取資料的工作暫時掛起,等響應訊息到達再繼續接收操作。注意,這裡只是掛起這項工作,協議棧並沒有停止工作,還會處理好多其他的工作。

應用程式在傳送資料和接收資料都依賴協議棧。

協議棧接收資料會先將資料放入緩衝區,然後將資料塊按順序連線,還原成原始資料,最後將資料交給應用程式。具體來說,協議棧會將接收方的資料複製到應用程式指定的記憶體地址中,然後將控制流程交給應用程式,同時,協議棧還要找到合適時機告訴傳送方更新視窗大小。

接收完成與伺服器斷開

應用程式接收資料,其判斷資料被全部接收完成,則這個時間就是收發資料結束的時間。協議棧在設計上允許通訊雙方的任意一方先發起斷開過程。大部分程式向伺服器傳送請求訊息,伺服器再返回響應訊息,這時收發資料的過程就全部結束了,伺服器一方會先發起斷開過程。也有一些程式是發完資料就先發起斷開過程。

協議棧在設計上允許通訊雙方的任意一方先發起斷開過程,具體哪方先斷開,由那方的程式決定。

我們以常見的伺服器斷開講解。首先,伺服器一方的程式會呼叫Socket庫的 close 程式。然後,伺服器的協議棧會生成包含斷開資訊的 TCP 頭部,具體來說就是將控制位的 FIN 位元設為1。接下來,協議棧會委託IP模組向客戶端傳送資料。同時,伺服器的套接字中也會記錄下斷開操作的相關資訊。

客戶端收到伺服器發來的 FIN 為 1 的TCP頭部時(①),客戶端協議棧會將自己的套接字標記進入斷開操作狀態。然後,為了告知伺服器已經收到 FIN 的包,客戶端會向伺服器返回一個 ACK 號(②)。這些操作完成後,就等待應用程式來取資料了。

過了一會,應用程式就回來呼叫 read 來讀取資料。這時,協議棧不會嚮應用程式傳遞資料,而是會告知應用程式來自伺服器的資料已經全部收到,客戶端收到全部資料,也會呼叫 close 結束資料收發操作,這時客戶端的協議棧也會和伺服器一樣,生成一個FIN位元為1的TCP包,然後委託IP模組傳送給伺服器(③)。隔一段時間,伺服器就會返回ACK號(④)。到此,客戶端和伺服器的通訊全部結束。

刪除連線管道

有沒有記到前面說過,通訊雙方在連線階段中間類似有一條管道,準備連線時,我們建立,現在收發資料結束,我們理應要刪除它,其實也就是刪除這條虛擬管道的兩方套接字。

通訊結束之後,我們要刪除套接字,不過,套接字不會立即被刪除,而是會等待一段時間之後再被刪除。等待一段時間是為了防止誤操作,引起誤操作的原因很多,比如說: 1、客戶端傳送FIN 2、伺服器返回ACK號 3、伺服器傳送FIN 4、客戶端傳送ACK號

如果最後客戶端返回的ACK號丟失了,伺服器沒有接受到ACK號,它可能會重新發送一次FIN。如果這個時候,客戶端的套接字已經刪除,那麼套接字中儲存的開工至資訊也跟著消失,套接字對應的埠號就會被釋放出來。這時,如果別的應用程式建立套接字,新套接字剛好被分配了同一個埠號,而伺服器重發的FIN正好到達,這個時候,FIN就會錯誤的跑到新套接字裡面,新套接字就開始執行斷開操作了。所以不馬上刪除套接字,就是由於這樣。

客戶端的埠號是從空閒的埠號中隨意選擇的。

等待多長時間才刪除套接字,這得看包重傳的操作方式。網路包丟失之後會進行重傳,這操作一般要持續幾分鐘。如果重傳了幾分鐘之後依然無效,則停止重傳。所以一般等待幾分鐘之後再刪除套接字。

總結

TCP收發資料的整體流程分為以下三個部分。

收發資料三個步驟開始前的操作是建立套接字,應用程式呼叫Socket庫的一個程式元件socket程式申請建立套接字,之後協議棧去執行操作。

一、連線操作。建立完套接字,就準備連線通訊物件。首先,客戶端會生成一個SYN為1的TCP包併發給伺服器。這個TCP包的頭部包含了客戶端向伺服器傳送資料時使用的seq(初始序號),以及伺服器傳送資料給客戶端需要用到的視窗大小。這個包到達伺服器後,伺服器會返回一個SYN為1的TCP包,這個TCP包同樣包含著序號和視窗大小,此外還包含表示已經收到客戶端發來的TCP包的ACK號。過段時間,客戶端會返回ACK號,表示已經收到伺服器傳送的TCP包。

二、收發操作。不同應用程式可能會有些異同。一般。客戶端會向伺服器傳送請求訊息。TCP會將資料拆分成很多個網路包分別傳送出去。每個包的TCP頭部都包含這序號,表示當前傳送的是第幾個位元組資料。伺服器收到包後,會返回ACK號,一定時間後也會返回更新視窗大小的包。當然通訊是雙向的,伺服器也會向客戶端傳送資料,也是類似的流程。

三、斷開操作。一般,伺服器會先發起斷開過程。伺服器先發一個 FIN 為1的TCP包給客戶端,客戶端返回 ACK號作為確認收到。客戶端收到全部資料,也會生成一個 FIN 位元為1的TCP包,傳送給伺服器,伺服器也返回ACK號,等待一段時間後,套接字會被刪除。到此,客戶端和伺服器的通訊全部結束。




參考文獻:

TCP/IP協議族

網路是怎樣連線的

歡迎關注技術公眾號「程式設計師大咖秀」