瀏覽器輸入一個網(wǎng)址發(fā)生了什么(二): TCP模塊封裝和傳輸機(jī)制
本系列文章開始將圍繞著“往瀏覽器輸入網(wǎng)址后發(fā)生了什么”介紹計(jì)算機(jī)網(wǎng)絡(luò)的相關(guān)基礎(chǔ)知識。上節(jié)簡單的介紹了http報文封裝和dns請求獲取目標(biāo)IP。
接上文:瀏覽器輸入一個網(wǎng)址發(fā)生了什么(一) 揭秘DNS請求和解析全流程
本節(jié)將介紹http報文在協(xié)議棧中如何進(jìn)一步處理并發(fā)送到網(wǎng)絡(luò)中。這里說的協(xié)議棧是指TCP/IP協(xié)議棧。
對于TCP傳輸而言,請求的發(fā)送和接收需要通過連接進(jìn)行;而UDP傳輸則不用;本節(jié)會先介紹TCP傳輸?shù)倪^程再介紹UDP傳輸。
在此之前,需要先介紹一些相關(guān)概念。
一、協(xié)議棧
協(xié)議棧是由多個具有層級的協(xié)議模塊組合而成的一個軟件程序。每個協(xié)議模塊通常都要和上下層的兩個其他協(xié)議模塊通信。最低級的協(xié)議總是描述與物理硬件交互,為每個高級的層次增加更多的特性。
協(xié)議是計(jì)算機(jī)在網(wǎng)絡(luò)通信時需要遵循的規(guī)范和約定,而協(xié)議棧則是這種規(guī)范和約定的實(shí)現(xiàn),也就是具體的代碼和函數(shù)以供上層調(diào)用。每一層協(xié)議模塊都是為上一層協(xié)議模塊服務(wù),本層的協(xié)議模塊完成某個操作只需要調(diào)用下層模塊的方法而無需關(guān)注其具體怎么實(shí)現(xiàn)。
二、套接字與連接
對于TCP傳輸而言,請求的發(fā)送和接收需要通過連接進(jìn)行。我們可以把連接看做是一條端到端之間的管道,這條管道的兩端都既可以是發(fā)送方也可以是接收方,數(shù)據(jù)在這條管道是雙向流動的。而建立這條管道的出入口我們稱之為套接字,分別在客戶端和服務(wù)端兩方,兩方需要先創(chuàng)建套接字才能建立連接。連接建立后,請求和響應(yīng)會通過套接字發(fā)出并通過這個管道進(jìn)行傳輸。
當(dāng)然管道只是一個比喻,現(xiàn)實(shí)中是不存在這么一個管道。
對于UDP傳輸,雖然不需要建立連接,但是依舊要創(chuàng)建套接字,并通過套接字發(fā)送和接收包。
三、什么是套接字
套接字本質(zhì)是存放諸如IP地址、端口號、通信操作狀態(tài)這樣的通信控制信息的內(nèi)存空間里的數(shù)據(jù)。協(xié)議棧在執(zhí)行操作時會用到這些控制信息,如封裝報文時需要知道通信對象的IP和端口,如協(xié)議棧需要知道數(shù)據(jù)發(fā)送后經(jīng)過了多長時間仍未返回以便重發(fā),如比對序號是否正確,如保存窗口大小從而將窗口大小帶到TCP頭部通知通信的另一方,這都要查詢套接字的控制信息得知。
協(xié)議棧是根據(jù)套接字中記錄的控制信息來工作的。
在windows下執(zhí)行netstat可以顯示套接字內(nèi)容。如下圖所示,每一行就是一個套接字:
- 第一列:協(xié)議,通常為TCP或UDP,表示該套接字是為TCP傳輸還是UDP傳輸而創(chuàng)建的。
- 第二列:本地端的ip和端口;如果安裝了多塊網(wǎng)卡則會顯示不同的IP;0.0.0.0表示不綁定IP地址。
- 第三列:遠(yuǎn)程端的IP和端口;0.0.0.0表示還未開始通信,沒有綁定IP和端口;此外,UDP協(xié)議中的套接字不綁定對方地址和端口因此顯示*:*。
- 第四列:通信狀態(tài);LISTENING 等待對方連接;ESTABLISHED 完成連接正在通信。
- 第五列:使用該套接字程序的進(jìn)程ID。
回到正題。
當(dāng)客戶端程序(瀏覽器)拿到目標(biāo)IP地址后,會經(jīng)歷:創(chuàng)建套接字、連接、發(fā)送數(shù)據(jù)、接收數(shù)據(jù)、斷開連接、刪除套接字。如下圖所示
1.創(chuàng)建套接字
應(yīng)用程序(瀏覽器)調(diào)用socket()方法,然后工作會切到操作系統(tǒng)的協(xié)議棧進(jìn)行。協(xié)議棧會先分配一個用于存放套接字的內(nèi)存空間,并向其寫入初始狀態(tài),協(xié)議棧會返回一個代表套接字的描述符(一個整型)給瀏覽器。
之后當(dāng)瀏覽器需要收發(fā)數(shù)據(jù)時(connect/read/write)就需要向協(xié)議棧提供這個描述符。協(xié)議棧會根據(jù)這個描述符找到對應(yīng)的套接字進(jìn)行操作。
2.建立連接
創(chuàng)建套接字后,應(yīng)用程序(瀏覽器)會調(diào)用connect方法,工作會切換到操作系統(tǒng)的協(xié)議棧,由協(xié)議棧負(fù)責(zé)將本地的套接字和服務(wù)器的套接字進(jìn)行連接。
連接的本質(zhì)是通信雙方交換控制信息并記錄到雙方套接字中,之后的每一次數(shù)據(jù)收發(fā)時數(shù)據(jù)包都要帶上套接字中的控制信息。例如在本地套接字中記錄下服務(wù)器的ip和端口,并告知服務(wù)器請求方的ip和端口,讓服務(wù)端把客戶端的ip端口記錄到服務(wù)端套接字中,此外還有其他的控制信息。
另外,連接操作過程中,通信雙方還會分配一塊用于臨時存放收發(fā)數(shù)據(jù)的內(nèi)存空間。
四、TCP控制信息(TCP頭部)
上面所說的控制信息會寫入到TCP報文的頭部發(fā)送到服務(wù)端,如下所示:
上面這些字段都是保存在套接字中,并且在需要構(gòu)建頭部的時候從套接字中取出這些信息。再通信雙方的不斷通信的過程中會更新到套接字中(如窗口和序號)。
五、TCP傳輸?shù)臄?shù)據(jù)包
無論是連接、收發(fā)數(shù)據(jù)和斷開連接,通信雙方都需要通過發(fā)送數(shù)據(jù)包來進(jìn)行。
對于連接和斷開連接而言,無需傳遞應(yīng)用程序(瀏覽器)數(shù)據(jù),因此數(shù)據(jù)包只用包含控制信息(也就是頭部信息)。而收發(fā)數(shù)據(jù)時的包會包含控制信息和數(shù)據(jù)塊內(nèi)容。應(yīng)用程序的數(shù)據(jù)可能會很大,因此協(xié)議棧會將數(shù)據(jù)切分為一個個數(shù)據(jù)塊,每個數(shù)據(jù)塊都添加上頭部控制信息做成包再發(fā)送。
如下所示:
下面我們正式介紹連接的過程:
- 應(yīng)用程序(瀏覽器)調(diào)用socket庫的connect(<套接字的描述符>, <服務(wù)端的IP和端口>, ...)
- 工作切換到TCP/IP協(xié)議棧,協(xié)議棧構(gòu)建包含諸多控制信息(通信雙方的端口,窗口,序號,ACK號等)的TCP頭部(TCP報文),并將頭部中的SYN比特位置為1,表示該報文是向服務(wù)端請求建立連接而建的。
- TCP模塊將構(gòu)建好的TCP報文(該報文只包含頭部信息,沒有數(shù)據(jù)內(nèi)容)交給IP模塊,由IP模塊進(jìn)一步封裝成包(添加IP頭部和MAC頭部),IP模塊再通過網(wǎng)卡將包轉(zhuǎn)為電信號,借由網(wǎng)線傳輸給服務(wù)端。(包是如何根據(jù)IP地址找到服務(wù)器,以及包在網(wǎng)絡(luò)中的傳輸過程會在后面介紹)。
- 包到達(dá)服務(wù)端之后(通過網(wǎng)線、網(wǎng)卡到達(dá)服務(wù)端協(xié)議棧),根據(jù)報文中指定的端口找到服務(wù)端對應(yīng)的套接字。服務(wù)端的套接字從監(jiān)聽狀態(tài)變成正在連接的狀態(tài),并且還會將報文中的控制信息寫入到套接字中保存。
- 之后服務(wù)端的協(xié)議棧也會封裝響應(yīng)報文,該過程和上述A/B/C步驟類似,并且報文中的TCP頭部的SYN和ACK位置為1,意味著服務(wù)端向發(fā)送方發(fā)起連接請求(SYN=1)和接收到發(fā)送方的包(ACK=1)。
- 響應(yīng)包到達(dá)發(fā)送方后,協(xié)議棧會把套接字的連接狀態(tài)改為已連接,并將服務(wù)端的IP和端口保存到套接字中。然后封裝一個ACK為1的包給服務(wù)端表示接受到服務(wù)端的連接請求包。
- 服務(wù)端接收到客戶端的ACK為1的包后連接操作才算完成。
到此為止,客戶端和服務(wù)端的套接字都保存了對方的信息,控制流程從操作系統(tǒng)回到了應(yīng)用程序,之后可以開始真正發(fā)送客戶端的http消息了。在執(zhí)行close之前,連接會一直保持。
上述雙方交換3次數(shù)據(jù)包的過程也是大家熟悉的“三次握手”過程。
http消息封裝成網(wǎng)絡(luò)包發(fā)送
連接完成以后,應(yīng)用程序(瀏覽器)會調(diào)用socket庫的write()方法將數(shù)據(jù)(封裝好的http消息)傳遞給協(xié)議棧,由協(xié)議棧發(fā)送給服務(wù)端。
在講數(shù)據(jù)收發(fā)流程之前,需要先介紹一些數(shù)據(jù)收發(fā)的細(xì)節(jié)和重點(diǎn):
六、網(wǎng)絡(luò)包
包是網(wǎng)絡(luò)數(shù)據(jù)在網(wǎng)絡(luò)層(如IP包和以太網(wǎng)包)的稱呼。
包是由記錄了控制信息的頭部和包的內(nèi)容組成。IP頭部和MAC頭部是在網(wǎng)絡(luò)層添加的,因此都能被稱為包:
TCP模塊負(fù)責(zé)將http消息加上TCP頭部控制信息,并將這些數(shù)據(jù)傳遞給下一層的IP模塊,由IP模塊封裝IP頭部和MAC頭部形成網(wǎng)絡(luò)包,其中TCP頭部記錄著通信雙方的端口號,IP頭部記錄著通信雙方的IP。無論是連接階段的數(shù)據(jù)還是數(shù)據(jù)收發(fā)的數(shù)據(jù)都會經(jīng)過IP模塊的封裝和發(fā)送。
IP頭部:
PS:IP地址不是分配給計(jì)算機(jī)的而是分配給網(wǎng)卡的。因此如果計(jì)算機(jī)內(nèi)包含多塊網(wǎng)卡就可以擁有多個IP地址。如果客戶端和服務(wù)端主機(jī)有多個IP地址,那么在IP頭部中填寫哪個IP地址取決于協(xié)議棧想讓哪塊網(wǎng)卡發(fā)送數(shù)據(jù)包,并把數(shù)據(jù)包發(fā)送到服務(wù)器的哪塊網(wǎng)卡上。
MAC頭部:
下章節(jié)再對IP頭部和MAC頭部進(jìn)行介紹。這里我們只需要知道添加了這兩個頭部之后數(shù)據(jù)就變成網(wǎng)絡(luò)包,再經(jīng)由IP模塊傳遞給網(wǎng)卡,傳遞給網(wǎng)卡的時候,網(wǎng)絡(luò)包才算正式離開了協(xié)議棧。
其他層的網(wǎng)絡(luò)數(shù)據(jù)稱呼:
- 幀:數(shù)據(jù)在數(shù)據(jù)鏈路層的單位
- 包:數(shù)據(jù)在網(wǎng)絡(luò)層的單位
- 段:數(shù)據(jù)在傳輸層的單位
- 消息:數(shù)據(jù)在應(yīng)用層的單位
七、數(shù)據(jù)發(fā)送時機(jī)
協(xié)議棧不一定會在一收到數(shù)據(jù)就馬上發(fā)出去,也可能是先將數(shù)據(jù)存到內(nèi)部的發(fā)送緩沖區(qū),等到積累到一定程度后再發(fā)送。
這樣做的原因是應(yīng)用程序每次傳遞多少數(shù)據(jù)給協(xié)議棧是不定的,有些應(yīng)用程序是逐字或逐行節(jié)傳遞給協(xié)議棧,有些是一次性傳遞整個請求的所有數(shù)據(jù)給協(xié)議棧,如果應(yīng)用程序每次傳遞給協(xié)議棧的數(shù)據(jù)很少,且協(xié)議棧每接收到一次數(shù)據(jù)就馬上發(fā)送就會發(fā)送大量的小包導(dǎo)致網(wǎng)絡(luò)效率下降。
每次傳遞多少數(shù)據(jù)給協(xié)議棧是由應(yīng)用程序決定的。
是否讓協(xié)議棧一接收到應(yīng)用程序的數(shù)據(jù)就馬上發(fā)送也是由應(yīng)用程序進(jìn)行系統(tǒng)調(diào)用時的指定的一些選項(xiàng)決定的。
譬如對于瀏覽器這種會話型應(yīng)用程序而言,它會指定讓協(xié)議棧馬上發(fā)送數(shù)據(jù)以避免延遲。
如果應(yīng)用程序決定讓協(xié)議棧先緩存數(shù)據(jù)再發(fā)送的話,那么協(xié)議棧會根據(jù)兩個要素決定發(fā)送的時機(jī):一個是網(wǎng)絡(luò)包能容納的最大數(shù)據(jù)長度,一個是應(yīng)用程序傳遞數(shù)據(jù)的頻率(每次傳遞數(shù)據(jù)的時間間隔)
網(wǎng)絡(luò)包能容納的最大數(shù)據(jù)長度: 協(xié)議棧規(guī)定一個網(wǎng)絡(luò)包能容納的最大長度是MTU(在以太網(wǎng)中是1500字節(jié)),即IP頭部+TCP頭部+真實(shí)數(shù)據(jù) <= 1500。除去頭部,真實(shí)數(shù)據(jù)的最大長度是MSS = 1500 - 20 - 20 = 1460。當(dāng)協(xié)議棧收到的數(shù)據(jù)接近或超過MSS的長度時再發(fā)出去就可以避免發(fā)送大量小包的問題。
應(yīng)用程序傳遞數(shù)據(jù)的時間間隔:
當(dāng)應(yīng)用程序傳遞消息的頻率不高的時候,如果每次都要等到長度接近MSS就會造成較大的發(fā)送延遲。協(xié)議棧中有一個計(jì)時器,當(dāng)經(jīng)過一定時間后即便緩沖區(qū)數(shù)據(jù)沒有達(dá)到MSS也會將其封裝為包發(fā)送出去。
八、大數(shù)據(jù)拆分
一般GET請求方式的HTTP消息不會很長,一個網(wǎng)絡(luò)包(這里的包是指IP模塊發(fā)出去的包)就能裝下。但如果要傳遞表單甚至上傳文件就可能超過一個包容納的數(shù)據(jù)量(MSS),此時發(fā)送緩沖區(qū)的數(shù)據(jù)量會大于MSS。這時協(xié)議棧的TCP模塊會將發(fā)送緩沖區(qū)中的數(shù)據(jù)(也就是http消息)以MSS為單位進(jìn)行拆分為多個數(shù)據(jù)塊,每個塊被單獨(dú)添加TCP頭部,單獨(dú)封裝為網(wǎng)絡(luò)包傳輸。
在拆分?jǐn)?shù)據(jù)時,TCP模塊會計(jì)算好每一塊數(shù)據(jù)的第一個字節(jié)是整體數(shù)據(jù)的第幾個字節(jié),以此作為每塊數(shù)據(jù)的TCP頭部中的序號(序號是應(yīng)用程序的數(shù)據(jù)序號而不是網(wǎng)絡(luò)包的長度序號)。
接收方可以根據(jù)接收到每個網(wǎng)絡(luò)包的長度減去頭部長度得到每個數(shù)據(jù)塊的長度。然后通過當(dāng)前最新序號+數(shù)據(jù)塊長度得知下一個接收到的包的序號應(yīng)該是多少。接收方返回的應(yīng)答包的ACK號應(yīng)該是當(dāng)前最新序號+數(shù)據(jù)塊長度+1,下一次發(fā)送方應(yīng)該發(fā)送的序號也應(yīng)該是接收方上一次回包的ACK號。
例如:當(dāng)前序號為A,包的長度是1460。那么響應(yīng)包ACK號應(yīng)該是A+1460+1 。接收方應(yīng)該接收到的下一個包的序號應(yīng)該是A+1460+1 = A + 1461。如果接收方收到的下個包的序號比A+1461大,就說明中間有包遺漏。
序號告訴接收方當(dāng)前發(fā)送方已經(jīng)發(fā)送了多少字節(jié)的數(shù)據(jù),ACK告訴發(fā)送方當(dāng)前接收方接收了多少字節(jié)的數(shù)據(jù)(所以同理,發(fā)送方能通過ACK號判斷接收方的回包是否有丟包)。
每次發(fā)送方發(fā)出的網(wǎng)絡(luò)包都會得到接收方包含ACK號的回包,這種機(jī)制成為確認(rèn)應(yīng)答機(jī)制。
實(shí)際通信中,序號不是從1開始,而是用隨機(jī)數(shù)計(jì)算出來的一個初始值。在連接階段,雙方設(shè)置SYN為1的時候,初始的序號也會被設(shè)置好并由通信雙方互相發(fā)送給對方(同理初始ACK號也在連接階段通知給對方)。
也就是說,通信的時候序號是有兩個,一個是客戶端生成的序號,一個是服務(wù)端生成的序號,因?yàn)門CP數(shù)據(jù)的收發(fā)是雙向的(例如服務(wù)端會接收到客戶端的包時回包,服務(wù)端也會在返回響應(yīng)數(shù)據(jù)時主動發(fā)送包[這里不是指ACK包,而是返回http響應(yīng)消息的網(wǎng)絡(luò)包],所以客戶端不只是發(fā)送方,服務(wù)端不只是接收方,應(yīng)該是客戶端和服務(wù)端都既能是發(fā)送方也能是接收方)。同理ACK號也有兩個。
如下:
序號和ACK號的出現(xiàn)是為了告訴通信雙方每次發(fā)包發(fā)送了多少數(shù)據(jù)和接收了多少數(shù)據(jù)從而判斷是否有丟包。丟包會引發(fā)TCP的重傳機(jī)制,重傳是TCP模塊獨(dú)有的錯誤補(bǔ)償機(jī)制,網(wǎng)卡、路由器、集線器一旦檢測到錯誤會直接丟棄包而不會重傳。
九、重傳機(jī)制之ACK號等待時間
當(dāng)發(fā)送方發(fā)送包后會等待接收方返回帶有ACK號的回包(在這個等待過程中發(fā)送方不會什么都不做,這里涉及到后面要介紹的滑動窗口,這里先不提),這段等待的時間叫做ACK號的等待時間,如果超過了這段時間都沒有接收到回包,發(fā)送方會認(rèn)為這個包在網(wǎng)絡(luò)中丟失,因而重新發(fā)送這個包。
發(fā)送方丟包 或者 發(fā)送方的包到達(dá)接收方后接收方的回包丟包 或者 接收方的回包因?yàn)榫W(wǎng)絡(luò)擁塞而返回緩慢都可能造成接收方等待超過ACK號的超時時間而引起重傳。
TCP模塊怎么設(shè)置一個合適的ACK等待時間?
由于不同服務(wù)器的距離不同,或者不同網(wǎng)絡(luò)環(huán)境下網(wǎng)絡(luò)擁塞的情況不同(如局域網(wǎng)內(nèi)可能幾毫秒能返回ACK號,而在互聯(lián)網(wǎng)中遇到擁塞可能幾百毫秒才能返回),ACK等待時間不是一個固定的時間,而是動態(tài)變化的時間。TCP模塊會持續(xù)測量多次ACK號的返回時間,如果前幾次ACK號返回變慢就會延長等待時間,如果前幾次ACK號能馬上返回則縮短等待時間。
每次因?yàn)槌^等待時間導(dǎo)致的重傳會延長這個超時時間,如果多次重傳后仍未收到確認(rèn)包則會斷開連接。
十、重傳機(jī)制之快速重傳
快速重傳是比超時重傳更有效率的重傳方式,當(dāng)接收方接收到亂序的報文(如 1-3-2,或者1-3-4)時,會立刻不延遲的返回重復(fù)ACK的回包給發(fā)送方,發(fā)送方重復(fù)接收到3次相同的ACK號之后就會重發(fā)丟失的包。如下所示:
圖中發(fā)送方第一個包到達(dá),接收方返回ACK號為ACK2的回包;但發(fā)送方的第二個報文丟失,之后第3~5個包到達(dá)接收方,接收方通過確認(rèn)包的序號得知第二個包沒有發(fā)送過來,因此對第3~5的包返回重復(fù)的ACK號(ACK2)。
接收方接收到多次重復(fù)的ACK號為ACK2的響應(yīng)包之后得知第2個包沒有發(fā)送到接收方,就會重新發(fā)送第2個包(序號為ACK2的包),并且重發(fā)第3~5個包(因?yàn)榘l(fā)送方得到的最新ACK號是ACK2,因此會重發(fā)序號為ACK2以后的所有包)。接收方接收到第2~5個包后,統(tǒng)一回復(fù)了一個ACK號為ACK6的包。
如果使用了SACK選項(xiàng),發(fā)送方只會重發(fā)第2個包,而不會重發(fā)3~5。
十一、簡述滑動窗口
如果TCP模塊發(fā)送一個包并等待回包的過程中什么都不做會顯得十分浪費(fèi)。為了減少這樣的浪費(fèi),TCP模塊使用滑動窗口的方式發(fā)送包,也就是說它不會非得等到一個包回包之后才發(fā)下一個包,而是連續(xù)發(fā)送多個包并連續(xù)接收多個回包。如下所示:
接收方在接收到包會先存放到接收緩沖區(qū),TCP模塊會從緩沖區(qū)中獲取包并計(jì)算包的ACK號,將多個包的數(shù)據(jù)塊組裝起來還原為原本的數(shù)據(jù)并傳遞給接收方的應(yīng)用程序。
如果包到達(dá)的速度要比數(shù)據(jù)處理并傳遞給應(yīng)用程序的速度快,那么緩沖區(qū)中數(shù)據(jù)就會越積越多最后溢出,溢出之后,接收方就無法接受后面的包。
如下圖所示,當(dāng)接收方收到包之后,會馬上從接收緩沖區(qū)中取出包并處理,緩沖區(qū)的空間會得到釋放,然后在返回包的TCP頭部注明接收緩沖區(qū)的剩余空間(也是窗口大?。@樣發(fā)送方收到回包后就知道下次發(fā)送的數(shù)據(jù)量不要超過這個窗口的大小的數(shù)據(jù)量。
在接收方不斷發(fā)送數(shù)據(jù)的過程中,它會自動計(jì)算自己用掉了多少窗口大小,當(dāng)計(jì)算到窗口大小為0時會暫停發(fā)送包。在這個過程中如果接收方回包,發(fā)送方會根據(jù)回包中的TCP頭部窗口大小來更新自己套接字內(nèi)的窗口大?。ㄗ詣佑?jì)算過程為4380->2920->1460,此時接收方返回ACK包并告知窗口大小剩余2920,發(fā)送方會更新當(dāng)前窗口大小為2920,再從2920繼續(xù)自動計(jì)算,2920->1460->0,此時暫停,直到接收方的ACK包又到達(dá)發(fā)送方,發(fā)送方再更新窗口大小,又開始繼續(xù)發(fā)包)。這就是滑動窗口的基本思路。
這張圖是為了講解方便,故意體現(xiàn)一種接收方來不及處理收到的包,導(dǎo)致緩沖區(qū)被填滿的情況。實(shí)際上,接收方在收到數(shù)據(jù)之后馬上就會開始進(jìn)行處理,如果接收方的性能高,處理速度比包的到達(dá)速率還快,緩沖區(qū)馬上就會被清空,并通過窗口字段告知發(fā)送方。
還有,圖中只顯示了從右往左發(fā)送數(shù)據(jù)的操作,實(shí)際上和序號、ACK號一樣,發(fā)送操作也是雙向進(jìn)行的。
另外需要注意的是:接收方不會對發(fā)送方的每個包都發(fā)送回包,因?yàn)榻邮辗桨l(fā)送回包其實(shí)是為了通知發(fā)送方ACK號和窗口大?。ㄒ簿褪歉嬖V發(fā)送方我收到了多少包以及我還能接收你多少包),所以接收方在一段時間內(nèi)連續(xù)收到發(fā)送方多個包后可能只會發(fā)送一個回包里面記錄著當(dāng)前最新的ACK號(即最后一個來包對應(yīng)的ACK號)和最新的窗口大小,中間的ACK號會省略,這樣減少了包的數(shù)量提升了網(wǎng)絡(luò)通信的效率。
當(dāng)服務(wù)端接收完客戶端某個HTTP消息的所有包并還原為http消息時,服務(wù)端的應(yīng)用程序就對這個http請求進(jìn)行處理,再響應(yīng)數(shù)據(jù)封裝為http響應(yīng)消息,經(jīng)過協(xié)議棧封裝成一個個包返回給客戶端。這時服務(wù)端變成了發(fā)送方,而客戶端變成接收方。
1.接收http響應(yīng)消息
當(dāng)協(xié)議棧將http消息以多個包的形式發(fā)送完畢之后,工作流程回到應(yīng)用程序(瀏覽器),應(yīng)用程序會調(diào)用read()接收響應(yīng)消息。工作流程再次轉(zhuǎn)移到協(xié)議棧。
響應(yīng)消息到達(dá)客戶端主機(jī)后會先暫存在接收緩沖區(qū),協(xié)議棧會嘗試從緩沖區(qū)獲取數(shù)據(jù),如果緩沖區(qū)中沒有數(shù)據(jù),則協(xié)議棧會暫停接收工作去做其他事情。直到消息到達(dá)才會將數(shù)據(jù)傳遞給應(yīng)用程序,即把數(shù)據(jù)復(fù)制到應(yīng)用程序指定的內(nèi)存地址(數(shù)據(jù)量大的情況下不會一次性全傳遞給應(yīng)用程序,而是會分多次傳遞)。
在這一步中,還略過了很多的細(xì)節(jié),如協(xié)議棧會檢查收到的數(shù)據(jù)塊和TCP頭部,判斷是否有數(shù)據(jù)丟失;向服務(wù)器方更新窗口大??;將多塊數(shù)據(jù)按照序號拼接為原始的數(shù)據(jù),等等。這些過程和客戶端發(fā)送發(fā)送方時的過程類似。
2.服務(wù)器斷開連接并刪除套接字
發(fā)送完數(shù)據(jù)的一方會主動發(fā)起斷開連接的操作。在Web通信中,服務(wù)器一方是最后發(fā)送數(shù)據(jù)的一方因此會主動關(guān)閉連接。
- 服務(wù)器會應(yīng)用程序會調(diào)用Socket庫的close函數(shù),工作流程轉(zhuǎn)到協(xié)議棧,協(xié)議棧會生成FIN比特位為1的TCP頭部,委托IP模塊發(fā)送給客戶端。此時服務(wù)器會進(jìn)入關(guān)閉等待狀態(tài)并記錄到套接字中。
- 客戶端收到服務(wù)端關(guān)閉連接的包后,會進(jìn)入關(guān)閉等待的狀態(tài)并記錄到套接字中。并且發(fā)送一個ACK包給服務(wù)端表示已經(jīng)收到了關(guān)閉連接的請求。
- 客戶端不會馬上關(guān)閉連接,而是等應(yīng)用程序?qū)⒔邮站彌_區(qū)中所有數(shù)據(jù)接收完畢(等待read()過程結(jié)束)。
- 當(dāng)操作系統(tǒng)中接收緩沖區(qū)的數(shù)據(jù)接收完畢后,工作流程回到應(yīng)用程序,應(yīng)用程序調(diào)用Socket庫的close函數(shù),工作流程轉(zhuǎn)到協(xié)議棧,客戶端的協(xié)議棧也會生成一個FIN比特位為1的TCP包通過IP模塊發(fā)送給服務(wù)端,告訴服務(wù)端可以關(guān)閉連接了。
- 服務(wù)端收到FIN比特位為1的TCP包后會回一個ACK包。客戶端接收到ACK包后會關(guān)閉連接(關(guān)閉連接的實(shí)質(zhì)是刪除套接字),而服務(wù)端會不會馬上關(guān)閉連接(即不會馬上刪除套接字),而是進(jìn)入到time-wait的等待狀態(tài)(將socket的狀態(tài)標(biāo)記為time-wait),time-wait狀態(tài)結(jié)束后服務(wù)器才會真正關(guān)閉連接和刪除套接字。
以上過程就是大家熟知的“四次揮手”過程。
服務(wù)端不馬上不關(guān)閉連接而是進(jìn)入time-wait狀態(tài)是為了防止誤操作。例如當(dāng)客戶端是主動發(fā)起斷開連接的一端(假定客戶端的套接字是綁定54305這個端口),那么最后發(fā)送ACK包的會是客戶端,如果這個ACK包丟包,服務(wù)端等待一段時間沒有收到這個ACK包會重新發(fā)FIN包給客戶端。
假如客戶端沒有time-wait狀態(tài)而是在發(fā)送ACK包后直接關(guān)閉連接(即刪除套接字,釋放54305這個端口),那么客戶端可能會為其他連接生成一個新的套接字綁定54305這個端口。服務(wù)端重發(fā)的FIN包到達(dá)客戶端后,客戶端根據(jù)包頭部的接收方端口找到了新的套接字上就會導(dǎo)致該新套接字上的連接關(guān)閉。
time-wait狀態(tài)一般會持續(xù)幾分鐘。
附:使用UDP協(xié)議收發(fā)包
相比于TCP協(xié)議,使用UDP協(xié)議發(fā)送包無需建立連接(通信雙方服務(wù)交換控制消息),因此數(shù)據(jù)的收發(fā)相比于TCP減小了開銷和發(fā)送延遲更加高效。但是UDP協(xié)議沒有TCP協(xié)議的安全傳輸機(jī)制如確認(rèn)應(yīng)答機(jī)制,重傳和窗口等。
UDP頭部的控制信息:
UDP適用于以下場景:
a. 短數(shù)據(jù)
如果可以僅用一個包就將所有數(shù)據(jù)發(fā)送給對端,那么就無需建立連接以保存雙方的IP和端口以及其他信息到套接字中。而且數(shù)據(jù)少意味著傳輸?shù)陌鼣?shù)量少,丟包的可能性就會減小,也就無需像TCP那樣對包的送達(dá)狀態(tài)進(jìn)行監(jiān)控。如果丟包,協(xié)議棧收不到對方的回復(fù)是不會自行重發(fā)數(shù)據(jù)包的,應(yīng)用程序可以自行組織重發(fā)數(shù)據(jù)(需要開發(fā)應(yīng)用程序的人編寫重試邏輯)。
b.視頻和音頻數(shù)據(jù)
音頻和視頻數(shù)據(jù)必須在規(guī)定的時間內(nèi)送達(dá),一旦送達(dá)晚了,就會錯過播放時機(jī),導(dǎo)致聲音和圖像卡頓。如果像TCP一樣通過接收確認(rèn)應(yīng)答來檢查錯誤并重發(fā),重發(fā)的過程需要消耗一定的時間,因此重發(fā)的數(shù)據(jù)很可能已經(jīng)錯過了播放的時機(jī)。
此外,音頻和視頻數(shù)據(jù)中缺少了某些包并不會產(chǎn)生嚴(yán)重的問題,只是會產(chǎn)生一些失真或者卡頓而已,一般都是可以接受的。使用UDP發(fā)送數(shù)據(jù)的效率會更高。
上面的操作都是圍繞著傳輸層TCP模塊介紹數(shù)據(jù)的收發(fā)。其實(shí)TCP報文還需要在網(wǎng)絡(luò)層的IP模塊中經(jīng)過添加IP頭部和MAC頭部封裝成包再委托給網(wǎng)卡發(fā)送出去。
下節(jié)預(yù)告:瀏覽器輸入一個網(wǎng)址發(fā)生了什么(三) IP模塊封裝、ARP協(xié)議、IP協(xié)議和網(wǎng)卡