
伺服器調用socket()、bind()、listen()完成初始化後,調用accept()阻塞等待,處于監聽端口的狀态,用戶端調用socket()初始化後,調用connect()發出syn段并阻塞等待伺服器應答,伺服器應答一個syn-ack段,用戶端收到後從connect()傳回,同時應答一個ack段,伺服器收到後從accept()傳回。
資料傳輸的過程:
建立連接配接後,tcp協定提供全雙工的通信服務,但是一般的用戶端/伺服器程式的流程是由用戶端主動發起請求,伺服器被動處理請求,一問一答的方式。是以,伺服器從accept()傳回後立刻調用read(),讀socket就像讀管道一樣,如果沒有資料到達就阻塞等待,這時用戶端調用write()發送請求給伺服器,伺服器收到後從read()傳回,對用戶端的請求進行處理,在此期間
用戶端調用read()阻塞等待伺服器的應答,伺服器調用write()将處理結果發回給用戶端,再次調用read()阻塞等待下一條請求,用戶端收到後從read()傳回,發送下一條請求,如此循環下去。
如果用戶端沒有更多的請求了,就調用close()關閉連接配接,就像寫端關閉的管道一樣,伺服器的read()傳回0,這樣伺服器就知道用戶端關閉了連接配接,也調用close()關閉連接配接。注意,任何一方調用close()後,連接配接的兩個傳輸方向都關閉,不能再發送資料了。如果一方調用shutdown()則連接配接處于半關閉狀态,仍可接收對方發來的資料。
notice:
在學習socket api時要注意應用程式和tcp協定層是如何互動的: *應用程式調用某個socket函數時tcp協定層完成什麼動作,比如調用connect()會發出syn段 *應用程式如何知道tcp協定層的狀态變化,比如從某個阻塞的socket函數傳回就表明tcp協定收到了某些段,再比如read()傳回0就表明收到了fin段。
轉兩篇比較給力的文章:
之是以想寫這篇文章,目的有三個,
一個是想鍛煉一下自己是否可以用簡單的篇幅把這麼複雜的tcp協定描清楚的能力。
另一個是覺得現在的好多程式員基本上不會認認真真地讀本書,喜歡快餐文化,是以,希望這篇快餐文章可以讓你對tcp這個古典技術有所了解,并能體會到軟體設計中的種種難處。并且你可以從中有一些軟體設計上的收獲。
最重要的希望這些基礎知識可以讓你搞清很多以前一些似是而非的東西,并且你能意識到基礎的重要。
是以,本文不會面面俱到,隻是對tcp協定、算法和原理的科普。
我本來隻想寫一個篇幅的文章的,但是tcp真tmd的複雜,比c++複雜多了,這30多年來,各種優化變種争論和修改。是以,寫着寫着就發現隻有砍成兩篇。
上篇中,主要向你介紹tcp協定的定義和丢包時的重傳機制。
下篇中,重點介紹tcp的流疊、擁塞處理。
廢話少說,首先,我們需要知道tcp在網絡osi的七層模型中的第四層——transport層,ip在第三層——network層,arp在第二層——data link層,在第二層上的資料,我們叫frame,在第三層上的資料叫packet,第四層的資料叫segment。
首先,我們需要知道,我們程式的資料首先會打到tcp的segment中,然後tcp的segment會打到ip的packet中,然後再打到以太網ethernet的frame中,傳到對端後,各個層解析自己的協定,然後把資料交給更高層的協定處理。
接下來,我們來看一下tcp頭的格式
你需要注意這麼幾點:
tcp的包是沒有ip位址的,那是ip層上的事。但是有源端口和目标端口。
一個tcp連接配接需要四個元組來表示是同一個連接配接(src_ip, src_port, dst_ip, dst_port)準确說是五元組,還有一個是協定。但因為這裡隻是說tcp協定,是以,這裡我隻說四元組。
注意上圖中的四個非常重要的東西:
sequence number是包的序号,用來解決網絡包亂序(reordering)問題。
acknowledgement number就是ack——用于确認收到,用來解決不丢包的問題。
window又叫advertised-window,也就是著名的滑動視窗(sliding window),用于解決流控的。
tcp flag ,也就是包的類型,主要是用于操控tcp的狀态機的。
關于其它的東西,可以參看下面的圖示
其實,網絡上的傳輸是沒有連接配接的,包括tcp也是一樣的。而tcp所謂的“連接配接”,其實隻不過是在通訊的雙方維護一個“連接配接狀态”,讓它看上去好像有連接配接一樣。是以,tcp的狀态變換是非常重要的。
的對照圖,我把兩個圖并排放在一起,這樣友善在你對照着看。另外,下面這兩個圖非常非常的重要,你一定要記牢。(吐個槽:看到這樣複雜的狀态機,就知道這個協定有多複雜,複雜的東西總是有很多坑爹的事情,是以tcp協定其實也挺坑爹的)
很多人會問,為什麼建連結要3次握手,斷連結需要4次揮手?
對于建連結的3次握手,主要是要初始化sequence number 的初始值。通信的雙方要互相通知對方自己的初始化的sequence number(縮寫為isn:inital sequence number)——是以叫syn,全稱synchronize sequence numbers。也就上圖中的 x 和 y。這個号要作為以後的資料通信的序号,以保證應用層接收到的資料不會因為網絡上的傳輸的問題而亂序(tcp會用這個序号來拼接資料)。
對于4次揮手,其實你仔細看是2次,因為tcp是全雙工的,是以,發送方和接收方都需要fin和ack。隻不過,有一方是被動的,是以看上去就成了所謂的4次揮手。如果兩邊同時斷連接配接,那就會就進入到closing狀态,然後到達time_wait狀态。下圖是雙方同時斷連接配接的示意圖(你同樣可以對照着tcp狀态機看):
另外,有幾個事情需要注意一下:
關于建連接配接時syn逾時。試想一下,如果server端接到了clien發的syn後回了syn-ack後client掉線了,server端沒有收到client回來的ack,那麼,這個連接配接處于一個中間狀态,即沒成功,也沒失敗。于是,server端如果在一定時間内沒有收到的tcp會重發syn-ack。在linux下,預設重試次數為5次,重試的間隔時間從1s開始每次都翻售,5次的重試時間間隔為1s,
2s, 4s, 8s, 16s,總共31s,第5次發出後還要等32s都知道第5次也逾時了,是以,總共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,tcp才會把斷開這個連接配接。
關于syn flood攻擊。一些惡意的人就為此制造了syn flood攻擊——給伺服器發了一個syn後,就下線了,于是伺服器需要預設等63s才會斷開連接配接,這樣,攻擊者就可以把伺服器的syn連接配接的隊列耗盡,讓正常的連接配接請求不能處理。于是,linux下給了一個叫tcp_syncookies的參數來應對這個事——當syn隊列滿了後,tcp會通過源位址端口、目标位址端口和時間戳打造出一個特别的sequence
number發回去(又叫cookie),如果是攻擊者則不會有響應,如果是正常連接配接,則會把這個 syn cookie發回來,然後服務端可以通過cookie建連接配接(即使你不在syn隊列中)。請注意,請先千萬别用tcp_syncookies來處理正常的大負載的連接配接的情況。因為,synccookies是妥協版的tcp協定,并不嚴謹。對于正常的請求,你應該調整三個tcp參數可供你選擇,第一個是:tcp_synack_retries 可以用他來減少重試次數;第二個是:tcp_max_syn_backlog,可以增大syn連接配接數;第三個是:tcp_abort_on_overflow
處理不過來幹脆就直接拒絕連接配接了。
should not be changed without advice/request of technical experts”)。
關于tcp_tw_recycle。如果是tcp_tw_recycle被打開了話,會假設對端開啟了tcp_timestamps,然後會去比較時間戳,如果時間戳變大了,就可以重用。但是,如果對端是一個nat網絡的話(如:一個公司隻用一個ip出公網)或是對端的ip被另一台重用了,這個事就複雜了。建連結的syn可能就被直接丢掉了(你可能會看到connection
關于tcp_max_tw_buckets。這個是控制并發的time_wait的數量,預設值是180000,如果超限,那麼,系統會把多的給destory掉,然後在日志裡打一個警告(如:time wait bucket
table overflow),官網文檔說這個參數是用來對抗ddos攻擊的。也說的預設值180000并不小。這個還是需要根據實際情況考慮。
下圖是我從wireshark中截了個我在通路coolshell.cn時的有資料傳輸的圖給你看一下,seqnum是怎麼變的。(使用wireshark菜單中的statistics ->flow graph… )
你可以看到,seqnum的增加是和傳輸的位元組數相關的。上圖中,三次握手後,來了兩個len:1440的包,而第二個包的seqnum就成了1441。然後第一個ack回的是1441,表示第一個1440收到了。
注意:如果你用wireshark抓包程式看3次握手,你會發現seqnum總是為0,不是這樣的,wireshark為了顯示更友好,使用了relative seqnum——相對序号,你隻要在右鍵菜單中的protocol preference 中取消掉就可以看到“absolute seqnum”了
tcp要保證所有的資料包都可以到達,是以,必需要有重傳機制。
注意,接收端給發送端的ack确認隻會确認最後一個連續的包,比如,發送端發了1,2,3,4,5一共五份資料,接收端收到了1,2,于是回ack 3,然後收到了4(注意此時3沒收到),此時的tcp會怎麼辦?我們要知道,因為正如前面所說的,seqnum和ack是以位元組數為機關,是以ack的時候,不能跳着确認,隻能确認最大的連續收到的包,不然,發送端就以為之前的都收到了。
一種是不回ack,死等3,當發送方發現收不到3的ack逾時後,會重傳3。一旦接收方收到3後,會ack 回 4——意味着3和4都收到了。
但是,這種方式會有比較嚴重的問題,那就是因為要死等3,是以會導緻4和5即便已經收到了,而發送方也完全不知道發生了什麼事,因為沒有收到ack,是以,發送方可能會悲觀地認為也丢了,是以有可能也會導緻4和5的重傳。
對此有兩種選擇:
一種是僅重傳timeout的包。也就是第3份資料。
另一種是重傳timeout後所有的資料,也就是第3,4,5這三份資料。
這兩種方式有好也有不好。第一種會節省帶寬,但是慢,第二種會快一點,但是會浪費帶寬,也可能會有無用功。但總體來說都不好。因為都在等timeout,timeout可能會很長(在下篇會說tcp是怎麼動态地計算出timeout的)
于是,tcp引入了一種叫fast retransmit 的算法,不以時間驅動,而以資料驅動重傳。也就是說,如果,包沒有連續到達,就ack最後那個可能被丢了的包,如果發送方連續收到3次相同的ack,就重傳。fast retransmit的好處是不用等timeout了再重傳。
比如:如果發送方發出了1,2,3,4,5份資料,第一份先到送了,于是就ack回2,結果2因為某些原因沒收到,3到達了,于是還是ack回2,後面的4和5都到了,但是還是ack回2,因為2還是沒有收到,于是發送端收到了三個ack=2的确認,知道了2還沒有到,于是就馬上重轉2。然後,接收端收到了2,此時因為3,4,5都收到了,于是ack回6。示意圖如下:
fast retransmit隻解決了一個問題,就是timeout的問題,它依然面臨一個艱難的選擇,就是重轉之前的一個還是重裝所有的問題。對于上面的示例來說,是重傳#2呢還是重傳#2,#3,#4,#5呢?因為發送端并不清楚這連續的3個ack(2)是誰傳回來的?也許發送端發了20份資料,是#6,#10,#20傳來的呢。這樣,發送端很有可能要重傳從2到20的這堆資料(這就是某些tcp的實際的實作)。可見,這是一把雙刃劍。
這樣,在發送端就可以根據回傳的sack來知道哪些資料到了,哪些沒有到。于是就優化了fast retransmit的算法。當然,這個協定需要兩邊都支援。在 linux下,可以通過tcp_sack參數打開這個功能(linux 2.4後預設打開)。
這裡還需要注意一個問題——接收方reneging,所謂reneging的意思就是接收方有權把已經報給發送端sack裡的資料給丢了。這樣幹是不被鼓勵的,因為這個事會把問題複雜化了,但是,接收方這麼做可能會有些極端情況,比如要把記憶體給别的更重要的東西。是以,發送方也不能完全依賴sack,還是要依賴ack,并維護time-out,如果後續的ack沒有增長,那麼還是要把sack的東西重傳,另外,接收端這邊永遠不能把sack的包标記為ack。
d-sack使用了sack的第一個段來做标志,
如果sack的第一個段的範圍被ack所覆寫,那麼就是d-sack
如果sack的第一個段的範圍被sack的第二個段覆寫,那麼就是d-sack
示例一:ack丢包
下面的示例中,丢了兩個ack,是以,發送端重傳了第一個資料包(3000-3499),于是接收端發現重複收到,于是回了一個sack=3000-3500,因為ack都到了4000意味着收到了4000之前的所有資料,是以這個sack就是d-sack——旨在告訴發送端我收到了重複的資料,而且我們的發送端還知道,資料包沒有丢,丢的是ack包。
示例二,網絡延誤
下面的示例中,網絡包(1000-1499)被網絡給延誤了,導緻發送方沒有收到ack,而後面到達的三個包觸發了“fast retransmit算法”,是以重傳,但重傳時,被延誤的包又到了,是以,回了一個sack=1000-1500,因為ack已到了3000,是以,這個sack是d-sack——辨別收到了重複的包。
這個案例下,發送端知道之前因為“fast retransmit算法”觸發的重傳不是因為發出去的包丢了,也不是因為回應的ack包丢了,而是因為網絡延時了。
可見,引入了d-sack,有這麼幾個好處:
1)可以讓發送方知道,是發出去的包丢了,還是回來的ack包丢了。
2)是不是自己的timeout太小了,導緻重傳。
3)網絡上出現了先發的包後到的情況(又稱reordering)
4)網絡上是不是把我的資料包給複制了。
知道這些東西可以很好得幫助tcp了解網絡情況,進而可以更好的做網絡上的流控。
linux下的tcp_dsack參數用于開啟這個功能(linux 2.4後預設打開)