Total Pageviews

Friday 8 April 2016

TDP(TCP-over-UDP library):基于UDP协议之上实现

随着互联网应用广泛推广,出现了越来越多的网络应用,其中基于p2p思想的各种网络技术的产品也越来越多的出现在我们的视野当中。从最早闻名的Napster到现在的Bittorrent、eMule、skype等产品,P2P这种网络应用模式已经从各个方面深入人心。这些产品在各自的网络实现技术上,都以各自的方法解决着同样面临的一个问题,如何让他们的软件产品在各异的网络拓扑结构中顺利的进行P2P通信。
众所周知,在当今的网络拓扑结构中,普遍存在使用NAT设备来进行网络地址转换,而让应用程序能跨越这些NAT设备进行全双工的通信,就成为非常重要的一个问题。对于实现跨越NAT通信可以采取很多种办法(对于能够直接连接、反向连接的情况不在此列):首先是通过服务器进行转发,这是比较粗暴的方法,而且在用户量大的时候,转发服务器需要付出相当大的代价;第二,可以使用NAT穿透技术。而大家知道关于NAT穿透中,UDP穿透的成功率比起TCP穿透要高出许多,这一点这里将不做多述,可以参考Bryan Ford的文章《Peer-to-Peer Communication Across Network Address Translators》(http://www.brynosaurus.com/pub/net/p2pnat/)。因此在UDP协议上构建一些大型的网络应用程序可能会成为很多人的需求。
当然也可能基于更多的原因,会有很多人希望能在UDP协议上进行大型应用程序的构建。然而UDP协议本身存在着不通信不可靠的缺点,于是对于基于UDP进行可靠通信的需求就浮现出来了。目前在网络上有许多人正做着这一工作,UDT、RakNet、eNet等都是构建在UDP之后网络可靠通信开发库。然后这些库开发时都针对了一些特殊应用来进行设计的,不具备通用性。比如RakNet是为游戏应用而设计,对于实时性等游戏相关的网络需求有很好的支持,对于大批量数据传输却有点力所不及。而UDT基于一种基于带宽速率控制的拥塞控制算法进行设计(http://udt.sourceforge.net/doc/draft-gg-udt-01.txt),主要用在小数量的bulk源共享富裕带宽的情况下,最典型的例子就是建立在光纤广域网上的网格计算,而在ISP提供带宽有限的情况下运行却显得消耗资源并性能不足。甚至可能被防火墙,或ISP服务商判断为恶意带宽使用攻击。这些都使用得他们不能被广泛地用于各种网络应用程序。另外大家也陆续发现目前的UDT实现版本存在的一些问题。比如UDT做服务端接收连接时,总是新开一个端口与客户端进行连接,这样会带来几个问题:1)较多客户端连接上来时,服务端新打开的众多端口中可能有的端口会被防火墙拦截而导致通信失败,2)如果客户端处于Symmetric NAT和Port-Restricted Cone NAT后面时,将导致服务器端与客户端连接无法成功建立,3)由于udp端口数最大值有限,所以UDT服务器端可接收的连接数也因些受限。再有就是不仅仅是UDT库,基本上所有的UDP-based可靠通信库,都未提供穿越proxy代理的功能(socks5);再有就是对UDP打洞技术有的支持得不完善或并不支持。
基于这些原因,使得我需要开发一个基于UDP协议之上实现一个可靠、高效、通用的通信库,来满足我目前所开发的项目的需要。TCP协议算法已经是经过多方面及多年的验证,是最具通用性,且可靠高效的。虽然UDT等各种库指出TCP在这样或那样的网络环境下存在不足,但众多实现当中他仍然是最通用、可靠、高效的。相信有许多人跟我一样,需要这么一个开发库,所以我打算在开发过程中,陆续公开相关的文档及这个开发库。
二、设计目标
TDP主要的目标就是在UDP层之上实现TCP的协议算法,使得应用程序能够在UDP层之上获得通用、可靠、高效的通信能力。
TDP网络开发库所实现的算法,都来自久经考验的TCP协议算法,网上有着非常多的参考资料。在实现当中,参考最多的是Richard Stevens的《TCP/IP详解》。
TDP提供的用于开发的应用程序接口与Socket API非常相像,姑且称之为TDP Socket API,基本上的函数名与参数等都与Socket API相一致,但是TDP Socket API的API接口都位于命名空间TDP当中。只要使用过Socket API进行开发过的朋友,将都会使用TDP库进行开发。下图为TDP及TDP Socket API所处在的协议栈应用中的位置,以及与TCP协议栈应用的对比。

三、协议说明
1.协议格式
TDP的实现的算法虽然与TCP实现的算法是大致相同的,但TDP的协议格式只是从TCP协议格式获得参考,但并不完全与他相同。TDP的协议格式如下:

接下来介绍一下协议格式的各个字段含义。
4位首部长度:表示用户数据在数据包中的起始位置。
LIV:连接保活标志,用于表示TDP连接通路存活状态。
ACK:确认序号有效。
PSH:接收方应该尽快将这个报文段交给应用层。
RST:重建连接。
SYN:同步序号用来发起一个连接。
FIN:发端完成发送任务。
16位窗口大小:接收端可接收数据的窗口大小。
选项:只有一个选项字段,为最长报文大小,即MSS。TDP选项格式与TCP选项格式一致,kind=0时表示选项结束,kind=1时表示无操作,kind=2时表示最大报文段长度。如下图:

数据:用户通过TDP传输的数据。
2.TDP连接建立与终止
TDP的连接建立与终止可以参考TCP的状态变迁图(此图的详细解释请参考《TCP/IP详解 卷一》第18章),如下:

2.1连接建立
2.1.1三次握手
连接建立分要经过三次握手过程:1)客户端发送一个SYN段到指明客户打算连接的服务器的端口,报文段中要设置客户端初始序号。2)服务器发回包含服务器的初始序号的SYN报文段作为应答。同时,将确认序号设置为客户的初始序号加1,并设置ACK位标志报文段为确认报文段。3)客户端必须将确认序号设置为服务器初始序号加1,对服务器的SYN报文段进行确认。
TDP在全局维护一个初始序号种子,这个初始序号为随时产生的32位整数。
连接建立的超时和重传初始值为3秒,超时采用指数退避算法,3秒超时后超时值为6秒,然后是12秒,24秒……。连接建立最长时间限制为75秒。
2.1.2 NAT UDP PUNCH模式
当TDP工作模式是NAT UDP PUNCH时,在三次握手之前,向对端NAT端口及预测端口间隔默认2ms发送默认为10个LIV报文段,一来用于打开自已的NAT端口,二来是用于进入对端NAT端口。默认值可以由用户程序设置。这时的LIV报文段中初始序号及确认序号都为0。
当接收到对端LIV报文段后,立即停止LIV报文段发送,发出SYN报文段进行连接建立。这时有两种可能:其一是另一端直到接收到该SYN报文段之前,都没有接收到LIV报文段,或是刚接收到但没有来得及发送SYN报文段,此时将会如上文描述的正常模式下连接建立的过程一致,将经历三次握手。基二是另一端在接收到该SYN报文段之前,也已经发送出SYN报文段,此时双方都需要对SYN报文段进行确认,可以称之为四次握手。

