我們用websocket和http來(lái)研究一下TCP/IP協(xié)議的一些特性,在上一篇文章《https連接的前幾毫秒發(fā)生了什么》里我們已經(jīng)研究了https建立的過(guò)程谅猾。
上一篇是用的wireshark的抓包工具定页,這一篇將用tcpdump命令行工具迅耘。
1. tcpdump
Linux系的系統(tǒng)有一個(gè)很好用的抓包工具,叫tcpdump冤馏,可以用來(lái)抓取網(wǎng)絡(luò)上的tcp包日麸,例如我要抓取8080端口的包,可以執(zhí)行以下命令:
sudo tcpdump port 8080 –n
-n的意思是端口號(hào)用數(shù)字表示宿接,還可以加上-v -vv顯示更詳細(xì)的信息:
sudo tcpdump port 8080 –n -v
再如我要抓取來(lái)自特定源IP和發(fā)往特定目的IP的包赘淮,可以用以下命令:
sudo tcpdump src host 10.2.200.11 or dst host 10.2.200.11
指定src host和dst host,并用or/and做條件的交集和并集睦霎。
在建立一個(gè)網(wǎng)頁(yè)的websocket之前先要建立一個(gè)http連接梢卸,為此我們簡(jiǎn)單寫一個(gè)小demo。
2. hello, world的http連接
(1)首先寫以下的html文件:
(2)然后再裝一個(gè)http-server的node包副女,監(jiān)聽(tīng)在8080端口蛤高,如下所示:
(3)電腦開(kāi)tcpdump命令,抓取通過(guò)8080端口通訊的包:
sudo tcpdump port 8080 –n
(4)用手機(jī)訪問(wèn):http://10.2.200.140.8080**碑幅,tcpdump就會(huì)打出所有傳輸?shù)膖cp包戴陡,如下圖所示。
我們拿它打印的這些TCP報(bào)文做一個(gè)研究沟涨。在建立一個(gè)http連接之前恤批,先要建立一個(gè)TCP連接,即上圖的頭3個(gè)報(bào)文裹赴。下面研究一下這個(gè)HTTP連接是怎么進(jìn)行的喜庞。
3. 一個(gè)完整的HTTP連接
(1)TCP三次握手
第一個(gè)報(bào)文:11 -> 140
10:11:06.151830 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [S], seq 2153742604, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 298297187 ecr 0,sackOK,eol], length 0
在10點(diǎn)11分的時(shí)候,IP為10.2.200.11(以后簡(jiǎn)稱11)的63826端口向IP為10.2.200.140(以后簡(jiǎn)稱140)的8080端口發(fā)了一個(gè)TCP的包棋返,帶上了標(biāo)志位SYN延都,表示要建立一個(gè)連接,并指明包開(kāi)始的序列號(hào)seq(單位為字節(jié))睛竣,以后傳送的字節(jié)編號(hào)都是以這個(gè)做為起點(diǎn)晰房,并告知能接收的最大報(bào)文段長(zhǎng)度mss為1460,一般mss都為1460.
第二個(gè)報(bào)文:140 -> 11
10:11:06.151917 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [S.], seq 1007874094, ack 2153742605, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 493556398 ecr 298297187,sackOK,eol], length 0
在過(guò)了87微秒之后,140進(jìn)行了回復(fù)殊者,發(fā)送了一個(gè)SYN + ACK的報(bào)文段与境,表示同意和11建立連接。
第三個(gè)報(bào)文:11 -> 140
10:11:06.190376 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [.], ack 1, win 4117, options [nop,nop,TS val 298297310 ecr 493556398], length 0
11收到SYN之后向140發(fā)送一個(gè)ACK幽污,同時(shí)改變接收窗口為4117 * 2 ^ 5 = 131kb嚷辅,完成三次握手。
什么是接收窗口呢距误?
(2)接收窗口
第四個(gè)報(bào)文里面簸搞,140也向11修改了它的接收窗口大小:
10:11:06.190422 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [.], ack 1, win 4117, options [nop,nop,TS val 493556436 ecr 298297310], length 0
大小為4117 * 2 ^ 5 = 131kb准潭,為什么接收窗口是這個(gè)數(shù)呢趁俊?因?yàn)槿缦耇CP的報(bào)文(頭):
窗口大小只有2個(gè)字節(jié)16位,最大只能表示2 ^ 16 - 1 = 65535即16Kb刑然,當(dāng)初設(shè)計(jì)TCP的人并沒(méi)有想到現(xiàn)在的網(wǎng)速會(huì)提升這么快寺擂,16Kb是不夠用的,所以在可選項(xiàng)里面加了一個(gè)wscale(window scale factor)的指數(shù)字段泼掠,最大值為14怔软,所以最大的接收窗口大概為1GB.
說(shuō)了這么多,接收窗口是用來(lái)做什么的呢择镇?它根據(jù)自身網(wǎng)絡(luò)情況設(shè)置不同大小的值用來(lái)控制對(duì)方發(fā)送速度挡逼,避免對(duì)方發(fā)送太快,導(dǎo)致網(wǎng)絡(luò)擁塞腻豌。下面講到擁塞控制會(huì)更進(jìn)一步地討論家坎。
(3)發(fā)送數(shù)據(jù)
建立好TCP連接后,11向140發(fā)送了一個(gè)http請(qǐng)求:
10:11:06.193435 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [P.], seq 1:404, ack 1, win 4117, options [nop,nop,TS val 298297312 ecr 493556398], length 403: HTTP: GET / HTTP/1.1
這里吝梅,它帶上了一個(gè)PUSH的標(biāo)志位虱疏,表示它是一個(gè)比較緊急的報(bào)文,要求對(duì)方立即把數(shù)據(jù)從緩存里面發(fā)送給應(yīng)用程序苏携,不能再繼續(xù)緩存了做瞪。
它發(fā)送的字節(jié)號(hào)為[1, 404),這個(gè)數(shù)字是tcpdump顯示的相對(duì)于握手的協(xié)議初始序列號(hào)顯示的偏移右冻,它是一個(gè)左閉右開(kāi)的表示穿扳,所以這個(gè)報(bào)文總共發(fā)送了403個(gè)字節(jié)的數(shù)據(jù)。它是一個(gè)GET請(qǐng)求国旷。
然后后140收到后給11回復(fù)了一個(gè)ACK:
10:11:06.193467 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [.], ack 404, win 4105, options [nop,nop,TS val 493556439 ecr 298297312], length 0
ACK 404表示期待收到第404字節(jié)的數(shù)據(jù),也就是說(shuō)前面403個(gè)字節(jié)的數(shù)據(jù)已經(jīng)都確認(rèn)收到茫死。
然后140進(jìn)行了http響應(yīng):
10:11:06.194840 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [P.], seq 1:290, ack 404, win 4105, options [nop,nop,TS val 493556440 ecr 298297312], length 289: HTTP: HTTP/1.1 200 OK
10:11:06.200295 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [.], ack 290, win 4108, options [nop,nop,TS val 298297318 ecr 493556440], length 0
10:11:06.200315 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [P.], seq 290:458, ack 404, win 4105, options [nop,nop,TS val 493556445 ecr 298297318], length 168: HTTP
10:11:06.204847 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [.], ack 458, win 4103, options [nop,nop,TS val 298297321 ecr 493556445], length 0
140總共發(fā)送了457個(gè)字節(jié)的數(shù)據(jù)跪但,分成了兩個(gè)包發(fā)送。而本地的html文件大小為:
所以可以認(rèn)為http報(bào)文頭占用了457 - 168 = 289字節(jié)。
(4)關(guān)閉連接
第一個(gè)報(bào)文:11 -> 140 FIN
10:11:36.359973 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [F.], seq 404, ack 458, win 4103, options [nop,nop,TS val 298327416 ecr 493556445], length 0
11等了30s后覺(jué)得不用再請(qǐng)求數(shù)據(jù)了屡久,于是要把連接關(guān)閉了忆首,它向140發(fā)送一個(gè)FIN的報(bào)文。為什么要等30s才關(guān)閉呢被环?這是HTTP請(qǐng)求的Connection: keep-alive字段影響的糙及,因?yàn)橥粋€(gè)域可能要請(qǐng)求多個(gè)資源,不能一個(gè)請(qǐng)求完了就把連接關(guān)閉了筛欢。如果不關(guān)閉又占用端口號(hào)資源浸锨,我們知道端口號(hào)最多只有65535個(gè)。
第二個(gè)報(bào)文:140 -> 11 ACK
10:11:36.360021 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [.], ack 405, win 4105, options [nop,nop,TS val 493586567 ecr 298327416], length 0
140收到這個(gè)包后向11發(fā)送一個(gè)ACK版姑,這個(gè)時(shí)候連接處于半關(guān)閉狀態(tài)柱搜,即11不可再向140發(fā)送數(shù)據(jù)了,但140還可以向11發(fā)送剥险。
第三個(gè)報(bào)文:140 -> 11 FIN
10:11:36.360537 IP 10.2.200.140.8080 > 10.2.200.11.63826: Flags [F.], seq 458, ack 405, win 4105, options [nop,nop,TS val 493586567 ecr 298327416], length 0
140也要把連接關(guān)閉了聪蘸,于是它向11發(fā)送FIN
第四個(gè)報(bào)文:11 -> 140 ACK
10:11:36.368758 IP 10.2.200.11.63826 > 10.2.200.140.8080: Flags [.], ack 459, win 4103, options [nop,nop,TS val 298327458 ecr 493586567], length 0
11收到后,向它發(fā)了一個(gè)ACK表制,此時(shí)連接完全關(guān)閉健爬。然后主動(dòng)關(guān)閉方11將進(jìn)入TIME_WAIT狀態(tài)
(5)MSS和TIME_WAIT
TIME_WAIT時(shí)間為2MSL,MSL的意思是maximum segment livetime么介,即報(bào)文段的最大生存時(shí)間娜遵,標(biāo)準(zhǔn)建議為2分鐘,實(shí)際實(shí)現(xiàn)有的為30s夭拌。在TIME_WAIT狀態(tài)下魔熏,上一次建立連接的套接字(socket)將不可再重新啟用,也就是同一個(gè)網(wǎng)卡/IP不可再建立同樣端口號(hào)的連接鸽扁,如上面是10.2.200.11.63826蒜绽,如果再重新創(chuàng)建系統(tǒng)將會(huì)報(bào)錯(cuò)。為什么要等待這個(gè)時(shí)間呢桶现?主要是為了避免有些報(bào)文段在網(wǎng)絡(luò)上滯留躲雅,被對(duì)方收到的時(shí)候如果剛好又啟用了一個(gè)完全一樣的套接字,那么就會(huì)被認(rèn)為是這個(gè)連接的數(shù)據(jù)骡和。因此為了讓所有“迷路”的報(bào)文徹底消失后相赁,才能啟用相同的套接字。但是有時(shí)候你會(huì)覺(jué)得兩分鐘不能重新啟動(dòng)相同的socket慰于,有點(diǎn)麻煩钮科,所以要把它禁了,可以在創(chuàng)建socket的時(shí)候婆赠,指定SO_REUSEADDR的選項(xiàng)绵脯,這樣就不用等待TIME_WAIT的時(shí)間了。
另外還有一個(gè)時(shí)間叫RTT(round trip time),即一個(gè)報(bào)文段的往返時(shí)間蛆挫,可以理解為我發(fā)一個(gè)數(shù)據(jù)給你赃承,你再回我一個(gè)ACK這個(gè)往返過(guò)程的時(shí)間,這個(gè)時(shí)間是動(dòng)態(tài)計(jì)算的悴侵,在下面講擁塞控制的時(shí)候?qū)?huì)提及這個(gè)時(shí)間瞧剖。
接下來(lái)討論兩個(gè)問(wèn)題。
4. 為什么TCP握手要三次可免?
為什么不是兩次抓于、四次呢?有人說(shuō)三次是建立一個(gè)可靠連接最少的次數(shù)巴元,那為什么不是兩次呢毡咏??jī)纱魏孟褚部梢园。拖翊螂娫挘?/p>
甲:喂逮刨,你聽(tīng)得到嗎呕缭?
乙:我聽(tīng)得到
然后甲就可以開(kāi)始說(shuō)話了。再舉另外一個(gè)例子做說(shuō)明修己,假設(shè)有三個(gè)山頭:A恢总、B、C睬愤,A山頭想要聯(lián)合B山頭的人晚上六點(diǎn)去攻打B山頭的人片仿,因?yàn)槿绻挥幸粋€(gè)山頭的人去攻打C的話會(huì)陣亡,所以A和B需要進(jìn)行握手尤辱。
于是:
- A就派了只鴿子帶上SYN的消息過(guò)去找B
- B收到后又派了只鴿子帶上ACK + SYN的消息回復(fù)A
- A收到后又派了只鴿子帶上ACK去回復(fù)B
這個(gè)就好像我們的三次握手砂豌,但是三次就夠了嗎?假設(shè)第三次A發(fā)的ACK C沒(méi)有收到光督,這時(shí)候B就要猶豫了:會(huì)不會(huì)A不知道我同意了阳距,如果A不知道我同意那么它可能不會(huì)去攻打了,然后我去了就得被滅了结借。由于A不知道它的回復(fù)有沒(méi)有被收到筐摘,所以它可能會(huì)想到B可能會(huì)怕它不會(huì)出擊,所以A也猶豫了船老。
因此三次握手并不能保證雙方完全地信任對(duì)方咖熟,即使是四次、五次也是同樣道理柳畔,至少有一方無(wú)法信任另一方馍管,另外一方一想到對(duì)方可能不信它,它也會(huì)變得不信對(duì)方薪韩。
但是這個(gè)例子并不是說(shuō)TCP連接建立是不可靠的咽斧,實(shí)際的場(chǎng)景往往是只要雙方確認(rèn)對(duì)方都在就好了铣猩,如下:
甲:你活著嗎躁垛?我想和你通話
乙:我活著呢,我們開(kāi)始通話吧
因此最少的握手次數(shù)應(yīng)該是兩次蝇更,三次可以提高可靠性岭洲,四次宛逗、五次就沒(méi)必要了,就會(huì)陷入上面山頭攻打無(wú)限循環(huán)確認(rèn)的漩渦盾剩。如下:
甲:你活著嗎雷激?我想和你通話
乙:我活著呢,我們開(kāi)始通話吧
甲:好的
最后的“好的”可能有點(diǎn)多余告私,但是它顯得比較有“人情味”屎暇。
難道兩個(gè)山頭通信真的沒(méi)有辦法解決嗎?有辦法驻粟,我們將在下面的擁塞控制提到根悼。
5. 為什么揮手要四次
分析了握手次數(shù)的原因,很容易可以知道為什么揮手要四次了蜀撑。前兩次揮手讓連接處于半關(guān)閉狀態(tài)挤巡,此時(shí)主動(dòng)關(guān)閉方不可再向被動(dòng)關(guān)閉方發(fā)送數(shù)據(jù),而被動(dòng)關(guān)閉可繼續(xù)向主動(dòng)關(guān)閉方發(fā)送數(shù)據(jù)酷麦。如下圖所示:
所以四次的原因是可以有一個(gè)處于半關(guān)閉的狀態(tài)矿卑。
接下來(lái)看一下四層網(wǎng)絡(luò)協(xié)議。
6. 四層網(wǎng)絡(luò)協(xié)議
如下圖所示沃饶,我們從發(fā)送數(shù)據(jù)的角度看四層網(wǎng)絡(luò)協(xié)議:
假設(shè)我要用HTTP發(fā)送一個(gè)文本母廷,那么它會(huì)最后會(huì)被層層包裝成這樣一個(gè)報(bào)文:
在廣域網(wǎng)是用的IP地址進(jìn)行報(bào)文轉(zhuǎn)發(fā),而到了局域網(wǎng)需要靠物理地址發(fā)送給對(duì)應(yīng)的主機(jī)糊肤。IP是點(diǎn)到點(diǎn)琴昆,負(fù)責(zé)發(fā)送給對(duì)應(yīng)的主機(jī),而TCP是端到端轩褐,即根據(jù)端口號(hào)椎咧,負(fù)責(zé)發(fā)送給對(duì)應(yīng)的應(yīng)用程序。
(1)物理地址
每個(gè)網(wǎng)卡都有全球唯一的物理地址把介,路由器向同一個(gè)局域網(wǎng)所有主機(jī)發(fā)送收到的數(shù)據(jù)包勤讽,本機(jī)的網(wǎng)卡比較一下包里指明的物理地址和本機(jī)的物理地址是否一致,如果一致則接收拗踢,否則則丟棄脚牍。所以可以在局域網(wǎng)監(jiān)聽(tīng)發(fā)給其它人的數(shù)據(jù)包,當(dāng)然也有一些反監(jiān)聽(tīng)的手段巢墅。
(2)網(wǎng)際層ARP
ARP是一個(gè)地址解析協(xié)議诸狭,當(dāng)我訪問(wèn)10.2.200.140的時(shí)候我需要知道它的物理地址是多少券膀,因?yàn)樗呀?jīng)是一個(gè)局域網(wǎng)的IP地址了。我怎么知道它的物理地址是多少呢驯遇?我就向局域網(wǎng)的機(jī)器廣播一個(gè)ARP請(qǐng)求:
09:51:32.966852 ARP, Request who-has 10.2.200.140 tell 10.2.200.11, length 28
過(guò)了33微秒一小會(huì)的功夫就有人告訴我了:
09:51:32.966885 ARP, Reply 10.2.200.140 is-at 98:5a:eb:89:a5:7e (oui Unknown), length 28
這個(gè)很可能是路由器告訴我的芹彬,上面的tcpdump輸出沒(méi)有打印源IP。
可以通過(guò)arp -an的命令叉庐,查看電腦上的arp表舒帮,如下圖所示:
(3)網(wǎng)際層traceroute
有一個(gè)很好用的命令叫traceroute,它可以追蹤路由路徑陡叠,它的原理是向目的主機(jī)發(fā)送ICMP報(bào)文玩郊,發(fā)送第一個(gè)報(bào)文時(shí),設(shè)置TTL為0枉阵,TTL即Time to Live译红,是報(bào)文的生存時(shí)間,由于它是0兴溜,所以下一個(gè)路由器由到這個(gè)報(bào)文后侦厚,不會(huì)再繼續(xù)轉(zhuǎn)發(fā)了,會(huì)給源主機(jī)發(fā)送ICMP出錯(cuò)的報(bào)文昵慌,就可以知道第一個(gè)路由的IP地址假夺,同理,設(shè)置TTL為1斋攀,就可以知道第二個(gè)路由的IP地址已卷,依次類推。
如在北京traceroute廣東電信淳蔼,運(yùn)行命令:
traceroute http://gd.189.cn
控制臺(tái)將不斷地打印經(jīng)過(guò)的路由侧蘸,traceroute每次都會(huì)發(fā)三個(gè)報(bào)文:
可以看到為了到廣東電信官網(wǎng)的服務(wù)器,經(jīng)過(guò)了這么一個(gè)過(guò)程——首先發(fā)給了直接路由器進(jìn)行轉(zhuǎn)發(fā)鹉梨,然后又在局域網(wǎng)的路由轉(zhuǎn)發(fā)了幾次讳癌,最后出來(lái)到了北京聯(lián)通,中間又經(jīng)過(guò)了北京電信和上海電信的路由器存皂,最后到了廣州電信的路由器晌坤。我們會(huì)發(fā)現(xiàn)每次走的路由可能會(huì)不一樣,它是活的旦袋。這里又涉及到路由轉(zhuǎn)發(fā)骤菠,本文不繼續(xù)探討。
每個(gè)報(bào)文都有一個(gè)TTL最大跳數(shù)疤孕,每經(jīng)過(guò)一個(gè)路由就會(huì)把它減1商乎,當(dāng)減到0的時(shí)候,就不再繼續(xù)轉(zhuǎn)發(fā)了祭阀。避免某些報(bào)文被無(wú)限循環(huán)轉(zhuǎn)發(fā)鹉戚,造成網(wǎng)絡(luò)資源的浪費(fèi)鲜戒。TTL位于IP報(bào)文的第9個(gè)字節(jié)。
(3)網(wǎng)際層Ping
另外一個(gè)很常用的命令是Ping抹凳,如Ping一下127.0.0.1可以看一下本機(jī)的網(wǎng)絡(luò)協(xié)議是否工作正常遏餐,Ping一下某個(gè)服務(wù)器,看這個(gè)服務(wù)器有沒(méi)有開(kāi)却桶,Ping一下某個(gè)域名境输,看它的IP地址是多少。Ping還可以這么用颖系,例如Ping一下baidu:
可以看到要到百度服務(wù)器中間經(jīng)過(guò)了64 – 49 = 15跳,所以可推測(cè)百度用的是Linux服務(wù)器辩越,為什么呢嘁扼,因?yàn)長(zhǎng)inux默認(rèn)的最大TTL = 64,而49和64最為接近黔攒。
Ping一下美國(guó)亞馬遜:
到美國(guó)亞馬遜趁啸,經(jīng)過(guò)了255 – 217 = 38跳,所以推測(cè)亞馬遜用的Unix服務(wù)器督惰,Unix服務(wù)器默認(rèn)的最大TTL為255.
再Ping一下中國(guó)版的w3school:
到中國(guó)版的w3school用了128 – 107 = 21跳不傅,Windows的默認(rèn)最大跳數(shù)為128,所以w3school用的是windows操作系統(tǒng) 赏胚,因?yàn)樗玫氖茿SP访娶,所以它必定是windows系統(tǒng)。
繼續(xù)回到demo實(shí)驗(yàn)的討論觉阅,下面分析一些異常的情況崖疤。
7. Reset報(bào)文
假設(shè)現(xiàn)在我把8080端口的http-server給殺了,然后再訪問(wèn)典勇,會(huì)怎么樣呢劫哼?會(huì)抓取到以下報(bào)文:
11:38:09.120488 IP 10.2.200.11.57049 > 10.2.200.140.8080: Flags [S], seq 1663158265, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 822068310 ecr 0,sackOK,eol], length 0
11:38:09.120524 IP 10.2.200.140.8080 > 10.2.200.11.57049: Flags [R.], seq 0, ack 1663158266, win 0, length 0
第一個(gè)報(bào)文還是SYN的報(bào)文,但是第二個(gè)報(bào)文服務(wù)器直接返回了RST割笙,告訴對(duì)方不可建立連接权烧。服務(wù)返回異常RST報(bào)文可能有以原因:
1.服務(wù)器沒(méi)開(kāi)服務(wù)
2.請(qǐng)求超時(shí)
3.服務(wù)程序突然掛了
4.在一個(gè)已關(guān)閉的socket上收到數(shù)據(jù)
8. 擁塞控制
現(xiàn)在我要上傳一個(gè)文件,觀察報(bào)文發(fā)送的情況伤溉,如下圖所示:
上面0.70s的時(shí)間內(nèi)般码,發(fā)送了1448 * 9 = 17k的數(shù)據(jù)(Mss 1460)
這個(gè)時(shí)候突然網(wǎng)卡了,又會(huì)怎么樣呢谈火?如下圖所示:
上面1.45s的時(shí)間內(nèi)侈询,總共發(fā)送了9個(gè)包,5kb數(shù)據(jù)糯耍。
正常情況經(jīng)常一次連續(xù)發(fā)送1448 * 6 = 8k數(shù)據(jù)扔字,網(wǎng)卡即帶寬下降的時(shí)候是如何控制發(fā)送速度的呢囊嘉?先來(lái)看一下什么是接收窗口和擁塞窗口。
(1)接收窗口和擁塞窗口
在上傳的過(guò)程中革为,服務(wù)器可能會(huì)不斷地調(diào)整它的接收窗口大信ち弧:
14:03:38.479417 IP ec2-54-153-103-33.us-west-1.compute.amazonaws.com.https > 10.2.200.140.56342: Flags [.], ack 59651, win 850, options [nop,nop,TS val 954992962 ecr 598512063,nop,nop,sack 1 {61099:62547}], length 0
如收到上面的ACK報(bào)文后,服務(wù)器的接收窗口rwnd為:
rwnd = 850 * 2 ^ 5 = 27200 B
我本機(jī)自己有一個(gè)擁塞窗口cwnd震檩,這個(gè)窗口用來(lái)控制我的發(fā)送速度琢蛤,避免網(wǎng)絡(luò)擁塞,這個(gè)擁塞窗口是動(dòng)態(tài)變化的抛虏,下面會(huì)提到博其。實(shí)際的發(fā)送窗口大小為:
發(fā)送窗口 = min(cwnd, rwnd)
當(dāng)cwnd > rwnd的時(shí)候是對(duì)方的接收能力限制了我的發(fā)送速度,而當(dāng)rwnd > cwnd的時(shí)候迂猴,是我的網(wǎng)絡(luò)情況造成了發(fā)送比較慢的情況慕淡。
發(fā)送窗口又是如何決定發(fā)送速度的呢:
(2)發(fā)送窗口
假設(shè)現(xiàn)在要發(fā)送hello, world這個(gè)文本,已經(jīng)知道發(fā)送窗口為5B沸毁,最大報(bào)文段MSS減掉報(bào)文頭占用的空間之后還剩下2B峰髓,那么發(fā)送如下圖所示:
當(dāng)我收到ACK報(bào)文之后,如ACK:3息尺,那么就可以將我的發(fā)送窗口向右移動(dòng)兩個(gè)字節(jié)携兵,然后繼續(xù)發(fā)送發(fā)送窗口里未發(fā)送的報(bào)文,如下圖所示:
如果沒(méi)有收到對(duì)方的ACK搂誉,那么發(fā)送窗口將不可向右移動(dòng)徐紧,也就是說(shuō)不會(huì)發(fā)送了,如果ACK回復(fù)得慢勒葱,或者發(fā)送窗口本身比較比小浪汪,那么發(fā)送的速度就沒(méi)那么快了。這就是發(fā)送窗口控制發(fā)送速度的原理凛虽。當(dāng)對(duì)方的帶寬下降時(shí)死遭,它減少它的接收窗口來(lái)控制我的發(fā)送速度,而當(dāng)我的網(wǎng)卡的時(shí)候我減少我的擁塞窗口控制發(fā)送速度凯旋。
但是怎么知道網(wǎng)卡了呢呀潭?
(3)慢啟動(dòng)和擁塞避免
由于建立完連接后,發(fā)送方不知道當(dāng)前的網(wǎng)絡(luò)情況怎么樣至非,所以它會(huì)非常地謹(jǐn)慎钠署,先慢慢地發(fā),如果對(duì)方的ACK回復(fù)很及時(shí)荒椭,那么說(shuō)明可以繼續(xù)加大發(fā)送的量谐鼎,并且指數(shù)位地增加,這個(gè)就是慢啟動(dòng)趣惠。如下訪問(wèn)一個(gè)Linux服務(wù)器的網(wǎng)址:
可以看到狸棍,服務(wù)在收到一個(gè)GET請(qǐng)求后進(jìn)行響應(yīng)身害,第一次同時(shí)只發(fā)3個(gè)包,并且從時(shí)間間隔上我們可以肯定它是故意的草戈。也就是說(shuō)它是一個(gè)慢啟動(dòng)塌鸯,為什么第一次是3個(gè)呢,因?yàn)長(zhǎng)inux 2的系統(tǒng)的初始化擁塞窗口initcwnd為3MSS唐片,3MSS說(shuō)明第一次只能發(fā)3個(gè)包(每個(gè)包不能超過(guò)最大報(bào)文段的長(zhǎng)度)丙猬,不同操作系統(tǒng)的initcwnd值如下所示,參考:
Linux3據(jù)說(shuō)是因?yàn)榻邮芰斯雀璧慕ㄗh费韭,所以改成了10MSS茧球。
具體慢啟動(dòng)的過(guò)程如下圖表所示:
擁塞窗口會(huì)以指數(shù)倍增長(zhǎng),一直增長(zhǎng)到擁塞閾值ssthresh星持,假設(shè)這個(gè)值為192袜腥。然后再以遞增的方式增加擁塞窗口,這個(gè)階段叫擁塞避免钉汗。也就說(shuō)當(dāng)cwnd < ssthresh時(shí)是慢啟動(dòng)的過(guò)程,而當(dāng)cwnd > ssthresh時(shí)是擁塞避免鲤屡。一直增長(zhǎng)到合適的帶寬大小损痰。
在慢啟動(dòng)和擁塞避免過(guò)程中,可能會(huì)遇到網(wǎng)絡(luò)擁塞的情況酒来,造成丟包的情況卢未,具體表現(xiàn)為很長(zhǎng)時(shí)間沒(méi)有收到對(duì)方的ACK,或者收到重復(fù)的ACK堰汉。
(4)超時(shí)重傳
假設(shè)很長(zhǎng)時(shí)間沒(méi)有收到對(duì)方發(fā)送的ACK辽社,這個(gè)時(shí)間超過(guò)了定時(shí)器的范圍,導(dǎo)致進(jìn)行重傳翘鸭,如下圖所示:
上圖總共重傳了三次滴铅,第一次重傳隔了約1.2s,第二次隔了2.4s就乓,第三次隔了3.5s汉匙,我們觀察到超時(shí)重傳的時(shí)間間隔會(huì)增加,并且發(fā)生超時(shí)之后最多只會(huì)發(fā)送一個(gè)報(bào)文生蚁,這個(gè)時(shí)候它進(jìn)入了慢啟動(dòng)的過(guò)程噩翠,如下圖表所示:
當(dāng)本機(jī)收到上傳服務(wù)器的ACK之后,又繼續(xù)發(fā)了兩個(gè)報(bào)文:
這個(gè)與上面的描述一致邦投,即重新進(jìn)入了慢啟動(dòng)伤锚。
到這里我們就可以解決兩個(gè)山頭如何可靠地通信、保證同時(shí)去攻打另一個(gè)山頭的問(wèn)題了志衣。很簡(jiǎn)單屯援,A派了只鴿子發(fā)一個(gè)消息給B之后猛们,B給他回了一個(gè)ACK,假設(shè)一只鴿子從B飛到A需要1個(gè)小時(shí)玄呛,B派出去鴿子之后如果過(guò)了兩個(gè)小時(shí)阅懦,B沒(méi)有收到A發(fā)送的一個(gè)重復(fù)的消息給它,即沒(méi)有進(jìn)行超時(shí)重傳徘铝,就可以認(rèn)為B派出去的那只鴿子A已經(jīng)收到了耳胎。那要是剛好不巧A派出去的第二只鴿子不見(jiàn)了呢,那A又再繼續(xù)超時(shí)重傳惕它,如果需要重傳很多次的話怕午,那就放棄吧,就像TCP一樣淹魄∮粝В客觀條件不允許,沒(méi)有辦法甲锡。
有一種情況不用等超時(shí)兆蕉,可以馬上進(jìn)行重傳。
(5)快速重傳和快速恢復(fù)
假設(shè)本機(jī)向服務(wù)器按順序發(fā)了三個(gè)包缤沦,但是這三個(gè)包可能并沒(méi)有按順序到達(dá)虎韵,有可能第三個(gè)包先到了,這個(gè)時(shí)候服務(wù)器收到了亂序的數(shù)據(jù)缸废,于是它馬上產(chǎn)生一個(gè)重復(fù)的ACK包蓝,要求重新獲取從第一個(gè)包開(kāi)始的數(shù)據(jù)。收到重復(fù)ACK時(shí)企量,不應(yīng)該馬上進(jìn)行重傳测萎,因?yàn)榭赡芎芸靵y序的另外兩個(gè)又及時(shí)到了。但是當(dāng)收到三個(gè)重復(fù)的ACK時(shí)就可以認(rèn)為那個(gè)包已經(jīng)丟了届巩,需要進(jìn)行重傳硅瞧,不用等到超時(shí),這個(gè)就叫做快速重傳姆泻。如下圖所示:
快速重傳之后就進(jìn)入了快速恢復(fù)的階段零酪。和超時(shí)重傳不一樣的地方是,超時(shí)重傳認(rèn)為當(dāng)前的網(wǎng)絡(luò)情況十分糟糕拇勃,所以一下子把擁塞窗口cwnd置成了1四苇,重新進(jìn)入慢啟動(dòng)。而快速恢復(fù)認(rèn)為當(dāng)前網(wǎng)絡(luò)并沒(méi)有那壞方咆,它把擁塞窗口cwnd置成了當(dāng)前擁塞窗口的一半加3月腋,ssthresh置成老擁塞窗口的一半:如下圖所示:
這個(gè)過(guò)程就叫做快速恢復(fù),當(dāng)收到一個(gè)新數(shù)據(jù)的ACK時(shí),將退出快速恢復(fù)榆骚,將cwnd置為ssthresh片拍,進(jìn)入擁塞避免。
(6)慢啟動(dòng)的缺點(diǎn)
慢啟動(dòng)的優(yōu)點(diǎn)是在比較擁塞的網(wǎng)絡(luò)妓肢,慢啟動(dòng)可以避免擁塞進(jìn)一步地加劇捌省,但是它的缺點(diǎn)也是明顯的,對(duì)于正常的網(wǎng)絡(luò)碉钠,慢啟動(dòng)將降低傳輸?shù)男矢倩海绫緛?lái)一個(gè)RTT就可以傳完的數(shù)據(jù),現(xiàn)在要分成幾個(gè)RTT(假設(shè)發(fā)送的數(shù)據(jù)量剛好是這樣)喊废,特別是Linux 2的服務(wù)器initcwnd只有3MSS祝高,所以可以手動(dòng)把它改大,如改成10污筷,可執(zhí)行以下命令:
sudo ip route change default via 192.168.1.1 dev eth0 proto static initcwnd 10
快速恢復(fù)的引入也是考慮到了慢啟動(dòng)的缺點(diǎn)工闺。
然后再討論一個(gè)很出名的算法
9. Nagle算法
假設(shè)要通過(guò)http發(fā)送hello, world這12個(gè)字節(jié),但是實(shí)際上要發(fā)送多少個(gè)字節(jié)呢瓣蛀?如下:
12:12:57.091926 IP 10.2.200.140.http-alt > 10.2.200.11.60882: Flags [P.], seq 288:301, ack 378, win 4105, options [nop,nop,TS val 678005709 ecr 845655611], length 13: HTTP
http數(shù)據(jù)總共發(fā)送了300個(gè)字節(jié)陆蟆,也就是說(shuō)http報(bào)文頭就占用了288個(gè)字節(jié),但是這還不包括其它報(bào)文頭惋增,如下所示:
也就是說(shuō)為了發(fā)送12個(gè)字節(jié)的數(shù)據(jù)遍搞,總共得發(fā)送356個(gè)字節(jié),有效內(nèi)容僅占了4%不到器腋。因此在那個(gè)需要用電話撥號(hào)上網(wǎng)的年代,這個(gè)代價(jià)就有點(diǎn)大了钩杰,所以Nagle算法的核心思想是:等數(shù)據(jù)積累多了再一起發(fā)出去纫塌,大概等待200ms,這樣可以提高網(wǎng)絡(luò)的吞吐率讲弄。
但是在現(xiàn)在光纖的時(shí)代措左,帶寬和速度已經(jīng)不是太大的問(wèn)題了,如果每個(gè)請(qǐng)求都要延遲200ms避除,會(huì)造成實(shí)時(shí)性比較差怎披。所以通常是要把Nagle算法禁掉,可以在創(chuàng)建套接字的時(shí)候設(shè)置TCP_NODELAY標(biāo)志位瓶摆。
10. HTTP報(bào)文頭大小限制
(1)請(qǐng)求頭大小限制
標(biāo)準(zhǔn)并沒(méi)有規(guī)定http請(qǐng)求頭的大小限制凉逛,但是在實(shí)際的實(shí)現(xiàn)上會(huì)有限制。如nginx限制為4k - 8k群井,tomcat最小支持8K状飞。
(2)url長(zhǎng)度限制
如下http報(bào)文格式所示:
URL是在請(qǐng)求行里面的,并不在請(qǐng)求頭里,同樣標(biāo)準(zhǔn)也沒(méi)有規(guī)定URL有長(zhǎng)度限制诬辈,但是實(shí)際的實(shí)現(xiàn)有限制酵使,如下圖所示:
一個(gè)比較安全的值應(yīng)該是8K,這樣兼容性最好焙糟。同時(shí)需要注意的是GET請(qǐng)求口渔,參數(shù)是在URL里面,而POST請(qǐng)求參數(shù)是在請(qǐng)求數(shù)據(jù)里面穿撮,所以GET請(qǐng)求的數(shù)據(jù)不能太大缺脉。
(3)cookie的長(zhǎng)度限制
cookie是在請(qǐng)求頭里以普通鍵值對(duì)的方式存在,一般一個(gè)domain的cookie不能超過(guò)4Kb混巧,50個(gè)cookie枪向,不然瀏覽器可能會(huì)不支持。服務(wù)可以通常Set-Cookie通知客戶端設(shè)置cookie咧党,而客戶端可以用Cookie字段告知服務(wù)現(xiàn)在的cookie數(shù)據(jù)是怎么樣的秘蛔,如下所示:
上面的一些基礎(chǔ)問(wèn)題討論完了,我們終于可以來(lái)分析websocket了傍衡。
11. Websocket
(1)實(shí)現(xiàn)一個(gè)web聊天
怎么實(shí)現(xiàn)一個(gè)http的web的實(shí)時(shí)聊天呢深员,怎么知道對(duì)方有沒(méi)有發(fā)送消息給我呢?有幾種方法蛙埂。
第一種辦法使用輪詢倦畅,例如每隔2s就發(fā)一個(gè)請(qǐng)求向服務(wù)端查詢,但是這種方法會(huì)造成資源的浪費(fèi)绣的。
第二種辦法使用Service Worker實(shí)現(xiàn)瀏覽器的Push叠赐,這種方法需要先注冊(cè)FCM賬號(hào),獲取到一個(gè)App Id屡江,用Service Worker監(jiān)聽(tīng)芭概,服務(wù)向https://android.googleapis.com/gcm/send發(fā)送消息,谷歌服務(wù)器就會(huì)向那個(gè)App Id發(fā)送一個(gè)推送惩嘉,就實(shí)現(xiàn)了瀏覽器的Push罢洲。但是這種辦法兼容性還不是很好,并且大陸的小伙伴無(wú)法在正常網(wǎng)絡(luò)環(huán)境收到谷歌服務(wù)器的消息文黎。
所以就有了websocket建立常連接惹苗。為此建立一個(gè)websocket的demo.
(2)websocket的demo
為了實(shí)驗(yàn),寫一個(gè)websocket的demo耸峭,先裝一個(gè)websocket的Node包桩蓉,然后監(jiān)聽(tīng)在8080端口,接著寫客戶端html5 websocket代碼:
var socket = new WebSocket("ws://10.2.200.140:8080");
socket.onopen = function(){
socket.send("長(zhǎng)江長(zhǎng)江劳闹,我是黃河");
}
socket.onmessage = function(event){
document.write("收到來(lái)自黃河的消息:" + event.data);
}
打開(kāi)這個(gè)頁(yè)面触机,瀏覽器就會(huì)顯示一個(gè)websocket的連接:
然后我們用tcpdump研究websocket連接建立的過(guò)程帚戳。
(3)Websocket連接建立
首先還是要先建立tcp連接,完成后客戶端發(fā)送一個(gè)upgrade的http請(qǐng)求:
14:23:36.926775 IP 10.2.200.11.61205 > 10.2.200.140.8080: Flags [P.], seq 1:435, ack 1, win 4117, options [nop,nop,TS val 848067548 ecr 685816156], length 434: HTTP: GET / HTTP/1.1
這個(gè)報(bào)文的詳細(xì)內(nèi)容如下:
服務(wù)端收到后同意握手儡首,返回Switching Protocols片任,連接建立,如下報(bào)文:
14:23:36.929714 IP 10.2.200.140.8080 > 10.2.200.11.61205: Flags [P.], seq 1:164, ack 435, win 4104, options [nop,nop,TS val 685816195 ecr 848067548], length 163: HTTP: HTTP/1.1 101 Switching Protocols
詳細(xì)內(nèi)容如下所示:
(4)傳送數(shù)據(jù)
發(fā)送“hello, world”12字節(jié)內(nèi)容蔬胯,用ws只需要發(fā)送18字節(jié)对供,這比http 300個(gè)字節(jié)要少了很多:
14:24:36.009503 IP 10.2.200.11.61205 > 10.2.200.140.8080: Flags [P.], seq 492:510, ack 168, win 4112, options [nop,nop,TS val 848126486 ecr 685863159], length 18: HTTP
14:24:36.009556 IP 10.2.200.140.8080 > 10.2.200.11.61205: Flags [.], ack 510, win 4101, options [nop,nop,TS val 685875098 ecr 848126486], length 0
具體可以定義消息的類型,例如type = 1表示心跳消息氛濒,type = 2表示用戶發(fā)送的消息产场,還可以再定義subtype,并自定義消息內(nèi)容的格式舞竿,再封裝一些自定義的消息機(jī)制等等京景。
(5)關(guān)閉連接
30s后,雙方?jīng)]有傳送數(shù)據(jù)骗奖,websocket連接關(guān)閉确徙,進(jìn)行四次揮手。
14:25:06.017016 IP 10.2.200.140.8080 > 10.2.200.11.61205: Flags [F.], seq 170, ack 510, win 4101, options [nop,nop,TS val 685904974 ecr 848146558], length 0
這樣就實(shí)現(xiàn)了一個(gè)實(shí)時(shí)的web聊天执桌,需要注意的是websocket是一套協(xié)議鄙皇,任何人只要遵守這套協(xié)議就可以使用并和其他人互聯(lián),不管你是JS還Android/IOS/C++/Java仰挣。ws默認(rèn)監(jiān)聽(tīng)在80端口伴逸,wss監(jiān)聽(tīng)在443端口,和http/https一樣膘壶。
最后再比較一下websocket和webRTC
(6)Websocket和WebRTC
Websocket是為了解決實(shí)時(shí)傳送消息的問(wèn)題错蝴,當(dāng)然也可以傳送數(shù)據(jù),但是不保證傳送的效率和質(zhì)量颓芭,而WebRTC可用于可靠地傳輸音視頻數(shù)據(jù)漱竖、文件等。并且可建立P2P連接畜伐,不需要服務(wù)進(jìn)行轉(zhuǎn)發(fā)數(shù)據(jù)。虛擬電話躺率、在線面試等現(xiàn)在很多都采用WebRTC實(shí)現(xiàn)玛界。
最后做個(gè)總結(jié)。這篇文章介紹了很多通信協(xié)議的東西悼吱,分析了TCP/IP的三次握手和四次揮手慎框,并討論了為什么握手是三次,而揮手是四次后添,還講了四層網(wǎng)絡(luò)模型笨枯,分析了工作在不同層的協(xié)議和工具,后面又重點(diǎn)分析了TCP的擁塞控制,包括超時(shí)重傳馅精、慢啟動(dòng)和擁塞避免严嗜、快速重傳和快速恢復(fù),接著還講了點(diǎn)HTTP的東西洲敢,最后簡(jiǎn)單分析了下Websocket連接的過(guò)程和它的特點(diǎn)以及和WebRTC的區(qū)別漫玄。上面可以說(shuō)是TCP/IP協(xié)議的核心內(nèi)容,我們通過(guò)一兩個(gè)demo把它給串了起來(lái)压彭,對(duì)讀者應(yīng)該有一個(gè)啟發(fā)作用展蒂,可以更深刻地理解網(wǎng)絡(luò)協(xié)議摩窃,當(dāng)你在寫一個(gè)請(qǐng)求的時(shí)候,你知道它的背后發(fā)生了什么。讀者可以根據(jù)本文再繼續(xù)查閱相關(guān)資料延伸擴(kuò)展曲初。如有不正確之處還請(qǐng)指出。
原文鏈接:http://www.renfed.com/2017/05/20/websocket-and-tcp-ip/
知乎專欄:https://zhuanlan.zhihu.com/p/27021102