TCP提供一種面向連接的、可靠的字節流服務。面向連接意味着兩個使用TCP的應用(通常是一個客戶和一個服務器)在彼此交換數據包之前必須先建立一個TCP連接。
應用數據被分割成TCP認爲最適合發送的數據塊。這和UDP完全不同,應用程序產生的數據長度將保持不變。由TCP傳遞給IP的信息單位稱爲報文段或段(segment)。
TCP是一種流協議(stream protocol)。這就意味着數據是以字節流的形式傳遞給接收者的,沒有固有的"報文"或"報文邊界"的概念。從這方面來說,讀取TCP數據就像從串行端口讀取數據一樣--無法預先得知在一次指定的讀調用中會返回多少字節。
爲了說明這一點,我們假設在主機A和主機B的應用程序之間有一條TCP連接,主機A上的應用程序向主機B發送一條報文。進一步假設主機A有兩條報文要發送,並兩次調用send來發送,每條報文調用一次。很自然就會想到從主機A向主機B發送的兩條報文是作爲兩個獨立實體,在各自的分組中發送的,如圖2-25所示。
圖2-25 發送兩條報文的錯誤模型 |
圖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通信的本質,不會應平臺的不同而改變。