咱們在平時的開發過程當中,或多或少都會涉獵到網絡傳輸這塊。
這篇文章,主要是整理一下 TCP 的一些知識要點,做爲一名開發者來講,儘管有那麼多的基礎設施(框架、組件)幫咱們屏蔽了這些細節。當我仍然認爲了解它的一些基本原理必有些裨益,尤爲是當你在分佈式環境上遇到一些棘手問題時,一些原理性的知識可能會讓你快速找到答案。面試
TCP 是傳輸層的協議,全稱是叫作 Transmission Control Protocol,這個協議在 IETF RFC 793 進行了定義。
在互聯網產生以前,咱們的電腦都是相互獨立的,每臺機器都有着本身的操做系統並保持着本身的運行。
因而,爲了將這些電腦鏈接起來,並可以基於一種"通道"的形式進行數據、資源的傳輸及交互,IETF 制定了 TCP 協議。緩存
那麼,IETF又是什麼? 這是一個使人尊敬的技術組織,叫 Internet Engineering Task Force,即互聯網工程任務組。
這是一個成立於1985年的開放性組織,如今咱們所提到的 HTTP、TCP、IP 這些重要的網絡協議,都是出自於該組織。
能夠這麼說,IETF 是互聯網的始做俑者,沒有它就沒有如今繁榮的互聯網了。tomcat
值得一提的是,IETF並不是權貴組織,它是一個"來自民間" 的自組織、自管理的團隊,很是崇尚於自由平等的精神。安全
整個互聯網的底層機制是由一套標準網絡協議組成的,爲了更方便於理解,人們便定義了所謂的「網絡分層模型"。
在學習計算機網絡課程的時候,都會提到兩種網絡模型,以下:服務器
在之前,因爲術語衆多,有許多人常常被OSI、ISO所迷惑..cookie
從上面的圖中能夠看出,TCP/IP 基本上是OSI 模型的簡化版,固然也更加容易理解。
在網絡層如下,物理層、數據鏈路層所涉及的一些技術手段及概念都相對晦澀難懂,就好比光纜、中繼器、交換機等須要一些專業背景才能掌握通透。
對於大多數的軟件應用來講,將網絡層如下的部分統稱爲「網絡接口層" 無疑是更加簡單的。網絡
所以,OSI 模型儘管很是完善且全面,但已經被 TCP/IP 模型所淘汰,在互聯網應用盛行的今天不多被說起。併發
圖-TCP/IP 網絡模型框架
TCP 是整個 TCP/IP 協議族中最重要的傳輸層協議,它定義了一種面向鏈接的、可靠的、基於流的傳輸方式。
HTTP 是基於 TCP 的,因此說 TCP 是整個互聯網的協議其一併不爲過。
同時,咱們在使用 HTTP 協議實現應用系統間的交互時,也常常免不了會與 TCP 打上交道。所以有必要了解一些基本機制。
首先,TCP 是基於鏈接的,也就是在進行數據傳輸以前,客戶端與服務端(或者說是通訊的雙方)須要先創建一個可信的鏈接。
在數據傳輸結束後,再經過一種協定的方式斷開鏈接,由通訊的雙方釋放資源。這裏涉及到的,就是常說的"三次握手"、"四次揮手"
其次,TCP 是可靠的,它定義了一種數據包的"超時重傳機制",簡單說,就是每個數據包在發送出去後的都會等待一個響應。
若是指定時間內沒有收到響應,由發送方進行必定次數的重傳來保證數據的可靠傳輸。
最後,TCP 是基於流的,這是指在傳輸數據時應用層不須要關注數據包的邊界,TCP在數據傳輸時會自動根據網絡環境將數據進行緩衝、分組、合併。
這點跟基於報文的協議(UDP)是大相徑庭的。固然,基於流的傳輸也保證了數據收發的有序性,所以每一個數據包都附帶上一個屬於當前鏈接的序列號。
全雙工是通信上的術語,通常在軟件開發領域提到的並很少。
這是指數據同時在兩個方向上傳輸,TCP 是基於全雙工的可信傳輸協議。
固然 UDP 也能夠實現全雙工的傳輸,但 TCP 只能實現點對點的傳輸,沒法支持廣播或者多播(分組)。
黑板:半雙工的區別在於,同一時間只能有一個方向的傳輸
透視一個協議的最原始的方法就是看它的數據包,一個TCP 的報文格式以下:
這裏面的字段就包括了:
源端口
代表發送端所使用的端口號,用於目標主機迴應。
目的端口
代表要鏈接的目標主機的端口號。
序號
代表發送的數據包的順序,通常爲上次發送包中的順序號+1。
若該數據包是整個TCP鏈接中的第一個包(SYN包),則該值是隨機生成的。
確認號
代表本端TCP已經接收到的數據,其值表示期待對端發送的下一個字節的序號。
實際上告訴對方,在這個序號減1之前的字節已正確接收。
若該數據包是整個TCP鏈接中的第一個包(SYN包),則確認號通常爲0。
數據偏移
表示以32位(4字節)爲單位的TCP分組頭的總長度(首部長度),用於肯定用戶數據區的起始位置。
在沒有可變內容的狀況下,TCP頭部的大小爲20字節,對應該值爲5。
標誌位
緊急標誌位(URG):開啓時代表此數據包處於緊急狀態應該優先處理
確認標誌位(ACK):開啓時代表確認號有效,不然忽略確認號
推送標誌位(PSH):開啓時代表應該儘快交付給應用進程,而沒必要等到緩存區填滿才推送,好比 telnet 的場景
復位標誌位(RST):開啓時代表TCP鏈接出現鏈接出現錯誤,數據包非法拒絕鏈接
同步標誌位(SYN):開啓時代表鏈接創建的標誌
終止標誌位(FIN):開啓時代表釋放一個鏈接
窗口大小
代表指望接受到的數據包字節數,用於擁塞控制。
校驗和
實現對TCP報文頭以及數據區進行校驗。
緊急指針
在緊急狀態下(URG打開),指出窗口中緊急數據的位置(末端)。
選項(可變)
用於支持一些特殊的變量,好比最大分組長度(MSS)。
填充
用於保證可變選項爲32 bit的整數倍。
黑板:通常狀況下TCP 頭部爲20字節,加上20字節的 IP頭部,一個數據包至少包含40字節的頭部
鏈是指鏈路,這個是物理層的概念,好比光纜光纖,或是無線的電磁波。
但這裏所說的鏈路實際上是網絡鏈接的意思,即IP 上層的概念。
那麼,一個TCP 正常的通信流程,會包含建鏈(創建鏈接)、傳輸數據、拆鏈(關閉鏈接),以下圖所示:
(圖來自網絡)
據上圖所示,在進行 TCP 進行數據傳輸時,都不可避免的會通過這兩個階段:
下面,重點說明下建鏈與拆鏈的過程
在創建TCP鏈接時,須要通過三次交互,也成爲三次握手(HandShake)。
一、客戶端發起鏈接請求,發送 SYN包(SYN=i)到服務器,並進入到SYN-SEND狀態,等待服務器確認
二、服務器收到SYN包後,必須確認客戶的 SYN(ack=i+1),同時本身也發送一個SYN包(SYN=k),即SYN+ACK包,此時服務器進入SYN-RECV狀態
三、客戶端收到服務器的SYN+ACK包,向服務器發送確認報ACK(ack=k+1),此後客戶端和服務器進入ESTABLISHED狀態,雙方能夠開始傳送數據。
在談論三次握手的時候,有幾個問題是須要關注的:
問題1. 爲何是三次握手
這個問題在技術面試時屢試不爽,原話是能不能兩次,或者是四次握手呢?
答案就是,TCP 是可靠的傳輸,在創建鏈接時就應該通過兩端的確認過程,如上面的流程,
只有在三次握手的狀況下,客戶端和服務端都通過了一次真正(SYN+ACK)的確認過程。這樣的鏈接便認爲是可信的。
此外,若是僅僅只是兩次握手,一旦網絡不穩定形成 SYN 包重傳則會直接致使重複創建鏈接,浪費資源。
問題2. 什麼是syn flood攻擊
syn flood 是一種經典的 ddos攻擊手段,這裏面用到了TCP 三次握手存在的漏洞。
在上面的圖中,能夠看到當服務端接收到 SYN 後進入 SYN-RECV 狀態,此時的鏈接稱爲半鏈接,同時會被服務端寫入一個 半鏈接隊列。
想象一下,若是攻擊者在短期內不斷的向服務端發送大量的 SYN 包而不響應,那麼服務器的 半鏈接隊列很快會被寫滿,從而致使沒法工做。
實現 syn flood 的手段,能夠經過僞造源 IP 的方式,這樣服務器的響應就永遠到達不了客戶端(握手沒法完成);
固然,經過設定客戶端防火牆規則也能夠達到一樣的目的。
對 syn flood 實現攔截是比較困難的,能夠經過啓用 syn_cookies 的方式實現緩解,但這一般不是最佳方案。
最好的辦法是經過專業的防火牆來解決,基本上全部的雲計算大T 都具有這個能力。
關於 syn flood 能夠看看這篇文章
問題3. 半鏈接隊列和全鏈接隊列如何調優
這裏提到了一個"半鏈接隊列"(syns queue),與其對應的還有一個 "全鏈接隊列"(accept queue)
前者用於暫存未創建徹底的鏈接,後者是鏈接在成功創建後進入的一個隊列。
半鏈接隊列默認大小能夠經過內核參數調整:
echo 4096 > /proc/sys/net/ipv4/tcp_max_syn_backlog
黑板:tcp_max_syn_backlog 在 syn_cookies 開啓時是無效的,這兩個選項存在衝突
對於全鏈接隊列,若是服務器未能及時經過 accept 調用將其中的鏈接取走,會致使隊列溢出(鏈接失效)
全鏈接隊列的大小的內核調優方式:
echo 4096 > /proc/sys/net/core/somaxconn
那麼,是否是隻有內核調優這種方法能影響這兩個參數呢?答案是否認的。
實際上,在應用層調用 socket listen 時也支持設置一個 backlog參數,這幾個之間的關係以下:
半鏈接隊列長度 = min(backlog,內核 net.core.somaxconn,內核 tcp_max_syn_backlog) 全鏈接隊列長度 = min(backlog,內核 net.core.somaxconn)
黑板:通常的應用服務器如 netty、tomcat 都支持設置 backlog 參數,可是在真正進行調優時還須要配合考慮內核參數的配置。
在釋放鏈接時,因爲TCP是全雙工的,所以最後要由兩端分別進行關閉,這個流程以下:
一、客戶端發送一個FIN,用來關閉客戶端到服務器的數據傳送,客戶端進入FIN_WAIT_1狀態。
二、服務器收到FIN後,發送一個ACK給客戶端,確認序號爲收到序號+1(與SYN相同,一個FIN佔用一個序號),服務器進入CLOSE_WAIT狀態,而客戶端進入FIN_WAIT2狀態。
三、服務器發送一個FIN,用來關閉服務器到客戶端的數據傳送,服務器進入LAST_ACK狀態。
四、客戶端收到FIN後,客戶端進入TIME_WAIT狀態,接着發送一個ACK給服務器,確認序號爲收到序號+1,服務器進入CLOSED狀態,完成釋放。
關閉鏈接有主動關閉和被動關閉一說,這裏爲了簡化理解,咱們以客戶端做爲主動關閉方,服務器爲被動關閉方。
四次揮手須要關注的問題:
問題1. 爲何是四次揮手
發送FIN的一方就是主動關閉(客戶端),而另外一方則爲被動關閉(服務器)。
當一方發送了FIN,則表示在這一方再也不會有數據的發送。
其中當被動關閉方受到對方的FIN時,此時每每可能還有數據須要發送過去,所以沒法當即發送FIN(也就是沒法將FIN與ACK合併發送),
而是在等待本身的數據發送完畢後再單獨發送FIN,所以整個過程須要四次交互。
問題2. 什麼是半關閉
客戶端在收到第一個FIN的ACK響應後,會進入FIN_WAIT2 狀態時,此時服務器處於 CLOSE_WAIT狀態,這種狀態就稱之爲半關閉。
從半關閉到全關閉,須要等待第二次FIN的確認纔算結束。此時,客戶端要等到服務器的FIN才能進入TIME_WAIT,
若是對方遲遲不發送FIN呢,則會等待一段時間後超時,這個能夠經過內核參數tcp_fin_timeout控制,默認是60s。
問題3. 爲何服務器會有大量 closewait
半關閉的狀態下的服務器鏈接會處於 closewait 狀態,直到服務器發送了FIN。
那麼在應用層則是調用socket.close()會執行FIN的發送,若是服務器出現大量CLOSE_WAIT狀態的鏈接,那麼有可能的緣由:
問題4. timewait 會帶來什麼問題
當客戶端收到了對方的FIN時,會進入TIME_WAIT狀態,此時會保持一段時間再進入CLOSE狀態。
這麼作的緣由主要仍是爲了可靠的關閉鏈接。在將TCP 進行可靠性設計之時就考慮了許多網絡的不穩定性的因素,好比:
發送給對方的ACK 可能會沒法及時收到,此時對方可能重傳FIN過來,若是提早進入CLOSE則會返回RST而不是ACK,就會影響關閉流程。
所以 TIME_WAIT 狀態默認會持續一段時間,直到確認不會再有重傳的數據包以後再安全的關閉。
黑板:這裏timewait的持續時間默認是 2*MSL(總共1分鐘),這個MSL叫Max Segment Lifetime,也就是關於一個數據包在網絡中傳輸的最大生命週期的預設。
MSL默認是30s,固然這個值在如今已經能夠大幅度縮減。可見在當時在設計之初,網絡情況有多麼的糟糕。
那麼timewait會帶來什麼問題?
若是頻繁的主動關閉鏈接,可能會產生大量 timewait,因爲timewait 的鏈接佔用了一個句柄及少許內存(4K),那麼就有可能會影響其餘鏈接的創建,好比:
出現 too many open files 異常..
該如何解決:
黑板:HTTP 協議裏頭髮現了timewait的問題,因而在 HTTP 1.1 中定義了 KeepAlive 用來支持鏈接的重用。
問題5. RST 是什麼,爲何會出現
RST 是一個特殊的標記,用來表示當前應該當即終止鏈接。如下這些狀況都會產生RST:
RST 機制有時候也會被利用,作一些端口的掃描,以下:
-> 端口開啓,可接受SYN
-> 端口關閉,響應RST
原文只是想總結下 TCP 參數調優的幾個細節,沒想到TCP 牽扯出來的東西實在太多,光是一個簡單的握手、揮手流程就存在這麼多的細節和坑。 能夠說爲了保證數據傳輸的可靠性,早期的設計者確實考慮了太多的東西。固然,這也爲上層的應用實現鋪平了道路。 鑑於篇幅緣由,只作了TCP 建鏈、拆鏈方面的介紹。關於數據的傳輸的一些細節,將在下篇文章梳理及分享。