不得不認可,tcp是一個很是複雜的協議。它包含了RFC793及以後的一些協議。能把tcp的全部方面面面具到地說清楚,自己就是個很複雜的事情。若是再講得枯燥,那麼就會更讓人昏昏欲睡了。本文但願能儘可能用稍顯通俗的話把tcp描述清楚。html
請忘掉大學課本上學的七層模型,咱們使用四層模型更爲貼合咱們的實際網絡。應用層,傳輸層,網絡層,網絡接入層。linux
分層是爲何,其實和公司中職位是同樣的,不一樣職位的人作不一樣的事情,而後不一樣職位的人合起來,一塊兒完成了數據傳輸的事情。面試
網絡傳輸層負責最底層的底層鏈路鏈接。兩臺主機之間進行互聯,基於網線的物理硬件上的協議。在這個層面,主機與主機的交互只認得硬件mac編號,並不認識IP。這個層須要瞭解的一個概念是MTU,網絡中每一個路由都會設置一個MTU,表明這個路由中能經過的最大的包的大小。那麼整個網絡鏈路的MTU值就是由網絡中全部路由的最小MTU決定。這個就比如水路管道,水流量是由管道鏈路中管子最小的那個鏈路來決定的。算法
IP的出現是頗有必要的,就像給網絡上每一個機器一個門牌號,網絡層,你能夠把它理解爲郵件運輸工,它的職能就是負責把一包東西,從這個門牌運輸到另一個門牌。shell
傳輸層相比於網絡層最大的不一樣就是引入了端口的概念。網絡層只管發送地址和目的地址。可是發送主機上有可能有多個程序和同一個接收主機進行傳輸數據,怎麼區分這多個程序呢?就引入了端口的概念。(發送IP地址,發送端口,接收IP地址,接收端口)四元組標示了一個主機的程序到另外一個主機程序的惟一標示。傳輸層的職能,就是維護這個四元組。windows
其實傳輸層還有一個職能是定義發送方和接收方基本處理包的行爲。上面說到網絡層就至關於郵件運輸工,它只負責把一包東西從一個地方放到另一個地方,可是,這包東西是否送達了,送達以後接收方又有什麼行爲。這些均可以在傳輸層進行定義。注意,這裏說的是能夠,你也能夠在傳輸層布無論這些,只作簡單的基本封裝四元組。你懂的,我說的就是UDP。服務器
應用層,就更抽象一層了。咱們這個端口和那個端口的鏈接是用來幹什麼的,傳輸文件?那麼可使用FTP。傳輸文本?那麼可使用HTTP。應用層就是實際上對具體的程序之間的交互功能進行定義的層。網絡
若是你比較鑽牛角尖的話,可能會提出的問題是:爲何要分爲四層?爲何不是分爲兩層,五層?ssh
這裏分爲幾個問題解釋:socket
分層其實是一個抽象的過程,和咱們寫代碼的時候的封裝是同樣的。你要說,我只有一層,一個協議把全部從程序到物理網絡的全部東西都描述一遍,這個能夠不能夠?能夠。可是,這樣你協議寫多了之後,就會發現,物理網絡部分幾乎全部協議都同樣,那麼我倒不如另外寫一個協議,而後其餘協議使用include的方式來包含這個協議。好了,網絡傳輸層就出來了。後面的原理也幾乎同樣了。因此分層是必要的。
代碼中有個過分設計的概念,分層中也照樣有個過分設計的概念。基本上,分層層數越多,是越符合抽象的。可是到最後咱們會發現,有一個協議整篇都是include,本身實際上並無任何實質的東西。這個就是過分設計。基本上咱們琢磨來琢磨去,按照上面說的,分爲四層,是個很合適的設計。四層中的每一層都有本身負責的一塊內容,內容大小適中,又沒有相互耦合的地方。四層中的每一層都不可缺乏。
說到一個協議,最早應該展現的是它的結構。
整個結構正如車子的零部件同樣,每一個字段都有對應的做用:
這兩個字段表示的就是發送地址和目的地址的端口號。或許有人會問,那四元組中的其餘兩個發送主機IP和目的主機IP呢?ok,那是IP層的事情,請查看IP協議頭。
這兩個字段就有的聊了。
首先第一個問題,序列號作什麼用呢?
序列號是用來標記包的順序的,假設有一段要傳輸的內容大小有9000字節,按照1460字節一個包的大小,假設初始序號爲10000,那麼咱們就把這段內容分爲10000-11460, 11460-12920,... 18760-19000 一共7個包。網絡包因爲網絡問題,可能並非順序到達接收端的,那麼接收端能夠按照序列號來從新組裝這段內容。
第二個問題,序列號有兩個說明什麼?
說明tcp是全雙工的,就是說,tcp的任意一端能夠發送數據,也能夠接收數據。那麼須要有個發送序列號seqence Number和接收序列號 Acknowledgement Number。
因爲tcp頭多是不固定大小的(由於存在可選字段),因此須要有這個值來表示當前這個包的tcp頭有多大。
Reserved就是保留字段
就是上圖中的URG,ACK,PSH,RST,SYN,FIN位,每一個位置一表示的意思是:
URG:緊急位,RFC已經建議廢棄
ACK:說明這個包中帶有回覆信息
PSH:說明這個包中有傳輸數據
RST:重置位,說明這個包是用來要對方重置鏈接
SYN:創建鏈接,說明發送方向另外一方發送創建鏈接的請求
FIN:結束位,說明發送一方告知另一方,要請求中斷鏈接
熟悉這些Tag位是很是必要的,咱們通常討論包請求的時候,使用的術語通常就是:
發送方請求一個SYN,接收方返回一個ACK。往往看到這種字眼,請不要傻眼。
還有一個誤區,一個包是否是隻能包含一個tag位?不是的。一個包能夠包含一個或者多個tag位。好比一個包能夠有ACK的功能,也能同時有SYN的請求功能。(在TCP三次握手的第二次握手的時候就是攜帶了ACK+SYN的標誌位)。
這個值就是著名的滑動窗口值。滑動窗口是接收端告訴發送端下次能夠發送多少包。好吧,這裏也須要面對幾個問題:
網絡上並非只有發送方發一個請求,接收方回覆一個ACK這種模式的。他們交互的模式更多是:發送方一次發送多個請求包,接收方回覆一個ACK,把這些請求包都回復了。這個使用前面的Acknowledgment Number是能夠作到的。
可是基本上,在接收方的角度,ACK包必定是收到一個包以後,才返回一個ACK,就是說,沒有平白無故的發送重複ACK,沒有一個請求,多個ACK這種狀況。可是有多個請求,多個重複ACK的狀況,這個時候,每每說明某個請求的包丟失了。
滑動窗口的存在是爲了控制網絡上包的數量。若是沒有滑動窗口,那麼就是一個很理想很理想的狀況,發送方一有數據,加上包頭達到MTU大小,直接發送,就和衝鋒槍同樣,突突突突。可是呢?這樣子,實際上,沒有考慮到接收方是否能接收完。接收端就像一個一直在吃飯的胖子,他的吃飯速度是固定的,它一次性最多能吃10碗飯,某個時刻可能已經吃了兩碗飯,可是還沒消化。因此這個時候,它只能再吃8碗飯了,若是這個時候你一會兒給它80碗,必然致使它堵死了,吃不下不吃下。這個滑動窗口就是接收端告訴發送端我還能吃幾碗飯的通話器。
總結下,這裏已經有兩個條件限制發送方的效率了,一個是MTU,全鏈路MTU大小,限制每次最大發送的包的大小。另外一個是滑動窗口,限制發送方一次發送的包數量。
滑動窗口我更願意理解爲發送方和接收方共同維護的。分別有發送窗口和接收窗口區別。
發送方數據有幾個狀態:數據已發送未收到ACK,數據已發送收到ACK
接收方數據有幾個狀態:數據已收到未被應用層消費,數據已收到已被應用層消費
把發送數據橫拍作長列狀,發送方一但有數據收到ACK,那麼滑動窗口左側邊就進行左移。一樣,一旦接收方有數據被應用層消費,那麼,滑動窗口的右側邊就進行右移。整個過程,就比如努力爬行的蚯蚓,尾巴向前挪一寸,頭部再向前走一寸,直到把整個數據都從頭至尾移動完畢。
回到tcp的windows字段,這個字段是接收端回覆給發送端,告訴發送端接收端的窗口大小的。咱們其實默認也把這個窗口大小叫作滑動窗口大小。
關於滑動窗口的概念的理解,個人感觸是網上各類各樣對這個滑動窗口的描述,不要陷入到咬文嚼字中,頭腦中形象有這個滑動窗口的滑動過程,就能夠了,不少文章不少描述多是先後矛盾的。好比,下面兩個關於發送窗口的描述:
校驗和。就是對TCP的頭部和數據部分進行檢驗,是否在中途被篡改過。它和IP頭中的校驗和的算法是同樣的,只是IP校驗內容中不包括數據,可是TCP是包括頭部和數據兩個部分的。
緊急數據指針。緊急數據指的是發送端告訴接收端,這個數據是很是緊急的,請優先讀取,設計初期多是因爲考慮到中斷或者異常等狀況,可是在RFC6093中已經明確,緊急數據已是廢棄功能了。不建議使用。只爲舊程序兼容而使用。
因此,對於Urgent Porinter和tag中的URG標示就不要使用了。
options字段至關於擴展使用的,RFC有哪些信息要傳輸,而頭部沒有安排的字段,就能夠放在options中進行傳輸。
它根據kind+length+ value的形式來定義存儲哪些屬性。
好比kind =2 表明存儲的是MSS值(最大內容大小,MTU-IP頭-TCP頭),它有四個字節的長度,具體值爲1460
具體kind和對應的值的映射能夠參考 http://www.iana.org/assignments/tcp-parameters/tcp-parameters.xhtml
一個最常遇到的月經雞湯麪試題就是,UDP和TCP有什麼不一樣。嗯,猴子和老虎就是不同的。常常咱們會說起的一點就是TCP是可靠的,UDP是不可靠的。TCP的可靠體如今哪裏呢?握手鍊接的創建和消失就是其中一個體現。
TCP著名的三次握手和四次揮手
這個圖裏面的client和server應該理解爲發送方和接收方。下面這一串描述請熟練練習到像串口相聲同樣:發送方發送一個SYN到接收方請求創建鏈接,接收方返回一個ACK確認收到請求,並攜帶一個SYN給發送方請求創建雙向鏈接,發送方再返回一個ACK給接收方確認,這個時候鏈接就創建了。
順勢說下四次揮手吧。發送方發送一個FIN給接收方主動請求斷開鏈接,接收方返回一個ACK確認,接着接收方再發送一個FIN請求斷開另外一方向的鏈接,發送方收到以後返回一個ACK確認。這個時候,鏈接就中斷了。
在三次握手和四次揮手的時候,發送方和接收方的socket是有狀態的,對,就是你使用netstat 能看到機器上socket的狀態。
SYN_SENT/SYN_RCVD/ESTABLISH/FIN_WAIT1/CLOSE_WAIT/FIN_WAIT2/LAST_ACK/TIME_WAIT
linux的TCP模塊維護兩個隊列,半鏈接隊列和連接隊列,當三次握手的時候,收到第一個SYN,發送完ACK以後,就會把這個鏈接放入到半鏈接隊列中,當第三步完成的時候,鏈接創建了,就把鏈接從半鏈接隊列放到鏈接隊列中。
半鏈接隊列長度是由net.ipv4.tcp_max_syn_backlog進行設置的。
鏈接隊列的長度由咱們建立socket的時候指定的backlog和net.core.somaxconn其中的較小值肯定的。
若是backlog或者net.core.somaxconn設置太小,那麼不少鏈接就沒法創建,服務端會發送RST拒絕鏈接,這個也是不少服務器性能上不去或斷開鏈接的緣由。
三次握手,咱們假設server端是按照正常的流程走的,可是client端是邪惡的,它發送了SYN以後,server端返回了ACK+SYN,可是client端一直hold住,或者直接掉線,不發送ack了,那麼這個時候,server端就一直保持在SYN_RCVD狀態。
若是這種client端很是多,就會把前面說的半鏈接隊列塞滿,後面的鏈接就沒法創建了,這個server的服務也就給停止了。
這種攻擊就叫作SYN_FLOOD攻擊。
說到四次揮手,主動發起請求的一方會在TIME_WAIT狀態持續2MSL。MSL就是Maximum Segment Lifetime,一個包在網絡上存活的最長時間,linux設置的MSL是30s。這個是爲了防止最後一個ACK可能被丟失了,那麼在2MSL中若是收到對方重複發送的FIN包,就須要從新發送ACK來關閉鏈接。TCP的這種行爲,咱們能夠看做是一種負責任的行爲,主動請求關閉的一方在很大程度上確保了對方收到斷開確認請求以後才關閉這個鏈接的。固然,這也能保證了若是我這個端口被其餘程序複用了,舊的請求不會發一個莫名其妙的FIN過來。
首先確認,TIME_WAIT不是邪惡的,咱們可能在服務器上很常常會看到TIME_WAIT的鏈接,沒必要驚慌,除非這種鏈接數已經超過了系統的fd數,若是沒有超過的話,個人建議,不要在壓TIME_WAIT數量上太下功夫,找出什麼致使出現大量TIME_WAIT的緣由比較靠譜(大多數是網絡問題)。
不過,服務端確實應當儘可能避免TIME_WAIT留在服務端,無論怎樣,這個會消耗一些資源的。因此,把TIME_WAIT留在客戶端,服務端不主動斷開鏈接是一個很好的方法,好比HTTP協議提供的KEEP_ALIVE(http://www.cnblogs.com/yjf512/p/5354055.html)就是在這個方面作的很好的,客戶端鏈接的時候告訴服務器不要主動斷開鏈接。
很容易會出現這個問題,第二次請求和第三次請求能不能一塊兒發送呢?這樣是否是能節約性能?答案是能夠的。而linux也確實是這麼實現的。你具體抓一個包,就會發現第二次請求和第三次請求是一塊兒發送的。
在實際網絡中抓包,你會發現,除了SYN以外,全部的請求包都帶有ACK標示。即便是上圖中的#197條已經對seq爲177的消息發送了ACK,# 325也還會發送一個ACK seq=177。
這個是RFC的建議
創建鏈接以後,每一個請求都要帶上ACK標誌。咱們能夠這麼理解,因爲在TCP的機制中,ACK是沒法確認中途有沒有丟失的,那麼本着不發白不發的原則,每一個請求都順帶帶上當前已經ACK的信息,
TCP不是一個自私的協議,它的設計充分考慮了互聯網的大環境。試想,若是全部的網絡發送方都無論網絡的狀況,明明網絡已經堵塞了,還一個勁地發送大量包,甚至重發,那麼這個時候,你們都沒得玩了。因而,漸漸的,TCP引入了擁塞窗口(cwnd)的概念。擁塞窗口的存在單純是爲了不網絡上有超過當前網絡能力而形成堵塞。擁塞窗口的單位是報文段個數。好比咱們平時會說,如今的擁塞窗口爲3,表明發送端能夠一次性發送3個報文段。固然,實際上,發送端的最大發送窗口數取決於擁塞窗口(cwnd)和滑動窗口(win)的最小值。
控制網絡擁塞的算法爲擁塞算法,這個算法在不斷演變,在不一樣操做系統中也有不一樣實現。
控制擁塞咱們首先會想到在剛剛鏈接網絡的時候,是否是最好先慢慢檢測網絡狀況,再肯定發送包的數量。這就是咱們說的慢啓動算法。發送方從1個包開始,收到ACK,下次就發送2個包,收到這兩個包的ACK(請注意,這裏有可能只有一個ACK),下次就發送4個包。
「每收到一個ACK,擁塞窗口就增長一個報文段」。
這句話我更願意理解爲「每確認一個包被ACK了,擁塞窗口就增長一個報文段」
這句話的理解就是,因爲有「延遲ACK」算法,頗有可能,當發送方發送兩個請求包過來的時候,我只發送一個ACK。確認你發送的兩個包,這個時候,cwnd其實是加2,而不是加1。以下圖中的cwnd爲4的ACK。
固然上圖的狀況太理想,實際的狀況,坑cwnd爲2的請求發出去兩個報文包的時候,先返回了一個ACK,而後cwnd這個時候就爲3,發送方就會繼續發送請求包。。。更貼近實際的正如這個圖:
慢啓動使得cwnd是呈指數增加。必定不多是無限增加的,這裏就有個閥值,超過這個閥值,就進入擁塞避免算法。
先說擁塞避免算法,擁塞避免算法說的是擁塞窗口的增長再也不是「每收到一個ACK,擁塞窗口就增長一個報文段」。 而是「每收到一個ACK,cwnd = cwnd + 1/cwnd」。 這個就表明,
咱們怎麼判斷擁塞呢?有兩種判斷方法:
a 超時重傳(發出去的包在指定時間內沒有收到ACK)
這個指定時間是經過超時定時器來計算的,發出去一個包,超時定時器就開始計時,當超時定時器到時間以後,沒有收到ACK,那麼這個時候就判斷爲擁堵了。須要進行重傳。
當被這個狀況觸發,TCP認爲網絡狀況很是糟糕,因此會直接把cwnd調整爲1,sshthread 調整爲cwnd/2 。 從新進入到慢啓動流程。
b 快速重傳(重複收到ACK)
這個是因爲發送方一次性發送多個請求(好比5個請求,可是第二個請求丟失了,第一三四五請求到了接收端)三四五請求觸發了三個ACK返回,可是因爲接收端沒有收到請求一,返回的三個ACK都是ACK一的,因此發送方就表現爲收到重複ACK。當連續收到三條重複ACK的時候就進行重傳,不須要等待重傳計時器
這個時候TCP會以爲網絡仍是能夠的,反應不會那麼激烈,cwnd調整爲cwnd/2, sshthresh調整爲cwnd大小,進入快速恢復算法。
快速恢復算法是爲了避免要有一個重傳就那麼大響應。能儘快恢復到網絡流暢時候穩定的狀態。
TCP中有四個定時器,有的定時器以前已經說過了。
是爲了重傳的時候使用的。
在上面說到TCP揮手的時候,四次揮手中最後一次揮手,主動發起的一方會進入TIME_WAIT狀態2MSL的時常,這個定時器就是用來計算這個的。
當滑動窗口爲0的時候,發送方不會再發送包給接收方了。可是不發送包怎麼知道接收方如今的窗口是否是還爲0呢。這個時候就須要不定時去接收方諮詢是否滑動窗口還爲0。這個不定時的算法就是使用堅持定時器來進行諮詢的。
這個算法是使用TCP指數退避方法,第一次1.5秒,第二次1.5x2秒,第三次1.5x4... 以此規律來進行輪詢的。
tcp有個keepalive機制,這個只有在必定時間內(tcp_keepalive_time,默認每2個小時),沒有數據包傳遞了,發送方在發送心跳檢測,若是發送成功,則鏈接繼續,若是沒有正常返回,則在指定次數內(tcp_keepalive_probes,默認是9次),指定間隔(tcp_keepalive_intvl,默認是17s)發送心跳包。
tcp的東西真是比較複雜。好吧,這玩意根本無法講通俗。個人理解或許有誤差,若是有錯誤,但願能幫忙留言提出。同時,強烈建議,看完這篇再看一遍耗子哥的《TCP的那些事兒》(http://coolshell.cn/articles/11564.html)。或許還能回來告訴下我哪些地方我理解有問題。