首頁>技術>

傳輸層中有兩個重要的協議,UDP和TCP,這也是在開發中經常用到的協議,同樣也是面試的重點。本篇將分為三節進行介紹:

UDP協議TCP協議套接字Socket一、UDP協議

很多人都會被問到 TCP和UDP的區別,那麼大部分人都會回答,TCP面向連線,UDP面向無連線;

建立連線:是為了在客戶端和服務端維護連線,而建立一定的資料結構來維護雙方互動的狀態,用這樣的資料結構來保證所謂的面向連線的特性;

簡單介紹下TCP和UDP之間的區別:

TCP 提供可靠交付,UDP繼承了IP包的特性,不保證不丟失,不保證按時到達;

TCP是面向位元組流的,傳送的時候發的是一個流,沒頭沒尾的。UDP繼承了IP的特性,基於資料報的,一個個發,一個個收;

TCP是可以有擁堵控制的,可以根據網路環境調整自己的行為;UDP就是應用讓我發,我就發,管它洪水滔天;

TCP是一個有狀態的服務,通俗的講就是有腦子的,可以精確的記著,自己傳送了沒有,接收到沒有,傳送到哪個了,應該接收到哪個了,錯一點兒都不行;UDP其實是一個無狀態服務,無腦子,天真無邪的發出去就發出去唄;

UDP的包頭

UDP的包頭格式很簡單,只有源埠號和目標埠號:

UDP的三大特點

溝通簡單,秉承性善論,相信網路通路預設就是很容易送達的,不容易被丟棄的;

輕信他人,不會建立連線,雖然有埠號,但是監聽在這個地方,誰都可以傳給他資料,也可以傳給任何人資料;

愣頭青,做事不懂權變,不會根據網路的情況進行發包的擁塞控制,無論網路丟包丟成啥樣了,它該怎麼發還怎麼發;

UDP的三大使用場景

需要資源少,在網路情況比較好的內網,或者對於丟包不敏感的應用;

不需要一對一溝通,建立連線,而是可以廣播的應用;UDP的不面向連線的功能,可以使得可以承載廣播或者多播的協議。DHCP就是一種廣播的形式,就是基於UDP協議的;

需要處理速度快,時延低,可以容忍少數丟包,即便網路堵塞,也毫不退縮,一往無前的時候;UDP簡單、處理速度快,不像TCP一樣,操那麼多心;TCP在網路不好出現丟包的時候,擁塞控制策略會主動的退縮,降低傳送速度,這就相當於本來環境就差,還自斷臂膀,使用者本來就卡,這下更卡了

基於UDP的實際應用

網頁或者APP的訪問,訪問網頁和手機APP都是基於HTTP協議(基於TCP)的,建立連線需要多次互動,比較耗時,Google提出了QUIC實現快速連線建立、減少重傳時延,自適應擁塞控制;

流媒體的協議,直播協議多使用RTMP(基於TCP),當資料丟包或者網路不好,影響直播的實時性,很多直播應用,都基於UDP實現了自己的視訊傳輸協議;

實時遊戲,採用自定義的可靠UDP協議,自定義重傳策略,能夠把丟包產生的延遲降到最低,儘量減少網路問題對遊戲性造成的影響;

IoT物聯網,物聯網通訊協議Thread,就是基於UDP協議的,解決了物聯網領域終端資源少,實時性要求高的問題;

行動通訊領域:4G網路裡,移動流量上網的資料面對的協議GTP-U是基於UDP的;

總結:

如果將TCP比作成熟的社會人,UDP則是頭腦簡單的小朋友;TCP複雜,UDP簡單;TCP維護連線,UDP誰都相信;TCP會堅持知進退;UDP鐵憨憨一個,勇往直前;

UDP簡單但有簡單的用法。它可以用在環境簡單、需要多播、應用層自己控制傳輸的地方。例如DHCP、VXLAN、QUIC等

二、TCP協議(上)

TCP秉承的是性惡論,天然認為網路環境是惡劣的,丟包、亂序、重傳、擁塞都是常見的事情,需要從演算法層面來保證可靠性。

