轉(zhuǎn):http://www.shnenglu.com/fwxjj/archive/2009/03/17/76923.html隨著互聯(lián)網(wǎng)應(yīng)用廣泛推廣,出現(xiàn)了越來越多的網(wǎng)絡(luò)應(yīng)用,其中基于p2p思想的各種網(wǎng)絡(luò)技術(shù)的產(chǎn)品也越來越多的出現(xiàn)在我們的視野當(dāng)中。從最早聞名的Napster到現(xiàn)在的Bittorrent、eMule、skype等產(chǎn)品,P2P這種網(wǎng)絡(luò)應(yīng)用模式已經(jīng)從各個方面深入人心。這些產(chǎn)品在各自的網(wǎng)絡(luò)實(shí)現(xiàn)技術(shù)上,都以各自的方法解決著同樣面臨的一個問題,如何讓他們的軟件產(chǎn)品在各異的網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)中順利的進(jìn)行P2P通信。
眾所周知,在當(dāng)今的網(wǎng)絡(luò)拓?fù)浣Y(jié)構(gòu)中,普遍存在使用NAT設(shè)備來進(jìn)行網(wǎng)絡(luò)地址轉(zhuǎn)換,而讓應(yīng)用程序能跨越這些NAT設(shè)備進(jìn)行全雙工的通信,就成為非常重要的一個問題。對于實(shí)現(xiàn)跨越NAT通信可以采取很多種辦法(對于能夠直接連接、反向連接的情況不在此列):首先是通過服務(wù)器進(jìn)行轉(zhuǎn)發(fā),這是比較粗暴的方法,而且在用戶量大的時候,轉(zhuǎn)發(fā)服務(wù)器需要付出相當(dāng)大的代價;第二,可以使用NAT穿透技術(shù)。而大家知道關(guān)于NAT穿透中,UDP穿透的成功率比起TCP穿透要高出許多,這一點(diǎn)這里將不做多述,可以參考Bryan Ford的文章《Peer-to-Peer Communication Across Network Address Translators》(
http://www.brynosaurus.com/pub/net/p2pnat/)。因此在UDP協(xié)議上構(gòu)建一些大型的網(wǎng)絡(luò)應(yīng)用程序可能會成為很多人的需求。
當(dāng)然也可能基于更多的原因,會有很多人希望能在UDP協(xié)議上進(jìn)行大型應(yīng)用程序的構(gòu)建。然而UDP協(xié)議本身存在著不通信不可靠的缺點(diǎn),于是對于基于UDP進(jìn)行可靠通信的需求就浮現(xiàn)出來了。目前在網(wǎng)絡(luò)上有許多人正做著這一工作,UDT、RakNet、eNet等都是構(gòu)建在UDP之后網(wǎng)絡(luò)可靠通信開發(fā)庫。然后這些庫開發(fā)時都針對了一些特殊應(yīng)用來進(jìn)行設(shè)計(jì)的,不具備通用性。比如RakNet是為游戲應(yīng)用而設(shè)計(jì),對于實(shí)時性等游戲相關(guān)的網(wǎng)絡(luò)需求有很好的支持,對于大批量數(shù)據(jù)傳輸卻有點(diǎn)力所不及。而UDT基于一種基于帶寬速率控制的擁塞控制算法進(jìn)行設(shè)計(jì)(
http://udt.sourceforge.net/doc/draft-gg-udt-01.txt),主要用在小數(shù)量的bulk源共享富裕帶寬的情況下,最典型的例子就是建立在光纖廣域網(wǎng)上的網(wǎng)格計(jì)算,而在ISP提供帶寬有限的情況下運(yùn)行卻顯得消耗資源并性能不足。甚至可能被防火墻,或ISP服務(wù)商判斷為惡意帶寬使用攻擊。這些都使用得他們不能被廣泛地用于各種網(wǎng)絡(luò)應(yīng)用程序。另外大家也陸續(xù)發(fā)現(xiàn)目前的UDT實(shí)現(xiàn)版本存在的一些問題。比如UDT做服務(wù)端接收連接時,總是新開一個端口與客戶端進(jìn)行連接,這樣會帶來幾個問題:1)較多客戶端連接上來時,服務(wù)端新打開的眾多端口中可能有的端口會被防火墻攔截而導(dǎo)致通信失敗,2)如果客戶端處于Symmetric NAT和Port-Restricted Cone NAT后面時,將導(dǎo)致服務(wù)器端與客戶端連接無法成功建立,3)由于udp端口數(shù)最大值有限,所以UDT服務(wù)器端可接收的連接數(shù)也因些受限。再有就是不僅僅是UDT庫,基本上所有的UDP-based可靠通信庫,都未提供穿越proxy代理的功能(socks5);再有就是對UDP打洞技術(shù)有的支持得不完善或并不支持。
基于這些原因,使得我需要開發(fā)一個基于UDP協(xié)議之上實(shí)現(xiàn)一個可靠、高效、通用的通信庫,來滿足我目前所開發(fā)的項(xiàng)目的需要。TCP協(xié)議算法已經(jīng)是經(jīng)過多方面及多年的驗(yàn)證,是最具通用性,且可靠高效的。雖然UDT等各種庫指出TCP在這樣或那樣的網(wǎng)絡(luò)環(huán)境下存在不足,但眾多實(shí)現(xiàn)當(dāng)中他仍然是最通用、可靠、高效的。相信有許多人跟我一樣,需要這么一個開發(fā)庫,所以我打算在開發(fā)過程中,陸續(xù)公開相關(guān)的文檔及這個開發(fā)庫。
二、設(shè)計(jì)目標(biāo)
TDP主要的目標(biāo)就是在UDP層之上實(shí)現(xiàn)TCP的協(xié)議算法,使得應(yīng)用程序能夠在UDP層之上獲得通用、可靠、高效的通信能力。
TDP網(wǎng)絡(luò)開發(fā)庫所實(shí)現(xiàn)的算法,都來自久經(jīng)考驗(yàn)的TCP協(xié)議算法,網(wǎng)上有著非常多的參考資料。在實(shí)現(xiàn)當(dāng)中,參考最多的是Richard Stevens的《TCP/IP詳解》。
TDP提供的用于開發(fā)的應(yīng)用程序接口與Socket API非常相像,姑且稱之為TDP Socket API,基本上的函數(shù)名與參數(shù)等都與Socket API相一致,但是TDP Socket API的API接口都位于命名空間TDP當(dāng)中。只要使用過Socket API進(jìn)行開發(fā)過的朋友,將都會使用TDP庫進(jìn)行開發(fā)。下圖為TDP及TDP Socket API所處在的協(xié)議棧應(yīng)用中的位置,以及與TCP協(xié)議棧應(yīng)用的對比。
三、協(xié)議說明
1.協(xié)議格式
TDP的實(shí)現(xiàn)的算法雖然與TCP實(shí)現(xiàn)的算法是大致相同的,但TDP的協(xié)議格式只是從TCP協(xié)議格式獲得參考,但并不完全與他相同。TDP的協(xié)議格式如下:
接下來介紹一下協(xié)議格式的各個字段含義。
4位首部長度:表示用戶數(shù)據(jù)在數(shù)據(jù)包中的起始位置。
LIV:連接?;顦?biāo)志,用于表示TDP連接通路存活狀態(tài)。
ACK:確認(rèn)序號有效。
PSH:接收方應(yīng)該盡快將這個報文段交給應(yīng)用層。
RST:重建連接。
SYN:同步序號用來發(fā)起一個連接。
FIN:發(fā)端完成發(fā)送任務(wù)。
16位窗口大?。航邮斩丝山邮諗?shù)據(jù)的窗口大小。
選項(xiàng):只有一個選項(xiàng)字段,為最長報文大小,即MSS。TDP選項(xiàng)格式與TCP選項(xiàng)格式一致,kind=0時表示選項(xiàng)結(jié)束,kind=1時表示無操作,kind=2時表示最大報文段長度。如下圖:
數(shù)據(jù):用戶通過TDP傳輸?shù)臄?shù)據(jù)。
2.TDP連接建立與終止
TDP的連接建立與終止可以參考TCP的狀態(tài)變遷圖(此圖的詳細(xì)解釋請參考《TCP/IP詳解 卷一》第18章),如下:
2.1連接建立
2.1.1三次握手
連接建立分要經(jīng)過三次握手過程:1)客戶端發(fā)送一個SYN段到指明客戶打算連接的服務(wù)器的端口,報文段中要設(shè)置客戶端初始序號。2)服務(wù)器發(fā)回包含服務(wù)器的初始序號的SYN報文段作為應(yīng)答。同時,將確認(rèn)序號設(shè)置為客戶的初始序號加1,并設(shè)置ACK位標(biāo)志報文段為確認(rèn)報文段。3)客戶端必須將確認(rèn)序號設(shè)置為服務(wù)器初始序號加1,對服務(wù)器的SYN報文段進(jìn)行確認(rèn)。
TDP在全局維護(hù)一個初始序號種子,這個初始序號為隨時產(chǎn)生的32位整數(shù)。
連接建立的超時和重傳初始值為3秒,超時采用指數(shù)退避算法,3秒超時后超時值為6秒,然后是12秒,24秒……。連接建立最長時間限制為75秒。
2.1.2 NAT UDP PUNCH模式
當(dāng)TDP工作模式是NAT UDP PUNCH時,在三次握手之前,向?qū)Χ薔AT端口及預(yù)測端口間隔默認(rèn)2ms發(fā)送默認(rèn)為10個LIV報文段,一來用于打開自已的NAT端口,二來是用于進(jìn)入對端NAT端口。默認(rèn)值可以由用戶程序設(shè)置。這時的LIV報文段中初始序號及確認(rèn)序號都為0。
當(dāng)接收到對端LIV報文段后,立即停止LIV報文段發(fā)送,發(fā)出SYN報文段進(jìn)行連接建立。這時有兩種可能:其一是另一端直到接收到該SYN報文段之前,都沒有接收到LIV報文段,或是剛接收到但沒有來得及發(fā)送SYN報文段,此時將會如上文描述的正常模式下連接建立的過程一致,將經(jīng)歷三次握手?;橇硪欢嗽诮邮盏皆揝YN報文段之前,也已經(jīng)發(fā)送出SYN報文段,此時雙方都需要對SYN報文段進(jìn)行確認(rèn),可以稱之為四次握手。
2.1.3 最大傳輸報文大小(MSS)
TCP報文段在連接建立時需要通報MSS,在TDP的實(shí)現(xiàn)中也進(jìn)行通報,默認(rèn)通報為1460字節(jié)(符合以太網(wǎng)標(biāo)準(zhǔn),這個默認(rèn)值允許20字節(jié)的IP首部、8字節(jié)的UDP首部和12字節(jié)的TDP首部,以適合 1500字節(jié)的IP數(shù)據(jù)報)默認(rèn)值可以由用戶程序設(shè)置。
TCP在對端地址為非本地IP時,默認(rèn)通報為536字節(jié)。TDP之所以默認(rèn)通報為1460,是因?yàn)門DP在數(shù)據(jù)傳輸過程中,實(shí)現(xiàn)了路徑MTU發(fā)現(xiàn)技術(shù),通過實(shí)際發(fā)現(xiàn)的MTU,進(jìn)行MSS的動態(tài)調(diào)整,以盡量避免報文段在網(wǎng)絡(luò)中的傳輸產(chǎn)生分片的情況。路徑MTU發(fā)現(xiàn)技術(shù)在傳輸數(shù)據(jù)流一節(jié)中進(jìn)行描述。
2.1.4 半打開連接及連接?;?br> 半打開連接是指對端異常關(guān)閉,如網(wǎng)線拔掉、突然斷電等情況將引發(fā)一端導(dǎo)演關(guān)閉,而另一端的連接卻仍然認(rèn)為連接處于打開當(dāng)中,這種情況稱之為半打開連接。TDP中的一個TDP SOCKET描述符由本地IP、本地端口、遠(yuǎn)端IP、遠(yuǎn)端端口唯一確定。當(dāng)遠(yuǎn)端客戶端連接請求到來時,服務(wù)端將接收到一個新的TDP SOCKET描述符,當(dāng)這一個描述符唯一確定信息已經(jīng)存在時,對新的連接請求發(fā)送RST報文段,通知其重置連接請求。對于舊的連接,由保活機(jī)制自動發(fā)現(xiàn)是否為半打開連接,如果是半打開連接,則自動關(guān)閉該連接。這里RST報文段與TCP中的RST報文段有些不一樣,TCP的RST報文段工作描述請參考《TCP/IP詳解 卷一》。
連接建立之后,TDP連接需要啟動保活機(jī)制。TCP連接在沒有數(shù)據(jù)通信的情況下也能保持連接,但TDP連接不行。TDP連接在一定時間段內(nèi)如果沒有數(shù)據(jù)交互的話,將主動發(fā)送保活LIV報文段。這個時間段根據(jù)TDP連接工作模塊不同有所差異,在NAT UDP PUNCH模式下,這個時間段默認(rèn)值為1分鐘(大多數(shù)的NAT中,UDP會話超時時間為2-5分鐘左右);而在常規(guī)模塊下這個時間段默認(rèn)值為5分鐘。默認(rèn)值可以由用戶程序設(shè)置,用戶程序需要指明兩種模塊下的?;顣r間周期。這里TDP的保活機(jī)制與TCP中的?;顧C(jī)制完全不一樣,TCP的保活機(jī)制描述請參考《TCP/IP詳解 卷一》。
2.2連接關(guān)閉
TDP連接與TCP連接一樣是全雙工的,因此每個方向必須單獨(dú)地進(jìn)行關(guān)閉??蛻魴C(jī)給服務(wù)器一個FIN報文段,然后服務(wù)器返回給客戶端一個確認(rèn)ACK報文,并且發(fā)送一個FIN報文段,當(dāng)客戶機(jī)回復(fù)ACK報文后(四次握手),連接就結(jié)束了。
TDP連接的一端接收到FIN報文段時,如果還有數(shù)據(jù)要發(fā)送,需要繼續(xù)將數(shù)據(jù)進(jìn)行發(fā)送完成,然后才發(fā)出FIN報文段;如果還有數(shù)據(jù)未從緩存中取出,將取出數(shù)據(jù),并進(jìn)行確認(rèn),直到所有確認(rèn)完成之后,然后才發(fā)出FIN報文段(此時如果有亂序的報文段情況不進(jìn)行處理)。上面的描述也表現(xiàn)出,TDP是支持半關(guān)閉的,當(dāng)一端發(fā)出FIN報文段時,仍然允許接收另一端數(shù)據(jù)。但是半關(guān)閉可能導(dǎo)致連接永遠(yuǎn)停留在狀態(tài)圖中FIN_WAIT_2狀態(tài)中,此時保活機(jī)制仍然在工作當(dāng)中,如果對端已經(jīng)關(guān)閉,那么?;顧C(jī)制將在檢測到時立即關(guān)閉這一連接。
下圖是一個典型的連接建立與連接關(guān)閉的示意圖,此圖摘自《TCP/IP詳解 卷一》。