2.1.3 最大传输报文大小(MSS)
TCP报文段在连接建立时需要通报MSS,在TDP的实现中也进行通报,默认通报为1460字节(符合以太网标准,这个默认值允许20字节的IP首部、8字节的UDP首部和12字节的TDP首部,以适合 1500字节的IP数据报)默认值可以由用户程序设置。
TCP在对端地址为非本地IP时,默认通报为536字节。TDP之所以默认通报为1460,是因为TDP在数据传输过程中,实现了路径MTU发现技术,通过实际发现的MTU,进行MSS的动态调整,以尽量避免报文段在网络中的传输产生分片的情况。路径MTU发现技术在传输数据流一节中进行描述。
2.1.4 半打开连接及连接保活
半打开连接是指对端异常关闭,如网线拔掉、突然断电等情况将引发一端导演关闭,而另一端的连接却仍然认为连接处于打开当中,这种情况称之为半打开连接。TDP中的一个TDP SOCKET描述符由本地IP、本地端口、远端IP、远端端口唯一确定。当远端客户端连接请求到来时,服务端将接收到一个新的TDP SOCKET描述符,当这一个描述符唯一确定信息已经存在时,对新的连接请求发送RST报文段,通知其重置连接请求。对于旧的连接,由保活机制自动发现是否为半打开连接,如果是半打开连接,则自动关闭该连接。这里RST报文段与TCP中的RST报文段有些不一样,TCP的RST报文段工作描述请参考《TCP/IP详解 卷一》。
连接建立之后,TDP连接需要启动保活机制。TCP连接在没有数据通信的情况下也能保持连接,但TDP连接不行。TDP连接在一定时间段内如果没有数据交互的话,将主动发送保活LIV报文段。这个时间段根据TDP连接工作模块不同有所差异,在NAT UDP PUNCH模式下,这个时间段默认值为1分钟(大多数的NAT中,UDP会话超时时间为2-5分钟左右);而在常规模块下这个时间段默认值为5分钟。默认值可以由用户程序设置,用户程序需要指明两种模块下的保活时间周期。这里TDP的保活机制与TCP中的保活机制完全不一样,TCP的保活机制描述请参考《TCP/IP详解 卷一》。
2.2连接关闭
TDP连接与TCP连接一样是全双工的,因此每个方向必须单独地进行关闭。客户机给服务器一个FIN报文段,然后服务器返回给客户端一个确认ACK报文,并且发送一个FIN报文段,当客户机回复ACK报文后(四次握手),连接就结束了。
TDP连接的一端接收到FIN报文段时,如果还有数据要发送,需要继续将数据进行发送完成,然后才发出FIN报文段;如果还有数据未从缓存中取出,将取出数据,并进行确认,直到所有确认完成之后,然后才发出FIN报文段(此时如果有乱序的报文段情况不进行处理)。上面的描述也表现出,TDP是支持半关闭的,当一端发出FIN报文段时,仍然允许接收另一端数据。但是半关闭可能导致连接永远停留在状态图中FIN_WAIT_2状态中,此时保活机制仍然在工作当中,如果对端已经关闭,那么保活机制将在检测到时立即关闭这一连接。
下图是一个典型的连接建立与连接关闭的示意图,此图摘自《TCP/IP详解 卷一》。

