網絡 : 傳輸層【TCP協議】
- 一、TCP協議段格式
- 1.1 32位序號與確認號
- 1.1.1 32位序號
- 1.1.2 確認號
- 1.2 4位首部長度
- 1.3 6位標志位
- 1.4 16位窗口大小
- 二、確認應答(ACK)機制
- 三、超時重傳機制
- 四、連接管理機制
- 4.1 三次握手(連接)
- listen的第二個參數
- 4.2 四次揮手(斷開連接)
- **TIME_WAIT狀態**
- 五、控制
- 5.1 流量控制
- 5.2 滑動窗口
- 5.3 擁塞控制
- 5.4 延遲應答
- 5.5 捎帶應答
- 六、其它問題
- 6.1 面向字節流
- 6.2 粘包問題
- 6.3 TCP異常情況
- 6.4 socket和文件之間的關系
一、TCP協議段格式
- 16位源端口: 發送方主機的應用程序的端口號
- 16位目的端口: 目的主機的應用程序的端口號
- 32位TCP序號: 表示本報文段所發送數據的第一個字節的編號。
- 32位TCP確認序號: 接收方期望收到發送方下一個報文段的第一個字節數據的編號。
- 4位首部長度: 指的是數據段中的“數據”部分起始處距TCP報文段起始處的字節偏移量。
- 6位保留字段: 為TCP將來的發展預留空間,目前必須全部為0.
- 6位標志位: 共有6個標志位,每個標志位占一個bit。
- 16位窗口大小: 表示發送該TCP報文的接收窗口還可以接受多少字節的數據量。該字段用于流量控制。
- 16位檢驗和字段: 用于確認傳輸的數據有無損壞。發送端基于數據內容校驗生成一個數值,接收端根據接受的數據校驗生成一個值。兩個值相同代表數據有效,反之無效,丟棄該數據包。校驗和根據 偽報頭 + TCP頭 + TCP數據 三部分進行計算。
- 16位緊急指針字段: 僅當標志位字段的URG標志位為1時才有意義。指出有效載荷中為緊急數據的字節數。當所有緊急數據處理完后,TCP就會告訴應用程序恢復到正常操作。即使接收方窗口大小為0,也可以發送緊急數據,因為緊急數據無須緩存。
- 選項字段: 長度不定,但長度必須是32bits的整數倍。內容可變,因此必須使用首部長度來區分選項的具體長度。
1.1 32位序號與確認號
1.1.1 32位序號
序號占用 4 字節,即 32 位。也就是說一共有 4 294 967 296 個序號。TCP 協議中的序號,指的是報文段序號。
- 初始序號 ISN
當新連接建立的時候,第一個字節數據的序號稱為 ISN(Initial Sequence Number),即初始序號。**ISN 一開始并不一定就是 1。**在 RFC (規定網絡協議的文檔)中規定,ISN 的分配是根據時間來的。當操作系統初始化的時候,有一個全局變量假設為 g_number 被初始化為 1(或 0),然后每隔 4us 加 1. 當 g_number 達到最大值的時候又繞回到 0.當新連接建立時,就把 g_number 的值賦值給 ISN.
在 BSD 系統中,這段代碼實現時并未遵守協議,它將 g_number 初始化為 1,每 8us 加 1,也就是說,每隔 1 秒增加 125000,約 9.5 小時后 g_number 又繞回到了 0.
初始序號是非常非常重要的概念,它告訴對端,第一個報文段是誰!而三次握手的目的,就是為了確認初始序號。
-
字節序號
TCP 連接中,為傳送的字節流(數據)中的每一個字節按順序編號。也就是說,在一次 TCP 連接建立的開始,到 TCP 連接的斷開,你要傳輸的所有數據的每一個字節都要編號。這個序號稱為字節序號。 -
報文段序號
所以 報文段序號 = 初始序號 + 字節序號(其中字節序號)
比如 初始序號為 1000,而要發送數據是緩沖區中的1~2000數據,那么報文段的序號為1000,它攜帶了2000字節的數據,就表示這2000個字節的數據的字節序號的范圍是[1000, 3000],該報文段攜帶的第一個字節序是1000,最后一個字節序號序號是3000。
再來一個例子:
如果一個 TCP 報文段的序號為 301,它攜帶了 100 字節的數據,就表示這 100 個字節的數據的字節序號范圍是 [301, 400],該報文段攜帶的第一個字節序號是 301,最后一個字節序號是 400.
序列號還有一個作用:
由于網絡,我們發送的數據不一定會按順序到達接收方,因此接收方拿到數據后,會在接受緩沖區中通過序列號對數據進行排序,保持數據不是紊亂的。
而且可能由于丟包的原因,發送方沒有及時收到來自接收方的應答,發送方會再次發送同意的報文給接收方,造成接收方收到多個同樣報文,而接收方通過判斷序列號可以做到去重的效果。
1.1.2 確認號
假設客服端給服務端發送了一個tcp報文,其中報文段的序號為301,并且攜帶了100字節的數據。那么作為服務器我們要給客戶端做出應答,此時服務器的應答報文中的確認號就是401。
表示服務器已經收到了字節序號為 [0, 400] 的數據,現在期望你發送字節序號為 401 以及以后的數據。
確認號表示:
確認應答號是接收方期望從發送方接收到的下一個報文段的序號。它實質上是接收方告訴發送方:“我已經成功接收到了哪個序號之前的所有數據,請從這個序號開始發送后續的數據。”
1.2 4位首部長度
4位首部占4個比特位,所以范圍是[0, 15],4位首部的單位是4字節,所以tcp報文的報頭最大有60字節,其中前20字節是固定的,后40字節是可選項(可有可無)。
因此tcp報文的報頭范圍是[15, 60] ,單位是 1 字節。
1.3 6位標志位
- URG: 緊急指針是否有效
- ACK: 確認號是否有效
- PSH: 提示接收端應用程序立刻從TCP緩沖區把數據讀走
- RST: 對方要求重新建立連接; 我們把攜帶RST標識的稱為復位報文段
- SYN: 請求建立連接; 我們把攜帶SYN標識的稱為同步報文段
- FIN: 通知對方, 本端要關閉了
1.4 16位窗口大小
窗口大小(Window Size)是 TCP(傳輸控制協議)數據包頭部中的一個重要字段,用于指定接收方的緩沖區大小。它告知發送方在不需要等待確認的情況下,可以發送多少數據。
二、確認應答(ACK)機制
- 當客戶端 發送報文 到 服務器時是要經過網絡的,因此報文可能會因為各種原因造成數據沒有傳送到服務端,而服務端為了告訴客戶端它已經接受到了數據,所以服務端會發送應當報文給客戶端。
需要注意的是即使是應答報文也會存在丟包的情況。 假設主機A同時發送 1~1000數據
和1001~2000
給B,但是主機B給A的1~1000
應答報文出現了丟包,主機B應答了 1001~2000數據
,此時主機A也認為B收到了1~1000的數據
,因為應答了1001~2000數據
的報文的確認號是2001。
上面我們說到確認號表示:“我已經成功接收到了哪個序號之前的所有數據,請從這個序號開始發送后續的數據。”
三、超時重傳機制
超時重傳就是發送數據發經過一段時間還沒有收到接收方的應答就會重新發送該報文。
情況一:
- 主機A發送數據給B之后, 可能因為網絡擁堵等原因, 數據無法到達主機B;
- 如果主機A在一個特定時間間隔內沒有收到B發來的確認應答, 就會進行重發;
情況二:
- 主機A未收到B發來的確認應答, 也可能是因為ACK丟失了;
因此主機B會收到很多重復數據. 那么TCP協議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉. , 就可以很容易做到去重的效果. 這時候我們可以利用前面提到的序列號。
那么, 如果超時的時間如何確定?
- 最理想的情況下, 找到一個最小的時間, 保證 “確認應答一定能在這個時間內返回”.
- 但是這個時間的長短, 隨著網絡環境的不同, 是有差異的.
- 如果超時時間設的太長, 會影響整體的重傳效率;
- 如果超時時間設的太短, 有可能會頻繁發送重復的包;
TCP為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超時時間.
- Linux中(BSD Unix和Windows也是如此), 超時以500ms為一個單位進行控制, 每次判定超時重發的超時時間都是500ms的整數倍.
- 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳.
- 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增.
- 累計到一定的重傳次數, TCP認為網絡或者對端主機出現異常, 強制關閉連接.
四、連接管理機制
在正常情況下, TCP要經過三次握手建立連接, 四次揮手斷開連接
4.1 三次握手(連接)
服務器和客戶端轉臺變化:
[CLOSED -> LISTEN] 服務器端調用listen后進入LISTEN狀態, 等待客戶端連接;(同步報文段), 就將該連接放入內核等待隊列中。
[CLOSED -> SYN_SENT] 客戶端調用connect, 發送SYN報文給服務端;
[LISTEN -> SYN_RCVD] 服務器一旦監聽到連接請求(接收到來自客戶端的SYN) , 并向客戶端發送SYN確認報文。
[SYN_SENT -> ESTABLISHED] 客戶端接受到來自服務端的SYN+ACK并且發送ACK給服務器后,connect調用成功, 則進入ESTABLISHED狀態, 開始讀寫數據;
[SYN_RCVD -> ESTABLISHED] 服務端一旦收到客戶端的確認報文, 就進入ESTABLISHED狀態, 可以進行讀寫數據了。
有的時候把這個建立連接的過程叫做“四次握手”,這是因為這種說法把第二次握手 ACK+SYN 拆分成了兩次握手,實際上都是一樣的。
還有一個問題,若第三次握手的報文在傳輸過程中丟失了怎么辦??那這不就造成了客戶端認為建立了連接,而實際上服務端沒有建立。
但這沒有關系,就算ACK報文段丟失了,那server不會認為連接建立成功,此時如果client給server發送消息,則server會感覺很奇怪,既然連接不建立成功,你還給我發消息,那就說明我們雙方產生了認為連接建立不一致的情況,那server就會給client發送復位報文段,請求重新三次握手,重新建立連接,因為我們現在連接的建立是不一致的,client認為連接建立成功,但server不認為成功。
或者還有另一種情況,server發送的SYN報文段超時沒有確認應答,則server就會進行超時重傳,當client收到重復的捎帶應答報文段時,client就會意識到自己給server發送的確認應答報文段可能丟失了,此時client就會重發ACK報文段。
listen的第二個參數
int listen(int sockfd, int backlog);
第二個參數是設置全連接隊列的長度,全連接隊列 = backlog + 1.
Linux內核協議棧為一個tcp連接管理使用兩個隊列:
- 半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態的請求)
- 全連接隊列(accpetd隊列)(用來保存處于established狀態,但是應用層沒有調用accept取走的請求)
而全連接隊列的長度會受到 listen 第二個參數的影響,全連接隊列滿了的時候, 就無法繼續讓當前連接的狀態進入 established 狀態了.
不知道各位發現沒有,我們建立連接的過程其實和accept并沒有關系。accept() 不需要參與三次握手的過程。三次握手是 TCP 協議在內核層面完成的,accept 只是在應用層面從全連接隊列中取出一個已經建立的連接,并返回一個新的套接字。也就是說,連接已經在內核中建立好了,accept() 只是一個查詢和返回的過程,并不影響三次握手的邏輯
因此建立好連接會被放在全連接隊列,若全連接隊列滿了就會造成連接丟失的問題,所以我們要合理控制全連接隊列的長度,和應用層要及時accept。
4.2 四次揮手(斷開連接)
服務器和客戶端轉臺變化:
[ESTABLISHED -> FIN_WAIT_1] 客戶端主動調用close時, 向服務器發送結束報文段FIN, 同時進入
FIN_WAIT_1;
[ESTABLISHED -> CLOSE_WAIT] 當客戶端主動關閉連接(調用close), 服務器會收到結束報文段, 服務器返回確認報文段并進入CLOSE_WAIT;
[FIN_WAIT_1 -> FIN_WAIT_2] 客戶端收到服務器對結束報文段的確認, 則進入FIN_WAIT_2, 開始等待服務器的結束報文段;
[CLOSE_WAIT -> LAST_ACK] 進入CLOSE_WAIT后說明服務器準備關閉連接(需要處理完之前的數據); 當服務器真正調用close關閉連接時, 會向客戶端發送FIN, 此時服務器進入LAST_ACK狀態, 等待最后一個ACK到來(這個ACK是客戶端確認收到了FIN)
[FIN_WAIT_2 -> TIME_WAIT] 客戶端收到服務器發來的結束報文段, 進入TIME_WAIT, 并發出LAST_ACK;
[LAST_ACK -> CLOSED] 服務器收到了客戶端對FIN的ACK, 徹底關閉連接.
[TIME_WAIT -> CLOSED] 客戶端要等待一個2MSL(Max Segment Life, 報文最大生存時間)的時間, 才會進入CLOSED狀態.
為什么是四次揮手不是三次揮手呢???
因為當客戶端在進入到TIME_WAIT狀態后并不是關閉所有的緩沖區,而是關閉發送緩沖區,而接受緩沖區是打開的,因為在關閉前,網絡中可能仍然存在傳遞中的數據,或者服務端要發送數據給客戶端,所以我們不能把服務端的FIN和ACK整合成一個報文發送。
TIME_WAIT狀態
- TCP協議規定,主動關閉連接的一方要處于TIME_ WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態.
- MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同, 在Centos7上默認配置的值是60s;
- 可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值。
為什么是TIME_WAIT的時間是2MSL?
- MSL是TCP報文的最大生存時間, 因此TIME_WAIT持續存在2MSL的話就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失(否則服務器立刻重啟, 可能會收到來自上一個進程的遲到的數據, 但是這種數據很可能是錯誤的);同時也是在理論上保證最后一個報文可靠到達(假設最后一個ACK丟失, 那么服務器會再重發一個FIN. 這時雖然客戶端的進程不在了, 但是TCP連接還在, 仍然可以重發LAST_ACK);
若我們使用Ctrl-C終止了server, 所以server是主動關閉連接的一方, 在TIME_WAIT期間仍然不能再次監聽同樣的server端口,而客戶端斷開就沒有這種現象?
- 因為客戶端的端口是隨機分配的,而服務端的端口通常是固定的,當我們再次連接客戶端會隨機分配一個端口,而重新啟動服務端這時服務器并沒有真正的關閉還在TIME_WAIT的狀態中。
- 怎么解決服務器的這種現象呢?只需要設置sockfd選項為重用本地地址SO_REUSEADDR,即使服務器(主動斷開連接)的sockfd對應的連接結構體處于TIME_WAIT狀態,與sockfd綁定的socket地址(struct sockaddr_in local)也可以立即被重用,這樣就可以實現服務器立即重啟依舊能bind原來的端口號了。
五、控制
5.1 流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送,就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應. 因此TCP支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制(Flow Control);
- 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段, 通過ACK端通知發送端;
- 窗口大小字段越大, 說明網絡的吞吐量越高;
- 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
- 發送端接受到這個窗口之后, 就會減慢自己的發送速度;
- 如果接收端緩沖區滿了, 就會將窗口置為0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端.
接收端如何把窗口大小告訴發送端呢? 回憶我們的TCP首部中, 有一個16位窗口字段, 就是存放了窗口大小信息; 那么問題來了, 16位數字最大表示65535, 那么TCP窗口最大就是65535字節么?
實際上, TCP首部40字節選項中還包含了一個窗口擴大因子M, 實際窗口大小是 窗口字段的值左移 M 位;
5.2 滑動窗口
早期的網絡通信中,通信雙方不會考慮網絡的 擁擠情況直接發送數據。由于大家不知道網絡擁塞狀況,同時發送數據,導致中間節點阻塞掉包, 誰也發不了數據,所以就有了滑動窗口機制來解決此問題。滑動窗口協議是用來改善吞吐量的一種 技術,即容許發送方在接收任何應答之前傳送附加的包。接收方告訴發送方在某一時刻能送多少包 (稱窗口尺寸)。
- 已經發送同時被ACK的數據(這部分數據可以被新數據覆蓋),
- 已經發送但沒有被ACK的數據(這部分數據不能被新數據覆蓋),
- 尚未被發送但可發送的數據(剛剛從應用層緩沖區中拷貝下來的數據),
- 未發送且不可發送(其實開辟空間時,有初始化的數據)
當數據已發送已確認,那么窗口最左邊就會向右移,當有數據成了可以發/已經發但未確認的,那么窗口就會向右移動。
情況一: 數據包已經抵達, ACK被丟了.
這種情況下, 部分ACK丟了并不要緊, 因為可以通過后續的ACK進行確認。
情況二: 數據包就直接丟了
滑動窗口保證了線性的連續的向后更新,不會出現跳躍的情況。表示的確認序號x之前的報文都收到了,不會出現報文遺漏。
- 當某一段報文段丟失之后, 發送端會一直收到 1001 這樣的ACK, 就像是在提醒發送端 “我想要的是 1001” 一樣;
- 如果發送端主機連續三次收到了同樣一個 “1001” 這樣的應答, 就會將對應的數據 1001 - 2000 重新發送;
- 這個時候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因為2001 - 7000)接收端其實之前就已經收到了, 被放到了接收端操作系統內核的接收緩沖區中
這種機制被稱為 “高速重發控制”(也叫 “快重傳”).
緩沖區是有大小的,那么滑動窗口有可能越界嗎??
不會,實際上tcp采用了類似環狀算法。類似循環隊列
已確認已應答的數據我們是可以覆蓋的,當我們的緩沖區已經到盡頭的時候,這時候緩沖區中肯定存在哪些已確認已應答的數據,而我們只要繼續在緩沖區開始覆蓋數據即可。
5.3 擁塞控制
雖然TCP有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題. 因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據,是很有可能引起雪上加霜的.
TCP引入 慢啟動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據;
- 此處引入一個概念程為擁塞窗口
- 發送開始的時候, 定義擁塞窗口大小為1;
- 每次收到一個ACK應答, 擁塞窗口加1;
- 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口;
像上面這樣的擁塞窗口增長速度, 是指數級別的. “慢啟動” 只是指初使時慢, 但是增長速度非常快.
- 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍.
- 此處引入一個叫做慢啟動的閾值,當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
當TCP開始啟動的時候, 慢啟動閾值等于窗口最大值;
在每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回1;
少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;
當TCP通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;
擁塞控制, 歸根結底是TCP協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案.
因此我們再次認識一下滑動窗口的大小:
滑動窗口=min(窗口大小,擁塞窗口,有效數據)。
窗口大小:對方主機的接收能力。
擁塞窗口:考慮的是動態的、網絡的接受能力。
有效數據 : 發送緩沖區的有效數據
所以我們網絡中有三大窗口:
滑動窗口
接收窗口
擁塞窗口:主機判斷網絡健康程度的指標(發送數據超過擁塞窗口,會引發網絡擁塞,否則就不會。而網絡是動態的,擁塞窗口本身肯定不能是靜態的!)
5.4 延遲應答
如果接收數據的主機立刻返回ACK應答, 這時候返回的窗口(接受緩沖區)可能比較小.
- 假設接收端緩沖區為1M. 一次收到了500K的數據; 如果立刻應答, 返回的窗口就是500K;
- 但實際上可能處理端處理的速度很快, 10ms之內就把500K數據從緩沖區消費掉了;
- 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
- 如果接收端稍微等一會再應答, 比如等待200ms再應答, 那么這個時候返回的窗口大小就是1M;
一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率;
那么所有的包都可以延遲應答么? 肯定也不是;
- 數量限制: 每隔N個包就應答一次;
- 時間限制: 超過最大延遲時間就應答一次;
具體的數量和超時時間, 依操作系統不同也有差異; 一般N取2, 超時時間取200ms;
5.5 捎帶應答
在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 “一發一收” 的.就是應答的同時捎帶其它數據不單單是應答。
比如在三次握手中,原本四次握手也可以,但是捎帶應答簡化了這一過程。
六、其它問題
6.1 面向字節流
當創建一個 TCP 的 socket 時,同時在內核中會創建一個發送緩沖區和一個接收緩沖區。
- 調用 write 函數就可以將數據寫入發送緩沖區中,但是如果發送緩沖區已滿,write 函數會阻塞,直到有足夠的空間可以寫入數據。發送緩沖區當中的數據會由 TCP 自行進行發送,但是發送的字節流的大小會根據窗口大小、擁塞控制、流量控制等因素來動態調整。如果發送的字節數太長,TCP 會將其拆分成多個數據包發出。如果發送的字節數太短,TCP 可能會先將其留在發送緩沖區當中,等到合適的時機再進行發送。
- 接收數據的時候,數據也是從網卡驅動程序到達內核的接收緩沖區,可以通過調用 read 函數來讀取接收緩沖區當中的數據。但是如果接收緩沖區為空,read 函數會阻塞,直到有數據到達。接收緩沖區當中的數據也是由 TCP 自行進行接收,但是接收的字節流的大小會根據窗口大小、確認機制等因素來動態調整。而調用 read 函數讀取接收緩沖區中的數據時,也可以按任意字節數進行讀取。
由于緩沖區的存在,TCP 程序的讀和寫不需要一一匹配,例如:
- 寫 100 個字節數據時,可以調用一次 write 寫 100 字節,也可以調用 100 次 write,每次寫一個字節。
- 讀 100 個字節數據時,也完全不需要考慮寫的時候是怎么寫的,既可以一次 read100 個字節,也可以一次 read 一個字節,重復 100 次。
6.2 粘包問題
- 首先要明確, 粘包問題中的 “包” , 是指的應用層的數據包.
- 在TCP的協議頭中, 沒有如同UDP一樣的 “報文長度” 這樣的字段, 但是有一個序號這樣的字段.
- 站在傳輸層的角度, TCP是一個一個報文過來的. 按照序號排好序放在緩沖區中.
- 站在應用層的角度, 看到的只是一串連續的字節數據.
- 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包.
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界.
- 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的Request結構, 是固定大小的, 那么就從緩沖區從頭開始按sizeof(Request)依次讀取即可;
- 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置;
- 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可)
對于UDP協議來說, 是否也存在 “粘包問題” 呢?
- 對于UDP, 如果還沒有上層交付數據, UDP的報文長度仍然在. 同時, UDP是一個一個把數據交付給應用層. 就有很明確的數據邊界.
- 站在應用層的站在應用層的角度, 使用UDP的時候, 要么收到完整的UDP報文, 要么不收. 不會出現"半個"的情況.
6.3 TCP異常情況
- 進程終止: 進程終止會釋放文件描述符, 仍然可以發送FIN. 和正常關閉沒有什么區別.
- 機器重啟: 和進程終止的情況相同.
- 機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行reset. 即使沒有寫入操作, TCP , 會定期詢問對方是否還在 自己也內置了一個保活定時器 . 如果對方不在, 也會把連接釋放.
另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如HTTP長連接中, 也會定期檢測對方的狀態. 例如QQ, 在QQ斷線之后, 也會定期嘗試重新連接.