四、TDP傳輸數(shù)據(jù)流
1.傳輸?shù)膱笪亩?br>
在TDP工作過程中傳輸?shù)乃袌笪亩?,只有SYN報文段、FIN報文段、數(shù)據(jù)報文段是可靠的之外,其它報文段如ACK報文段、LIV報文段、RST報文段等都不是可靠的。SYN報文段與FIN報文段傳輸中都占用一個序號,數(shù)據(jù)報文段在傳輸中根據(jù)傳輸?shù)臄?shù)據(jù)字節(jié)數(shù)占用相應(yīng)的序號,其它報文段不占用傳輸序號。
成功接收數(shù)據(jù)報文段,應(yīng)當(dāng)將按序?qū)ο乱粋€期望的數(shù)據(jù)報文段的序號作為確認(rèn)序號發(fā)送ACK報文段進(jìn)行確認(rèn)。當(dāng)出現(xiàn)接收到亂序的數(shù)據(jù)報文段時,將亂序數(shù)據(jù)報文段按序緩存,并發(fā)送期望報文段的ACK報文段進(jìn)行確認(rèn)。ACK報文段的發(fā)送并非即時的,也并非是對應(yīng)接收數(shù)據(jù)報進(jìn)行一對一確認(rèn)發(fā)送。ACK報文段由200ms定時觸發(fā)發(fā)送,也就是說ACK報文段要經(jīng)受最多200ms的時延進(jìn)行發(fā)送。ACK報文段對此時期望的數(shù)據(jù)序號進(jìn)行確認(rèn),因此并不是與接收數(shù)據(jù)報相對應(yīng)。ACK報文段是不可靠的,當(dāng)丟失時對端將無法了解接收情況,因此發(fā)送方將會有一個超時機(jī)制,如果發(fā)現(xiàn)確認(rèn)的ACK報文段超時,發(fā)送方將重發(fā)該數(shù)據(jù)報,這一點(diǎn)在第五節(jié)進(jìn)行詳細(xì)描述。
2.路徑MTU發(fā)現(xiàn)及MSS通告
前面已經(jīng)提到要在連接建立過程中會通告初始MSS,這個值可以由用戶程序進(jìn)行設(shè)置。但這個初始值是一個靜態(tài)的。當(dāng)通信的兩個端點(diǎn)之間跨越多個網(wǎng)絡(luò)時,使用設(shè)置的MSS進(jìn)行報文段發(fā)送時,可能導(dǎo)致傳輸?shù)腎P報文分片情況的產(chǎn)生。為了避免分片情況的產(chǎn)生,TDP在數(shù)據(jù)傳輸過程中進(jìn)行動態(tài)的路徑MTU發(fā)現(xiàn),并進(jìn)行MSS的更新及通告。
TDP創(chuàng)建UDP SOCKET時,即將描述符設(shè)置IP選項(xiàng)為不允許進(jìn)行分片(setsockopt (clientSock, IPPROTO_IP, IP_DONTFRAGMENT,(char*)&dwFlags, sizeof(dwFlags)))。在發(fā)送數(shù)據(jù)時以當(dāng)前MSS大小值進(jìn)行數(shù)據(jù)發(fā)送,如果返回值為錯誤碼WSAEMSGSIZE(10040)表示為報文段盡寸大于MTU,需要進(jìn)行IP分片傳輸。此時,縮減MSS大小再次進(jìn)行報文段發(fā)送,直至不再返回錯誤碼WSAEMSGSIZE(10040)。當(dāng)MSS變更并能成功發(fā)送報文段后,需要向?qū)Χ送▓笮碌腗SS值。每次MSS縮小后,默認(rèn)隔30秒,TDP將默認(rèn)擴(kuò)大MSS大小,以檢查是否路徑MTU增大了(默認(rèn)值可以由用戶程序設(shè)置),之后隔30*2秒、30*2*2秒進(jìn)行檢測,如果三次都未發(fā)現(xiàn)MTU增大則停止進(jìn)行檢測。見RFC1191描述,網(wǎng)絡(luò)中MTU值的個數(shù)是有限的,如下圖描述(摘自RFC1191)。因此MSS的擴(kuò)大及縮減,可依據(jù)一些由近似值按序構(gòu)成的表,依照此表索引進(jìn)行MSS值的擴(kuò)大與縮減計(jì)算。