四、TDP传输数据流
1.传输的报文段
在TDP工作过程中传输的所有报文段,只有SYN报文段、FIN报文段、数据报文段是可靠的之外,其它报文段如ACK报文段、LIV报文段、RST报文段等都不是可靠的。SYN报文段与FIN报文段传输中都占用一个序号,数据报文段在传输中根据传输的数据字节数占用相应的序号,其它报文段不占用传输序号。
成功接收数据报文段,应当将按序对下一个期望的数据报文段的序号作为确认序号发送ACK报文段进行确认。当出现接收到乱序的数据报文段时,将乱序数据报文段按序缓存,并发送期望报文段的ACK报文段进行确认。ACK报文段的发送并非即时的,也并非是对应接收数据报进行一对一确认发送。ACK报文段由200ms定时触发发送,也就是说ACK报文段要经受最多200ms的时延进行发送。ACK报文段对此时期望的数据序号进行确认,因此并不是与接收数据报相对应。ACK报文段是不可靠的,当丢失时对端将无法了解接收情况,因此发送方将会有一个超时机制,如果发现确认的ACK报文段超时,发送方将重发该数据报,这一点在第五节进行详细描述。
2.路径MTU发现及MSS通告
前面已经提到要在连接建立过程中会通告初始MSS,这个值可以由用户程序进行设置。但这个初始值是一个静态的。当通信的两个端点之间跨越多个网络时,使用设置的MSS进行报文段发送时,可能导致传输的IP报文分片情况的产生。为了避免分片情况的产生,TDP在数据传输过程中进行动态的路径MTU发现,并进行MSS的更新及通告。
TDP创建UDP SOCKET时,即将描述符设置IP选项为不允许进行分片(setsockopt (clientSock, IPPROTO_IP, IP_DONTFRAGMENT,(char*)&dwFlags, sizeof(dwFlags)))。在发送数据时以当前MSS大小值进行数据发送,如果返回值为错误码WSAEMSGSIZE(10040)表示为报文段尽寸大于MTU,需要进行IP分片传输。此时,缩减MSS大小再次进行报文段发送,直至不再返回错误码WSAEMSGSIZE(10040)。当MSS变更并能成功发送报文段后,需要向对端通报新的MSS值。每次MSS缩小后,默认隔30秒,TDP将默认扩大MSS大小,以检查是否路径MTU增大了(默认值可以由用户程序设置),之后隔30*2秒、30*2*2秒进行检测,如果三次都未发现MTU增大则停止进行检测。见RFC1191描述,网络中MTU值的个数是有限的,如下图描述(摘自RFC1191)。因此MSS的扩大及缩减,可依据一些由近似值按序构成的表,依照此表索引进行MSS值的扩大与缩减计算。

