搞明白 WebSocket與TCP/IP
01tcpdump
Linux系的系統有一個很好用的抓包工具,叫tcpdump,可以用來抓取網路上的tcp包,例如我要抓取8080埠的包,可以執行以下命令:
sudo tcpdump port 8080 –n
-n的意思是埠號用數字表示,還可以加上-v -vv顯示更詳細的信息:
sudo tcpdump port 8080 –n -v
再如我要抓取來自特定源IP和發往特定目的IP的包,可以用以下命令:
sudo tcpdump src host 10.2.200.11 or dst host 10.2.200.11
指定src host和dst host,並用or/and做條件的交集和並集。
在建立一個網頁的websocket之前先要建立一個http連接,為此我們簡單寫一個小demo。
02hello, world的http連接
(1)首先寫以下的html文件:
(2)然後再裝一個http-server的node包,監聽在8080埠,如下所示:
(3)電腦開tcpdump命令,抓取通過8080埠通訊的包:
sudo tcpdump port 8080 –n
(4)用手機訪問:http://10.2.200.140.8080,tcpdump就會打出所有傳輸的tcp包,如下圖所示。
我們拿它列印的這些TCP報文做一個研究。在建立一個http連接之前,先要建立一個TCP連接,即上圖的頭3個報文。下面研究一下這個HTTP連接是怎麼進行的。
03一個完整的HTTP連接
(1)TCP三次握手
第一個報文:11 -> 140
在10點11分的時候,IP為10.2.200.11(以後簡稱11)的63826埠向IP為10.2.200.140(以後簡稱140)的8080埠發了一個TCP的包,帶上了標誌位SYN,表示要建立一個連接,並指明包開始的序列號seq(單位為位元組),以後傳送的位元組編號都是以這個做為起點,並告知能接收的最大報文段長度mss為1460,一般mss都為1460.
第二個報文:140 -> 11
在過了87微秒之後,140進行了回復,發送了一個SYN + ACK的報文段,表示同意和11建立連接。
第三個報文:11 -> 140
11收到SYN之後向140發送一個ACK,同時改變接收窗口為4117 * 2 ^ 5 = 131kb,完成三次握手。
什麼是接收窗口呢?
(2)接收窗口
第四個報文裡面,140也向11修改了它的接收窗口大小:
大小為4117 * 2 ^ 5 = 131kb,為什麼接收窗口是這個數呢?因為如下TCP的報文(頭):
窗口大小只有2個位元組16位,最大只能表示2 ^ 16 – 1 = 65535即16Kb,當初設計TCP的人並沒有想到現在的網速會提升這麼快,16Kb是不夠用的,所以在可選項裡面加了一個wscale(window scale factor)的指數欄位,最大值為14,所以最大的接收窗口大概為1GB.
說了這麼多,接收窗口是用來做什麼的呢?它根據自身網路情況設置不同大小的值用來控制對方發送速度,避免對方發送太快,導致網路擁塞。下面講到擁塞控制會更進一步地討論。
(3)發送數據
建立好TCP連接後,11向140發送了一個http請求:
這裡,它帶上了一個PUSH的標誌位,表示它是一個比較緊急的報文,要求對方立即把數據從緩存裡面發送給應用程序,不能再繼續緩存了。
它發送的位元組號為[1, 404),這個數字是tcpdump顯示的相對於握手的協議初始序列號顯示的偏移,它是一個左閉右開的表示,所以這個報文總共發送了403個位元組的數據。它是一個GET請求。
然後後140收到後給11回復了一個ACK:
ACK 404表示期待收到第404位元組的數據,也就是說前面403個位元組的數據已經都確認收到。
然後140進行了http響應:
140總共發送了457個位元組的數據,分成了兩個包發送。而本地的html文件大小為:
所以可以認為http報文頭佔用了457 – 168 = 289位元組。
(4)關閉連接
第一個報文:11 -> 140 FIN
11等了30s後覺得不用再請求數據了,於是要把連接關閉了,它向140發送一個FIN的報文。為什麼要等30s才關閉呢?這是HTTP請求的Connection: keep-alive欄位影響的,因為同一個域可能要請求多個資源,不能一個請求完了就把連接關閉了。如果不關閉又佔用埠號資源,我們知道埠號最多只有65535個。
第二個報文:140 -> 11 ACK
140收到這個包後向11發送一個ACK,這個時候連接處於半關閉狀態,即11不可再向140發送數據了,但140還可以向11發送。
第三個報文:140 -> 11 FIN
140也要把連接關閉了,於是它向11發送FIN
第四個報文:11 -> 140 ACK
11收到後,向它發了一個ACK,此時連接完全關閉。然後主動關閉方11將進入TIME_WAIT狀態
(5)MSS和TIME_WAIT
TIME_WAIT時間為2MSL,MSL的意思是maximum segment livetime,即報文段的最大生存時間,標準建議為2分鐘,實際實現有的為30s。在TIME_WAIT狀態下,上一次建立連接的套接字(socket)將不可再重新啟用,也就是同一個網卡/IP不可再建立同樣埠號的連接,如上面是10.2.200.11.63826,如果再重新創建系統將會報錯。為什麼要等待這個時間呢?主要是為了避免有些報文段在網路上滯留,被對方收到的時候如果剛好又啟用了一個完全一樣的套接字,那麼就會被認為是這個連接的數據。因此為了讓所有「迷路」的報文徹底消失後,才能啟用相同的套接字。但是有時候你會覺得兩分鐘不能重新啟動相同的socket,有點麻煩,所以要把它禁了,可以在創建socket的時候,指定SO_REUSEADDR的選項,這樣就不用等待TIME_WAIT的時間了。
另外還有一個時間叫RTT(round trip time),即一個報文段的往返時間,可以理解為我發一個數據給你,你再回我一個ACK這個往返過程的時間,這個時間是動態計算的,在下面講擁塞控制的時候將會提及這個時間。
接下來討論兩個問題。
04為什麼TCP握手要三次?
為什麼不是兩次、四次呢?有人說三次是建立一個可靠連接最少的次數,那為什麼不是兩次呢?兩次好像也可以啊,就像打電話:
甲:喂,你聽得到嗎?
乙:我聽得到
然後甲就可以開始說話了。再舉另外一個例子做說明,假設有三個山頭:A、B、C,A山頭想要聯合B山頭的人晚上六點去攻打
C山頭的人,因為如果只有一個山頭的人去攻打C的話會陣亡,所以A和B需要進行握手。
於是:
A就派了只鴿子帶上SYN的消息過去找B
B收到後又派了只鴿子帶上ACK + SYN的消息回復A
A收到後又派了只鴿子帶上ACK去回復B
這個就好像我們的三次握手,但是三次就夠了嗎?假設第三次A發的ACK B沒有收到,這時候B就要猶豫了:會不會A不知道我同意了,如果A不知道我同意那麼它可能不會去攻打了,然後我去了就得被滅了。由於A不知道它的回復有沒有被收到,所以它可能會想到B可能會怕它不會出擊,所以A也猶豫了。
因此三次握手並不能保證雙方完全地信任對方,即使是四次、五次也是同樣道理,至少有一方無法信任另一方,另外一方一想到對方可能不信它,它也會變得不信對方。
但是這個例子並不是說TCP連接建立是不可靠的,實際的場景往往是只要雙方確認對方都在就好了,如下:
甲:你活著嗎?我想和你通話
乙:我活著呢,我們開始通話吧
因此最少的握手次數應該是兩次,三次可以提高可靠性,四次、五次就沒必要了,就會陷入上面山頭攻打無限循環確認的漩渦。如下:
甲:你活著嗎?我想和你通話
乙:我活著呢,我們開始通話吧
甲:好的
最後的「好的」可能有點多餘,但是它顯得比較有「人情味」。
難道兩個山頭通信真的沒有辦法解決嗎?有辦法,我們將在下面的擁塞控制提到。
05為什麼揮手要四次
分析了握手次數的原因,很容易可以知道為什麼揮手要四次了。前兩次揮手讓連接處於半關閉狀態,此時主動關閉方不可再向被動關閉方發送數據,而被動關閉可繼續向主動關閉方發送數據。如下圖所示:
所以四次的原因是可以有一個處於半關閉的狀態。
接下來看一下四層網路協議。
06四層網路協議
如下圖所示,我們從發送數據的角度看四層網路協議:
假設我要用HTTP發送一個文本,那麼它會最後會被層層包裝成這樣一個報文
在廣域網是用的IP地址進行報文轉發,而到了區域網需要靠物理地址發送給對應的主機。IP是點到點,負責發送給對應的主機,而TCP是端到端,即根據埠號,負責發送給對應的應用程序。
(1)物理地址
每個網卡都有全球唯一的物理地址,路由器向同一個區域網所有主機發送收到的數據包,本機的網卡比較一下包里指明的物理地址和本機的物理地址是否一致,如果一致則接收,否則則丟棄。所以可以在區域網監聽發給其它人的數據包,當然也有一些反監聽的手段。
(2)網際層ARP
ARP是一個地址解析協議,當我訪問10.2.200.140的時候我需要知道它的物理地址是多少,因為它已經是一個區域網的IP地址了。我怎麼知道它的物理地址是多少呢?我就向區域網的機器廣播一個ARP請求:
09:51:32.966852 ARP, Request who-has 10.2.200.140 tell 10.2.200.11, length 28
過了33微秒一小會的功夫就有人告訴我了:
09:51:32.966885 ARP, Reply 10.2.200.140 is-at 98:5a:eb:89:a5:7e (oui Unknown), length 28
這個很可能是路由器告訴我的,上面的tcpdump輸出沒有列印源IP。
可以通過arp -an的命令,查看電腦上的arp表,如下圖所示:
(3)網際層traceroute
有一個很好用的命令叫traceroute,它可以追蹤路由路徑,它的原理是向目的主機發送ICMP報文,發送第一個報文時,設置TTL為0,TTL即Time to Live,是報文的生存時間,由於它是0,所以下一個路由器由到這個報文後,不會再繼續轉發了,會給源主機發送ICMP出錯的報文,就可以知道第一個路由的IP地址,同理,設置TTL為1,就可以知道第二個路由的IP地址,依次類推。
如在北京traceroute廣東電信,運行命令:
控制台將不斷地列印經過的路由,traceroute每次都會發三個報文:
可以看到為了到廣東電信官網的伺服器,經過了這麼一個過程——首先發給了直接路由器進行轉發,然後又在區域網的路由轉發了幾次,最後出來到了北京聯通,中間又經過了北京電信和上海電信的路由器,最後到了廣州電信的路由器。我們會發現每次走的路由可能會不一樣,它是活的。這裡又涉及到路由轉發,本文不繼續探討。
每個報文都有一個TTL最大跳數,每經過一個路由就會把它減1,當減到0的時候,就不再繼續轉發了。避免某些報文被無限循環轉發,造成網路資源的浪費。TTL位於IP報文的第9個位元組。
(3)網際層Ping
另外一個很常用的命令是Ping,如Ping一下127.0.0.1可以看一下本機的網路協議是否工作正常,Ping一下某個伺服器,看這個伺服器有沒有開,Ping一下某個域名,看它的IP地址是多少。Ping還可以這麼用,例如Ping一下baidu:
可以看到要到百度伺服器中間經過了64 – 49 = 15跳,所以可推測百度用的是Linux伺服器,為什麼呢,因為Linux默認的最大TTL = 64,而49和64最為接近。
Ping一下美國亞馬遜:
到美國亞馬遜,經過了255 – 217 = 38跳,所以推測亞馬遜用的Unix伺服器,Unix伺服器默認的最大TTL為255.
再Ping一下中國版的w3school:
到中國版的w3school用了128 – 107 = 21跳,Windows的默認最大跳數為128,所以w3school用的是windows操作系統 ,因為它用的是ASP,所以它必定是windows系統。
繼續回到demo實驗的討論,下面分析一些異常的情況。
07Reset報文
假設現在我把8080埠的http-server給殺了,然後再訪問,會怎麼樣呢?會抓取到以下報文:
第一個報文還是SYN的報文,但是第二個報文伺服器直接返回了RST,告訴對方不可建立連接。服務返回異常RST報文可能有以原因:
伺服器沒開服務
請求超時
服務程序突然掛了
在一個已關閉的socket上收到數據
08擁塞控制
現在我要上傳一個文件,觀察報文發送的情況,如下圖所示:
上面0.70s的時間內,發送了1448 * 9 = 17k的數據(Mss 1460)
這個時候突然網卡了,又會怎麼樣呢?如下圖所示:
上面1.45s的時間內,總共發送了9個包,5kb數據。
正常情況經常一次連續發送1448 * 6 = 8k數據,網卡即帶寬下降的時候是如何控制發送速度的呢?先來看一下什麼是接收窗口和擁塞窗口。
(1)接收窗口和擁塞窗口
在上傳的過程中,伺服器可能會不斷地調整它的接收窗口大小:
如收到上面的ACK報文後,伺服器的接收窗口rwnd為:
rwnd = 850 * 2 ^ 5 = 27200 B
我本機自己有一個擁塞窗口cwnd,這個窗口用來控制我的發送速度,避免網路擁塞,這個擁塞窗口是動態變化的,下面會提到。實際的發送窗口大小為:
發送窗口 = min(cwnd, rwnd)
當cwnd > rwnd的時候是對方的接收能力限制了我的發送速度,而當rwnd > cwnd的時候,是我的網路情況造成了發送比較慢的情況。
發送窗口又是如何決定發送速度的呢:
(2)發送窗口
假設現在要發送hello, world這個文本,已經知道發送窗口為5B,最大報文段MSS減掉報文頭佔用的空間之後還剩下2B,那麼發送如下圖所示:
當我收到ACK報文之後,如ACK:3,那麼就可以將我的發送窗口向右移動兩個位元組,然後繼續發送發送窗口裡未發送的報文,如下圖所示:
如果沒有收到對方的ACK,那麼發送窗口將不可向右移動,也就是說不會發送了,如果ACK回復得慢,或者發送窗口本身比較比小,那麼發送的速度就沒那麼快了。這就是發送窗口控制發送速度的原理。當對方的帶寬下降時,它減少它的接收窗口來控制我的發送速度,而當我的網卡的時候我減少我的擁塞窗口控制發送速度。
但是怎麼知道網卡了呢?
(3)慢啟動和擁塞避免
由於建立完連接後,發送方不知道當前的網路情況怎麼樣,所以它會非常地謹慎,先慢慢地發,如果對方的ACK回復很及時,那麼說明可以繼續加大發送的量,並且指數位地增加,這個就是慢啟動。如下訪問一個Linux伺服器的網址:
可以看到,服務在收到一個GET請求後進行響應,第一次同時只發3個包,並且從時間間隔上我們可以肯定它是故意的。也就是說它是一個慢啟動,為什麼第一次是3個呢,因為Linux 2的系統的初始化擁塞窗口initcwnd為3MSS,3MSS說明第一次只能發3個包(每個包不能超過最大報文段的長度),不同操作系統的initcwnd值如下所示,參考:
Linux3據說是因為接受了谷歌的建議,所以改成了10MSS。
具體慢啟動的過程如下圖表所示:
擁塞窗口會以指數倍增長,一直增長到擁塞閾值ssthresh,假設這個值為192。然後再以遞增的方式增加擁塞窗口,這個階段叫擁塞避免。也就說當cwnd < ssthresh時是慢啟動的過程,而當cwnd > ssthresh時是擁塞避免。一直增長到合適的帶寬大小。
在慢啟動和擁塞避免過程中,可能會遇到網路擁塞的情況,造成丟包的情況,具體表現為很長時間沒有收到對方的ACK,或者收到重複的ACK。
(4)超時重傳
假設很長時間沒有收到對方發送的ACK,這個時間超過了定時器的範圍,導致進行重傳,如下圖所示:
上圖總共重傳了三次,第一次重傳隔了約1.2s,第二次隔了2.4s,第三次隔了3.5s,我們觀察到超時重傳的時間間隔會增加,並且發生超時之後最多只會發送一個報文,這個時候它進入了慢啟動的過程,如下圖表所示:
當本機收到上傳伺服器的ACK之後,又繼續發了兩個報文:
這個與上面的描述一致,即重新進入了慢啟動。
到這裡我們就可以解決兩個山頭如何可靠地通信、保證同時去攻打另一個山頭的問題了。很簡單,A派了只鴿子發一個消息給B之後,B給他回了一個ACK,假設一隻鴿子從B飛到A需要1個小時,B派出去鴿子之後如果過了兩個小時,B沒有收到A發送的一個重複的消息給它,即沒有進行超時重傳,就可以認為B派出去的那隻鴿子A已經收到了。那要是剛好不巧A派出去的第二隻鴿子不見了呢,那A又再繼續超時重傳,如果需要重傳很多次的話,那就放棄吧,就像TCP一樣。客觀條件不允許,沒有辦法。
有一種情況不用等超時,可以馬上進行重傳。
(5)快速重傳和快速恢復
假設本機向伺服器按順序發了三個包,但是這三個包可能並沒有按順序到達,有可能第三個包先到了,這個時候伺服器收到了亂序的數據,於是它馬上產生一個重複的ACK,要求重新獲取從第一個包開始的數據。收到重複ACK時,不應該馬上進行重傳,因為可能很快亂序的另外兩個又及時到了。但是當收到三個重複的ACK時就可以認為那個包已經丟了,需要進行重傳,不用等到超時,這個就叫做快速重傳。如下圖所示:
快速重傳之後就進入了快速恢復的階段。和超時重傳不一樣的地方是,超時重傳認為當前的網路情況十分糟糕,所以一下子把擁塞窗口cwnd置成了1,重新進入慢啟動。而快速恢復認為當前網路並沒有那壞,它把擁塞窗口cwnd置成了當前擁塞窗口的一半加3,ssthresh置成老擁塞窗口的一半:如下圖所示:
這個過程就叫做快速恢復,當收到一個新數據的ACK時,將退出快速恢復,將cwnd置為ssthresh,進入擁塞避免。
(6)慢啟動的缺點
慢啟動的優點是在比較擁塞的網路,慢啟動可以避免擁塞進一步地加劇,但是它的缺點也是明顯的,對於正常的網路,慢啟動將降低傳輸的效率,例如本來一個RTT就可以傳完的數據,現在要分成幾個RTT(假設發送的數據量剛好是這樣),特別是Linux 2的伺服器initcwnd只有3MSS,所以可以手動把它改大,如改成10,可執行以下命令:
sudo ip route change default via 192.168.1.1 dev eth0 proto static initcwnd 10
快速恢復的引入也是考慮到了慢啟動的缺點。
然後再討論一個很出名的演算法
09Nagle演算法
假設要通過http發送hello, world這12個位元組,但是實際上要發送多少個位元組呢?如下:
http數據總共發送了300個位元組,也就是說http報文頭就佔用了288個位元組,但是這還不包括其它報文頭,如下所示:
也就是說為了發送12個位元組的數據,總共得發送356個位元組,有效內容僅佔了4%不到。因此在那個需要用電話撥號上網的年代,這個代價就有點大了,所以Nagle演算法的核心思想是:等數據積累多了再一起發出去,大概等待200ms,這樣可以提高網路的吞吐率。
但是在現在光纖的時代,帶寬和速度已經不是太大的問題了,如果每個請求都要延遲200ms,會造成實時性比較差。所以通常是要把Nagle演算法禁掉,可以在創建套接字的時候設置TCP_NODELAY標誌位。
10HTTP報文頭大小限制
(1)請求頭大小限制
標準並沒有規定http請求頭的大小限制,但是在實際的實現上會有限制。如nginx限制為4k – 8k,tomcat最小支持8K。
(2)url長度限制
如下http報文格式所示:
URL是在請求行裡面的,並不在請求頭裡,同樣標準也沒有規定URL有長度限制,但是實際的實現有限制,如下圖所示:
一個比較安全的值應該是8K,這樣兼容性最好。同時需要注意的是GET請求,參數是在URL裡面,而POST請求參數是在請求數據裡面,所以GET請求的數據不能太大。
(3)cookie的長度限制
cookie是在請求頭裡以普通鍵值對的方式存在,一般一個domain的cookie不能超過4Kb,50個cookie,不然瀏覽器可能會不支持。服務可以通常Set-Cookie通知客戶端設置cookie,而客戶端可以用Cookie欄位告知服務現在的cookie數據是怎麼樣的,如下所示:
上面的一些基礎問題討論完了,我們終於可以來分析websocket了。
11Websocket
(1)實現一個web聊天
怎麼實現一個http的web的實時聊天呢,怎麼知道對方有沒有發送消息給我呢?有幾種方法。
第一種辦法使用輪詢,例如每隔2s就發一個請求向服務端查詢,但是這種方法會造成資源的浪費。
第二種辦法使用Service Worker實現瀏覽器的Push,這種方法需要先註冊FCM賬號,獲取到一個App Id,用Service Worker監聽,服務向https://android.googleapis.com/gcm/send發送消息,谷歌伺服器就會向那個App Id發送一個推送,就實現了瀏覽器的Push。但是這種辦法兼容性還不是很好,並且大陸的小夥伴無法在正常網路環境收到谷歌伺服器的消息。
所以就有了websocket建立常連接。為此建立一個websocket的demo.
(2)websocket的demo
為了實驗,寫一個websocket的demo,先裝一個websocket的Node包,然後監聽在8080埠,接著寫客戶端html5 websocket代碼:
打開這個頁面,瀏覽器就會顯示一個websocket的連接:
然後我們用tcpdump研究websocket連接建立的過程。
(3)Websocket連接建立
首先還是要先建立tcp連接,完成後客戶端發送一個upgrade的http請求:
這個報文的詳細內容如下:
服務端收到後同意握手,返回Switching Protocols,連接建立,如下報文:
詳細內容如下所示:
(4)傳送數據
發送「hello, world」12位元組內容,用ws只需要發送18位元組,這比http 300個位元組要少了很多:
具體可以定義消息的類型,例如type = 1表示心跳消息,type = 2表示用戶發送的消息,還可以再定義subtype,並自定義消息內容的格式,再封裝一些自定義的消息機制等等。
(5)關閉連接
30s後,雙方沒有傳送數據,websocket連接關閉,進行四次揮手。
這樣就實現了一個實時的web聊天,需要注意的是websocket是一套協議,任何人只要遵守這套協議就可以使用並和其他人互聯,不管你是JS還Android/IOS/C++/Java。ws默認監聽在80埠,wss監聽在443埠,和http/https一樣。
最後再比較一下websocket和webRTC
(6)Websocket和WebRTC
Websocket是為了解決實時傳送消息的問題,當然也可以傳送數據,但是不保證傳送的效率和質量,而WebRTC可用於可靠地傳輸音視頻數據、文件等。並且可建立P2P連接,不需要服務進行轉發數據。虛擬電話、在線面試等現在很多都採用WebRTC實現。
最後做個總結。這篇文章介紹了很多通信協議的東西,分析了TCP/IP的三次握手和四次揮手,並討論了為什麼握手是三次,而揮手是四次,還講了四層網路模型,分析了工作在不同層的協議和工具,後面又重點分析了TCP的擁塞控制,包括超時重傳、慢啟動和擁塞避免、快速重傳和快速恢復,接著還講了點HTTP的東西,最後簡單分析了下Websocket連接的過程和它的特點以及和WebRTC的區別。上面可以說是TCP/IP協議的核心內容,我們通過一兩個demo把它給串了起來,對讀者應該有一個啟發作用,可以更深刻地理解網路協議,當你在寫一個請求的時候,你知道它的背後發生了什麼。讀者可以根據本文再繼續查閱相關資料延伸擴展。如有不正確之處還請指出。
點擊展開全文
※關於「開源」的思考總結
※像大牛一樣寫代碼:31個Android 開發者工具
※兩萬份數據分析碼農的身份和學習方式
※62 歲的 Java 之父加盟亞馬遜(其實 PHP 之父也在 AWS
TAG:1KE互聯網教育 |
※快速搭建CentOS+ASP.NET Core環境支持WebSocket
※SpringBoot | 第十九章:web 應用開發之 WebSocket
※Swoole實現基於WebSocket的群聊私聊
※Django Channel處理Websocket鏈接
※DDoS攻擊新玩法之WebSocket
※Python如何爬取實時變化的WebSocket數據
※App Engine彈性環境開始提供WebSockets協議
※AWS語音轉文本服務開始支持WebSockets
※JMeter測試WebSocket的經驗總結
※websocket與爬蟲
※springboot websocket後台主動推送消息
※ajax,long poll,websocket連接的區別原理
※由淺及深初探websocket
※「詳解」WebSocket相關知識整理
※websocket實現用戶雙方通信
※記WEBLOGIC部署BUG(WEBSOCKET)
※Nginx代理webSocket經常中斷的解決方案,如何保持長連接
※千萬級WebSocket消息推送服務技術分析
※webSocket如何解決自動關閉的意思