TDP中MSS與MTU之間關(guān)系的計(jì)算公式如下:
MSS = MTU – 20(IP首部) – 8(UDP首部) – 12(TDP首部)。
3.Nagle算法
有些人誤認(rèn)為經(jīng)受時延的捎帶ACK發(fā)送是Nagle算法,其實(shí)不是。經(jīng)受時延的捎帶ACK發(fā)送是TCP的通常實(shí)現(xiàn),在TDP中也是如此。而Nagle算法是要求一個TCP(TDP也是如此)連接上最多只能有一個未被確認(rèn)的未完成的報文段,在該報文段的確認(rèn)到達(dá)之前不能發(fā)送其他的報文段。相反,TCP(TDP也是如此)在這個時候收集這些報文段,關(guān)在確認(rèn)到來時合并作為一個報文段發(fā)送出去。Nagle算法對于處理應(yīng)用程序產(chǎn)生大量小報文段的情況,有利于避免網(wǎng)絡(luò)中由于發(fā)送太多的包而過載(這便是發(fā)送端的糊涂窗口綜合癥,關(guān)于糊涂窗口綜合癥在下文將做更詳細(xì)描述)。
Nagle算法適用于產(chǎn)生大量小報文段的情況,但有時我們需要關(guān)閉Nagle算法。一個典型的例子是X窗口系統(tǒng)服務(wù)器:小消息(鼠標(biāo)移動)必須無時延地發(fā)送,以便為進(jìn)行某種操作的交互用戶提供實(shí)時的反饋。
默認(rèn)的TDP實(shí)現(xiàn)中Nagle算法是關(guān)閉的,用戶程序可以設(shè)置打開它。
4.窗口大小通告與滑動窗口
雙方接收模塊需要依據(jù)各自的緩沖區(qū)大小,相互通告還能接受對方數(shù)據(jù)的尺寸。雙方發(fā)送模塊則必須根據(jù)對方通告的接收窗口大小,進(jìn)行數(shù)據(jù)發(fā)送。這種機(jī)制稱之謂滑動窗口,它是TDP接收方的流量控制方法。它允許發(fā)送方在停止并等待確認(rèn)前可以連續(xù)發(fā)送多個分組(依據(jù)滑動窗口的大小),由于發(fā)送方不必每發(fā)一個分組就停下來等待確認(rèn),因此可以加速數(shù)據(jù)的傳輸。
參照《TCP/IP詳解 卷一 20.3滑動窗口》一節(jié),滑動窗口在排序數(shù)據(jù)流上不時的向右移動,窗口兩個邊沿的相對運(yùn)動增加或減少了窗口的大小,關(guān)于窗口邊沿的運(yùn)動有三個術(shù)語:窗口合攏(當(dāng)左邊沿向右邊沿靠近)、窗口張開(當(dāng)右邊沿向右移動)、窗口收縮(當(dāng)右邊沿向左移動)。RFC文檔強(qiáng)烈建議不要在實(shí)現(xiàn)當(dāng)中出現(xiàn)窗口收縮的情況出現(xiàn),在我們的實(shí)現(xiàn)中也將不會出現(xiàn)。
當(dāng)遇到快的發(fā)送方與慢的接收方的情況時,接收方的窗口會很快被發(fā)送方的數(shù)據(jù)填滿,此時接收方將通告窗口大小為0,發(fā)送方則停止發(fā)送數(shù)據(jù)。直到接收方用戶程序取走數(shù)據(jù)后更新窗口大小,發(fā)送方可以繼續(xù)發(fā)送數(shù)據(jù);另外,因?yàn)锳CK報文段有可能丟失,發(fā)送方可能沒有成功接收到更新的窗口大小,因此發(fā)送方將啟動一個堅(jiān)持定時器,當(dāng)堅(jiān)持定時器超時,發(fā)送方將發(fā)送一個字節(jié)的數(shù)據(jù)到接收方,嘗試檢查窗口大小的更新。
在Nagle算法中接到過糊涂窗口綜合癥,在這里要進(jìn)一步進(jìn)行描述。糊涂窗口綜合癥是指眾多少量數(shù)據(jù)的報文段將通過連接進(jìn)行交換,而不是滿長度的報文段,這將導(dǎo)致連接占用過多帶寬,降低傳輸速率。糊涂窗口綜合癥產(chǎn)生是分兩端的,接收方可以通告一個小的窗口(而不是一直等到有大的窗口時才通告),發(fā)送方也可以發(fā)送少量的數(shù)據(jù)(而不是等待其他的數(shù)據(jù)以便發(fā)送一個大的報文段)。要以采用如下方法避免這一現(xiàn)象:
1)接收方不通告小窗口。通常的算法是接收方不通告一個比當(dāng)前窗口大的窗口(可以為0),除非窗口可以增加一個報文段大小(也就是將要接收的MSS)或者可以增加緩存空間的一半,不論實(shí)際有多少。
2)發(fā)送方避免出現(xiàn)糊涂窗口綜合癥的措施是只有以下條件之一滿足時才發(fā)送數(shù)據(jù):(a)可以發(fā)送一個滿長度的報文段;(b)可以發(fā)送至少是接收方通告窗口大小一半的報文段;(c)可以發(fā)送任何數(shù)據(jù)并且不希望接收ACK(也就是說,我們沒有還未被確認(rèn)的數(shù)據(jù))或者該連接上不能使用Nagle算法。
5.PUSH標(biāo)志
PSUH標(biāo)志的作用是發(fā)送方使用PUSH標(biāo)志通知接收方將所收到的數(shù)據(jù)全部提交給接收進(jìn)程。在TDP實(shí)現(xiàn)中,用戶程序并不需要關(guān)心PUSH標(biāo)志。因?yàn)門DP實(shí)現(xiàn)從不將接收到的數(shù)據(jù)推遲交付給用戶程序,因此這個標(biāo)志在TDP的實(shí)現(xiàn)中是被忽略的。
五、TDP超時與重傳
1.帶寬時延乘積與擁塞
每個網(wǎng)絡(luò)通道都有一定的容量,可以計(jì)算通道的容量大小:
Capacity(bit) = bandwidth(b/s) * round-trip time(s)
這個值一般稱之為帶寬時延乘積。這個值依賴于網(wǎng)絡(luò)速度和兩端的RTT,可以有很大的變動。不論是帶寬還是時延均會影響發(fā)送方與接收方之間通路的容量。
當(dāng)數(shù)據(jù)到達(dá)一個大的網(wǎng)絡(luò)通道并向一個小的網(wǎng)絡(luò)通道發(fā)送,將發(fā)生擁塞現(xiàn)象。另外當(dāng)多個輸入流到達(dá)一個路由器,而路由器的輸出流小于這些輸入流的總和時也會發(fā)生擁塞。TDP超時與重傳機(jī)制剛采用TCP的擁塞控制算法來進(jìn)行發(fā)送端的流量控制。
2.往返時間與重傳超時時間測量
超時與重傳中最重要的部分就是對一個給定連接的往返時間(RTT)的測量。由于路由器和網(wǎng)絡(luò)流量均會發(fā)生變化,因此一般認(rèn)為RTT可能經(jīng)常會發(fā)生變化,TDP應(yīng)該跟蹤這些變化并相應(yīng)地改變相應(yīng)的超時時間。
首先是必須測量在發(fā)送一個帶有特別序號的字節(jié)和接收到包含字節(jié)的確認(rèn)之間的RTT。由于數(shù)據(jù)報文段與ACK之間通常沒有一一對應(yīng)的關(guān)系,如下圖(摘自《TCP/IP詳解 卷一》圖20.1)中,這意味著發(fā)送方可以測量到的一個RTT,是在發(fā)送報文段4和接收報文段7之間的時間,用M表示所測量到的RTT。
根據(jù)[Jacobson 1988]描述(見《TCP/IP詳解 卷一》參考文獻(xiàn)),用A表示被平滑的RTT(均值估計(jì)器),用D表示被平滑的均值偏差,用Err表示剛得到的測量結(jié)果M與當(dāng)前RTT估計(jì)器之差,則可以計(jì)算下一個超時重傳時間(用RTO表示下一個超時重傳時間)。
A = 0 (未進(jìn)行測量往返時間之前,A的初始值)
D = 3 (未進(jìn)行測量往返時間之前,D的初始值)
RTO = A + 2D = 6 (未進(jìn)行測量往返時間之前,RTO的初始值)
A = M + 0.5 (第一次測量到往返時間結(jié)果,對RTT估計(jì)器計(jì)算初始值)
D = A / 2 (第一次測量到往返時間結(jié)果,對均值偏差D計(jì)算初始值)
RTO = A + 4D (第一次測量到往返時間結(jié)果,對均值偏差RTO計(jì)算初始值)
之后的計(jì)算方法如下:
Err = M – A
A <- A + gErr
D <- D + h(|Err| - D)
RTO = A + 4D
其中g(shù)是常量增量,取值為1/8(0.125);h也是常量增量,取值為1/4(0.25)。