TDP中MSS与MTU之间关系的计算公式如下:
MSS = MTU – 20(IP首部) – 8(UDP首部) – 12(TDP首部)。
3.Nagle算法
有些人误认为经受时延的捎带ACK发送是Nagle算法,其实不是。经受时延的捎带ACK发送是TCP的通常实现,在TDP中也是如此。而Nagle算法是要求一个TCP(TDP也是如此)连接上最多只能有一个未被确认的未完成的报文段,在该报文段的确认到达之前不能发送其他的报文段。相反,TCP(TDP也是如此)在这个时候收集这些报文段,关在确认到来时合并作为一个报文段发送出去。Nagle算法对于处理应用程序产生大量小报文段的情况,有利于避免网络中由于发送太多的包而过载(这便是发送端的糊涂窗口综合症,关于糊涂窗口综合症在下文将做更详细描述)。
Nagle算法适用于产生大量小报文段的情况,但有时我们需要关闭Nagle算法。一个典型的例子是X窗口系统服务器:小消息(鼠标移动)必须无时延地发送,以便为进行某种操作的交互用户提供实时的反馈。
默认的TDP实现中Nagle算法是关闭的,用户程序可以设置打开它。
4.窗口大小通告与滑动窗口
双方接收模块需要依据各自的缓冲区大小,相互通告还能接受对方数据的尺寸。双方发送模块则必须根据对方通告的接收窗口大小,进行数据发送。这种机制称之谓滑动窗口,它是TDP接收方的流量控制方法。它允许发送方在停止并等待确认前可以连续发送多个分组(依据滑动窗口的大小),由于发送方不必每发一个分组就停下来等待确认,因此可以加速数据的传输。
参照《TCP/IP详解 卷一 20.3滑动窗口》一节,滑动窗口在排序数据流上不时的向右移动,窗口两个边沿的相对运动增加或减少了窗口的大小,关于窗口边沿的运动有三个术语:窗口合拢(当左边沿向右边沿靠近)、窗口张开(当右边沿向右移动)、窗口收缩(当右边沿向左移动)。RFC文档强烈建议不要在实现当中出现窗口收缩的情况出现,在我们的实现中也将不会出现。
当遇到快的发送方与慢的接收方的情况时,接收方的窗口会很快被发送方的数据填满,此时接收方将通告窗口大小为0,发送方则停止发送数据。直到接收方用户程序取走数据后更新窗口大小,发送方可以继续发送数据;另外,因为ACK报文段有可能丢失,发送方可能没有成功接收到更新的窗口大小,因此发送方将启动一个坚持定时器,当坚持定时器超时,发送方将发送一个字节的数据到接收方,尝试检查窗口大小的更新。
在Nagle算法中接到过糊涂窗口综合症,在这里要进一步进行描述。糊涂窗口综合症是指众多少量数据的报文段将通过连接进行交换,而不是满长度的报文段,这将导致连接占用过多带宽,降低传输速率。糊涂窗口综合症产生是分两端的,接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告),发送方也可以发送少量的数据(而不是等待其他的数据以便发送一个大的报文段)。要以采用如下方法避免这一现象:
1)接收方不通告小窗口。通常的算法是接收方不通告一个比当前窗口大的窗口(可以为0),除非窗口可以增加一个报文段大小(也就是将要接收的MSS)或者可以增加缓存空间的一半,不论实际有多少。
2)发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据:(a)可以发送一个满长度的报文段;(b)可以发送至少是接收方通告窗口大小一半的报文段;(c)可以发送任何数据并且不希望接收ACK(也就是说,我们没有还未被确认的数据)或者该连接上不能使用Nagle算法。
5.PUSH标志
PSUH标志的作用是发送方使用PUSH标志通知接收方将所收到的数据全部提交给接收进程。在TDP实现中,用户程序并不需要关心PUSH标志。因为TDP实现从不将接收到的数据推迟交付给用户程序,因此这个标志在TDP的实现中是被忽略的。
五、TDP超时与重传
1.带宽时延乘积与拥塞
每个网络通道都有一定的容量,可以计算通道的容量大小:
Capacity(bit) = bandwidth(b/s) * round-trip time(s)
这个值一般称之为带宽时延乘积。这个值依赖于网络速度和两端的RTT,可以有很大的变动。不论是带宽还是时延均会影响发送方与接收方之间通路的容量。
当数据到达一个大的网络通道并向一个小的网络通道发送,将发生拥塞现象。另外当多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和时也会发生拥塞。TDP超时与重传机制刚采用TCP的拥塞控制算法来进行发送端的流量控制。
2.往返时间与重传超时时间测量
超时与重传中最重要的部分就是对一个给定连接的往返时间(RTT)的测量。由于路由器和网络流量均会发生变化,因此一般认为RTT可能经常会发生变化,TDP应该跟踪这些变化并相应地改变相应的超时时间。
首先是必须测量在发送一个带有特别序号的字节和接收到包含字节的确认之间的RTT。由于数据报文段与ACK之间通常没有一一对应的关系,如下图(摘自《TCP/IP详解 卷一》图20.1)中,这意味着发送方可以测量到的一个RTT,是在发送报文段4和接收报文段7之间的时间,用M表示所测量到的RTT。
根据[Jacobson 1988]描述(见《TCP/IP详解 卷一》参考文献),用A表示被平滑的RTT(均值估计器),用D表示被平滑的均值偏差,用Err表示刚得到的测量结果M与当前RTT估计器之差,则可以计算下一个超时重传时间(用RTO表示下一个超时重传时间)。
A = 0 (未进行测量往返时间之前,A的初始值)
D = 3 (未进行测量往返时间之前,D的初始值)
RTO = A + 2D = 6 (未进行测量往返时间之前,RTO的初始值)
A = M + 0.5 (第一次测量到往返时间结果,对RTT估计器计算初始值)
D = A / 2 (第一次测量到往返时间结果,对均值偏差D计算初始值)
RTO = A + 4D (第一次测量到往返时间结果,对均值偏差RTO计算初始值)
之后的计算方法如下:
Err = M – A
A <- A + gErr
D <- D + h(|Err| – D)
RTO = A + 4D
其中g是常量增量,取值为1/8(0.125);h也是常量增量,取值为1/4(0.25)。

