TCP 流協議和消息分幀的理解

TCP提供一種面向連接的、可靠的字節流服務。面向連接意味着兩個使用TCP的應用(通常是一個客戶和一個服務器)在彼此交換數據包之前必須先建立一個TCP連接。

應用數據被分割成TCP認爲最適合發送的數據塊。這和UDP完全不同,應用程序產生的數據長度將保持不變。由TCP傳遞給IP的信息單位稱爲報文段或段(segment)。

兩個應用程序通過TCP連接交換8bit字節構成的字節流。TCP不在字節流中插入記錄標識符。我們將這稱爲字節流服務(bytestreamservice)。如果一方的應用程序先傳10字節,又傳20字節,再傳50字節,連接的另一方將無法瞭解發方每次發送了多少字節。只要自己的接收緩存沒有塞滿,TCP 接收方將有多少就收多少。一端將字節流放到TCP連接上,同樣的字節流將出現在TCP連接的另一端。
另外,TCP對字節流的內容不作任何解釋。TCP不知道傳輸的數據字節流是二進制數據,還是ASCⅡ字符、EBCDIC字符或者其他類型數據。對字節流的解釋由TCP連接雙方的應用層解釋。
這種對字節流的處理方式與Unix操作系統對文件的處理方式很相似。Unix的內核對一個應用讀或寫的內容不作任何解釋,而是交給應用程序處理。對Unix的內核來說,它無法區分一個二進制文件與一個文本文件。

以下節選 : 《TCP/IP高效編程:改善網絡程序的44個技巧》

TCP是一種流協議(stream protocol)。這就意味着數據是以字節流的形式傳遞給接收者的,沒有固有的"報文"或"報文邊界"的概念。從這方面來說,讀取TCP數據就像從串行端口讀取數據一樣--無法預先得知在一次指定的讀調用中會返回多少字節。

爲了說明這一點,我們假設在主機A和主機B的應用程序之間有一條TCP連接,主機A上的應用程序向主機B發送一條報文。進一步假設主機A有兩條報文要發送,並兩次調用send來發送,每條報文調用一次。很自然就會想到從主機A向主機B發送的兩條報文是作爲兩個獨立實體,在各自的分組中發送的,如圖2-25所示。

 
圖2-25 發送兩條報文的錯誤模型
但不幸的是,實際的數據傳輸過程很可能不會遵循這個模型。主機A上的應用程序會調用send,我們假設這條寫操作的數據被封裝在一個分組中傳送給B。實際上,send通常只是將數據複製到主機A的TCP/IP棧中,就返回了。由TCP來決定(如果有的話)需要立即發送多少數據。做這種決定的過程很複雜,取決於很多因素,比如發送窗口(當時主機B能夠接收的數據量),擁塞窗口(對網絡擁塞的估計),路徑上的最大傳輸單元(沿着主機A和B之間的網絡路徑一次可以傳輸的最大數據量),以及連接的輸出隊列中有多少數據。更多與此有關的內容請參見技巧15。圖2-26只顯示了主機A的TCP封裝數據時可能使用的諸多方法中的4種。在圖2-26中,M11和M12表示M1的第一和第二部分,M21和M22與之類似。如圖2-26所示,TCP不一定會將一條報文的全部內容都放在一個分組中傳送出去。
 
圖2-26 封裝兩條報文可能採用的4種方式

現在,我們從主機B應用程序的角度來看這種情形。總的來說,主機B應用程序任意一次調用recv時,都不會對TCP發送給它的數據量做任何假設。比如,當主機B應用程序讀取第一條報文時,可能會出現下列4種結果。

實際上,可能的結果不止4種,但我們忽略了出錯和EOF之類的結果。我們還假設應用程序讀取了所有可讀的數據。

(1)沒有數據可讀,應用程序阻塞,或者recv返回一條指示說明沒有數據可讀。到底會發生什麼情況取決於套接字是否標識爲阻塞,以及主機B的操作系統爲系統調用recv指定了什麼樣的語義。

(2)應用程序獲取了報文M1中的部分而不是全部數據。比如,發送端TCP像圖2-26D那樣對數據進行分組就會發生這種情況。