Karn算法:Karn算法是解決所謂的重傳多義性問題的。[Karn and Partridge 1987]規(guī)定(見《TCP/IP詳解 卷一》參考文獻(xiàn)),當(dāng)一個超時和重傳發(fā)生時,在重傳數(shù)據(jù)的確認(rèn)最后到達(dá)之前,不能更新RTT估計(jì)器,因?yàn)槲覀儾⒉恢繟CK對應(yīng)哪次傳輸(也許第一次傳輸被延遲而并沒有被丟棄,也有可能是第一次傳輸?shù)腁CK被延遲丟棄)。并且,由于數(shù)據(jù)被重傳,RTO已經(jīng)得到了一個指數(shù)退避,我們在下一次傳輸時使用這個退避后的RTO。對一個沒有被重傳的報文段而言,除非收到了一個確認(rèn),否則不要計(jì)算新的RTO。
在任何時候?qū)γ總€連接并行僅測量一次RTT值,在發(fā)送一個報文段時,如果給定連接的定時器已經(jīng)被使用,則該報文段不被計(jì)時,反之如果給定連接的定時器未被使用,則開始計(jì)時以測量RTT值。即并非每個發(fā)出報文段都進(jìn)行測量RTT值,同一時間段里只能有一個RTT值測量行為進(jìn)行,不會并行進(jìn)行多個RTT值測量。
3.慢啟動
如果發(fā)送方一開始便向網(wǎng)絡(luò)發(fā)送多個報文段,直至達(dá)到接收方通告窗口大小為止。當(dāng)發(fā)送方與接收方在同一局域網(wǎng)時,這種方式是可以的。但如果在發(fā)送方與接收方之間存在多個路由器和速率較慢的鏈路時,就可能出現(xiàn)問題。一些中間路由器必須緩存分組,并有可能耗盡存儲器的空間,將來得降低TCP連接的吞吐量。于是需要一種叫“慢啟動”的擁塞控制算法。
慢啟動為發(fā)送方增加一個擁塞窗口,記為cwnd,當(dāng)與另一個網(wǎng)絡(luò)的主機(jī)建立連接時,擁塞窗口被初始化為1個報文段。每收到一個ACK,擁塞窗口就增加一個報文段(cwnd以字節(jié)為單位,但慢啟動以報文段大小為單位進(jìn)行增加)。發(fā)送方取擁塞窗口與通告窗口中的最小值作為發(fā)送上限。擁塞窗口是發(fā)送方使用的流量控制,而通告窗口是接收方使用的流量控制。
發(fā)送方開始時發(fā)送一個報文段,然后等待ACK。當(dāng)收到該ACK時,擁塞窗口從1增加到2,即可以發(fā)送兩個報文段。當(dāng)收到這兩個報文段的ACK時,擁塞窗口就增加為4。這是一種指數(shù)增加的關(guān)系。
4.擁塞避免
慢啟動算法增加擁塞窗口大小到某些點(diǎn)上可能達(dá)到了互聯(lián)網(wǎng)的容量,于是中間路由器開始丟棄分組。這就通知發(fā)送方它的擁塞窗口開得太大。擁塞避免算法是一種處理丟失分組的方法。該算法假定由于分組受到損壞引起的丟失是非常少的(遠(yuǎn)小于1%),因此分組丟失就意味著在源主機(jī)和目標(biāo)主機(jī)之間的某處網(wǎng)絡(luò)上發(fā)生了擁塞。有兩種分組丟失的指示:發(fā)生超時和接收到重復(fù)的確認(rèn)。擁塞避免算法與慢啟動算法是兩個獨(dú)立的算法,但實(shí)際中這兩個算法通常在一起實(shí)現(xiàn)。
擁塞避免算法和慢啟動算法需要對每個連接維持兩個變量:一個擁塞窗口cwnd和一個慢啟動門限ssthresh。算法的工作過程如下:
1) 對一個給定的連接,初始化cwnd為1個報文段,ssthresh為65535個字節(jié)。
2) TCP輸出例程的輸出不能超過cwnd和接收方通告窗口的大小。擁塞避免是發(fā)送方使用的流量控制,而通告窗口則是接收方進(jìn)行的流量控制。前者是發(fā)送方感受到的網(wǎng)絡(luò)擁塞的估計(jì),而后者則與接收方在該連接上的可用緩存大小有關(guān)。
3) 當(dāng)擁塞發(fā)生時(超時或收到重復(fù)確認(rèn)),ssthresh被設(shè)置為當(dāng)前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少為2個報文段)。此外,如果是超時引起了擁塞,則cwnd被設(shè)置為1個報文段(這就是慢啟動)。
4) 當(dāng)新的數(shù)據(jù)被對方確認(rèn)時,就增加cwnd,但增加的方法依賴于我們是否正在進(jìn)行慢啟動或擁塞避免。如果cwnd小于或等于ssthresh,則正在進(jìn)行慢啟動,否則正在進(jìn)行擁塞避免。慢啟動一直持續(xù)到我們回到當(dāng)擁塞發(fā)生時所處位置的半時候才停止(因?yàn)槲覀冇涗浟嗽诓襟E2中給我們制造麻煩的窗口大小的一半),然后轉(zhuǎn)為執(zhí)行擁塞避免。
慢啟動算法初始設(shè)置cwnd為1個報文段,此后每收到一個確認(rèn)就加1。這會使窗口按指數(shù)方式增長:發(fā)送1個報文段,然后是2個,接著是4個……。擁塞避免算法要求每次收到一個確認(rèn)時將cwnd增加1/cwnd。與慢啟動的指數(shù)增加比起來,這是一種加性增長。我們希望在一個往返時間內(nèi)最多為cwnd增加1個報文段(不管在這個RT T中收到了多少個ACK),然而慢啟動將根據(jù)這個往返時間中所收到的確認(rèn)的個數(shù)增加cwnd。
處于擁塞避免狀態(tài)時,擁塞窗口的計(jì)算公式如下(引公式參照BSD的實(shí)現(xiàn),segsize/8的值是一個匹配補(bǔ)充量,不在算法描述當(dāng)中):
cwnd <- cwnd + segsize * segsize / cwnd + segsize / 8
5.快速重傳與快速恢復(fù)
由于我們不知道一個重復(fù)的ACK是由一個丟失的報文段引起的,還是由于僅僅出現(xiàn)了幾個報文段的重新排序,因此我們等待少量重復(fù)的ACK到來。假如這只是一些報文段的重新排序,則在重新排序的報文段被處理并產(chǎn)生一個新的ACK之前,只可能產(chǎn)生1 ~ 2個重復(fù)的ACK。如果一連串收到3個或3個以上的重復(fù)ACK,就非??赡苁且粋€報文段丟失了。于是我們就重傳丟失的數(shù)據(jù)報文段,而無需等待超時定時器溢出。這就是快速重傳算法。接下來執(zhí)行的不是慢啟動算法而是擁塞避免算法。這就是快速恢復(fù)算法。
這個算法通常按如下過程進(jìn)行實(shí)現(xiàn):
1) 當(dāng)收到第3個重復(fù)的ACK時,將ssthresh設(shè)置為當(dāng)前擁塞窗口cwnd的一半。重傳丟失的報文段。設(shè)置cwnd為ssthresh加上3倍的報文段大小。
2) 每次收到另一個重復(fù)的ACK時,cwnd增加1個報文段大小并發(fā)送1個分組(如果新的cwnd允許發(fā)送)。
3) 當(dāng)下一個確認(rèn)新數(shù)據(jù)的ACK到達(dá)時,設(shè)置cwnd為ssthresh(在第1步中設(shè)置的值)。這個ACK應(yīng)該是在進(jìn)行重傳后的一個往返時間內(nèi)對步驟1中重傳的確認(rèn)。另外,這個ACK也應(yīng)該是對丟失的分組和收到的第1個重復(fù)的A C K之間的所有中間報文段的確認(rèn)。這一步采用的是擁塞避免,因?yàn)楫?dāng)分組丟失時我們將當(dāng)前的速率減半。
六、代理socks5支持
參照RFC1928、RFC1929,在TDP實(shí)現(xiàn)中,支持匿名通過socks5代理以及用戶名/密碼驗(yàn)證方式通過socks5代理。
由于socks5代理是工作于運(yùn)輸層上,因此連接當(dāng)中對IP層選項(xiàng)的設(shè)置都將沒有效果。socks5代理起到的作用只是應(yīng)用數(shù)據(jù)的轉(zhuǎn)發(fā),但這已經(jīng)基本上能支持大部分用戶程序的應(yīng)用需求。在使用socks5代理進(jìn)行工作中,路徑MTU的發(fā)現(xiàn)機(jī)制,將無法有效工作,此時MSS默認(rèn)為536(MTU默認(rèn)為576),用戶程序可以修改使用的MSS值。
七、安全考慮
TDP協(xié)議及算法方面并不對數(shù)據(jù)的安全性做任何考慮,用戶程序在傳輸數(shù)據(jù)時如果對安全性有要求,可以自行在應(yīng)用數(shù)據(jù)層做相應(yīng)的工作。但TDP實(shí)現(xiàn)中,會提供一個簡單的AES256位加解密方法,提供給用戶程序使用。用戶程序可以調(diào)用該加解密方法,對數(shù)據(jù)進(jìn)行加密然后再通過網(wǎng)絡(luò)進(jìn)行發(fā)送,接收時將加密數(shù)據(jù)流進(jìn)行解密再將會用戶程序數(shù)據(jù)邏輯處理模塊進(jìn)行處理。
八、定時器
如BSD的TCP實(shí)現(xiàn)類似,TDP也為每條連接建立了六個定時器,簡要介紹如下:
1)“連接建立”定時器,在發(fā)送SYN報文段建立一條新的連接時啟動。如果沒有在75秒內(nèi)收到響應(yīng),連接建立將中止。
2)“重傳”定時器,在發(fā)送數(shù)據(jù)時設(shè)定。如果定時器已超時而對端的確認(rèn)還未到達(dá),將重傳數(shù)據(jù)。重傳定時器的值是動態(tài)計(jì)算的,取決來RTT與該報文段被重傳的次數(shù)。
3)“延遲ACK”定時器,收到必須確認(rèn)但無需馬上發(fā)出確認(rèn)的數(shù)據(jù)時設(shè)定。等待200ms后發(fā)送確認(rèn)響應(yīng)。如果,在這200ms內(nèi),有數(shù)據(jù)要在該連接上發(fā)送,延遲的ACK響應(yīng)就可隨數(shù)據(jù)一起發(fā)送回對端,稱為捎帶確認(rèn)。
4)“堅(jiān)持”定時器,在連接對端通告接收窗口為0,阻止繼續(xù)發(fā)送數(shù)據(jù)時設(shè)定。堅(jiān)持定時器在超時后向?qū)Χ税l(fā)送1字節(jié)的數(shù)據(jù),判定對端接收窗口是否已經(jīng)打開。堅(jiān)持定時器的值是動態(tài)的計(jì)算的,取決于RTT值,在5秒與60秒之間取值。
5)“?;?#8221;定時器。TDP連接在一定時間段內(nèi)如果沒有數(shù)據(jù)交互的話,將主動發(fā)送?;頛IV報文段。即當(dāng)“?;?#8221;定時器超時,說明沒有數(shù)據(jù)交互,則發(fā)送保活數(shù)據(jù)包。?;疃〞r器默認(rèn)時間為2分鐘,用戶程序可以進(jìn)行設(shè)置。
6)TIME_WAIT定時器,也可稱為2MSL定時器(實(shí)現(xiàn)中,一個MSL為1分鐘)。當(dāng)連接狀態(tài)轉(zhuǎn)移到TIME_WAIT時,即連接主動關(guān)閉時,定時器啟動。
九、開發(fā)接口
使用TDP進(jìn)行網(wǎng)絡(luò)程序開發(fā)是非常容易的,它的開發(fā)接口(API)與socket API是非常相似的,尤其是對應(yīng)功能的函數(shù)名稱都是一致的,需要注意的是TDP的所有API都處于名稱空間TDP之下。開發(fā)接口見下表:
函數(shù) 描述
TDP::accept 接受一個鏈接
TDP::bind 綁定本地地址到一個TDP::SOCKET句柄
TDP::cleanup 清除TDP全局資源,一個進(jìn)程中只需要調(diào)用一次
TDP::close 關(guān)閉已打開的TDP::SOCKET句柄,并關(guān)閉連接
TDP::connect 連接到服務(wù)器端
TDP::getlasterror 獲得TDP最后的一個錯誤
TDP::getpeername 讀取連接的對端的地址信息
TDP::getsockname 讀取連接的本地的地址信息
TDP::getsockopt 讀取TDP的選項(xiàng)信息
TDP::listen 等待客戶端來連接
TDP::recv 接收數(shù)據(jù)
TDP::select 等待集合中的TDP SOCKET改變狀態(tài)
TDP::send 發(fā)送數(shù)據(jù)
TDP::setsockopt 修改TDP的選項(xiàng)信息
TDP::shutdown 指定關(guān)閉連接上雙工通信的部分或全部
TDP::socket 創(chuàng)建一個TDP SOCKET
TDP::startup 初始化TDP全局信息,一個進(jìn)程中只需要調(diào)用一次
posted on 2010-04-12 16:35
小王 閱讀(6231)
評論(0) 編輯 收藏 引用 所屬分類:
網(wǎng)絡(luò)通訊