前言:網(wǎng)絡(luò)知識非常的重要,如果你不是做程序的,那么一些網(wǎng)絡(luò)常識還是得知道的;而做程序的,就更不用說了,不僅需要了解一些網(wǎng)絡(luò)知識,還是知道其原理,如果不了解原理,不敢說他不是程序員,但是總?cè)绷它c意思,就像去北京沒去過長城一樣。
網(wǎng)絡(luò)原理系列文章:
一、五分鐘了解網(wǎng)絡(luò)連接(已完成)
二、收發(fā)數(shù)據(jù)的原理(上)(已完成)
三、收發(fā)數(shù)據(jù)的原理(下)(已完成)
四、收發(fā)數(shù)據(jù)的番外篇(未完成)
因為網(wǎng)絡(luò)原理不是三言兩語可以講完,如果讀者很忙,可以直接拉到最底下,看總結(jié),知道個大概,再回頭細讀此文章。感謝關(guān)注。廢話不多說,直接進入主題。在上篇我們已經(jīng)講了TCP收發(fā)數(shù)據(jù)的前兩步,接下來是最后兩步。
將HTTP消息傳給協(xié)議棧
上篇講到控制流程從 connect 回到應(yīng)用程序之后,就到了數(shù)據(jù)收發(fā)階段。
數(shù)據(jù)收發(fā)數(shù)據(jù)是從應(yīng)用程序調(diào)用write將要發(fā)送的數(shù)據(jù)交給協(xié)議棧開始的,協(xié)議棧收到數(shù)據(jù)后執(zhí)行發(fā)送操作,這一操作包含如下要點。
首先,協(xié)議棧并不關(guān)心應(yīng)用程序傳來的數(shù)據(jù)是什么內(nèi)容。應(yīng)用程序調(diào)用write時會指定發(fā)送數(shù)據(jù)的長度,在協(xié)議棧看來,要發(fā)送的數(shù)據(jù)數(shù)據(jù)就是一定長度的二進制字節(jié)序列而已。
其次并不是一收到數(shù)據(jù)就馬上發(fā)送出去,而是會將數(shù)據(jù)存放在內(nèi)部的發(fā)送緩沖區(qū)中,并且繼續(xù)等下一段數(shù)據(jù)。不過應(yīng)用程序交給協(xié)議棧發(fā)送的數(shù)據(jù)長度是由應(yīng)用程序本身決定,有些應(yīng)用程序會一次性傳遞所有的數(shù)據(jù),有些程序則會逐字節(jié)或者逐行傳遞數(shù)據(jù)。
總之,一次將多少數(shù)據(jù)交給協(xié)議棧是由應(yīng)用程序決定的,協(xié)議棧沒有這個控制行為。
協(xié)議棧之所以不一收到數(shù)據(jù)就發(fā)出去,是因為那樣可能會發(fā)送大量的小包,導(dǎo)致網(wǎng)絡(luò)效率下降。至于積累多少數(shù)據(jù)才發(fā)送,有以下兩個要素判斷。
第一,每個網(wǎng)絡(luò)包能容納的數(shù)據(jù)長度。協(xié)議棧會根據(jù)一個叫做MTU的參數(shù)來進行判斷。MTU表示一個網(wǎng)絡(luò)包的最大長度,在以太網(wǎng)中一般是1500字節(jié)。MTU包含了頭部的總長度,所以MTU減去頭部長度才是一個網(wǎng)絡(luò)包所能容納的最大數(shù)據(jù)長度,這一長度叫做MSS。當(dāng)協(xié)議棧收到的長度大于或者接近MSS時發(fā)送出去,就很好的解決大量小包的問題。
MTU表示一個網(wǎng)絡(luò)包的最大長度,在以太網(wǎng)中一般是1500字節(jié)。MTU包含了頭部的總長度,MTU = MSS + 頭部,所以MSS是一個網(wǎng)絡(luò)包所能容納的最大數(shù)據(jù)長度。
第二,等待時間。當(dāng)應(yīng)用程序發(fā)送數(shù)據(jù)頻率不高的時候,協(xié)議棧收到的數(shù)據(jù)要接近MSS,可能要等非常久,而造成發(fā)送延遲,所以在這種情況下,即時緩沖區(qū)的數(shù)據(jù)沒接到MSS,都發(fā)送出去。協(xié)議棧內(nèi)部里面有計時器,經(jīng)過一定時間,就會把網(wǎng)絡(luò)包發(fā)送出去。
協(xié)議棧內(nèi)部里面有計時器,經(jīng)過一定時間,就會把網(wǎng)絡(luò)包發(fā)送出去。
讀者可以發(fā)現(xiàn),其實這兩個判斷要素是相互矛盾的。如果長度優(yōu)先,網(wǎng)絡(luò)效率會提高,但可能因為等待而產(chǎn)生發(fā)送延遲;相反,時間優(yōu)先,則會降低網(wǎng)絡(luò)效率,但延遲時間減少。所以這兩個要素要綜合考慮,以達到平衡。這個平衡由協(xié)議棧的開發(fā)者來決定,所以不同種類和版本的操作系統(tǒng)在相關(guān)操作上也就存在差異。當(dāng)然應(yīng)用程序在發(fā)送數(shù)據(jù)時,可以指定發(fā)送選項,比如說讓網(wǎng)絡(luò)包直接發(fā)送,不用存在緩沖區(qū)了。
對較大數(shù)據(jù)進行拆分
HTTP請求消息一般不會很長,一個網(wǎng)絡(luò)就可裝下,但如果要發(fā)送一張圖片或者發(fā)送一篇長文呢,發(fā)送緩沖區(qū)的數(shù)據(jù)肯定超過MSS的長度。這時,我們除了不等到后面的數(shù)據(jù),還要對現(xiàn)有數(shù)據(jù)進行拆分,拆分的每塊數(shù)據(jù)會放進每個單獨的網(wǎng)絡(luò)包。
上一篇也講過,發(fā)送數(shù)據(jù)前,要在每一塊數(shù)據(jù)添加TCP頭部,并根據(jù)套接字中包含的通信對象的信息(發(fā)送方和接收方的端口號),然后交給IP模塊處理發(fā)送操作,IP模塊會在每個網(wǎng)絡(luò)包前面添加IP頭部和以太網(wǎng)頭部,具體操作,后面再講。
網(wǎng)絡(luò)錯誤檢測和補償機制
網(wǎng)絡(luò)以及其他環(huán)境很復(fù)雜,收發(fā)數(shù)據(jù)時,難免會在發(fā)送中出現(xiàn)錯誤,所以需要檢測和補償機制。
網(wǎng)絡(luò)包發(fā)往服務(wù)器,需要確認對方是否收到網(wǎng)絡(luò)包,對方?jīng)]收到時及時重發(fā)。那么確認原理是什么?
TCP模塊在拆分數(shù)據(jù)時,會算好每一塊數(shù)據(jù)相當(dāng)于從頭開始的第幾個字節(jié),接下來在發(fā)送此塊數(shù)據(jù),會將算好的字節(jié)數(shù)寫在TCP頭部中,上一篇中說到的seq作用就在這里。然后告知接收方數(shù)據(jù)長度,但是數(shù)據(jù)長度不是通過TCP頭部傳輸,因為接收方可以通過整個網(wǎng)絡(luò)包的長度減去頭部長度得出。所以,我們可以知道發(fā)送的數(shù)據(jù)是從第幾個字節(jié)開始,長度是多少。
通過上面兩個數(shù)值,接收方還可以檢查收到的網(wǎng)絡(luò)包有沒遺漏。比如:上次接收到第1120字節(jié),如果接下來收到序號是第1121的包,則表示沒有遺漏。收到第2200字節(jié),則有包遺漏了。如果確認沒有遺漏,接收方會將到目前為止接收到的數(shù)據(jù)長度加起來,計算出一共已經(jīng)收到了多少個字節(jié),然后將這個數(shù)值寫入TCP頭部的ACK號中發(fā)送給發(fā)送方(TCP的seq和ack號計算方法),返回ACK號這一操作稱作確認響應(yīng)。
有個需要注意的是,seq序號不是從1開始,因為從1開始,很容易被猜到,被攻擊者發(fā)動攻擊。所以seq序號初始值是用隨機數(shù)算出來,開始收發(fā)數(shù)據(jù)前需要告知通信對象序號初始值。上文講到連接過程中,有一個將SYN控制位設(shè)為1并發(fā)送給服務(wù)器的操作,就是在這一步將序號的初始值告知對方的。實際上,在將SYN設(shè)為1的同時,還需要同時設(shè)置序號字段的值,而這里的值就是初始值。
通過seq序號和ACK號可以確認數(shù)據(jù),我們前面只考慮了單向傳輸,但TCP數(shù)據(jù)收發(fā)是雙向的,所以客戶端向服務(wù)器發(fā)送數(shù)據(jù),服務(wù)器也會向客戶端發(fā)送。所以收發(fā)雙方都需要計算序號,并且在連接過程中相互告訴對方自己計算的序號初始值。