TCP包頭格式

源埠號和目標埠號:知道誰發的和發給誰的;序號:編號是為了解決亂序問題;確認序號:發出去的包應該有確認,沒有收到就應該重新發送,直到送達;狀態位:SYN是發起一個連線、ACK是回覆、RST是重新連線、FIN是結束連線;視窗大小:TCP要做流量控制,通訊雙方各宣告一個視窗,標識自己當前能夠的處

理能力,別傳送的太快,撐死我,也別發的太慢,餓死我;

通過對TCP頭的解析,我們知道要掌握TCP協議,重點應該關注以下幾個問題:

順序問題 ,穩重不亂;丟包問題,承諾靠譜;連線維護,有始有終;流量控制,把握分寸;擁塞控制,知進知退;

2.1 TCP的三次握手

TCP中所有的問題,都要先建立連線,需要先看連線維護的問題,TCP的連線建立,常被稱為三次握手;

採用 請求->應答->應答之應答的方式,保證二者的訊息傳送都是有來有回的;

三次握手除了雙方建立連線外,主要還是為了溝通一件事情,就是TCP包的序號的問題。 每個連線都要有不同的序號。這個序號的起始序號是隨著時間變化的,可以看成一個32位的計數器,每4ms加一,其時序圖如下:

1、剛開始客戶端和服務端都處於CLOSED狀態,服務端先監聽某個埠,處於LISTEN狀態;

2、客戶端主動發起連線請求SYN=1,ACK=0,初始序號為x,之後處於SYN-SENT狀態;

3、服務端收到發起的連線請求,如果同意連線就返回SYN=1,ACK=1,確認號為 x+1,同時也選擇一個初始的序號 y,之後處於SYN-RCVD狀態;

4、客戶端收到服務端傳送的SYN和ACK之後,傳送ACK的ACK,確認號為 y+1,序號為 x+1。之後處於ESTABLISHED狀態,因為它一發一收成功了;

5、服務端收到ACK的ACK之後,處於ESTABLISHED狀態,因為它也一發一收了。

兩次握手或者四次不行嗎?

舉個例子:

在一個網路環境不可靠的情況下,A發出一個連線請求,發出一個請求杳無音信就會一直髮,終於有一個包到B了,但是A還不知道會繼續發;

收到A的請求之後,B如果同意連線就會發送應答包給A;但是B的應答包也是一入網路深似海啊,不知道能不能到A,所以當然不能認為和A已經建立了連線;

還有一個問題就是,A和B建立起短暫的連線通訊之後,A之前傳送的請求包饒了地球不知道多少圈竟然又到了B,假如B認為這是一個正常的連線請求,同意建立連線,但這個連線不會進行下去,也沒有個終結的時候,純屬單相思了,因而兩次握手肯定不行。

B傳送的應答可能會發送多次,但是隻要一次到達A,A就認為連線已經建立了,因為對於A來講,他的訊息有去有回。A會給B傳送應答之應答,而B也在等這個訊息,才能確認連線的建立,只有等到了這個訊息,對於B來講,才算它的訊息有去有回。

當然A發給B的應答之應答也會丟,也會繞路,甚至B掛了。按理來說,還應該有個應答之應答之應答,這樣下去就沒底了。四次握手、還是四十次握手都是可以的,哪怕四百次握手也不能百分百保證可靠,只要雙方的訊息都有去有回就可以了。

我們在程式設計的時候可以開啟keepalive機制,防止A建立連線後空著,不發資料;

2.2 TCP的四次揮手

過程如下:

A:B啊,我不想玩了;

B:哦,你不想玩了啊,我知道;

此時的A很可能是傳送完最後的資料就準備不玩了,不能在ACK的時候就關閉連線,此時B還沒有忙完自己的事情,還是可以傳送資料的,稱為半關閉狀態;

B:A啊,好吧,那我也不玩了,拜拜;

A:好的,拜拜;

斷開連線的時序圖如下所示:

雙方一開始都是處於建立連線的狀態:

A 傳送連線釋放報文,FIN=1,就進入FIN_WAIT_1的狀態;

