目錄
一.再談端口概念
?二.UDP協議
1.UDP協議格式
2.UDP的特點
3.面向數據報
4.UDP的緩沖區?
5.UDP使用注意事項?
?6.UDP協議在內核中的表現形式
?7.基于UDP的應用層協議
?三.TCP協議
1.TCP協議格式
2.TCP確認應答機制
3.超時重傳機制
4.TCP報文六位標志位
5.滑動窗口
6.流量控制
7.擁塞控制
8.延遲應答
9.捎帶應答?
10.面向字節流?
11.粘包問題?
12.連接管理機制?
13.listen 的第二個參數
14.TCP異常情況?
15.TCP小結
16.基于TCP應用層協議?
四.TCP/UDP對比
一.再談端口概念
端口號(Port)標識了一個主機上進行通信的不同的應用程序;
在TCP/IP協議中, 用 "源IP", "源端口號", "目的IP", "目的端口號", "協議號" 這樣一個五元組來標識一個通信(可以通過netstat -n查看);
netstat 常用選項:
- n 拒絕顯示別名,能顯示數字的全部轉化成數字
- l 僅列出有在 Listen (監聽) 的服務狀態
- p 顯示建立相關鏈接的程序名
- t (tcp)僅顯示tcp相關選項
- u (udp)僅顯示udp相關選項
- a (all)顯示所有選項,默認不顯示LISTEN相關
pidof:在查看服務器的進程id時非常方便。
- 語法:pidof [進程名]
- 功能:通過進程名, 查看進程id
?二.UDP協議
1.UDP協議格式
16位UDP長度: 表示整個數據報(UDP首部+UDP數據)的最大長度;
16位檢驗和:如果校驗和出錯, 就會直接丟棄;?
UDP協議如何做到報頭和有效載荷分離:
- udp協議有著定長的報頭。
- 定長讀取報頭就能夠,得到16位報文的長度即,報頭和有效載荷的長度。
- 16位報文長度減去定長報頭8字節,得到有效載荷長度。
UDP協議如何做到向上交付:
- 只要讀取到了報頭,也就知道了目的端口,目的端口就是我們此次向上交付的應用層進程。
2.UDP的特點
UDP傳輸的過程類似于寄信.
- 無連接: 知道對端的IP和端口號就直接進行傳輸, 不需要建立連接;
- 不可靠: 沒有確認機制, 沒有重傳機制; 如果因為網絡故障該段無法發到對方, UDP協議層也不會給應用層返回任何錯誤信息;
- 面向數據報: 不能夠靈活的控制讀寫數據的次數和數量;
3.面向數據報
應用層交給UDP多長的報文, UDP原樣發送, 既不會拆分, 也不會合并;
用UDP傳輸100個字節的數據:
- 如果發送端調用一次sendto, 發送100個字節, 那么接收端也必須調用對應的一次recvfrom, 接收100個字節; 而不能循環調用10次recvfrom, 每次接收10個字節;
4.UDP的緩沖區?
- UDP沒有真正意義上的 發送緩沖區. 調用sendto會直接交給內核, 由內核將數據傳給網絡層協議進行后續的傳輸動作;
- UDP具有接收緩沖區. 但是這個接收緩沖區不能保證收到的UDP報的順序和發送UDP報的順序一致; 如果緩沖區滿了, 再到達的UDP數據就會被丟棄,這也是UDP表現出的相對于TCP的不可靠性。
- UDP的socket既能讀, 也能寫, 這個概念叫做 全雙工。
5.UDP使用注意事項?
- 我們注意到, UDP協議首部中有一個16位的最大長度. 也就是說一個UDP能傳輸的數據最大長度是64K(包含UDP首部).
- 然而64K在當今的互聯網環境下, 是一個非常小的數字.
- 如果我們需要傳輸的數據超過64K, 就需要在應用層手動的分包, 多次發送, 并在接收端手動拼裝;
?6.UDP協議在內核中的表現形式
Linux內核是由C語言寫的,傳輸層和網絡層又隸屬于操作系統,那么傳輸層的協議也是用C語言寫的。既然是使用C語言寫的那么想UDP這種格式的結構,我們很容易就可以使用,結構體,或者位段來實現。有一個疑惑,無法確定有效載荷的大小,那么又該問怎么定義出結構呢?C99語法支持柔性數組。
struct Udp
{uint16_t src_port;uint16_t dst_port;uint16_t udp_len;uint16_t check;char date[];//柔性數組。
};
?7.基于UDP的應用層協議
- NFS: 網絡文件系統
- TFTP: 簡單文件傳輸協議
- DHCP: 動態主機配置協議
- BOOTP: 啟動協議(用于無盤設備啟動)
- DNS: 域名解析協議
- 也包括你自己寫UDP程序時自定義的應用層協議;?
?三.TCP協議
TCP全稱為 "傳輸控制協議(Transmission Control Protocol"). 人如其名, 要對數據的傳輸進行一個詳細的控制;
當上層應用層服務將需要發送的數據,使用send和write發送到"網絡"的時候,實際上對于應用層,他認為只要自己調用了send和write就已經將數發送出去了,實際上并不是這樣,數據還需要經過傳輸層的協議才能發送。對于應用層,他對傳輸層到底是怎么發送的,是什么時候發送的數據,表示不知道,不清楚,不關心。而怎么發送,什么時候發送,如何確保傳輸的數據的可靠性,這就是傳輸層的TCP該做的事情。
1.TCP協議格式
1. 源/目的端口號: 表示數據是從哪個進程來, 到哪個進程去;?
2. 32位序號/32位確認號: 后面詳細講;
3. 4位TCP報頭長度: 表示該TCP頭部有多少個32位bit(有多少個4字節); 所以TCP頭部最大長度是15 * 4 = 60
4. 6位標志位:
- URG: 緊急指針是否有效
- ACK: 確認號是否有效
- PSH: 提示接收端應用程序立刻從TCP緩沖區把數據讀走
- RST: 對方要求重新建立連接; 我們把攜帶RST標識的稱為復位報文段
- SYN: 請求建立連接; 我們把攜帶SYN標識的稱為同步報文段
- FIN: 通知對方, 本端要關閉了, 我們稱攜帶FIN標識的為結束報文段
5. 16位窗口大小: 后面再說。
6. 16位校驗和: 發送端填充, CRC校驗. 接收端校驗不通過, 則認為數據有問題. 此處的檢驗和不光包含TCP首部, 也包含TCP數據部分.
7. 16位緊急指針: 標識哪部分數據是緊急數據;
8. 40字節頭部選項: 暫時忽略;
TCP協議如何做到報頭和有效載荷分離:
首先讀取到四位首部長度冷len,報頭長度就是len*4字節,報文去除報頭數據,剩下的就是有效載荷。
說明:在沒有選項長度的情況下,四位報頭的長度就是20字節,自然四位首部長度填寫的二進制字段就是1001。
TCP如何做到向上交付:
當我們讀取到了報頭,自然也就知道了目的端口,目的端口就是我們此次向上交付的應用層進程。
2.TCP確認應答機制
可靠性:
我們一提到TCP首先就能想到可靠性,那么到底哪些是可靠性哪些不是可靠性?
不可靠性:丟失數據(丟包),傳輸太快了,傳輸太慢了,亂序,重復等,都是不可靠性。
與之相對的自然也就是可靠性。
確認應答:解決丟包的問題
TCP為保證可靠性,我們向對端主機發送數報文的時候,我們怎么得知對端主機有沒有收到我們的報文呢?非常簡單,對端只要給你一句回應,我們也就知道了,對方收到了我們發送的報文數據。
但是這樣的每次都是一個發一個應答這樣的串行的過程,效率難免會有些低,所以在實際中,發報文和應答并不是穿行的,而是并發的。
?序號和確認序號的作用:
那么如果有一條數據報文,沒有得到回應,而且我們接收端收到的報文順序也不一定就是發送端發送的順序,那么怎么確定是哪一條數據報文丟失呢?
在我們的數據,沒有發送到對端主機的時候,我們的數據都會存儲在TCP的緩沖區里面,那么TCP的緩沖區是什么樣子的呢?
實際上TCP的發送緩沖區就是一個char數組,又因為數組天然帶有下標,所以TCP對緩沖區的每一個字節都是有編號的。而這個編號其實就是TCP協議報頭里的序號。當我們每次發送的數據,都是有序號的,那么接收端只要按序號進行排序和去重,首先可以做到接收端保證數據的有序性,也能夠輕松的知道了哪些報文沒有收到。
對于接收端,我們收到了一個報文得到了序號,我們就可以給發送端應答一個帶有確認信號的應答報文,該應答報文的確認序號就是上一個報文的序號下一個位置,代表下一次你可以從該位置繼續發送。
確認序號的機制的意思是告知發送端,下一次的發送位置,換一句話講,也就是告訴發送方,從當當前確認號X之前的報文我都是收到的。
3.超時重傳機制
如果我們發送一個數據報文,但是并沒有收到應答,此時我們應該立馬給對方補發一個報文嗎?不應該,首先我們要知道如果我們沒有收到應答,一般會有兩種情況:
- 對方收到了數據報文,但是發送給發送方的應答丟失了。
- 對方確確實實沒有收到報文,所以自然也就不會發出應答。
如果是情況一:雖然我們沒有收到當前報文數據的應答報文,但是過了一會我們收到了,下一個報文的應答報文,由于應答報文的機制,我們收到下一個報文的應答,也就代表這當前所有報文對方都是收到的。
如果是情況一,主機B會收到很多重復數據. 那么TCP協議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉.這時候我們可以利用前面提到的序列號, 就可以很容易做到去重的效果.?
如果是情況二:我們我們并不知到發送的報文確實丟失了,我們還在期望第一種情況的發生,但是過了一會仍沒有收到確認應答,那么此時我們真的需要給出一個補發報文。但是總歸到底,我們都是不能直接補發報文的。面對情況二這種等待一段時間之后,仍沒有應答報文我們再補發的場景,就是超時重傳機制。
那么, 如果超時的時間如何確定??
- 最理想的情況下, 找到一個最小的時間, 保證 "確認應答一定能在這個時間內返回".
- 但是這個時間的長短, 隨著網絡環境的不同, 是有差異的.
- 如果超時時間設的太長, 會影響整體的重傳效率;
- 如果超時時間設的太短, 有可能會頻繁發送重復的包;
TCP為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超時時間。
- Linux中(BSD Unix和Windows也是如此), 超時以500ms為一個單位進行控制, 每次判定超時重發的超時時間都是500ms的整數倍.
- 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳.
- 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增.
- 累計到一定的重傳次數, TCP認為網絡或者對端主機出現異常, 強制關閉連接.
?4.TCP報文六位標志位
1.ACK
說明:確認好是否有效,也就是說明當前報文確認好如果有效,那么當前報文一定是一個應答報文。
2.RST
說明:對方要求重新建立連接,例如當客戶端因為網絡抖動,導致連接斷開,但是服務器并不知道,這就導致了雙方再連接建立上有不一致的地方。客戶端就可以發送一個帶有SRT的報文,請求重新建立連接。
3.URG
說明:緊急指針是否有效,緊急指針是一個16位整形數據,如果緊急指針生效,這次的報文是可以不需要在接收緩沖區中等待,可以直接插隊被上層應用拿到,而且此次的有效載荷中還攜帶者1字節的緊急數據,16位的緊急指針,就標識了緊急數據在有效載荷中的起始位置。
4.SYN
說明:TCP是面向連接的,那么就會有報文時請求發起TCP連接的,發起連接的報文就會攜帶SYN標志位。
5.FIN
說明:當雙方通信結束,斷開連接時,需要有報文提出斷開連接的請求。就會攜帶FIN標志位。
6.PSH
如果通信雙方,有一方覺得對方的接收緩沖區,剩余空間不是很充足,可以催促對方的應用層,抓緊把數據從緩沖區讀走。這種報文就會攜帶PSH標志位。
5.滑動窗口
剛才我們討論了確認應答策略, 對每一個發送的數據段, 都要給一個ACK確認應答. 收到ACK后再發送下一個數據段,這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返的時間較長的時候.
既然這樣一發一收的方式性能較低, 那么我們一次發送多條數據, 就可以大大的提高性能(其實是將多個段的等待時間重疊在一起了).
滑動窗口在哪里?是什么?
今天我們知道了,TCP的傳輸控制的主要是針對要發送的數據報文,因為數據報文在TCP發送緩沖區中,那么滑動窗口就在TCP發送緩沖區中。滑動窗口就是兩個指針。
滑動窗口可以變大嗎?可以變小嗎?可以為0嗎?
可以變大,也可以變小,根據對方的接受能力,僅僅改變兩個指針的位置,即可。
可以為0,即發送端不發送數據。
滑動窗口可以一直滑動嗎?怎么滑動?
滑動窗口天然的將整個緩沖區劃分為三個部分,滑動窗口前是,已經接受到應答的數據報文,滑動窗口后,是還沒有發送的報文。滑動窗口中是不需要等待任何ACK, 直接發送的數據報文,或者是已經發送還沒有收到應答的數據報文。
當窗口中的已經發送的報文的應答被接受,那就可以直接直接將Win_begin向右邊移動。
而且滑動窗口左端的報文已經被對對方接受,所以對于發送緩沖區來說就是空閑的,可以發送緩沖區設計成環狀的,滑動窗口也不會出現越界的情況。
華東窗口的大小怎么更新?依據是什么?
滑動窗口的大小決定了此次發送的數據報文量的大小,TCP不僅僅保證數據不會漏掉,即使漏掉了,也能讓我及時的知道。還要保證我們每次發的數據量對方有能力接受,如果對方的接受能力弱,我們就少發一點,對方接受能力強,我們就多發一點。所以我們需要知道對方的接收緩沖區還有多大,這就要用到TCP報文格式中的16位窗口大小了。
16為窗口大小:用于通告給對方自己的接受能力。
所以我們的滑動窗口的大小就應該是對方的窗口大小,即:
- Win_begin = 確認序列化。
- Win_end = Win_begin + Win_size.
如果滑動窗口中數據報文有丟失怎么半?
如果有數據丟失那丟失的數據必然在窗口的第一位。因為收到應答的報文將會被移除窗口,所以當出現報文丟失,直接重發第一個報文就可以了。
說明:
窗口大小指的是無需等待確認應答而可以繼續發送數據的最大值. 上圖的窗口大小就是4000個字節(四個段).發送前四個段的時候, 不需要等待任何ACK, 直接發送;
收到第一個ACK后, 滑動窗口向后移動, 繼續發送后面的段的數據; 依次類推;
操作系統內核為了維護這個滑動窗口, 需要開辟 發送緩沖區 來記錄當前還有哪些數據沒有應答; 只有確認應答過的數據, 才能從緩沖區刪掉;窗口越大, 則網絡的吞吐率就越高;
快重傳:
- 當某一段報文段丟失之后, 發送端會一直收到 1001 這樣的ACK, 就像是在提醒發送端 "我想要的是 1001"一樣;
- 如果發送端主機連續三次收到了同樣一個 "1001" 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送;
- 這個時候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因為2001 - 7000)接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩沖區中;
這種機制被稱為 "高速重發控制"(也叫 "快重傳").?
6.流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送,就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應.
因此TCP支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制(Flow Control);
- 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 "窗口大小" 字段, 通過ACK端通知發送端;
- 窗口大小字段越大, 說明網絡的吞吐量越高;
- 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
- 發送端接受到這個窗口之后, 就會減慢自己的發送速度,即減小滑動窗口。
- 如果接收端緩沖區滿了, 就會將窗口置為0,發送端的滑動窗口大小為0,這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端。
接收端如何把窗口大小告訴發送端呢? 回憶我們的TCP首部中, 有一個16位窗口字段, 就是存放了窗口大小信息;那么問題來了, 16位數字最大表示65535, 那么TCP窗口最大就是65535字節么?
實際上, TCP首部40字節選項中還包含了一個窗口擴大因子M, 實際窗口大小是 窗口字段的值左移 M 位;?
7.擁塞控制
雖然TCP有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題.
因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據,是很有可能引起雪上加霜的.
這里是不是太瞧得起我了,我能夠發的那幾千個數據報文,對于整個網絡來說不是九牛一毛嗎,怎么會很大可能加重網絡的擁塞呢?
我們要有一個共識,互聯網中的主機可不止你一臺,每一時刻都會有大量的主機向網絡中發送數報文,而且大家都是用的是TCP協議。所以當網絡出現擁塞時,只要TCP能夠制止主機減緩發送,那么也就使得整個網絡上的主機都減少了發送給數據。網絡的壓力就會慢慢恢復。
TCP引入 慢啟動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據;
- 此處引入一個概念稱為擁塞窗口
- 發送開始的時候, 定義擁塞窗口大小為1;
- 每次收到一個ACK應答, 擁塞窗口加1;
- 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際的滑動窗口,因為此時影響我們發送的因素不僅僅是,對方的接受能力了,還加上當前網絡的擁塞程度。兩者取較小值,作為滑動窗口的大小。保證網絡擁塞不加重,對方能接受。
像上面這樣的擁塞窗口增長速度, 是指數級別的. "慢啟動" 只是指初使時慢, 但是增長速度非常快.?
- 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍.
- 此處引入一個叫做慢啟動的閾值
- 當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
- 當TCP開始啟動的時候, 慢啟動閾值等于窗口最大值;
- 在每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回1?
少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;
當TCP通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;
擁塞控制, 歸根結底是TCP協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案.
TCP擁塞控制這樣的過程, 就好像 熱戀的感覺?
8.延遲應答
如果接收數據的主機立刻返回ACK應答, 這時候返回的窗口可能比較小.
- 假設接收端緩沖區為1M. 一次收到了500K的數據; 如果立刻應答, 返回的窗口就是500K;
- 但實際上可能處理端處理的速度很快, 10ms之內就把500K數據從緩沖區消費掉了;
- 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
- 如果接收端稍微等一會再應答, 比如等待200ms再應答, 那么這個時候返回的窗口大小就是1M;
一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率:
- 那么所有的包都可以延遲應答么? 肯定也不是;
- 數量限制: 每隔N個包就應答一次;
- 時間限制: 超過最大延遲時間就應答一次;
具體的數量和超時時間, 依操作系統不同也有差異; 一般N取2, 超時時間取200ms;
9.捎帶應答?
在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 "一發一收" 的. 意味著客戶端給服務器說了 "How are you", 服務器也會給客戶端回一個 "Fine, thank you";
那么這個時候ACK就可以搭順風車, 和服務器回應的 "Fine, thank you" 一起回給客戶端。
簡單來說:就是此次的應答不僅僅是一個應答,還攜帶了一些其他信息,這就叫捎帶應答。
10.面向字節流?
創建一個TCP的socket, 同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區;
- 調用write時, 數據會先寫入發送緩沖區中;
- 如果發送的字節數太長, 會被拆分成多個TCP的數據包發出;
- 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去;
- 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區;
- 然后應用程序可以調用read從接收緩沖區拿數據;
- 另一方面, TCP的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做 全雙工;
由于緩沖區的存在, TCP程序的讀和寫不需要一一匹配, 例如:
- 寫100個字節數據時, 可以調用一次write寫100個字節, 也可以調用100次write, 每次寫一個字節;
- 讀100個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100個字節, 也可以一次read一個字節, 重復100次;
- 就如同水流一般,可以一次多取,也可以多次少取,所以叫面向字節流。
11.粘包問題?
- 首先要明確, 粘包問題中的 "包" , 是指的應用層的數據包.
- 在TCP的協議頭中, 沒有如同UDP一樣的 "報文長度" 這樣的字段, 但是有一個序號這樣的字段.
- 站在傳輸層的角度, TCP是一個一個報文過來的. 按照序號排好序放在緩沖區中.
- 站在應用層的角度, 看到的只是一串連續的字節數據.
- 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包.
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界.?
- 對于定長的包, 保證每次都按固定大小讀取即可; 例如某一個Request結構,是固定大小的, 那么就從緩沖區從頭開始按sizeof(Request)依次讀取即可;
- 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置,就像我們自己寫的網絡版本計算器。
- 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可);
對于UDP協議來說, 是否也存在 "粘包問題" 呢??
- 不存在,UDP面向數據報的,對于數據報的接受是原子的,要么接收到了一個完整的,要么沒有接收到,對于UDP, 如果還沒有上層交付數據, UDP的報文長度仍然在. 同時, UDP是一個一個把數據交付給應用層. 就有很明確的數據邊界.
- 站在應用層的站在應用層的角度, 使用UDP的時候, 要么收到完整的UDP報文, 要么不收. 不會出現"半個"的情況.
12.連接管理機制?
在正常情況下, TCP要經過三次握手建立連接, 四次揮手斷開連接。
服務端狀態轉化:?
- [CLOSED -> LISTEN] 服務器端調用listen后進入LISTEN狀態, 等待客戶端連接。
- [LISTEN -> SYN_RCVD] 一旦監聽到連接請求(同步報文段), 就將該連接放入內核等待隊列中, 并向客戶端發送SYN確認報文。
- [SYN_RCVD -> ESTABLISHED] 服務端一旦收到客戶端的確認報文, 就進入ESTABLISHED狀態, 可以進行讀寫數據了。
- [ESTABLISHED -> CLOSE_WAIT] 當客戶端主動關閉連接(調用close), 服務器會收到結束報文段, 服務器返回確認報文段并進入CLOSE_WAIT。
- [CLOSE_WAIT -> LAST_ACK] 進入CLOSE_WAIT后說明服務器準備關閉連接(需要處理完之前的數據); 當服務器真正調用close關閉連接時, 會向客戶端發送FIN, 此時服務器進入LAST_ACK狀態, 等待最后一個ACK到來(這個ACK是客戶端確認收到了FIN)。
- [LAST_ACK -> CLOSED] 服務器收到了對FIN的ACK, 徹底關閉連接。
客戶端狀態轉化:
- [CLOSED -> SYN_SENT] 客戶端調用connect, 發送同步報文段。
- [SYN_SENT -> ESTABLISHED] connect調用成功, 則進入ESTABLISHED狀態, 開始讀寫數據。
- [ESTABLISHED -> FIN_WAIT_1] 客戶端主動調用close時, 向服務器發送結束報文段, 同時進入FIN_WAIT_1。
- [FIN_WAIT_1 -> FIN_WAIT_2] 客戶端收到服務器對結束報文段的確認, 則進入FIN_WAIT_2, 開始等待服務器的結束報文段。
- [FIN_WAIT_2 -> TIME_WAIT] 客戶端收到服務器發來的結束報文段, 進入TIME_WAIT, 并發出LAST_ACK。
- [TIME_WAIT -> CLOSED] 客戶端要等待一個2MSL(Max Segment Life, 報文最大生存時間)的時間, 才會進入CLOSED狀態。
什么是連接?
在OS內部必然會同時存在大量的連接,操作系統肯定需要對這些連接做管理,那么就會有這些鏈接數據結構,和管理結構。struct link{……};需要占有內存和CPU資源。
為什么在建立連接時,會是三次握手,2次,4次,5次,行不行?
我們注意就不難發現,如果是兩次握手建立連接,那么僅僅需要客戶端發起一次SYN,服務端就會建立連接,這樣就會導致,服務端使用很低的成本就建立服務端的來連接,連接的的創建也是需要CPU和內存資源的,如果一臺機器,發送大量的SYN給服務器,那么很快就會使服務器承載壓力過大,這就是SYN洪水攻擊。偶數次的連接次數都會有些這樣的問題,奇數次,最小成本的建立連接就是三次握手。
第二次握手就是一次捎帶應答。
?斷開連接時的幾種狀態:
1.TIME_WAIT
我們從圖中可以看出,主動斷開連接的一方,會進入一個TIME_WAIT狀態。
TCP協議規定,主動關閉連接的一方要處于TIME_ WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態.
MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同, 在Centos7上默認配置的值是60s;
可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值。
這個內核文件可以修改,必須是root用戶,但是修改以后沒有效果。
測試:
啟動服務,Ctrl C斷開,再次啟動:
我們查看當前8081端口的連接,發現服務仍在使用8081端口。,這也就是使得我們綁定失敗的原因。
為什么是TIME_WAIT的時間是2MSL?
- MSL是TCP報文的最大生存時間, 因此TIME_WAIT持續存在2MSL的話
- 就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失(否則服務器立刻重啟, 可能會收到來自上一個進程的遲到的數據, 但是這種數據很可能是錯誤的);
- 同時也是在理論上保證最后一個報文可靠到達(假設最后一個ACK丟失, 那么服務器會再重發一個FIN. 這時雖然客戶端的進程不在了, 但是TCP連接還在, 仍然可以重發LAST_ACK);
解決TIME_WAIT狀態引起的bind失敗的方法:
在server的TCP連接沒有完全斷開之前不允許重新監聽, 某些情況下可能是不合理的:
- 服務器需要處理非常大量的客戶端的連接(每個連接的生存時間可能很短, 但是每秒都有很大數量的客戶端來請求).
- 這個時候如果由服務器端主動關閉連接(比如某些客戶端不活躍, 就需要被服務器端主動清理掉), 就會產生大量TIME_WAIT連接.
- 由于我們的請求量很大, 就可能導致TIME_WAIT的連接數很多, 每個連接都會占用一個通信五元組( 源ip,源端口, 目的ip, 目的端口, 協議). 其中服務器的ip和端口和協議是固定的. 如果新來的客戶端連接的ip和端口號和TIME_WAIT占用的鏈接重復了, 就會出現問題.
使用setsockopt()設置socket描述符的 選項SO_REUSEADDR為1, 表示允許創建端口號相同但IP地址不同的多個socket描述符。
void Bind(){// 設置無需等待TIME_WAIT狀態int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));struct sockaddr_in host;host.sin_family = AF_INET;host.sin_port = htons(_port);host.sin_addr.s_addr = INADDR_ANY; // #define INADDR_ANY 0x00000000socklen_t hostlen = sizeof(host);int n = bind(_listensock, (struct sockaddr *)&host, hostlen);if (n == -1){Logmessage(Fatal, "bind err ,error code %d,%s", errno, strerror(errno));exit(BING_ERR);}}
2.CLOSE_WAIT狀態
當對方主機首先close自己的 fd ,發起FIN請求之后,另一端接收到FIN之后,就會也會進入CLOSE_WAIT狀態,關閉通信文件描述符之后發送ACK,進入LAST_ACK狀態。但是連接的文件描述符的關閉是程序員自己控制,如果我們不關閉文件描述符呢:
斷開客戶端連接:
此時服務器進入了 CLOSE_WAIT 狀態, 結合我們四次揮手的流程圖, 可以認為四次揮手沒有正確完成.
?小結: 對于服務器上出現大量的 CLOSE_WAIT 狀態, 原因就是服務器沒有正確的關閉 socket, 導致四次揮手沒有正確完成. 這是一個 BUG. 只需要加上對應的 close 即可解決問題.
13.listen 的第二個參數
const static int backlog = 1;
int n = listen(_listensock, backlog);
使服務器只監聽,但是不accept,接受連接,但是不把鏈接拿到上層:
使用telnet連接:
此時啟動 2 個客戶端同時連接服務器, 用 netstat 查看服務器狀態, 一切正常.
但是啟動第三個客戶端時, 發現服務器對于第三個連接的狀態存在問題了。
客戶端狀態正常, 但是服務器端出現了 SYN_RECV 狀態, 而不是 ESTABLISHED 狀態
這是因為, Linux內核協議棧為一個tcp連接管理使用兩個隊列:
- 1. 半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態的請求)
- 2. 全連接隊列(accpetd隊列)(用來保存處于established狀態,但是應用層沒有調用accept取走的請求)
而全連接隊列的長度會受到 listen 第二個參數的影響.
全連接隊列滿了的時候, 就無法繼續讓當前連接的狀態進入 established 狀態了.
這個隊列的長度通過上述實驗可知, 是 listen 的第二個參數 + 1.
注意:listen的第二個參數+1,不是服務器的最大處理鏈接數,而是暫存沒有向上拿給應用層的最大鏈接數。
14.TCP異常情況?
- 進程終止: 進程終止會釋放文件描述符, 仍然可以發送FIN. 和正常關閉沒有什么區別.
- 機器重啟: 和進程終止的情況相同.
- 機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行reset. 即使沒有寫入操作, TCP自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放.
- 另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如HTTP長連接中,也會定期檢測對方的狀態. 例如QQ, 在QQ斷線之后, 也會定期嘗試重新連接.
15.TCP小結
為什么TCP這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能.
可靠性:
- 校驗和
- 序列號(按序到達)
- 確認應答
- 超時重發
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動窗口
- 快速重傳
- 延遲應答
- 捎帶應答
其他:
定時器(超時重傳定時器, 保活定時器, TIME_WAIT定時器等)
16.基于TCP應用層協議?
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
當然, 也包括你自己寫TCP程序時自定義的應用層協議;
四.TCP/UDP對比
我們說了TCP是可靠連接, 那么是不是TCP一定就優于UDP呢? TCP和UDP之間的優點和缺點, 不能簡單, 絕對的進行比較:
- TCP用于可靠傳輸的情況, 應用于文件傳輸, 重要狀態更新等場景;
- UDP用于對高速傳輸和實時性要求較高的通信領域, 例如, 早期的QQ, 視頻傳輸等. 另外UDP可以用于廣播;
歸根結底, TCP和UDP都是程序員的工具, 什么時機用, 具體怎么用, 還是要根據具體的需求場景去判定。
用UDP實現可靠傳輸(經典面試題)
參考TCP的可靠性機制, 在應用層實現類似的邏輯;
例如:
- 引入序列號, 保證數據順序;
- 引入確認應答, 確保對端收到了數據;
- 引入超時重傳, 如果隔一段時間沒有應答, 就重發數據;
- ......