上圖表示了實際的工作過程。首先,客戶端在連接時需要計算出序號初始值并告知服務(wù)器(①)。接下來,服務(wù)器會通過初始值計算出ACK號并返回給客戶端(②)。初始值有可能在通信中丟失,所以服務(wù)器需要返回ACK號給客戶端作為確認。因為數(shù)據(jù)傳輸是雙向,服務(wù)器也需要告知客戶端它計算出來的序號初始值,并將其發(fā)給客戶端(②)。接下來,客戶端也會計算出ACK號告知服務(wù)器,已經(jīng)收到了其發(fā)來的初始值(③)。到此,連接操作工作完成。接下來到收發(fā)操作工作,數(shù)據(jù)收發(fā)工作可以雙向同時進行。客戶端向服務(wù)器發(fā)送請求,序號也會跟隨數(shù)據(jù)一起發(fā)送(④),服務(wù)器收到數(shù)據(jù)返回ACK號(⑤)。同理,服務(wù)器向客戶端發(fā)送數(shù)據(jù)(⑥⑦)。
在得到對方確認之前,發(fā)送過的網(wǎng)絡(luò)包都會保存在緩沖區(qū)中,如果出現(xiàn)丟包現(xiàn)象,也就是通信對象沒有返回ACK,協(xié)議棧中的TCP模塊重新發(fā)送這些包。
通過“seq”和“ACK”可以確認對方是否收到網(wǎng)絡(luò)包。
返回ACK號的等待時間(也叫超時時間),當(dāng)網(wǎng)絡(luò)繁忙時會發(fā)生擁塞,這時需要把等待時間設(shè)置長點,否則重發(fā)包了,上次需要返回的ACK號才來,這樣會導(dǎo)致本來就擁塞的網(wǎng)絡(luò)更加要命。如果設(shè)置等待時間過長,也不行,重傳包會有很大延遲。這又要找一個時間平衡,真難!所以TCP采用了動態(tài)調(diào)整等待時間的方法。這個等待時間根據(jù)ACK號返回所需的時間來判斷的。具體來說,TCP會在發(fā)送數(shù)據(jù)的過程中,不斷的測量ACK號的返回時間,如果ACK號返回很慢,則延長等待時間,相反,如果返回很快,則縮短等待時間。
采用滑動窗口來管理數(shù)據(jù)發(fā)送和ACK操作
每發(fā)送一個網(wǎng)絡(luò)包,就等到一個ACK號返回,這個很容易理解,但是在等待ACK返回這段時間,如果什么都不做,就非常浪費。為了減少浪費,TCP采用滑動窗口管理數(shù)據(jù)發(fā)送和ACK號的操作。所謂滑動窗口,就是在發(fā)送一個包,不等待ACK號返回,直接發(fā)送后續(xù)的一系列包。
但是這樣有可能出現(xiàn)以下問題,在不返回ACK號的時候,就連續(xù)發(fā)送包,可能導(dǎo)致發(fā)送包的頻率超過接收方處理能力的情況。具體來說,接收方TCP接收到包,會先將數(shù)據(jù)存放到接收緩沖區(qū)中。然后,接收方需要計算ACK號,將數(shù)據(jù)塊組裝起來還原成原本的數(shù)據(jù)并傳遞給應(yīng)用程序,如果該操作未完成,又有下一個包到來,同樣是存入接收緩沖區(qū)中,如果包到來速率比將數(shù)據(jù)塊組裝數(shù)據(jù)并傳給應(yīng)用程序速率快,緩沖區(qū)數(shù)據(jù)就會越積越多,最后溢出,接收方就收不到后面的包了。所以,接收方需要告訴發(fā)送方自己最多能接收多少數(shù)據(jù),然后發(fā)送方根據(jù)這個值對數(shù)據(jù)發(fā)送進行控制,這個最大值稱為窗口大小。這就是滑動窗口方式的基本思路。
能夠接收的最大數(shù)據(jù)量稱為窗口大小,它屬于TCP調(diào)優(yōu)的一個重要參數(shù)