B 收到之後發出確認,此時 TCP 屬於CLOSE_WAIT(半關閉)狀態,B 能向 A 傳送資料但是 A 不能向 B 傳送資料;

當 B 不再需要連線時,傳送連線釋放報文,FIN=1,就進入FIN_WAIT_2的狀態

A 收到後發出確認,進入 TIME-WAIT 狀態,等待 2 MSL(最大報文存活時間)後釋放連線;

B 收到 A 的確認後釋放連線;

四次揮手的原因

客戶端傳送了 FIN 連線釋放報文之後,伺服器收到了這個報文,就進入了 CLOSE-WAIT 狀態。這個狀態是為了讓伺服器端傳送還未傳送完畢的資料,傳送完畢之後,伺服器會發送 FIN 連線釋放報文。

TIME_WAIT

客戶端接收到伺服器端的 FIN 報文後進入此狀態,此時並不是直接進入 CLOSED 狀態,還需要等待一個時間計時器設定的時間 2MSL。這麼做有兩個理由:

確保最後一個確認報文能夠到達。如果 B 沒收到 A 傳送來的確認報文,那麼就會重新發送連線釋放請求報文,A

等待一段時間就是為了處理這種情況的發生。

等待一段時間是為了讓本連線持續時間內所產生的所有報文都從網路中消失,使得下一個新的連線不會出現舊的連線請求報文。

2.3 TCP狀態機

加黑加粗的部分,是上面說到的主要流程,其中阿拉伯數字的序號,是連線過程中的順

序,而大寫中文數字的序號,是連線斷開過程中的順序。加粗的實線是客戶端A的狀態變遷,加粗的虛線是服務端B的狀態變遷;

三、TCP協議(下)

參考了CS-Notes的博文,總結的很好!

TCP傳輸是可靠的,需要很多機制保證傳輸的可靠性,裡面也要有恆心,就是各種重傳的策略;還需要有智慧,裡面包含著大量的演算法。

如何成為一個靠譜的協議?

TCP中為了保證順序性,每一個包都有一個ID;建立連線的時候,會商定起始的ID是什麼,然後按照ID一個個傳送。採用**累計確認或者累計應答(cumulative acknowledgment)**的方式去保證不丟包;

為了記錄所有傳送的包和接收的包,TCP也需要傳送端和接收端分別都有快取來儲存這些記錄。傳送端的快取裡是按照包的ID一個個排列,根據處理的情況分成四個部分:

傳送了並且已經確認的;

傳送了並且尚未確認的;

沒有傳送,但是已經等待發送的;

沒有傳送,並且暫時還不會發送的;

3.1 可靠傳輸

TCP 使用超時重傳來實現可靠傳輸:如果一個已經發送的報文段在超時時間內沒有收到確認,那麼就重傳這個報文段。

一個報文段從傳送再到接收到確認所經過的時間稱為往返時間 RTT,加權平均往返時間 RTTs 計算如下:

其中,0 ≤ a < 1,RTTs 隨著 a 的增加更容易受到 RTT 的影響。

超時時間 RTO 應該略大於 RTTs,TCP 使用的超時時間計算如下:

其中 RTTd 為偏差的加權平均值。

3.2 TCP滑動視窗

視窗是快取的一部分,用來暫時存放位元組流。傳送方和接收方各有一個視窗,接收方通過 TCP 報文段中的視窗欄位告訴傳送方自己的視窗大小,傳送方根據這個值和其它資訊設定自己的視窗大小。

傳送視窗內的位元組都允許被髮送,接收視窗內的位元組都允許被接收。如果傳送視窗左部的位元組已經發送並且收到了確認,那麼就將傳送視窗向右滑動一定距離,直到左部第一個位元組不是已傳送並且已確認的狀態;接收視窗的滑動類似,接收視窗左部位元組已經發送確認並交付主機,就向右滑動接收視窗。

接收視窗只會對視窗內最後一個按序到達的位元組進行確認,例如接收視窗已經收到的位元組為 {31, 34, 35},其中 {31} 按序到達,而 {34, 35} 就不是,因此只對位元組 31 進行確認。傳送方得到一個位元組的確認之後,就知道這個位元組之前的所有位元組都已經被接收。

