數(shù)據(jù)在網(wǎng)絡(luò)中是如何傳輸?shù)?/h1>
交互過程如下圖所示:
套接字如何創(chuàng)建的
協(xié)議棧內(nèi)部結(jié)構(gòu)
如上圖所示,整個(gè)請求交互過程分為了幾個(gè)部分,首先最上層就是應(yīng)用程序,接著往下是 Socket 庫。
再下面就是操作系統(tǒng)的內(nèi)部了,這里面就包括了協(xié)議棧,協(xié)議棧上半部分為 TCP 和 UDP ,它們都是負(fù)責(zé)數(shù)據(jù)的收發(fā)。
只是一個(gè)需要 連接,一個(gè)不需要連接可以直接收發(fā)數(shù)據(jù),這兩者的詳細(xì)區(qū)別我會在后期文章單獨(dú)講解,這里大家先了解下就行。
協(xié)議棧的下半部分是 IP 協(xié)議,用來真正將數(shù)據(jù)轉(zhuǎn)變?yōu)榫W(wǎng)絡(luò)包進(jìn)行實(shí)際數(shù)據(jù)傳送的媒介。
IP 下面就是網(wǎng)卡驅(qū)動程序,用來控制網(wǎng)卡硬件。
認(rèn)識套接字
在協(xié)議棧內(nèi)部有一塊用來存放控制信息的內(nèi)存空間,這里面記錄了需要連接的對象 IP 地址、端口號、進(jìn)行狀態(tài)等信息。
而套接字本身其實(shí)只是一個(gè)概念,實(shí)際并沒有這樣一個(gè)東西,這個(gè)概念如果非要賦予它一個(gè)實(shí)體,那控制信息可以認(rèn)為就是它的實(shí)體。
在發(fā)送數(shù)據(jù)時(shí),我們需要看下套接字要進(jìn)行連接的對象 IP 地址和端口號;發(fā)送數(shù)據(jù)之后,套接字里面會記錄發(fā)送數(shù)據(jù)經(jīng)過了多長時(shí)間,如果發(fā)送收到響應(yīng),也會進(jìn)行記錄。
我們來實(shí)際看下 套接字 都有哪些信息,可以在你電腦的控制臺輸入 netstat 命令進(jìn)行查詢:
- Proto: 表示協(xié)議類型。這里是 tcp ,如果用到了 udp 就會顯示為 udp。
- Local Address : 本機(jī)的 IP 地址。
- Foreign Address : 通信對象的 IP 地址
- state : 通信狀態(tài)。ESTABLISHED 表示完成連接 ,CLOSE_WAIT 表示等待關(guān)閉,還有一個(gè)狀態(tài)也很常見,LISTENING:等待對方連接。
當(dāng)瀏覽器通過 Socket 庫向協(xié)議棧發(fā)出 socket 調(diào)用時(shí),協(xié)議棧就會根據(jù)申請執(zhí)行創(chuàng)建套接字的操作。
協(xié)議棧首先會分配一個(gè)存放套接字的內(nèi)存空間,然后往里面存入控制信息,這樣套接字就創(chuàng)建好了。
連接服務(wù)器
創(chuàng)建好套接字后,瀏覽器會調(diào)用 connect ,協(xié)議棧就會將本地的套接字和服務(wù)器的套接字進(jìn)行連接。
連接就是通信雙方互相交換控制信息,連接操作所交換的控制信息是根據(jù)通信規(guī)則來確定的,只要雙方根據(jù)規(guī)則進(jìn)行連接,就能建立起連接關(guān)系,完成數(shù)據(jù)收發(fā)的準(zhǔn)備。
控制信息
控制信息一般可以分為兩類,一類是客戶端和服務(wù)器相互聯(lián)系時(shí)交換的控制信息,這個(gè)信息是兩者建立連接、數(shù)據(jù)收發(fā)、斷開連接整個(gè)通信過程都需要的信息。
一般這些內(nèi)容是通過 TCP 協(xié)議進(jìn)行定義的。這些信息會被添加進(jìn)網(wǎng)絡(luò)包的開頭,因此也叫作頭部,以太網(wǎng)和 IP 協(xié)議也有自己的控制信息,這個(gè)信息也叫頭部,為了進(jìn)行區(qū)分,我們分別叫作 TCP 頭部、以太網(wǎng)頭部、IP 頭部。
這里羅列了部分 TCP 頭部的信息,僅供參考。
控制信息還有一類,是保存在套接字里的,應(yīng)用程序傳遞的信息和從通信對象接受的信息都會保存在這里,以及數(shù)據(jù)收發(fā)操作的執(zhí)行狀態(tài)也會在這里面。
連接操作的實(shí)際過程
連接操作的第一步就是在 TCP 模塊處創(chuàng)建表示連接控制信息的頭部。當(dāng) TCP 頭部創(chuàng)建好后,TCP 模塊會將信息傳遞給 IP 模塊委托其進(jìn)行發(fā)送。
IP 執(zhí)行發(fā)送后,網(wǎng)絡(luò)包會通過網(wǎng)絡(luò)到達(dá)服務(wù)器,服務(wù)器上的 IP 模塊將接收到的數(shù)據(jù)傳遞給 TCP 模塊,TCP 模塊根據(jù)頭部信息找到對應(yīng)的套接字,套接字中會寫入相應(yīng)的信息,然后將狀態(tài)改為正在連接。
于此同時(shí),在返回響應(yīng)時(shí),會將 ACK 控制位設(shè)為 1,代表已接收到網(wǎng)絡(luò)包。服務(wù)器 TCP 模塊會將響應(yīng)消息通過 IP 模塊向客戶端做出響應(yīng)。
客戶端接收到響應(yīng)后,其 IP 模塊將信息傳遞給 TCP 模塊,然后通過 TCP 頭部信息確認(rèn)連接是否成功,SYN 等于 1 就代表成功,客戶端還會將 ACK 設(shè)置為1 并發(fā)回給服務(wù)器,服務(wù)器收到這個(gè)包后才算連接操作真正的完成。
建立連接后,就可以隨時(shí)進(jìn)行收發(fā)數(shù)據(jù)了,在調(diào)用 close 之前,連接會一直存在。
收發(fā)數(shù)據(jù)
收發(fā)數(shù)據(jù)的觸發(fā)操作是應(yīng)用程序發(fā)起的,通過調(diào)研 write,指定發(fā)送數(shù)據(jù)的長度。
一般當(dāng)協(xié)議棧接受到數(shù)據(jù)時(shí)可能并不會馬上發(fā)出去,而是放在發(fā)送緩沖區(qū)中,為什么要這樣做呢?
有些程序可能一次性會傳所有數(shù)據(jù),但有些程序會逐行傳遞,在這種情況下,如果收到數(shù)據(jù)就發(fā)送,可能會造成發(fā)送大量小包數(shù)據(jù),導(dǎo)致效率低下。
至于需要積累多少數(shù)據(jù)才發(fā)送一般是根據(jù)兩方面因素來考量,一個(gè)是每個(gè)網(wǎng)絡(luò)包的數(shù)據(jù)長度,還有一個(gè)緯度是處理時(shí)間。
網(wǎng)絡(luò)包容納的數(shù)據(jù)長度
首先介紹下兩個(gè)名詞:
MTU: 一個(gè)網(wǎng)絡(luò)包的最大長度,以太網(wǎng)中一般是1500字節(jié),是包含頭部的總長度。
MSS: 除去頭部后,一個(gè)網(wǎng)絡(luò)包所有容納的數(shù)據(jù)最大長度。
處理時(shí)間
當(dāng)一個(gè)應(yīng)用程序發(fā)送數(shù)據(jù)的頻率不高時(shí),如果每次都需要等到長度達(dá)到 MSS 才發(fā)送,就會造成等待時(shí)間過長。
為了解決這種情況,協(xié)議棧會有一個(gè)計(jì)時(shí)器,如果達(dá)到一定時(shí)間,即使還遠(yuǎn)未達(dá)到 MSS 長度,也會把網(wǎng)絡(luò)包發(fā)送出去。
ACK 機(jī)制確認(rèn)網(wǎng)絡(luò)包接收情況
當(dāng)客戶端向服務(wù)端發(fā)送數(shù)據(jù)時(shí),TCP會將數(shù)據(jù)的字節(jié)數(shù)算好寫在 TCP 頭部,同時(shí)會生成一個(gè)隨機(jī)數(shù) 當(dāng)作 ACK 一并發(fā)送給服務(wù)端,服務(wù)端接受后就會根據(jù)實(shí)際收到的長度和TCP頭部給的長度做對比,來確保數(shù)據(jù)沒有遺漏。
同時(shí)客戶端還需要告知服務(wù)端是從哪個(gè)字節(jié)開始發(fā)送的,而我們的 ACK是個(gè)隨機(jī)值,這時(shí)候我們就需要通過 SYN 控制位設(shè)置為1發(fā)送給服務(wù)器,這樣服務(wù)器就知道其初始是從哪個(gè)字節(jié)開始發(fā)送的。
接受方收到數(shù)據(jù)后,如果數(shù)據(jù)沒問題,就需要告知發(fā)送方收到了多少數(shù)據(jù),也是通過 ACK 號的操作來返回的,這個(gè) ACK 的值就是一共接收了多少字節(jié)。
通過這種機(jī)制,我們就可以確認(rèn)接收方是否正確收到數(shù)據(jù),如果沒有準(zhǔn)確收到,就可以重新發(fā)送網(wǎng)絡(luò)包。
無論網(wǎng)絡(luò)發(fā)生何種錯(cuò)誤,我們就都可以發(fā)現(xiàn)并采取補(bǔ)救措施。
窗口滑動
一般如果我們每發(fā)送一個(gè)網(wǎng)絡(luò)包就等待 ACK 返回確認(rèn)后再發(fā)送下一個(gè)包,這個(gè)等待 ACK 的時(shí)間啥都不做就會很浪費(fèi)。
窗口滑動的概念就是每次發(fā)送一個(gè)網(wǎng)絡(luò)包,不會等 ACK 返回就會繼續(xù)發(fā)送下一個(gè)包,減少等待時(shí)間的浪費(fèi)。
但這種方式也會存在問題,假如發(fā)送方不斷發(fā)送數(shù)據(jù)給接收方,接收方第一個(gè)數(shù)據(jù)還沒處理完,第二個(gè)數(shù)據(jù)就來了,這些來不及處理的數(shù)據(jù)會進(jìn)入接收緩沖區(qū),數(shù)據(jù)會不斷增多,就會造成溢出。避免這種方式的處理是通過接收方告知發(fā)送方自己最大能接收多少數(shù)據(jù),發(fā)送方會根據(jù)這個(gè)值對發(fā)送的數(shù)據(jù)進(jìn)行控制。
刪除套接字
當(dāng)我們數(shù)據(jù)收發(fā)完成后,就會啟動斷開機(jī)制,以 Web 為例,收發(fā)數(shù)據(jù)結(jié)束時(shí),服務(wù)器會發(fā)起斷開過程,會調(diào)用 Socket 庫的 close 程序,服務(wù)器協(xié)議棧會生成一個(gè)包含斷開信息的 TCP 頭部,就是將 FIN 比特設(shè)置為1。協(xié)議棧會委托 IP 模塊向客戶端發(fā)送數(shù)據(jù)。
當(dāng)客戶端接收到 FIN 為 1 的 TCP 頭部時(shí),客戶端協(xié)議棧會將自己的套接字標(biāo)記為進(jìn)入斷開操作狀態(tài),然后告知服務(wù)器已經(jīng)收到 FIN 為 1的包,客戶端會向服務(wù)器返回一個(gè) ACK 號。
UDP 協(xié)議收發(fā)操作
之前我們都是以 TCP 協(xié)議講解的數(shù)據(jù)收發(fā)操作,可以看出整個(gè)流程下來其實(shí)是挺復(fù)雜的,但是有時(shí)候可能我們并不需要這么復(fù)雜的安全校驗(yàn),UDP 就可以滿足一些簡單的數(shù)據(jù)收發(fā)。例如像我們之前提到的 向 DNS 服務(wù)器查詢 IP 地址,我們就是用的 UDP 協(xié)議。
UDP 沒有 TCP 的接收確認(rèn)、窗口等機(jī)制,在收發(fā)數(shù)據(jù)之前是不需要進(jìn)行交換控制信息,不需要進(jìn)行連接操作。
接收數(shù)據(jù)也很簡單,只需要根據(jù) IP 頭部中的接收方和發(fā)送方 IP 地址,以及 UDP 頭部中的接收方和發(fā)送方端口號,找到對應(yīng)的套接字然后將數(shù)據(jù)交給相應(yīng)的應(yīng)用程序即可。