ACK與窗口包的合并
前面說過窗口大小就是最大接收量,當(dāng)接收的數(shù)據(jù)存入緩沖區(qū)中,沒必要馬上向發(fā)送方更新窗口大小,更新窗口大小時機應(yīng)該是接收方從緩沖區(qū)中取出數(shù)據(jù)傳遞給應(yīng)用程序的時候,因為這時,緩沖區(qū)中數(shù)據(jù)減少,剩余的空間變大,理應(yīng)告訴發(fā)送方。
接收方收到數(shù)據(jù),確認內(nèi)容沒有問題,就應(yīng)該向發(fā)送方返回ACK號。假設(shè)ACK包是一個包,而更新窗口大小又是另外一個包,這樣可能會收到一個包的情況下,接收方需要向發(fā)送方返回兩個包。這樣一來,接收方發(fā)給發(fā)送方的包就太多了,導(dǎo)致網(wǎng)絡(luò)效率下降。
所以,如果在等待發(fā)送ACK的時候,剛好也要更新窗口大小,就可以把這兩個包合并成一個包發(fā)送,從而減少的包的數(shù)量。當(dāng)需要連續(xù)發(fā)送多個ACK號,也可以減少包的數(shù)量,這是因為ACK號表示的是已經(jīng)收到的數(shù)據(jù)量,也就是說,它是告訴發(fā)送方目前已接收的數(shù)據(jù)最后位置在哪里,因為當(dāng)需要連續(xù)發(fā)送ACK號時,只要發(fā)送最后一個ACK號就可以了。同理,當(dāng)需要連續(xù)發(fā)送多個窗口更新也可以減少包的數(shù)量。
接收HTTP響應(yīng)消息
客戶端委托協(xié)議棧發(fā)送請求后,等待服務(wù)端返回的消息,調(diào)用read程序來獲取響應(yīng)消息。和發(fā)送數(shù)據(jù)一樣,接收數(shù)據(jù)也需要將數(shù)據(jù)暫存到接收緩沖區(qū)中。具體操作如下,協(xié)議棧嘗試從接收緩沖區(qū)取出數(shù)據(jù)并傳遞給應(yīng)用程序,但這個時候可能響應(yīng)消息還沒返回,所以接收操作就沒法繼續(xù)。那么,協(xié)議棧會將應(yīng)用程序的委托,也就是從緩沖區(qū)取數(shù)據(jù)的工作暫時掛起,等響應(yīng)消息到達再繼續(xù)接收操作。注意,這里只是掛起這項工作,協(xié)議棧并沒有停止工作,還會處理好多其他的工作。
應(yīng)用程序在發(fā)送數(shù)據(jù)和接收數(shù)據(jù)都依賴協(xié)議棧。
協(xié)議棧接收數(shù)據(jù)會先將數(shù)據(jù)放入緩沖區(qū),然后將數(shù)據(jù)塊按順序連接,還原成原始數(shù)據(jù),最后將數(shù)據(jù)交給應(yīng)用程序。具體來說,協(xié)議棧會將接收方的數(shù)據(jù)復(fù)制到應(yīng)用程序指定的內(nèi)存地址中,然后將控制流程交給應(yīng)用程序,同時,協(xié)議棧還要找到合適時機告訴發(fā)送方更新窗口大小。
接收完成與服務(wù)器斷開
應(yīng)用程序接收數(shù)據(jù),其判斷數(shù)據(jù)被全部接收完成,則這個時間就是收發(fā)數(shù)據(jù)結(jié)束的時間。協(xié)議棧在設(shè)計上允許通信雙方的任意一方先發(fā)起斷開過程。大部分程序向服務(wù)器發(fā)送請求消息,服務(wù)器再返回響應(yīng)消息,這時收發(fā)數(shù)據(jù)的過程就全部結(jié)束了,服務(wù)器一方會先發(fā)起斷開過程。也有一些程序是發(fā)完數(shù)據(jù)就先發(fā)起斷開過程。
協(xié)議棧在設(shè)計上允許通信雙方的任意一方先發(fā)起斷開過程,具體哪方先斷開,由那方的程序決定。
我們以常見的服務(wù)器斷開講解。首先,服務(wù)器一方的程序會調(diào)用Socket庫的 close 程序。然后,服務(wù)器的協(xié)議棧會生成包含斷開信息的 TCP 頭部,具體來說就是將控制位的 FIN 比特設(shè)為1。接下來,協(xié)議棧會委托IP模塊向客戶端發(fā)送數(shù)據(jù)。同時,服務(wù)器的套接字中也會記錄下斷開操作的相關(guān)信息。