Karn算法:Karn算法是解决所谓的重传多义性问题的。[Karn and Partridge 1987]规定(见《TCP/IP详解 卷一》参考文献),当一个超时和重传发生时,在重传数据的确认最后到达之前,不能更新RTT估计器,因为我们并不知道ACK对应哪次传输(也许第一次传输被延迟而并没有被丢弃,也有可能是第一次传输的ACK被延迟丢弃)。并且,由于数据被重传,RTO已经得到了一个指数退避,我们在下一次传输时使用这个退避后的RTO。对一个没有被重传的报文段而言,除非收到了一个确认,否则不要计算新的RTO。
在任何时候对每个连接并行仅测量一次RTT值,在发送一个报文段时,如果给定连接的定时器已经被使用,则该报文段不被计时,反之如果给定连接的定时器未被使用,则开始计时以测量RTT值。即并非每个发出报文段都进行测量RTT值,同一时间段里只能有一个RTT值测量行为进行,不会并行进行多个RTT值测量。
3.慢启动
如果发送方一开始便向网络发送多个报文段,直至达到接收方通告窗口大小为止。当发送方与接收方在同一局域网时,这种方式是可以的。但如果在发送方与接收方之间存在多个路由器和速率较慢的链路时,就可能出现问题。一些中间路由器必须缓存分组,并有可能耗尽存储器的空间,将来得降低TCP连接的吞吐量。于是需要一种叫“慢启动”的拥塞控制算法。
慢启动为发送方增加一个拥塞窗口,记为cwnd,当与另一个网络的主机建立连接时,拥塞窗口被初始化为1个报文段。每收到一个ACK,拥塞窗口就增加一个报文段(cwnd以字节为单位,但慢启动以报文段大小为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口是接收方使用的流量控制。
发送方开始时发送一个报文段,然后等待ACK。当收到该ACK时,拥塞窗口从1增加到2,即可以发送两个报文段。当收到这两个报文段的ACK时,拥塞窗口就增加为4。这是一种指数增加的关系。
4.拥塞避免
慢启动算法增加拥塞窗口大小到某些点上可能达到了互联网的容量,于是中间路由器开始丢弃分组。这就通知发送方它的拥塞窗口开得太大。拥塞避免算法是一种处理丢失分组的方法。该算法假定由于分组受到损坏引起的丢失是非常少的(远小于1%),因此分组丢失就意味着在源主机和目标主机之间的某处网络上发生了拥塞。有两种分组丢失的指示:发生超时和接收到重复的确认。拥塞避免算法与慢启动算法是两个独立的算法,但实际中这两个算法通常在一起实现。
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh。算法的工作过程如下:
1) 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
2) TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
3) 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
4) 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止(因为我们记录了在步骤2中给我们制造麻烦的窗口大小的一半),然后转为执行拥塞避免。
慢启动算法初始设置cwnd为1个报文段,此后每收到一个确认就加1。这会使窗口按指数方式增长:发送1个报文段,然后是2个,接着是4个……。拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加比起来,这是一种加性增长。我们希望在一个往返时间内最多为cwnd增加1个报文段(不管在这个RT T中收到了多少个ACK),然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。
处于拥塞避免状态时,拥塞窗口的计算公式如下(引公式参照BSD的实现,segsize/8的值是一个匹配补充量,不在算法描述当中):
cwnd <- cwnd + segsize * segsize / cwnd + segsize / 8
5.快速重传与快速恢复
由于我们不知道一个重复的ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的ACK到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的ACK之前,只可能产生1 ~ 2个重复的ACK。如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。接下来执行的不是慢启动算法而是拥塞避免算法。这就是快速恢复算法。
这个算法通常按如下过程进行实现:
1) 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
2) 每次收到另一个重复的ACK时,cwnd增加1个报文段大小并发送1个分组(如果新的cwnd允许发送)。
3) 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
六、代理socks5支持
参照RFC1928、RFC1929,在TDP实现中,支持匿名通过socks5代理以及用户名/密码验证方式通过socks5代理。
由于socks5代理是工作于运输层上,因此连接当中对IP层选项的设置都将没有效果。socks5代理起到的作用只是应用数据的转发,但这已经基本上能支持大部分用户程序的应用需求。在使用socks5代理进行工作中,路径MTU的发现机制,将无法有效工作,此时MSS默认为536(MTU默认为576),用户程序可以修改使用的MSS值。
七、安全考虑
TDP协议及算法方面并不对数据的安全性做任何考虑,用户程序在传输数据时如果对安全性有要求,可以自行在应用数据层做相应的工作。但TDP实现中,会提供一个简单的AES256位加解密方法,提供给用户程序使用。用户程序可以调用该加解密方法,对数据进行加密然后再通过网络进行发送,接收时将加密数据流进行解密再将会用户程序数据逻辑处理模块进行处理。
八、定时器
如BSD的TCP实现类似,TDP也为每条连接建立了六个定时器,简要介绍如下:
1)“连接建立”定时器,在发送SYN报文段建立一条新的连接时启动。如果没有在75秒内收到响应,连接建立将中止。
2)“重传”定时器,在发送数据时设定。如果定时器已超时而对端的确认还未到达,将重传数据。重传定时器的值是动态计算的,取决来RTT与该报文段被重传的次数。
3)“延迟ACK”定时器,收到必须确认但无需马上发出确认的数据时设定。等待200ms后发送确认响应。如果,在这200ms内,有数据要在该连接上发送,延迟的ACK响应就可随数据一起发送回对端,称为捎带确认。
4)“坚持”定时器,在连接对端通告接收窗口为0,阻止继续发送数据时设定。坚持定时器在超时后向对端发送1字节的数据,判定对端接收窗口是否已经打开。坚持定时器的值是动态的计算的,取决于RTT值,在5秒与60秒之间取值。
5)“保活”定时器。TDP连接在一定时间段内如果没有数据交互的话,将主动发送保活LIV报文段。即当“保活”定时器超时,说明没有数据交互,则发送保活数据包。保活定时器默认时间为2分钟,用户程序可以进行设置。
6)TIME_WAIT定时器,也可称为2MSL定时器(实现中,一个MSL为1分钟)。当连接状态转移到TIME_WAIT时,即连接主动关闭时,定时器启动。
九、开发接口
使用TDP进行网络程序开发是非常容易的,它的开发接口(API)与socket API是非常相似的,尤其是对应功能的函数名称都是一致的,需要注意的是TDP的所有API都处于名称空间TDP之下。
-----------------