3.3 TCP 流量控制

流量控制是為了控制傳送方傳送速率,保證接收方來得及接收。

接收方傳送的確認報文中的視窗欄位可以用來控制傳送方視窗大小,從而影響傳送方的傳送速率。將視窗欄位設定為 0,則傳送方不能傳送資料。

3.4 TCP 擁塞控制

如果網路出現擁塞,分組將會丟失,此時傳送方會繼續重傳,從而導致網路擁塞程度更高。因此當出現擁塞時,應當控制傳送方的速率。這一點和流量控制很像,但是出發點不同。流量控制是為了讓接收方能來得及接收,而擁塞控制是為了降低整個網路的擁塞程度

TCP 主要通過四個演算法來進行擁塞控制:慢開始、擁塞避免、快重傳、快恢復。

傳送方需要維護一個叫做擁塞視窗(cwnd)的狀態變數,注意擁塞視窗與傳送方視窗的區別:擁塞視窗只是一個狀態變數,實際決定傳送方能傳送多少資料的是傳送方視窗。

接收方有足夠大的接收快取,因此不會發生流量控制;

雖然 TCP 的視窗基於位元組,但是這裡設視窗的大小單位為報文段。

3.4.1 慢開始與擁塞避免

傳送的最初執行慢開始,令 cwnd = 1,傳送方只能傳送 1 個報文段;當收到確認後,將 cwnd 加倍,因此之後傳送方能夠傳送的報文段數量為:2、4、8 …

注意到慢開始每個輪次都將 cwnd 加倍,這樣會讓 cwnd 增長速度非常快,從而使得傳送方傳送的速度增長速度過快,網路擁塞的可能性也就更高。設定一個慢開始門限 ssthresh,當 cwnd >= ssthresh 時,進入擁塞避免,每個輪次只將 cwnd 加 1。

如果出現了超時,則令 ssthresh = cwnd / 2,然後重新執行慢開始。

3.4.2 快重傳與快恢復

在接收方,要求每次接收到報文段都應該對最後一個已收到的有序報文段進行確認。例如已經接收到 M1 和 M2,此時收到 M4,應當傳送對 M2 的確認。

在傳送方,如果收到三個重複確認,那麼可以知道下一個報文段丟失,此時執行快重傳,立即重傳下一個報文段。例如收到三個 M2,則 M3 丟失,立即重傳 M3。

在這種情況下,只是丟失個別報文段,而不是網路擁塞。因此執行快恢復,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此時直接進入擁塞避免。

慢開始和快恢復的快慢指的是 cwnd 的設定值,而不是 cwnd 的增長速率。慢開始 cwnd 設定為 1,而快恢復 cwnd 設定為 ssthresh。

四、套接字Socket

在通訊之前,雙方都要建立一個Socket。Socket程式設計進行的是端到端的通訊,也只能是端到端協議之上網路層和傳輸層的。

在網路層中,Socket函式需要指定到底是IPv4還是IPv6,分別對應設定為AF_INET和AF_INET6。還要指定到底是TCP還是UDP,TCP協議是基於資料流的,所以設定為SOCK_STREAM,而UDP是基於資料報的,因而設定為SOCK_DGRAM。

4.1 基於TCP協議的Socket程式函式呼叫過程

兩端建立Socket之後,TCP的服務端呼叫bind函式監聽一個埠, 給這個Socket賦予一個IP地址和埠;

當服務端有了IP和埠號,就可以呼叫listen函式進行監聽。此時的客戶端就可以發起連線請求了;

在核心中為每個Socket維護兩個佇列,分別是已經建立了連線、完成三次握手後處於established狀態的佇列;一個是還沒有完全建立連線的佇列,三次握手還沒完成,處於syn_rcvd的狀態。

接下來,服務端呼叫accept函式,拿出一個已經完成的連線進行處理。

