傳輸層協議
- 再談端口號
- 端口號范圍劃分
- 認識知名端口號
- 兩個問題
- netstat
- pidof
- UDP協議
- UDP協議端格式
- UDP的特點
- 面向數據報
- UDP的緩沖區
- UDP使用注意事項
- 基于UDP的應用層協議
- TCP協議
- TCP協議段格式
- 確認應答(ACK)機制
- 超時重傳機制
- 連接管理機制
- 理解 CLOSE_WAIT 狀態
- 理解TIME_WAIT狀態
- 解決TIME_WAIT狀態引起的bind失敗的方法
- 滑動窗口
- 流量控制
- 擁塞控制
- 延遲應答
- 捎帶應答
- 面向字節流
- 粘包問題
- TCP異常情況
- TCP小結
- TCP/UDP對比
- TCP 相關實驗
- 理解 listen 的第二個參數
負責數據能夠從發送端傳輸接收端
再談端口號
端口號(Port)標識了一個主機上進行通信的不同的應用程序
在TCP/IP協議中, 用 “源IP”, “源端口號”, “目的IP”, “目的端口號”, “協議號” 這樣一個五元組來標識一個通信(可以通過netstat -n查看)
端口號范圍劃分
- 0 - 1023: 知名端口號, HTTP, FTP, SSH等這些廣為使用的應用層協議, 他們的端口號都是固定的
- 1024 - 65535: 操作系統動態分配的端口號. 客戶端程序的端口號, 就是由操作系統從這個范圍分配的
認識知名端口號
有些服務器是非常常用的, 為了使用方便, 人們約定一些常用的服務器, 都是用以下這些固定的端口號:
- ssh服務器, 使用22端口
- http服務器, 使用80端口
- https服務器, 使用443
- telnet服務器, 使用23端口
- ftp服務器, 使用21端口
兩個問題
一個進程是否可以bind多個端口號?
可以
一個端口號是否可以被多個進程bind?
端口號->進程需要確保唯一關系,所以不可以
netstat
用來查看網絡狀態
常用選項:
- n 拒絕顯示別名,能顯示數字的全部轉化成數字
- l 僅列出有在 Listen (監聽) 的服務狀態
- p 顯示建立相關鏈接的程序名
- t(tcp)僅顯示tcp相關選項
- u (udp)僅顯示udp相關選項
- a (all)顯示所有選項,默認不顯示LISTEN相關
pidof
用來查看服務器的進程id
UDP協議
所有協議都包括包括和有效載荷,學習協議的目的就是學習如何解包和分用
UDP協議端格式
- 16位UDP長度, 表示整個數據報(UDP首部+UDP數據)的最大長度;
- 如果校驗和出錯, 就會直接丟棄
所謂的協議報頭其本質就是一種結構化數據對象
UDP的特點
UDP傳輸的過程類似于寄信:
- 無連接: 知道對端的IP和端口號就直接進行傳輸, 不需要建立連接
- 不可靠: 沒有確認機制, 沒有重傳機制; 如果因為網絡故障該段無法發到對方, UDP協議層也不會給應用層返回任何錯誤信息
- 面向數據報: 不能夠靈活的控制讀寫數據的次數和數量
面向數據報
應用層交給UDP多長的報文, UDP原樣發送, 既不會拆分, 也不會合并
例如,發送端發送100次,接收端也必須接收100次
UDP的緩沖區
在網絡通信中,發送端應用層其實并不是直接將數據發送至網絡中,而是將數據層層向下傳遞;通信所使用的IO接口,其實也不是發送接收接口,而是拷貝接口
- UDP沒有真正意義上的 發送緩沖區. 調用sendto會直接交給內核, 由內核將數據傳給網絡層協議進行后續的傳輸動作
- UDP具有接收緩沖區. 但是這個接收緩沖區不能保證收到的UDP報的順序和發送UDP報的順序一致; 如果緩沖區滿了, 再到達的UDP數據就會被丟棄
UDP的socket既能讀, 也能寫, 這個概念叫做 全雙工
UDP使用注意事項
我們注意到, UDP協議首部中有一個16位的最大長度. 也就是說一個UDP能傳輸的數據最大長度是64K(包含UDP首部).
然而64K在當今的互聯網環境下, 是一個非常小的數字.
如果我們需要傳輸的數據超過64K, 就需要在應用層手動的分包, 多次發送, 并在接收端手動拼裝
基于UDP的應用層協議
- NFS: 網絡文件系統
- TFTP: 簡單文件傳輸協議
- DHCP: 動態主機配置協議
- BOOTP: 啟動協議(用于無盤設備啟動)
- DNS: 域名解析協議
TCP協議
TCP全稱為 “傳輸控制協議(Transmission Control Protocol”). 人如其名, 要對數據的傳輸進行一個詳細的控制
TCP協議段格式
- 源/目的端口號: 表示數據是從哪個進程來, 到哪個進程去
- 32位序號/32位確認號: 后面詳細講
- 4位首部長度: 表示該TCP報頭有多少個32位bit(有多少個4字節); 所以TCP報頭最大長度是15 * 4 = 60
- 6位標志位:標識報文的類型,接收方根據不同類型的報文,采取對應的動作
URG: 緊急指針是否有效
ACK: 確認序號是否有效
PSH: 提示接收端應用程序立刻從TCP緩沖區把數據讀走
RST: 對方要求重新建立連接; 我們把攜帶RST標識的稱為復位報文段
SYN: 請求建立連接; 我們把攜帶SYN標識的稱為同步報文段
FIN: 通知對方, 本端要關閉了, 我們稱攜帶FIN標識的為結束報文段 - 16位窗口大小: 后面再說
- 16位校驗和: 發送端填充, CRC校驗. 接收端校驗不通過, 則認為數據有問題. 此處的檢驗和不光包含TCP首部, 也包含TCP數據部分
- 16位緊急指針: 標識哪部分數據是緊急數據
- 40字節頭部選項: 暫時忽略
如何解包?
- TCP協議是定長報頭:20字節;先讀取前20字節
- 因為協議報頭是結構化數據,所以提取報頭中的4位首部長度
- 計算后續報頭的大小:選項=4位首部長度-20
- 報頭處理完畢,剩余的便是有效載荷
如何分用?
協議報頭中包含目的端口號,找到應用層對應的進程,將數據交付給進程
接收端收到一報文后,如何找到曾經綁定的特定端口的進程?網絡協議棧和文件又是什么關系?
系統中有很多場景需要快速定位一個進程,所采取的方式就是將進程和端口放入哈希表中,如此一來通過解包報文便可得到端口,再到表中快速定位進程;找到進程之后,每個進程都有PCB進程管理。通過讀取到的socket(文件描述符),PCB指向文件描述符數據,便可找到對應的文件結構體,網絡協議棧中傳輸層將數據拷貝到文件結構體的讀寫緩存區,依次向上交付,最終到達應用層
確認應答(ACK)機制
為什么網絡傳輸中,會存在不可靠問題?
理解起來很簡單,舉個栗子:如果兩個人面對面交談,沒有什么問題,但是如果兩個人相隔很遠,進行交談,對方說話的內容都很難聽到何談交流呢?網絡也是如此
不可靠的場景如:丟包,亂序,重復,校驗錯誤等
如何保證報文的可靠性呢?
我們認為一個報文只要收到了應答就能保證此報文的可靠性
例如:
收到了應答,只能確保歷史消息對方已經收到;由于是雙方通信,一定存在最新的消息,沒有被應答
TCP收發消息的工作模式
真實的工作模式
雙方在通信的過程中,每一方都需要應答;除了正常的數據報,也包括確認數據報
TCP將每個字節的數據都進行了編號. 即為序列號
雙方通信過程:
因為數據(報文)到達接收方時,順序不一定和發送時一致,如果采取任何措施,便會造成數據亂序的結果
TCP數據段采取序列號來標識數據段,因此應答報文中,對應的報頭中確認報頭;所以確認應答和確認序號確保了接收方已經收到ack序號之前的所有報文(必須是連續的);同時也解釋了為什么報頭中會存在兩組序號(序號,確認序號)
每一個ACK都帶有對應的確認序列號, 意思是告訴發送者, 我已經收到了哪些數據; 下一次你從哪里開始發
至此TCP報頭中還剩余16位窗口大小沒有介紹,在這之前先引入一個新的問題:TCP通信時,數據不能發送太快但也不能太慢,必須有合適的速度;但是作為發送方又該如何得知,發送的數據是適合的呢?只能通過對方的反饋從而得知對方的接收緩沖區的剩余大小,不過雙方都還沒通信,怎么會知道對方緩沖區大小呢?這不就變成了先有雞還是先有蛋的問題了嗎?
因此,在第一次請求時,發送方就會將自己的接收緩沖區大小存放在16位窗口大小中發送給對方,對方亦是如此;并且發送的數據并不會立刻被移除,而是必須在發送緩沖區維持一段時間;由此雙方便交換了接收能力,也能達到流量控制
超時重傳機制
- client發送數據給server之后, 可能因為網絡擁堵等原因, 數據無法到達server;
- 如果client在一個特定時間間隔內沒有收到server發來的確認應答, 就會進行重發;
不過, client未收到server發來的確認應答, 也可能是因為ACK丟失
因此server會收到很多重復數據. 那么TCP協議需要能夠識別出那些包是重復的包, 并且把重復的丟棄掉.
這時候我們可以利用前面提到的序列號, 就可以很容易做到去重的效果
發送方是如何判定丟包的呢?
其實真正有沒有丟包,發送方也不清楚,只是如果超時了,就會被判定丟包
發送端發送出去的數據,并不會立刻被移除。而是會在發送緩沖區中保存一段時間,以防丟包,再次發送
那么, 如果超時的時間如何確定?
- 最理想的情況下, 找到一個最小的時間, 保證 “確認應答一定能在這個時間內返回”
- 但是這個時間的長短, 隨著網絡環境的不同, 是有差異的
- 如果超時時間設的太長, 會影響整體的重傳效率
- 如果超時時間設的太短, 有可能會頻繁發送重復的包
TCP為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超時時間
- Linux中(BSD Unix和Windows也是如此), 超時以500ms為一個單位進行控制, 每次判定超時重發的超時時間都是500ms的整數倍
- 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳
- 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增
- 累計到一定的重傳次數, TCP認為網絡或者對端主機出現異常, 強制關閉連接
連接管理機制
在正常情況下, TCP要經過三次握手建立連接, 四次揮手斷開連接
三次握手
客戶端狀態轉化:
- [CLOSED -> SYN_SENT] 客戶端調用connect, 發送同步報文段
- [SYN_SENT -> ESTABLISHED] connect調用成功, 則進入ESTABLISHED狀態, 開始讀寫數據
服務端狀態轉化:
- [CLOSED -> LISTEN] 服務器端調用listen后進入LISTEN狀態, 等待客戶端連接
- [LISTEN -> SYN_RCVD] 一旦監聽到連接請求(同步報文段), 就將該連接放入內核等待隊列中, 并向客戶端發送SYN確認報文
- [SYN_RCVD -> ESTABLISHED] 服務端一旦收到客戶端的確認報文, 就進入ESTABLISHED狀態, 可以進行讀寫數據
為什么要三次握手呢?既然是三次握手就表示一次,兩次都不可以,為什么呢?
一次握手:sever需要維護好已經建立好的鏈接,如果只握手一次,可能會遭到SYN洪水;兩次握手也是如此
四次握手其實也可以,但是沒有必要
三次握手是用最小成本驗證全雙工通信是否通暢,也可以有效防止單機對服務器進行攻擊
三次握手不一定會成功,最擔心的是最后一個ACK丟失,不過存在對應的解決措施;鏈接也是要被管理的(OS),先描述,再組織
四次揮手
客戶端狀態轉化:
- [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狀態
服務端狀態轉化:
- [ESTABLISHED -> CLOSE_WAIT] 當客戶端主動關閉連接(調用close), 服務器會收到結束報文段, 服務器返回確認報文段并進入CLOSE_WAIT
- [CLOSE_WAIT -> LAST_ACK] 進入CLOSE_WAIT后說明服務器準備關閉連接(需要處理完之前的數據); 當服務器真正調用close關閉連接時, 會向客戶端發送FIN, 此時服務器進入LAST_ACK狀態, 等待最后一個ACK到來(這個ACK是客戶端確認收到了FIN)
- [LAST_ACK -> CLOSED] 服務器收到了對FIN的ACK, 徹底關閉連接
客戶端在FIN_WAIT_2狀態時,所謂的不發數據(用戶),指的是不發送用戶數據,并不代表底層沒有管理報文的交互;斷開是雙方的事情,需要征得雙方同意
主動斷開鏈接的一方,最終狀態是TIME_WAIT狀態
被動斷開鏈接的一方,兩次揮手完成,會進入CLOSED_WAIT狀態
理解 CLOSE_WAIT 狀態
如果在服務器中將關閉文件描述符的代碼注釋掉,觀察運行結果
客服端斷開之前
客戶端斷開之后
此時服務器進入了 CLOSE_WAIT 狀態, 結合我們四次揮手的流程圖, 可以認為四次揮手沒有正確完成,因為關閉文件描述符的操作沒有實現
對于服務器上出現大量的 CLOSE_WAIT 狀態, 原因就是服務器沒有正確的關閉 socket, 導致四次揮手沒有正確完成. 這是一個 BUG. 只需要加上對應的 close 即可解決問題
理解TIME_WAIT狀態
首先啟動server,然后啟動client,然后用Ctrl-C使server終止,這時馬上再運行server, 結果是
這是因為,雖然server的應用程序終止了,但TCP協議層的連接并沒有完全斷開,因此不能再次監 聽同樣的server端口
- TCP協議規定,主動關閉連接的一方要處于TIME_ WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能回到CLOSED狀態
- 我們使用Ctrl-C終止了server, 所以server是主動關閉連接的一方, 在TIME_WAIT期間仍然不能再次監聽同樣的server端口
- MSL在RFC1122中規定為兩分鐘,但是各操作系統的實現不同, 在Centos7上默認配置的值是60s
為什么是TIME_WAIT的時間是2MSL?
- MSL是TCP報文的最大生存時間, 因此TIME_WAIT持續存在2MSL的話
- 就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失(否則服務器立刻重啟, 可能會收到來自上一個進程的遲到的數據, 但是這種數據很可能是錯誤的)
- 同時也是在理論上保證最后一個報文可靠到達(假設最后一個ACK丟失, 那么服務器會再重發一個FIN. 這時雖然客戶端的進程不在了, 但是TCP連接還在, 仍然可以重發LAST_ACK)
四次揮手的動作已經完成,但是主動斷開鏈接的一方要維持一段時間的TIME_WAIT,為什么呢?
需要保證最后一個ACK盡可能地被對方收到;雙方在斷開鏈接時,網絡中可能還有滯留的報文,保證滯留報文進行消散
服務器有時可以立即重啟,但是上面這種情況無法進行立即重啟,如果在服務器運行繁忙時,此時會非常棘手
解決TIME_WAIT狀態引起的bind失敗的方法
在server的TCP連接沒有完全斷開之前不允許重新監聽, 某些情況下可能是不合理的
- 服務器需要處理非常大量的客戶端的連接(每個連接的生存時間可能很短, 但是每秒都有很大數量的客戶端來請求)
- 這個時候如果由服務器端主動關閉連接(比如某些客戶端不活躍, 就需要被服務器端主動清理掉), 就會產生大量TIME_WAIT連接
- 由于我們的請求量很大, 就可能導致TIME_WAIT的連接數很多, 每個連接都會占用一個通信五元組(源ip, 源端口, 目的ip, 目的端口, 協議). 其中服務器的ip和端口和協議是固定的. 如果新來的客戶端連接的ip和端口號和TIME_WAIT占用的鏈接重復了就會出現問題
使用setsockopt()設置socket描述符的 選項SO_REUSEADDR為1, 表示允許創建端口號相同但IP地址不同的多個socket描述符
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
再次運行服務器
滑動窗口
剛才我們討論了確認應答策略, 對每一個發送的數據段, 都要給一個ACK確認應答. 收到ACK后再發送下一個數據段;這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返的時間較長的時候;并且如果我們發送數據,沒有收到應答之前,必須將已發送的數據暫時保存起來,為了支持超時重傳,所保存的地方其實就是發送緩沖區,發送緩沖區和滑動窗口緊密關系
不嚴格的情況下,發送緩沖區可以分為三部分:
其中已發送&&!收到應答的部分稱作滑動窗口
如何看待滑動窗口呢?
可以將其理解為一個字符數組
窗口移動,其本質就是下標在進行更新
目前的理解是滑動窗口的大小和對方的接收能力有關,無論未來怎么滑動,都要保證對方能夠進行正常的接收
win_start=ack_seq;win_end=win_start+tcp_win
ack_seq
報文的確認序號 tcp_win
對方的接收緩沖區剩余空間
窗口一定會向右滑動嗎?會向左滑動嗎?
可能向右滑動,取決于右邊界的變化;一定不會向左滑動,因為收到應答的數據不可能變成沒有收到應答
窗口一定會一直不變嗎?會變大嗎?會變小嗎?為什么?
窗口一定會變化的;當發送的數據越來越多,對方一次將大量的數據從接收緩沖區中取走,此時窗口就會變大;當對方一直不取走接收緩沖區中數據,窗口便會變小
收到應答的時候,如果不是最左側發送的報文的確認,而是中間的,或者最右側的怎么辦?需要滑動嗎?
兩種可能
- 數據并沒有丟,而是應答丟了,根據確認序號的定義,最左側的下標會向右移動;同時也說明了確認序號也是為了支持滑動窗口的規則而設定的
- 數據真的丟了,接收方接收的ACK序號會重復,且是該丟失的報文的確認序號;當連續接收三次時,會觸發重傳機制
滑動窗口必須要滑動嗎?會不會不動?或者變為0?
不一定滑動,可能不動,也可能變為零
只取決于 win_start=ack_seq;win_end=win_start+tcp_win
兩者的差值
如果一直向后滑動,空間不夠怎么辦?
發送緩沖區是環狀結構的,所以空間不存在不夠
流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送,就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應.
因此TCP支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制(Flow Control);
- 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段, 通過ACK端通知發送端
- 窗口大小字段越大, 說明網絡的吞吐量越高;
- 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端
- 發送端接受到這個窗口之后, 就會減慢自己的發送速度
- 如果接收端緩沖區滿了, 就會將窗口置為0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端
接收端如何把窗口大小告訴發送端呢?
在三次握手時就已經交換了窗口大小
擁塞控制
雖然TCP有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題
在之前我們考慮的只是通信雙方,并沒有考慮到兩者之間的網絡
因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據,是很有可能引起雪上加霜的
TCP引入 慢啟動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據
- 此處引入一個概念程為擁塞窗口
- 發送開始的時候, 定義擁塞窗口大小為1
- 每次收到一個ACK應答, 擁塞窗口加1
- 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口:滑動窗口=min(擁塞窗口,自己的接收能力)
像上面這樣的擁塞窗口增長速度, 是指數級別的. “慢啟動” 只是指初使時慢, 但是增長速度非常快
- 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍
- 此處引入一個叫做慢啟動的閾值
- 當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
- 當TCP開始啟動的時候, 慢啟動閾值等于窗口最大值;
- 在每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回1
少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;當TCP通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;擁塞控制, 歸根結底是TCP協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案
延遲應答
如果接收數據的主機立刻返回ACK應答, 這時候返回的窗口可能比較小
- 假設接收端緩沖區為1M. 一次收到了500K的數據; 如果立刻應答, 返回的窗口就是500K
- 但實際上可能處理端處理的速度很快, 10ms之內就把500K數據從緩沖區消費掉了
- 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來
- 如果接收端稍微等一會再應答, 比如等待200ms再應答, 那么這個時候返回的窗口大小就是1M
一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率
那么所有的包都可以延遲應答么? 肯定也不是
- 數量限制: 每隔N個包就應答一次
- 時間限制: 超過最大延遲時間就應答一次
具體的數量和超時時間, 依操作系統不同也有差異; 一般N取2, 超時時間取200ms;
捎帶應答
在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 “一發一收” 的. 意味著客戶端給服務器說了 “How are you”, 服務器也會給客戶端回一個 “Fine, thank you”; 那么這個時候ACK就可以搭順風車, 和服務器回應的 “Fine, thank you” 一起回給客戶端
簡單理解就是在確認應答時也發送數據
面向字節流
創建一個TCP的socket, 同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區
- 調用write時, 數據會先寫入發送緩沖區中
- 如果發送的字節數太長, 會被拆分成多個TCP的數據包發出
- 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去
- 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區
- 然后應用程序可以調用read從接收緩沖區拿數據
- 另一方面, TCP的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做 全雙工
由于緩沖區的存在, TCP程序的讀和寫不需要一一匹配, 例如:
- 寫100個字節數據時, 可以調用一次write寫100個字節, 也可以調用100次write, 每次寫一個字節
- 讀100個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100個字節, 也可以一次read一個字節, 重復100次
粘包問題
- 首先要明確, 粘包問題中的 “包” , 是指的應用層的數據包
- 在TCP的協議頭中, 沒有如同UDP一樣的 “報文長度” 這樣的字段, 但是有一個序號這樣的字段
- 站在傳輸層的角度, TCP是一個一個報文過來的. 按照序號排好序放在緩沖區中
- 站在應用層的角度, 看到的只是一串連續的字節數據
- 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界
- 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的Request結構, 是固定大小的, 那么就從緩沖區從頭開始按sizeof(Request)依次讀取即可
- 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置
- 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可)
對于UDP協議來說, 是否也存在 “粘包問題” 呢?
- 對于UDP, 如果還沒有上層交付數據, UDP的報文長度仍然在. 同時, UDP是一個一個把數據交付給應用層. 就有很明確的數據邊界
- 站在應用層的站在應用層的角度, 使用UDP的時候, 要么收到完整的UDP報文, 要么不收. 不會出現"半個"的情況
TCP異常情況
進程終止: 進程終止會釋放文件描述符, 仍然可以發送FIN. 和正常關閉沒有什么區別
機器重啟: 和進程終止的情況相同
機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行reset. 即使沒有寫入操作, TCP自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放
另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如HTTP長連接中, 也會定期檢測對方的狀態. 例如QQ, 在QQ斷線之后, 也會定期嘗試重新連接
TCP小結
為什么TCP這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能.
可靠性:
- 校驗和
- 序列號(按序到達)
- 確認應答
- 超時重發
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動窗口
- 快速重傳
- 延遲應答
- 捎帶應答
TCP/UDP對比
我們說了TCP是可靠連接, 那么是不是TCP一定就優于UDP呢? TCP和UDP之間的優點和缺點, 不能簡單, 絕對的進行比較
- TCP用于可靠傳輸的情況, 應用于文件傳輸, 重要狀態更新等場景
- UDP用于對高速傳輸和實時性要求較高的通信領域, 例如, 早期的QQ, 視頻傳輸等. 另外UDP可以用于廣播
歸根結底, TCP和UDP都是程序員的工具, 什么時機用, 具體怎么用, 還是要根據具體的需求場景去判定
TCP 相關實驗
理解 listen 的第二個參數
舉個栗子
在大型商場中,每個飯館外面都有閑置的座椅,每當到飯點時,都會有顧客在外面排隊,仔細觀察會發現,這些座椅并不會很多,因為沒有必要;讓顧客排隊的目的就是在里面有資源空閑時,立刻投入使用,提高入座率;在TCP協議中同樣如此
TCP協議,要為上層維護一個鏈接隊列,當服務器有空閑的資源時,隊列中的客戶端立刻進行連接;隊列的長度受listen的第二個參數影響
對于服務器, listen 的第二個參數設置為 2, 并且不調用 accept
當對服務器進行三次連接之后,查詢結果如下
當進行第四次連接時,查詢結果
當第四次連接時,客戶端認為自己已經完成握手,但是服務端并沒有完成握手,并沒有最后一次確認,這種情況也稱半連接
Linux內核協議棧為一個tcp連接管理使用兩個隊列:
- 半鏈接隊列(用來保存處于SYN_SENT和SYN_RECV狀態的請求)
- 全連接隊列(accpetd隊列)(用來保存處于established狀態,但是應用層沒有調用accept取走的請求)
全連接隊列的長度會受到 listen 第二個參數的影響;全連接隊列滿了的時候, 就無法繼續讓當前連接的狀態進入 established 狀態
;這個隊列的長度通過上述實驗可知, 是 listen 的第二個參數 + 1;同時也說明了accpet與三次握手沒有關系