UDP/TCP封包


MTU
1500字节: 以太网.
1492字节: PPPoE.
1472字节: ping
1468字节: DHCP
1430字节: VPN and PPTP
576字节: 拨号ISP
RFC 1883: 最小576,新的可能会是1280
UDP一次发送数据包的大小,TCP一次发送数据包的大小。
MTU最大传输单元,这个最大传输单元实际上和链路层协议有着密切的关系,EthernetII帧的结构DMAC+SMAC+Type+Data+CRC
由于以太网传输电气方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes,对于小于或者大于这个限制的以太网帧
我们都可以视之为错误的数据帧,一般的以太网转发设备会丢弃这些数据帧。
由于以太网EthernetII最大的数据帧是1518Bytes这样,刨去以太网帧的帧头(DMAC目的MAC地址48bit=6Bytes+SMAC
源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾CRC校验部分4Bytes那么剩下承载上层协议的地方也就是
Data域最大就只能有1500Bytes这个值我们就把它称之为MTU。
PPPoE所谓PPPoE就是在以太网上面跑PPP协议,有人奇怪了,PPP协议和Ethernet不都是链路层协议吗?怎么一个链路层跑到另外一个链路
层上面去了,难道升级成网络层协议了不成。其实这是个误区:就是某层协议只能承载更上一层协议。
为什么会产生这种奇怪的需求呢?这是因为随着宽带接入(这种宽带接入一般为Cable
Modem或者xDSL或者以太网的接入),因为以太网缺乏认证计费机制而传统运营商是通过PPP协议来对拨号等接入服务进行认证计费的.
PPPoE带来了好处,也带来了一些坏处,比如:二次封装耗费资源,降低了传输效能等等,这些坏处俺也不多说了,最大的坏处就是PPPoE
导致MTU变小了以太网的MTU是1500,再减去PPP的包头包尾的开销(8Bytes),就变成1492。
UDP 包的大小就应该是 1492 – IP头(20) – UDP头(8) = 1464(BYTES)
TCP 包的大小就应该是 1492 – IP头(20) – TCP头(20) = 1452(BYTES)
目前大多数的路由设备的MTU都为1500
浅谈以太网中的UDP编程   
  
1.在进行UDP编程的时候,我们最容易想到的问题就是,一次发送多少bytes好?   
当然,这个没有唯一答案,相对于不同的系统,不同的要求,其得到的答案是不一样的,我这里仅对   
像ICQ一类的发送聊天消息的情况作分析,对于其他情况,你或许也能得到一点帮助:   
首先,我们知道,TCP/IP通常被认为是一个四层协议系统,包括链路层,网络层,运输层,应用层.   
UDP属于运输层,下面我们由下至上一步一步来看:   
以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的.   
这个1500字节被称为链路层的MTU(最大传输单元).   
但这并不是指链路层的长度被限制在1500字节,其实这这个MTU指的是链路层的数据区.   
并不包括链路层的首部和尾部的18个字节.   
所以,事实上,这个1500字节就是网络层IP数据报的长度限制.   
因为IP数据报的首部为20字节,所以IP数据报的数据区长度最大为1480字节.   
而这个1480字节就是用来放TCP传来的TCP报文段或UDP传来的UDP数据报的.   
又因为UDP数据报的首部8字节,所以UDP数据报的数据区最大长度为1472字节.   
这个1472字节就是我们可以使用的字节数。:)   
  