在服務端等待的時候,客戶端可以通過connect函式發起連線。先在引數中指明要連線的IP地址和埠號,然後開始發起三次握手。核心會給客戶端分配一個臨時的埠。一旦握手成功,服務端的accept就會返回另一個Socket。

連線建立之後雙方開始通過read和write函式來讀寫資料,下圖是基於TCP協議的Socket程式函式呼叫過程:

4.2 基於UDP協議的Socket程式函式呼叫過程

UDP是沒有連線的,所以不需要三次握手,也就不需要呼叫listen和connect,但是,UDP的的互動仍然需要IP和埠號,因而也需要bind函式;但正是沒有連線狀態,每次通訊的時候,都呼叫sendto和recvfrom,都可以傳入IP地址和埠;

下圖就是基於UDP協議的Socket程式函式呼叫過程:

4.3 伺服器如何支援高併發?

在學習了上面的Socket函式之後,可以寫一個簡單的網路互動程式;

系統會用一個四元組來標識一個TCP連線:

{本機IP, 本機埠, 對端IP, 對端埠}

1

最大TCP連線數=客戶端IP數×客戶端埠數,對IPv4,客戶端的IP數最多為2的32次方,客戶端的埠數最多為2的16次方,也就是服務端單機最大TCP連線數,約為2的48次方。

當然最大的TCP連線數還要受到 Socket中的檔案描述符以及記憶體的限制;

如何在資源有限的情況下,進行更多的連線?

基於程序或者執行緒模型都存在一個問題:

新到來一個TCP連線,就需要分配一個程序或者執行緒。一臺機器無法建立很多程序或者執行緒,就是C10K的問題;

C10K:

一臺機器要維護1萬個連線,就要建立1萬個程序或者執行緒,那麼作業系統是無法承受的。如果維持1億使用者線上需要10萬臺伺服器,成本也太高了。

方案三:IO多路複用,一個執行緒維護多個Socket

簡述一下就是,一個專案組可以看多個專案,每個專案組都應該有個專案進度牆,將自己組看的專案列在那裡,然後每天通過專案牆看每個專案的進度,一旦某個專案有了進展,就派人去盯一下。

Socket是檔案描述符,因而某個執行緒盯的所有的Socket,都放在一個檔案描述符集合fd_set(專案進度牆)中,呼叫select函式來監聽檔案描述符集合是否有變化,一旦有變化,就會依次檢視每個檔案描述符。那些發生變化的檔案描述符在fd_set對應的位都設為1,表示Socket可讀或者可寫,從而可以進行讀寫操作,然後再呼叫select,接著盯著下一輪的變化。

方案四:IO多路複用

方案三中採用select函式來檢視fd_set是否有Socket發生變化,每次輪詢都會影響效能,且能檢視的數量由FD_SETSIZE限制;

改成事件通知的方式,情況就會好很多,專案組不需要通過輪詢挨個盯著這些專案,而是當專案進度發生變化的時候,主動通知專案組,然後專案組再根據專案進展情況做相應的操作。

通過epoll多路複用模型,它不是通過輪詢的方式,而是通過註冊callback函式的

方式,當某個檔案描述符傳送變化的時候,就會主動通知。

如上圖所示,程序打開了Socket m, n, x等多個檔案描述符,現在需要通過epoll來監聽這些Socket是否都有事件發生。其中epoll_create建立一個epoll物件,對應著開啟檔案列表中那個的一項,通過紅黑樹來儲存這個epoll要監聽的所有Socket。

當epoll_ctl新增一個Socket的時候,其實是加入這個紅黑樹;當一個Socket來了一個事件的時候,可以從這個列表中得到epoll物件,並呼叫call back通知它。

這種通知方式使得監聽的Socket資料增加的時候,效率不會大幅度降低,能夠同時監聽的Socket的數目也非常的多;

總結

需要記住TCP和UDP的Socket的程式設計中,客戶端和服務端都需要呼叫哪些函式;

能夠支撐大量連線的高併發的服務端不容易,需要多程序、多執行緒,而epoll機制能解決C10K問題。

————————————————

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • IT兄弟連 HTML5教程 CSS3揭祕 CSS簡介