客戶端收到服務(wù)器發(fā)來的 FIN 為 1 的TCP頭部時(①),客戶端協(xié)議棧會將自己的套接字標記進入斷開操作狀態(tài)。然后,為了告知服務(wù)器已經(jīng)收到 FIN 的包,客戶端會向服務(wù)器返回一個 ACK 號(②)。這些操作完成后,就等待應(yīng)用程序來取數(shù)據(jù)了。
過了一會,應(yīng)用程序就回來調(diào)用 read 來讀取數(shù)據(jù)。這時,協(xié)議棧不會向應(yīng)用程序傳遞數(shù)據(jù),而是會告知應(yīng)用程序來自服務(wù)器的數(shù)據(jù)已經(jīng)全部收到,客戶端收到全部數(shù)據(jù),也會調(diào)用 close 結(jié)束數(shù)據(jù)收發(fā)操作,這時客戶端的協(xié)議棧也會和服務(wù)器一樣,生成一個FIN比特為1的TCP包,然后委托IP模塊發(fā)送給服務(wù)器(③)。隔一段時間,服務(wù)器就會返回ACK號(④)。到此,客戶端和服務(wù)器的通信全部結(jié)束。
刪除連接管道
有沒有記到前面說過,通信雙方在連接階段中間類似有一條管道,準備連接時,我們建立,現(xiàn)在收發(fā)數(shù)據(jù)結(jié)束,我們理應(yīng)要刪除它,其實也就是刪除這條虛擬管道的兩方套接字。
通信結(jié)束之后,我們要刪除套接字,不過,套接字不會立即被刪除,而是會等待一段時間之后再被刪除。等待一段時間是為了防止誤操作,引起誤操作的原因很多,比如說:
1、客戶端發(fā)送FIN
2、服務(wù)器返回ACK號
3、服務(wù)器發(fā)送FIN
4、客戶端發(fā)送ACK號
如果最后客戶端返回的ACK號丟失了,服務(wù)器沒有接受到ACK號,它可能會重新發(fā)送一次FIN。如果這個時候,客戶端的套接字已經(jīng)刪除,那么套接字中保存的開工至信息也跟著消失,套接字對應(yīng)的端口號就會被釋放出來。這時,如果別的應(yīng)用程序創(chuàng)建套接字,新套接字剛好被分配了同一個端口號,而服務(wù)器重發(fā)的FIN正好到達,這個時候,F(xiàn)IN就會錯誤的跑到新套接字里面,新套接字就開始執(zhí)行斷開操作了。所以不馬上刪除套接字,就是由于這樣。
客戶端的端口號是從空閑的端口號中隨意選擇的。
等待多長時間才刪除套接字,這得看包重傳的操作方式。網(wǎng)絡(luò)包丟失之后會進行重傳,這操作一般要持續(xù)幾分鐘。如果重傳了幾分鐘之后依然無效,則停止重傳。所以一般等待幾分鐘之后再刪除套接字。
總結(jié)
TCP收發(fā)數(shù)據(jù)的整體流程分為以下三個部分。
收發(fā)數(shù)據(jù)三個步驟開始前的操作是創(chuàng)建套接字,應(yīng)用程序調(diào)用Socket庫的一個程序組件socket程序申請創(chuàng)建套接字,之后協(xié)議棧去執(zhí)行操作。
一、連接操作。創(chuàng)建完套接字,就準備連接通信對象。首先,客戶端會生成一個SYN為1的TCP包并發(fā)給服務(wù)器。這個TCP包的頭部包含了客戶端向服務(wù)器發(fā)送數(shù)據(jù)時使用的seq(初始序號),以及服務(wù)器發(fā)送數(shù)據(jù)給客戶端需要用到的窗口大小。這個包到達服務(wù)器后,服務(wù)器會返回一個SYN為1的TCP包,這個TCP包同樣包含著序號和窗口大小,此外還包含表示已經(jīng)收到客戶端發(fā)來的TCP包的ACK號。過段時間,客戶端會返回ACK號,表示已經(jīng)收到服務(wù)器發(fā)送的TCP包。
二、收發(fā)操作。不同應(yīng)用程序可能會有些異同。一般??蛻舳藭蚍?wù)器發(fā)送請求消息。TCP會將數(shù)據(jù)拆分成很多個網(wǎng)絡(luò)包分別發(fā)送出去。每個包的TCP頭部都包含這序號,表示當(dāng)前發(fā)送的是第幾個字節(jié)數(shù)據(jù)。服務(wù)器收到包后,會返回ACK號,一定時間后也會返回更新窗口大小的包。當(dāng)然通信是雙向的,服務(wù)器也會向客戶端發(fā)送數(shù)據(jù),也是類似的流程。
三、斷開操作。一般,服務(wù)器會先發(fā)起斷開過程。服務(wù)器先發(fā)一個 FIN 為1的TCP包給客戶端,客戶端返回 ACK號作為確認收到??蛻舳耸盏饺繑?shù)據(jù),也會生成一個 FIN 比特為1的TCP包,發(fā)送給服務(wù)器,服務(wù)器也返回ACK號,等待一段時間后,套接字會被刪除。到此,客戶端和服務(wù)器的通信全部結(jié)束。
參考文獻:
TCP/IP協(xié)議族
網(wǎng)絡(luò)是怎樣連接的
歡迎關(guān)注技術(shù)公眾號「程序員大咖秀」