当我们发送的UDP数据大于1472的时候会怎样呢?   
这也就是说IP数据报大于1500字节,大于MTU.这个时候发送方IP层就需要分片(fragmentation).   
把数据报分成若干片,使每一片都小于MTU.而接收方IP层则需要进行数据报的重组.   
这样就会多做许多事情,而更严重的是,由于UDP的特性,当某一片数据传送中丢失时,接收方便   
无法重组数据报.将导致丢弃整个UDP数据报。   
  
因此,在普通的局域网环境下,我建议将UDP的数据控制在1472字节以下为好.   
  
进行Internet编程时则不同,因为Internet上的路由器可能会将MTU设为不同的值.   
如果我们假定MTU为1500来发送数据的,而途经的某个网络的MTU值小于1500字节,那么系统将会使用一系列的机   
制来调整MTU值,使数据报能够顺利到达目的地,这样就会做许多不必要的操作.   
  
鉴于Internet上的标准MTU值为576字节,所以我建议在进行Internet的UDP编程时.   
最好将UDP的数据长度控件在548字节(576-8-20)以内.   
  
2.UDP数据报的覆盖和重叠问题?   
有的兄弟说使用UDP编程时会出现数据的覆盖和重叠问题,   
所谓覆盖,即发送第一条消息为”第一条”,第二条消息为”第二条”.   
而接收到的两条消息皆为”第一条”.   
而重叠,即指当发送”第一条”,”第二条”两条消息后.   
收到的第一条消息可能是不固定的,比如”第一条第二”,”第一条第二条”等.   
这种重叠的情况在TCP编程中是常见的.   
但是在我的编程经验中,从来没有遇到过这两种情况.   (UDP面向包的,但不保证顺序,TCP面向流的,保证顺序,但不保证整包)
  
我在局域网中在机器A用使死循环连续不断的向机器B发送UDP数据报.   
但一直没有出现上面的两个问题.   
  
因此我认为,根据UDP协议的特性,不会象基于字节流连接的TCP一样出现重叠问题.   
有兄弟说,他在局域网试也没有问题,但在Internet上会.是不是路由器对数据进行组合,   
或者说两条消息同时到达?   
  
我想两条消息到达的时延无论如何不会比在局域网中的时延短吧?即时有,那种机率也是很少的.   
而对于路由数据进行了重组,我认为,即使假定路由器对数据进行了重组,这也对导致UDP数据报   
在接收时发现数据报中的校验和与数据不一致而丢弃该数据报。 

from http://www.wy182000.com/2011/07/18/udp-tcp-udptcp%E5%B0%81%E5%8C%85/