1. 程式人生 > >記住,TCP是一種流協議

記住,TCP是一種流協議

TCP是一種流協議(stream protocol)。

這就意味著資料是以位元組流的形式傳遞給接收者的,沒有固有的”報文”或”報文邊界”的概念。從這方面來說,讀取TCP資料就像從串列埠讀取資料一樣–無法預先得知在一次指定的讀呼叫中會返回多少位元組。

為了說明這一點,我們假設在主機A和主機B的應用程式之間有一條TCP連線,主機A上的應用程式向主機B傳送一條報文。進一步假設主機A有兩條報文要傳送,並兩次呼叫send來發送,每條報文呼叫一次。很自然就會想到從主機A向主機B傳送的兩條報文是作為兩個獨立實體,在各自的分組中傳送的,如圖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不一定會將一條報文的全部內容都放在一個分組中傳送出去。 在這裡插入圖片描述

現在,我們從主機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);

這樣的簡單呼叫是不夠的。圖2-27顯示了處理這種情況的標準方法。 在這裡插入圖片描述

函式readn的用法與read非常相似,但在讀到len位元組,並從對等實體收到EOF,或出現錯誤之前,它是不會返回的。我們將其定義如下。

#include "etcp.h"  
int readn( SOCKET s, char *buf, size_t len );  

readn使用的邏輯與從串列埠,或者從其他基於流的、在任意指定時間內可讀取資料量都未知的源端,讀取指定數量的位元組所使用的邏輯一樣,這不足為奇。實際上,在所有這些情況下都可以,也經常使用readn(用int代替SOCKET,用read代替recv)。

如果recv呼叫被訊號中斷,第11行和第12行的if語句

if ( error == EINTR)        /* interrupted? */   
    continue;               /* restart the read*/  

會重啟recv呼叫。有些系統會自動重啟被中斷的系統呼叫,這種系統就不需要這兩行程式了。從另一個角度來看,這兩行程式碼也不會帶來什麼問題,因此為了實現最大限度的可移植性,最好還是把它們放在那裡。

對必須支援可變長報文的應用程式來說,有兩種可用的方法。第一種,可以用記錄結束標記來分隔記錄。如前所述,使用fgets這樣標準的I/O程式將報文分成單個行時,就會發生這種情況。使用標準I/O程式時,很自然地會將新行作為記錄結束標記使用。但使用這種方法通常會有一些問題。首先,除非在報文主體中從未用到記錄結束標記,否則傳送程式就要在報文中掃描這些標記,對其進行轉義,或者編碼,以免將其誤認作記錄結束標記。比如,如果將記錄分隔字元RS作為記錄結束標記使用,傳送端就要搜尋報文主體,找到所有RS字元,並對其進行轉義,比如在前面加上一個\。這就意味著要轉移資料以便為轉義字元騰出位置。當然,還要對出現的所有轉義字元進行轉義。因此,如果用\作轉義字元的話,就要將報文主體中出現的所有\都改成\。

在接收端,必須再次對整條報文進行掃描,這次要移除轉義字元,並搜尋(未轉義的)記錄結束標記。使用記錄結束標記要對整條報文掃描兩次,所以最好只在那些有”自然”記錄結束標記的情況下使用,比如用換行符分隔文字行記錄的時候。

另外一種處理可變記錄的方法是在每條報文前面加上一個首部,這個首部(至少)包含下面的報文長度,如圖2-28所示。 在這裡插入圖片描述

在這裡插入圖片描述

讀取記錄長度

6-8 將記錄長度讀入reclen中,如果readn返回的長度不等於interger型別的大小,readvrec就返回0(EOF),如果出錯就返回 1。

9 將記錄長度從網路位元組序轉換為主機位元組序。更多相關內容請參見技巧28。

檢視是否裝得下記錄

10-27 檢視呼叫程式的緩衝區大小,驗證它能否裝下整條記錄。如果緩衝區中沒有足夠的空間,就依次將長度為len的片段讀入緩衝區,並將記錄丟棄。丟棄記錄之後,將errno設定為EMSGSIZE,readvrec返回 1。

讀取記錄

29-32 最後,讀取記錄本身。根據readn返回的是錯誤、不足計數還是成功返回,readvrec會向呼叫程式返回 1、0或者reclen。

readvrec是個很有用的函式,會在其他一些技巧中用到,所以將其定義記錄如下。

#include "etcp. h"  
int readvrec( SOCKET s, char *buf, size_t len );  
// 返回:讀取的位元組數,或者在出錯時返回-1  

圖2-30顯示了一個用readvrec從TCP連線中讀取變長記錄,並將其寫入stdout的簡單伺服器程式碼。

在這裡插入圖片描述 在這裡插入圖片描述

10-17 初始化伺服器,並接受一個連線。

20-24 呼叫readvrec讀取下一個變長記錄。如果出錯,列印一條診斷資訊,並讀取下一條記錄。如果readvrec返回一個EOF,就列印一條提示訊息,伺服器退出。

26 將記錄寫入stdout。

圖2-31顯示了對應的客戶端程式,這個程式從其標準輸入讀取報文,附加上報文長度,然後將其傳送給伺服器。 在這裡插入圖片描述

定義分組結構

6-10 定義packet結構,呼叫send時用它來裝載報文及其長度。資料型別u_int32_t是一個無符號的32位元整數。

連線、讀取並逐行傳送

12 客戶端通過呼叫tcp_client連線到伺服器。

13-21 呼叫fgets從stdin中讀取一行資料,並將其放入報文分組的buf欄位中。行的長度由對strlen的呼叫決定,將這個值轉換成網路位元組序後,放入報文分組的reclen欄位中。最後,呼叫send向伺服器傳送分組。

傳送這些由兩個或多個部分組成的報文的另一種方法請參見技巧24。

在sparc上啟動伺服器vrs,然後在bsd上啟動客戶端vrc,來測試這些程式。將程式的執行並排顯示出來,就可以看到客戶端的輸入,以及相應的伺服器輸出了。第4行還對錯誤訊息進行了換行顯示。 在這裡插入圖片描述 伺服器緩衝區有10位元組,所以傳送11位元組1, …, 0, 時,readvrec會返回一條錯誤。

小結

初級網路程式設計師最常犯的錯誤之一就是無法理解TCP傳送的是一個沒有記錄邊界概念的位元組流。這一點很重要,可以總結為TCP中沒有使用者可見的”分組”概念,它只是傳送了一個位元組流,我們無法準確地預測在一個特定的讀操作中會返回多少位元組。

轉載自《TCP/IP高效程式設計 改善網路程式的44個技巧》–技巧6:記住TCP是一種流協議