(3)應用程序獲取了報文M1中所有的數據,除此之外沒有任何其他內容。如果像圖2-26A那樣對數據分組就會發生這種情況。

(4)應用程序獲取了報文M1的所有數據,以及報文M2的部分或全部數據。如果像圖2-26B或圖2-26C那樣對數據進行分組就會發生這種情況。

注意,這裏還有一個定時問題。如果主機B的應用程序在主機A發送了第二條報文之後一段時間內都沒有讀取第一條報文,那麼這兩條報文都會成爲可讀的。這就和圖2-26B所示情況相同了。這些描述說明,通常,在任意指定時刻,可讀的數據量都是不確定的。

需要再次說明的是,TCP是一個流協議(stream protocol),儘管數據是以IP分組的形式傳輸的,但分組中的數據量與send調用中傳送給TCP多少數據並沒有直接關係。而且,接收程序也沒有什麼可靠的方法可以判斷數據是如何分組的,因爲在兩次recv調用之間可能會有多個分組到來。

即使接收端應用程序的響應非常及時,也可能會發生這種情況。例如,一個分組丟失了(參見技巧12,在當今的因特網中,這是非常常見的情況),而且後繼分組都安全到達,TCP會將後繼分組中的數據保存起來,直到重傳第一個分組並正確收到爲止。此時,所有數據對應用程序都是可用的。

TCP會記錄它發送了多少字節,以及確認的字節,但它不會記錄這些字節是如何分組的。實際上,有些實現在重傳丟失分組的時候傳送的數據可能比原來的多一些或少一些。這就足以支撐下面再次重複說明的內容了。

對TCP應用程序來說,就沒有"分組"這種概念。如果應用程序的設計與TCP對數據的分組方式有所關聯,就應該考慮重新設計這個應用程序了。

既然任意一次指定的讀操作中返回的數據量都是不可預測的,就必須在應用程序中做好應對這種情況的準備。通常這不是什麼問題。比如說,我們可能在用fgets這樣標準的I/O庫程序讀取數據。在這種情況下,fgets會將字節流劃分成行。圖3-6顯示了一個這樣的例子。在其他情況下的確需要關注報文邊界問題,而這些情況下邊界都是由應用程序級維護的。

最簡單的情況就是定長報文。在這種情況下,只需要讀取報文中固定數量的字節就可以了。根據前面的討論,讀操作返回的字節數可能小於sizeof(msg)(圖2-26D),所以只進行

recv(s, msg, sizeof(msg), 0); 

處理這種情況的標準方法,完成數據的完整接收:

int readn(SOCKET fd, char *buf, size_t len)
{
     int cnt = len;
     int recvlen;
     recvlen = recv(fd, buf, cnt, 0)
     if(recvlen < 0)
     {
        /* EINTR: function was interrupted by a signal that was caught, before any data was available */
            if(errno == EINTR)
                  continue;
             return -1;
      }
      /* EOF */
      if(recvlen == 0)
         return len - cnt;
     
      buf += recvlen;
      cnt -=  recvlen;
   }
   return len;
}     

簡單理解:

TCP是流協議,不區分流邊界,如果不分幀,那麼可能會發生你send了無數次,但是對方只接收一次的情況

client 發送字節流時,TCP會保證server 按順序接收到全部的字節流,其他諸如數據包的大小等,TCP協議對我們來說是透明的,我們可以全部不考慮。

    通俗點說,我們發送數據只需要調用send函數,我們只需要關注send函數的返回值,從而知道了發送了多少個字節,在服務端,我們調用recv函數,我們只需要關注recv函數的返回值,從而知道接收了多少個字節,其他情況通通不管。

   在TCP通信過程中,我們不需要關心(也沒法關心,但可以設置)數據包的大小,個數,我們只需要在客戶端建立一個緩衝區不斷髮送,在服務端建立一個緩衝區不斷接收就夠了,當然,我們還可以定義一個包頭,來實現諸如發送文件這樣更強大的功能。

這就是TCP通信的本質,不會應平臺的不同而改變




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章