目錄
一,UDP協議
1,UDP協議的格式
2,UDP的特點?
3,面向數據報
4,UDP的緩沖區
5,UDP使用注意事項
6,基于UDP的應用層協議
二,對于報文的理解
三,TCP協議
1,TCP協議格式
2,可靠性的本質
(1) 確認應答(ACK)機制
(2)序號和確認序號?
?(3)超時重傳機制
(4)流量控制?
(5)六個標志位
(6)連接管理機制?
【 三次握手 】
【 四次揮手 】
(7)滑動窗口
【理解滑動窗口】
?【丟包問題】
(8)擁塞控制
(9)延遲應答
(10)面向字節流
(11)粘包問題
(12)TCP異常情況
四,TCP小結
前言:
傳輸層是實現端到端通信的核心,主要涉及TCP和UDP
一,UDP協議
1,UDP協議的格式
16位UDP長度,表示整個數據報(UDP首部+UDP數據)的最大長度。
如果校驗和出錯,則會直接丟棄該報文。
2,UDP的特點?
無連接:直到對端的IP和端口號就直接進行傳輸,不需要建立連接。
不可靠:沒有確認機制,沒有重傳機制;如果因為網絡故障數據無法發送到對方,UDP協議層也不會給應用層返回任何信息。
面向數據報:不能夠靈活的控制讀寫數據的次數;
3,面向數據報
應用層交給UDP多長的報文,UDP原樣發送,既不會拆分,也不會合并。
比如用UDP傳輸100個字節的數據:
如果發送端調用一次sendto,發送100個字節。那么接受端必須調用一次recvfrom,接受100個字節的數據;而不能循環調用10次recvform,每次接受10個字節的數據。
4,UDP的緩沖區
對于TCP,上層在調用write發送數據的時候,本質是將數據拷貝到TCP的發送緩沖區中,什么時候發,發多少由操作系統據決定。發送發將數據發送給對端,本質是將數據拷貝到對端的接受緩沖區中,對端在調用 read讀取數據的時候,本質是從接受緩沖區中讀取數據。
所以TCP有兩個緩沖區:發送緩沖區和接受緩沖區。
而對于UDP而言,是不需要發送緩沖區,它只有一個接受緩沖區。在調用sendto發送數據的時候,會直接將數據交給內核,由內核將數據交給網絡層進行后序的傳輸動作。
UDP具有接受緩沖區,但這個接受緩沖區不能保證收到的UDP報的順序和發送的UDP報的順序一致;如果接受緩沖區滿了,再到達的數據報就會被丟棄。
5,UDP使用注意事項
UDP協議首部有一個16位的最大長度,也就是說一個UDP能傳輸的數據報的最大長度是64KB(包含UDP首部)。
然而64KB在當前的互聯網環境下,是一個非常小的數字。
如果我們需要傳輸的數據超過64KB,就需要在應用層手動的進行分包,多次發送,并在接收端手動拼裝。
6,基于UDP的應用層協議
NFS:網絡文件系統
TFTP:簡單文件傳輸系統
DHCP:動態主機配置協議
DNS:域名解析協議
二,對于報文的理解
操作系統是基于中斷運行的,如果應用層正在進行報文的解析,到收到新的報文時,外圍設備就緒觸發中斷,操作系統就會從設備中(網卡)讀取數據,拿到數據報。
那么此時操作系統中一定會存在大量的報文,所以操作系統就需要對這些報文進行管理。如何管理?先描述,再組織:
sk_buff結構就是用來描述一個數據報的。在Linux內核中的部分源代碼如下:
struct sk_buff{struct sk_buff* next;struct sk_buff* prev;//……//…… unsigned char *head,*data,*tail,*end;
}
sk_buff結構中重要的4個指針指向一段緩沖區 ?。有點類似于進程PCB和進程對應的代碼和數據。
當數據報詳細交付時,當從應用層交付到傳輸層時,data指針向上移動一段空間,如果使用UDP協議,移動UDP報頭的大小,然后填充對應UDP報頭。如果使用TCP協議,移動一個TCP報頭的大小,然后填充 TCP報頭。當數據報從傳輸層交付到網絡層時,data指針向上移動,然后再填充上對應的報頭 ,依次類推。
如果是向上層移動,那么就是data指針向下移動。
所以,對于數據報的解包和分用,其實就是 data指針在對應 緩沖區中的指向!!!
三,TCP協議
tcp協議,全稱為"傳輸控制協議"。人如其名,要對數據的傳輸進行 詳細的控制。
1,TCP協議格式
源/目的端口號:表示數據從哪個進程來,要到哪個進程去。
16位校驗和:發送端填充,CRC校驗,接收端不通過,則認為數據有問題。此處的校驗,不僅包含TCP首部,也包含TCP數據部分。
4位首部長度:表示TCP報頭的大小(包含選項),單位是4字節。比如,1111表示15,對應的報頭大小為15*4=60字節。又因為TCP報頭大小至少為20字節(不包含選項),所以4位首部長度最小的數值是5,最大是15.
在TCP報文的格式中,沒有對應的數據部分的大小,只有報頭的大小。
而UDP報文中記錄了整個報文的大小,通過減去報頭的大小,我們就可以獲取到對應數據的大小。所以,對于UDP報文,數據和數據之間的邊界是很明顯的。
而在TCP中,是沒有的,也是不需要有的。因為TCP不能保證接受上來的報文就是一個完整的請求,它將接受到的報文去掉報頭,放入到接受緩沖區中,上層進行讀取數據,由上層自己判斷是不是一個完整的請求。而UDP可以保證,它接受上來的報文就是一個完整的請求。
所以說UDP是面向數據報的,TCP是面向字節流的。
2,可靠性的本質
(1) 確認應答(ACK)機制
在兩臺主機通過TCP進行通信的時候,如上圖,主機A給主機B發送數據,主機A發送了數據,但是無法確定主機B是否成功收到了數據,此時需要主機B的操作系統給主機A發送一個應答報文。?表示主機B收到了數據,這個應答報文沒有數據部分,并且6個標志位中的ACK設為1。表示該報文是一個應答報文。
注:應答是由操作系統自己完成的。主機B在接收到數據后,主機B的操作系統會自動構建一個應答報文返回給主機B。
?但是有時候主機B也要給主機A發送數據,所以在主機B給主機A發送數據的時候,會攜帶上應答。
這種應答方式稱為捎帶應答。
(2)序號和確認序號?
?當主機A向主機 B發送數據的時候,在TCP報頭部分會填充上序號的值,同時主機B在做應答的時候,會根據序號,計算出確認序號。確認序號=序號+1。
對于主機A,也就是發送端,會收到應答報文(或者捎帶應答),比如收到確認序號為2001的應答報文,主機A就會認為序號為2001之前的所有報文,對端已經全部收到了。這種方式可以大大提高數據傳輸的效率。
對于主機B,也就是接收端。當主機A發送數據的時候,每個數據到達主機B的時間可能不同,有的報文發的早,但是到達主機B的時間晚,有的報文發的晚,但是到達主機B的時間早。通過序號,可以將收到的報文進行排序,并且實現去重的目的(有的數據報可能會發送多次)。
?(3)超時重傳機制
主機A給主機B發送數據之后,可能因為網絡擁堵等原因,數據無法到達主機B。
如果主機A在特定的時間間隔內沒有收到B發來的確認應答,就會重發數據。
但是,主機A未收到主機B發來的應答時,也可能是因為應答(ACK)丟失了。?
?此時主機A仍然會進行超時重傳,所以主機B就會收到多個重復的數據報。TCP協議是可以區分出來這些重復的數據報的(通過序號,很容易做到去重的效果),并且把重復的丟棄掉。
超時的時間是如何確定的???
- 最理想的情況是,找一個最小的時間,保證"確認應答"一定能在這個事件內到達。
- 但是這個時間的長短,隨著網絡環境的不同,是有差異的。
- 如果超時重傳時間設的太長,會影響整體的重傳效率。
- 如果超時時間設的太短,有可能會頻繁發送重復的包。
這個時間應該和網絡當前的狀態是有關的。而網絡狀態時刻在變化,所以這個值應該也是一個變化的。
TCP為了保證無論在任何環境下都能夠比較高效的通信,因此一般會動態計算這個超時時間。
Linux中(BSD Unix和Windows也是如此),超時以500ms為一個單位 進行控制,每次判定超時重發的超時時間都是500ms的整數倍。
- 如果重發一次后,仍然得不到應答,等待2*500ms再進行重傳。
- 如果仍然得不到應答,等待4*500ms再重傳,依次類推,以指數形式遞增。
- 累積到一定的重傳次數,TCP認為網絡或對端主機出現異常,強制關閉連接。
(4)流量控制?
前面提到過,TCP有發送緩沖區和接受緩沖區。
- 上層在調用write接口發送數據時,實際是把數據拷貝到對應的發送緩沖區,什么時候發,發多少由操作系統決定。將數據發送到對端的接受緩沖區中。
- 上層在調用read接口讀取數據時,實際是把數據從 接受緩沖區中讀上來,如果接受緩沖區為空,則會在read接口處阻塞住。
當發送方 發送數據的時候,對端(接收方)的接受緩沖區可能滿了,放不下數據了?。那么此時對端在收到數據后,就會將該數據直接丟棄。
這是不合理的,因為數據報在網絡傳輸中會消耗電力,帶寬等等網絡資源。如果直接丟棄,這就是一種浪費資源的現象。
為了解決這種問題,發送方就需要知道對端的接受能力,也就是對端接受緩沖區剩余空間的大小。從而決定發送多少數據。
在TCP報頭中,16位窗口大小,該字段就表示對端的接受能力的大小。這種機制叫做流量控制。
那么問題來了,16位表示的最大數字是65535,那么TCP接受緩沖區最大就是65535字節嗎???
實際上,TCP首部40字節選項中還包含一個窗口擴大因子M,實際窗口大小是窗口字段的值左移M位。
通過流量控制機制,一方面可以提高TCP傳輸的可靠性,另一方面,可以提高傳輸的效率。?
(5)六個標志位
在TCP報頭中,存在六個標志位:URG,ACK,PSH,RST,SYN,FIN
首先,在使用TCP進行通信的時候,雙方需要先建立連接。(三次握手)
雙方結束通信的時候,需要釋放對應連接。(四次揮手)
三次握手和四次揮手重點在連接管理機制部分說明。
這六個標志位,在TCP報頭中對應一個比特位,要么為0,要么為1。為1表示給標志生效。
SYN:同步標志位,請求建立連接,握手過程使用 的標志位。
ACK:確認序號是否有序。ACK為1,表示該報文是一個應答報文(或者捎帶應答)。
PSH:提示接受端應用程序立刻從TCP緩沖區把數據讀走。
RST:對方要求重新建立連接。我們 把攜帶RST的報文稱為復位報文段。
?FIN:通知對方,本端要關閉連接了,我們稱攜帶FIN標識的為結束報文段。
URG:標識TCP報頭中的16位緊急指針是否有效。如果將URG設置為1,表示當前報文中包含緊急數據,需要將該報文立即交給上層處理。
舉個例子:
當我們使用百度網盤上傳文件時,將數據發送到服務器的接受緩沖區中。
上傳過程中,如果發現上傳錯了,想要取消上傳。取消上傳也是以報文的形式發送給服務器的。
此時就希望服務器立馬處理這個報文。而不是先將該報文放到接受緩沖區中,當前面的報文處理完了,才處理該報文。
所以此時就可以將URG置為1,表示該報文是一個 緊急報文,需要立即處理。
暫停上傳也是類似的思路。
實際上,為了實現這個功能,不一定非要使用URG標志位,也可以通過建立兩條連接的方式,一條連接用來發送數據,另一條連接用來發送指令。
URG為1時,表示16位緊急指針有效,假設該16位有效指針表示的數字是n。意味著在該報文有效載荷中,偏移量為n處,有緊急數據,該緊急數據的大小是一個字節。這個緊急數據本身就是一個標志位,比如,標志位為1表示取消,標志位為2表示暫停等等。?
(6)連接管理機制?
正常情況下,TCP要通過三次握手建立連接,通過四次揮手斷開連接。
?
上圖中,諸如SYN_SENT,ESTABLISHED,FIN_WAIT1等等,都表示的是當前服務器或者客戶端所處的狀態,其本質就是一個數字,?通過宏定義出來的。
【 三次握手 】
三次握手的大致過程:客戶端發起建立連接的請求SYN,服務器收到請求后。向客戶端發送ACK(確認應答 ),表示收到請求了。同時服務器端向客戶端發送建立連接的請求(SYN),這兩個合成一個報文。最后,客戶但發送ACK(確認應答),表示收到請求了。至此建立連接完成,三次握手成功。
客戶端調用connect發起三次握手,向服務器發起建立連接的請求。而服務器端通過listen將自己設置為監聽狀態。
本質上,connect是發起三次握手,但是不參與三次握手的。服務器端通過accept獲取底層建立好的連接,也不參與三次握手。三次握手由雙方客戶端和服務器端操作性系統自動完成。
為什么要進行三次握手???
1,通過三次握手,可以以最短的方式進行驗證雙方的全雙工通信。本質是驗證雙方所處的網絡是否通常,是否能夠支持全雙工。
2,以最小的成本,100%確認雙方通信的意愿。
【 四次揮手 】
四次揮手的大致過程:通信雙方斷開連接的過程。客戶端發送FIN,表示本端想要關閉連接,服務器端收到后發送確認應答(ACK)。接著服務器端也發送FIN,表示本段也想要關閉連接,客戶端收到后,發送確認應答(ACK),服務器端收到應答。至此,四次揮手完成,雙方斷開連接。
四次揮手也是由操作系統自動完成的。本質是建立雙方斷開連接的共識。客戶端(服務端)認為要發送的數據已經發送完了,調用close(fd)斷開連接。?
在三次握手中,將SYN+ACK合并了,本質上是4次握手,降級成了三次握手。
而對于四次揮手,不一定能進行合并,當客戶端數據發送完了,向服務器發送斷開連接的請求時,服務器先要回復一個確認應答(ACK),但此時由于服務器要發送的數據可能還沒發完,所以服務器要等待一段時間,將這些數據全部發送完,才會向客戶端發起斷開連接的請求,客戶端收到后向服務器發送確認應答(ACK)。至此,雙方完成斷開連接。因此,對于斷開連接的過程,中間兩步一般無法合并成 一次,所以大部分情況下是四次揮手。
TIME_WAIT狀態:
關于四次揮手,還有一個細節,下圖中,客戶端主動發起斷開連接,四次揮手完成后最后客戶端會先處于TIME_WAIT狀態,而不是立即處于CLOSED狀態,經過一段時間,才會處于CLOSED狀態。而對于服務器端,四次揮手完成后,會立即處于CLOSED狀態。
這是因為,TCP協議規定:主動關閉連接的一方要先處于TIME_WAIT狀態,等待兩個MSL(maximum segment lifetime)的時間后才能會得到CLOSED狀態。MSL是TCP報文的最大生存時間。
原因:當通信雙方想要斷開連接的時候,歷史發出的報文可能還在網絡中進行轉發,還沒到達接收端,這時雙方關閉連接。當雙方重新建立連接的時候,這個在網絡中殘留的報文此時可能就會到來,根據目的IP和目的端口找到對應的主機,但此時連接都還沒建立,三次握手都還沒完成。所以這個報文是會影響 通信雙方重新建立連接和發送數據的,等待2個MSL是為了讓這個報文在網絡中消散。
- 當雙方在通過網絡TCP進行通信的時候,如果有一方主動斷開連接,會進入TIME_WAIT狀態。在該狀態下,無法重新啟動程序,bind會失敗,因為之前的端口號還在被占用。
- 比如:我們使用 Ctrl-C 終止了 server, 所以 server 是主動關閉連接的一方, TIME_WAIT 期間仍然不能再次監聽同樣的 server 端口;需要更換端口號,或者等待TIME_WAIT狀態結束
站在應用角度,一般都是既要實現TIME_WAIT,還要服務器可以立即重啟。
使用 setsockopt()設置 socket 描述符的 選項 SO_REUSEADDR 為 1, 表示端口號復用。
? ? ? ?int setsockopt(int sockfd, int level, int optname,
? ? ? ? ? ? ? ? ? ? ? const void *optval, socklen_t optlen);
- 使用這種方式可以使程序立即重啟,但是也可能會造成歷史報文對雙方通信的影響,但是 TCP為了解決這一問題,還有一個"后背隱藏能源"——序號。接收端還會根據序號來判斷這個 報文是否有效。
- 通信雙方在三次握手的時候,會完成序號和確認序號的協商,比如客戶端發送報文的序號從1000開始,接收端發送應答報文的確認序號從2000開始。當一段收到的序號發現不匹配時,就會將該報文丟棄掉。
TIME_WAIT+序號,通過以上的處理方式,就可以大大減少陳舊報文對新連接建立產生的影響。
(7)滑動窗口
【理解滑動窗口】
在上面我們討論了確認應答機制,對每一個發送的數據段,都需要給一個ACK應答。收到ACK后再發送下一個數據段。這樣做有一個比較大的缺點,就是性能較差,尤其是數往返時間較長的時候。
既然這樣一發一收的方式性能較低, 那么我們一次發送多條數據, 就可以大大的提高性能(其實是將多個段的等待時間重疊在一起了)。
主機A向主機B發送數據,1~1000表示的是數據的大小,當發送的數據大小是1~1000時,序號就是1000,主機B收到后,進行應答,確認序號=序號+1,所以確認序號就是1001,同時下一個數據從1001開始發送,選后是依次增大的。發送前4個報文的時候,不需要等待ACK,可以直接發送。
滑動窗口大小指的是無需等待確認應答而可以繼續發送數據的最大值。
在前面提到過,TCP有自己的接受緩沖區和發送緩沖區。如下圖所示:
我們可以將發送緩沖區看成是一個char類型的數組,滑動窗口看成是發送緩沖區的一部分 。start指向滑動窗口的開始,end指向滑動窗口的結束。
根據滑動窗口,可以將發送緩沖區劃分為三部分:
- 對于已發送已確認的數據:其實不需要真正將這部分空間清空。本質是這部分數據無效了,可以再次 被利用了。不需要可以的去清空 緩沖區。
- 對于滑動窗口:可以直接發送 ,暫時不需要應答。滑動窗口的大小由對端的接受緩沖區剩余空間的大小決定(還有一個因素,在擁塞控制部分講解)。滑動窗口移動的本質:strat和end下標增加。所以,滑動窗口的大小決定一次可以直接發多少的數據。
- 滑動窗口是流量控制的具體實現方案。
- 窗口越大,證明當前網絡的吞吐率越高。
滑動窗口大致工作過程如下圖:
注意:窗口大小是可能會改變的,變大或者變小,取決于對方的接受能力。?
?【丟包問題】
發送端在發送數據的時候,可能會存在丟包問題。
大致分為三種:滑動窗口最左側丟失,中間丟失,最右側丟失。
如果中間報文丟失,那么滑動窗口首先會向右滑動,滑倒丟失的位置,此時就又變成了最左側丟失問題。如果最右側報文丟失,同理,也是最左側丟失問題。
最左側丟失,又分為兩種情況,最左側對應的應答丟失,最左側對應的數據真的丟了。:
- 最左側報文真的丟了,滑動窗口左側不變。
- 應答丟失,滑動窗口正常工作。
情況一:數據包真的丟失了
- 當1001~2000的報文丟失后,發送端會一直接受到1001這樣的ACK應答,就像是在提醒發送端"我想要1001一樣"。
- 如果發送端主機連續三次收到同一個1001這樣的應答報文后,就會將對應的數據進行重傳。
- 這個時候,接收端再收到1001之后,再次返回的ACK就是7001了(因為2001~7000接受端之前已經收到了,被放到了接收端操作系統內核的接收緩沖區中。
這種機制被稱為 "高速重發控制"(也叫 "快重傳")。
上述的情況只能確認1001~2000的報文丟失了,不能確定中間的是否丟失了。假設中間的4001~5000也丟失了,由于我們的通信一直再進行著,比如我們下面還會發送7001~8000,8001~9000的數據,那么此時的確認應答就是40001。也就是如果中間部分丟失了,會有后面的通信做兜底。同時,還有超時重傳機制來確保。
快重傳vs超時重傳
快重傳:收到三個相同的確認應答時進行重傳,可以提高效率。
超時重傳:超時并且沒有收到應答,用來做兜底的。
?總結:發出數據后,沒有收到應答 ,必須讓對應的報文暫時保存起來,以方便后續的而重傳!
保存在滑動窗口中,也就是左側不移動。
情況二:數據已抵達,ACK丟失
這種情況下, 部分 ACK 丟了并不要緊, 因為可以通過后續的 ACK 進行確認。
因為確認序號的定義就是該序號之前的所有報文對端都已經收到了。 所以,滑動窗口可以正常進行工作。
(8)擁塞控制
雖然TCP有了滑動窗口這個大殺器,能夠高效可靠的發送大量數據,但是如果一開始發送大量的數據,也可能會出現問題。
因為網絡上又很多計算機,可能當前的網絡狀態已經很擁堵了。在不清楚當前網絡狀態的前提下,貿然發送大量數據,是很有可能造成雪上加霜的。
TCP引入慢啟動機制,先發送少量的數據,探探路,摸清當前的網絡狀況,再決定按照多大的速度傳輸數據。
為了衡量當前網絡的擁塞狀態,TCP引入了一個新的概念——擁塞窗口。
剛開始發送數據的時候,擁塞窗口大小為2^0。
當收到一個ACK應答時,擁塞窗口大小為2^1。
以此類推,2^2,2^3......呈現指數形式增長。如下圖所示:
在前面滑動窗口 部分提到過,滑動 窗口的大小是對方接受能力的大小(也是就是對端緩沖區剩余空間的大小),現在再加一個因素,就是擁塞窗口的大小。取兩者的最小值。
滑動窗口大小=min(對端接受能力大小,擁塞控制大小),之所以取最小值,是因為對端接受能力很強,但是網絡太擁堵了,發送太多數據會造成大量的丟包;同理,網絡狀態很好,但是對端接受能力太差,同樣發送給大量數據也會造成大面積丟包。
但是關于 擁塞窗口的大小,不能一直這么增長下去,因為可能會造成指數爆炸,需要通過慢啟動機制來控制。
- 使用指數增長,剛開始時窗口大小較小,但是增長速度很快。
- 此處引入一個慢啟動的閾值(下圖中的ssthresh)。
- 當擁塞窗口大小超過這個閾值的時候,不會再按照指數形式增長,而是按照線性方式增長。
前面指數增長階段,本質是解決擁塞,嘗試回復網絡通信的階段。
剛開始擁塞窗口一直在增加,但是發送的數據不一定一直在增長。
當擁塞窗口大小一直再增大,增大到某一個數值時,此時發送數據發送大量丟包,那么說明此時的擁塞窗口大小就是當前網絡的擁塞狀況。這個過程就是我們 在探測當前網絡的擁塞情況。之后,重新開始滿開始 增長,此時的閾值就是上次擁塞窗口值的一半。
?這整個過程本質是不斷探測當前網絡的擁塞窗口的值。
(9)延遲應答
- 如果接收數據的主機立刻返回 ACK 應答, 這時候返回的窗口可能比較小。假設接收端緩沖區為 1M. 一次收到了 500K 的數據; 如果立刻應答, 返回的窗口就是 500K;
- 但實際上可能處理端處理的速度很快, 10ms 之內就把 500K 數據從緩沖區消費掉了;
- 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
- 如果接收端稍微等一會再應答, 比如等待 200ms 再應答, 那么這個時候返回的窗口大小就是 1M;
窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率;
(10)面向字節流
創建一個 TCP 的 socket, 同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區。
- 調用 write 時, 數據會先寫入發送緩沖區中;
- 如果發送的字節數太長, 會被拆分成多個 TCP 的數據包發出;
- 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去;
- 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區;
- 然后應用程序可以調用 read 從接收緩沖區拿數據;
- 另一方面, TCP 的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做 全雙工。
由于緩沖區的存在, TCP 程序的讀和寫不需要一一匹配, 例如:
寫 100 個字節數據時, 可以調用一次 write 寫 100 個字節, 也可以調用 100 次
write, 每次寫一個字節。讀 100 個字節數據時, 也完全不需要考慮寫的時候是怎么寫的,既可以一次
read 100 個字節, 也可以一次 read 一個字節, 重復 100 次;
(11)粘包問題
首先要明確, 粘包問題中的 "包" , 是指的應用層的數據包.
- 在 TCP 的協議頭中, 沒有如同 UDP 一樣的 "報文長度" 這樣的字段, 但是有一個序號這樣的字段.
- 站在傳輸層的角度, TCP 是一個一個報文過來的,按照序號排好序放在緩沖區中。
- 站在應用層的角度, 看到的只是一串連續的字節數據.
- 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包.
那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界.
- 對于定長的包, 保證每次都按固定大小讀取即可;?
- 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置;
- 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可);
對于 UDP 協議來說, 是否也存在 "粘包問題" 呢?
- 對于 UDP, 如果還沒有上層交付數據, UDP 的報文長度仍然在. 同時, UDP 是一個一個把數據交付給應用層. 就有很明確的數據邊界.
- 站在應用層的站在應用層的角度, 使用 UDP 的時候, 要么收到完整的 UDP 報文, 要么不收. 不會出現"半個"的情況。
(12)TCP異常情況
- 進程終止: 進程終止會釋放文件描述符, 仍然可以發送 FIN. 和正常關閉沒有什么區別.
- 機器重啟: 和進程終止的情況相同.
- 機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 reset. 即使沒有寫入操作, TCP 自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放。
- 另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如 HTTP 長連接中, 也會定期檢測對方的狀態. 例如 QQ, 在 QQ 斷線之后, 也會定期嘗試重新連接。
四,TCP小結
為什么 TCP 這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能。
可靠性:
- 校驗和
- 序列號(按序到達)
- 確認應答
- 超時重發
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動窗口
- 快速重傳
- 延遲應答
- 捎帶應答