??xml version="1.0" encoding="utf-8" standalone="yes"?> 另外Q承自TCP拥塞控制的不公^的RTT也成为在分布式数据密集程序中的严重问题。拥有不同RTT的ƈ发TCP将不公q_分n带宽。尽在的BDP|络中用通常的TCP实现来相对^{的׃n带宽Q但在拥有大量BDP的网l中Q通常的基于TCP的程序就必须承受严重的不公^的问题。这个RTTZ的算法严重的限制了其在广域网分布式计的效率Q例如:internet上的|格计算?/p>
一直到今天Q对标准的TCP的提高一直都不能在高BDP环境中效率和公^性方面达到满意的E度Q特别是ZRTT的问题)。例如:TCP的修改,RFC1423Q高性能扩展Q,RFC2018QSACKQ、RFC2582QNew RenoQ、RFC2883QD-SACKQ、和RFC2988QRTO计算Q都或多或少的提高了Ҏ率,但最Ҏ的AIMD法没有解决。HS TCPQRFC 3649Q通过Ҏ上改变TCP拥塞控制法来在高BDP|络中获得高带宽利用率,但公qx问题仍然存在?/p>
考虑C面的背景Q需要一U在高BDP|络支持高性能数据传输的传输协议。我们推荐一个应用程序别的传输协议Q叫UDT或基于UDP的数据传输协议ƈ拥有用塞控制法?/p>
本文描述两个正交的部分,UDP协议和UDT拥塞控制法。一个应用层U别的协议,位于UDP之上Q用其他的拥塞法Q然而这些本文中描述的算法也可以在其他协议中实现Q例如:TCP?/p>
一个协议的参考实现叫[UDT]Q详l的拥塞控制法的性能分析在[GHG04]中可以找到?/p>
2. 设计目标 UDT的主要目标是效率、公q뀁稳定。单个的或少量的UDT应该利用所有高速连接提供的可用带宽Q即使带宽变化的很剧烈。同Ӟ所有ƈ发的必dq_׃n带宽Q不依赖于不同的带宽瓶劲、v始时间、RTT。稳定性要求包发送速率应该一直会聚可用带宽非常快Qƈ且必避免拥塞碰撞?/p>
UDTq不是在瓶劲带宽相对较小的和大量多元短文件流的情况下用来取代TCP的?/p>
UDT主要作ؓTCP的朋友,和TCPq存QUDT分配的带宽不应该过ҎMAX-MIN规则的最大最公q_享原则。(备注Q最大最规则允许UDT在高BDPq接下分配TCP不能使用的可用带宽)。我?/p>
3. 协议说明 接收者也负责触发和处理所有的控制事gQ包括拥塞控制和可靠性控制和他们的相ҎӞ例如RTT估计、带宽估计、应{和重传?/p>
UDTL试着应用层数据打包成固定的大小Q除非数据不够这么大。和TCP怼的是Q这个固定的包大叫做MSSQ最大包大小Q。由于期望UDT用来传输大块数据,我们假定只有很小的一部分不规则的大小的包在UDT session中。MSS可以通过应用E序来安装,MTU是其最优|包括所有包_?/p>
UDT拥塞控制法速率控制和窗口(量控制Q合qv来,前者调整包的发送周期,后者限制最大的位被应答的包。在速率控制中用的参数通过带宽估计技术来更新Q它l承来自Z接收的包Ҏ。同Ӟ速率控制周期是估计RTT的常量,控制参C赖于Ҏ的数据到N度Q另外接收端释放的缓冲区的大?/p>
3.2.1. 数据?/p>
0 1 3 4 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 0 包序hUDT数据包头中唯一的内宏V它是一个无W号整数Q用标志位后的31位,UDT使用包基的需要,例如Q每个非重传的包都增加序?。序号在到达最大?^31-1的时候覆盖。紧跟在q些数据后面的是应用E序数据?/p>
3.2.2. 控制?br>控制包结构如下: 0 1 3 4 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 1 ?U类型的控制包在UDT中,bit1-3表示q些信息。前32位在包头中必d在。控制信息字D包?Q例如,它不存在Q或者多?2位无W号整数Q这由包cd军_?/p>
UDT使用应答子序LҎ。每个ACK/ACK2包有一个无W号?6位序P它独立于数据包需要。它使用?6-31。应{需要从0刎ͼ2^16-1Q。位16-31在其他控制包中没有定义?/p>
cd 2Q?2?内部序?/p>
3Q?2?MSSQ字节) 4Q?2?最大流量窗口大(字节Q?/p>
2Q?2位,RTTQ微U) 3Q?2位,RTT 变量或者RTTVar (微秒) 4Q?2位,量H口大小Q包的数量) 5Q?2位,q接定w估计Q每U包的数量) 注意Q对于数据和控制包来_可以从UDP协议头中得到实际的包大小。包大小信息能被用来得到有效的数据负载和NAK包中的控制信息字D大?/p>
3.3. 定时?br>UDT在接收端使用4个定时器来触发不同的周期事gQ包括速率控制、应{、丢失报告(negative应答Q和重传/q接l护?/p>
UDT中的定时器用系l时间作为源。UDT接收端主动查询系l时间来查一个定时器是否q期。对于某个定时器T来说Q其拥有周期TPQ将定变量t用来记录最qT被设|或复位的时间。如果T在系l时间t0Qt= t0Q被复位Q那么Q何t1Qt1-t>=TPQ是Tq期的条件?/p>
四个定时器是QRC定时器、ACK定时器、NAK定时器、EXP定时器。他们的周期分别是:RCTP、ATP、NTP、ETP?/p>
RC定时器用来触发周期性的速率控制。ACK定时器用来触发周期性的有选择的应{(应答包)。RCTP和ATP是常量|gؓQRCTP=ATP=0.01U?/p>
NAK被用来触发negative应答QNAK包)。重传定时器被用来触发一个数据包的重传和l护q接状态。他们周期依赖于对于RTT的估计。ETPg依赖于连lEXP旉溢出的次数。推荐的RTT初始值是0.1U,而NTP和ETP的初始值是QNTP=3*RTTQETP=3*RTT+ATP?/p>
在每ơbounded UDP接收操作Q如果收C个UDP包,一些额外的必须的数据处理时_时查询系l时间来查四个定时器是否已经q期。推荐的周期_度是微U。UDP接收旉溢出值是实现的一个选择Q这依赖于@环查询的负担和事件周期精度之间的权衡?/p>
速率控制事g更新包发送周期,UDT发送端使用STP来安排数据包的发送。假定一个在旉t0被发送,那么下一ơ包发送时间是Qt0+ STPQ。换句话_如果前面的包发送花费了t’旉Q发送端等待(STP-t’Q来发送下一个数据包Q如果STP-t’ <0Q就不需要等待了Q。这个等待间隔需要一个高_度的实现Q推荐用CPU旉周期_度?/p>
3.4. 发送端法 BQ?发送端丢失链表Q发送段丢失列表是一个连接链表,用来存储被接收方NAK包中q回的丢失包序号。这些数字以增加的顺序存储?/p>
3.4.2. 数据发送算?br>AQ?如果发送端的丢失链表是非空的,重传W一个在list中的包,q删除该成员Q到5?/p>
BQ?{待有应用程序数据需要发?/p>
CQ?如果未应{的包数量超q了两量H口的大,转到1。如果不是包装一个新的包q发送它?/p>
DQ如果当前包的序h16nQn是一个整敎ͼ转第2步?/p>
EQ?在SND PKT历史H口中记录包的发送时?/p>
FQ?如果q是自上ơ发送速率降低之后的第一个包Q等外SYN旉?/p>
GQ等外(STP – tQ时_t是第1到第4步之间的L_然后转到1?/p>
3.5. 接收端算?br>3.5.1. 数据l构和变?br>AQ?接收端丢失链表:是一个dupleq接链表Q元素的值包括:丢失数据包的序号、最q丢失包的反馈时间和包已l被反馈的次数。g包序号增序的方式存储?/p>
BQ?应答历史H口Q每个发送ACK的和旉一个@环数l;׃其@环的Ҏ,意味着如果数组中没有更多空间的时候新的值将覆盖老的倹{?/p>
CQ?RCV PKT历史H口Q一个用来记录每个包到达旉的@环数l?/p>
DQ对包窗口:一个用来记录每个探包对之间的旉间隔?/p>
EQ?LRSNQ一个用来记录最大接收数据包需要的变量。LRSN被初始化为初始序号减1?/p>
3.5.2. 数据接收法 BQ?启动一个时间bounded UDP接收。如果每个包刎ͼ??/p>
CQ?讄exp-count?Qƈ更新ETP为:ETP=RTT+4*RTTVar + ATP?/p>
DQ如果所有的发送数据包已经被应{,复位EXP旉变量?/p>
EQ?查包头的标志位。如果是一个控制包Q根据类型处理它Q然后{1?/p>
FQ?如果当前数据包的需要是16n+1Qn是一个整敎ͼ记录当前包和上个在对包窗口中数据包的旉间隔?/p>
GQ在PKT历史H口中记录包到达旉 HQ?如果当前数据包的序号大于LRSN+1Q将所有在Q但不包括)q两个g间的序号攑օ接收丢失链表Qƈ在一个NAK包中这些序号发送给发送端。如果序号小于LRSNQ从接收丢失链表中删除它?/p>
IQ?nbsp; 更新LRSNQ{1?/p>
3.5.3. RC定时器到 q程如下Q?/p>
AQ?按照下面的原则查找接收端所接收到的所有包之前的序P如果接收者丢失链表是I的QACKL是LRSN+1Q否则是在接收丢失队列中的最序受?/p>
BQ?如果应答号不大于曄被ACK2应答的最大应{号Q或{于上次应答的应{号q且两次应答之间的时间间隔小于RTT+4*RTTVarQ停止(不发送应{)?/p>
CQ?分配q个应答一个唯一增加的ACK序列P推荐采用ACK序列h步骤1增加Qƈ且重叠在辑ֈ最大g后?/p>
DQ根据下面的法来计包的抵N度Q用PKT历史H口中的D最q?6个包抵达间隔QAIQ中倹{在q?6个gQ删除那些大于AI*8或小于AI*8的包Q如果最后剩?个|计算他们的^均?AI’)Q包抵达速度?/AI’Q每U包的数量)Q否则是0?/p>
EQ?Ҏ3.7节中的内容ؓ每端QWQ计流量窗口。然后计有效的量H口大小为:最大(WQ可用接收方~冲大小Q,2Q?/p>
FQ?Ҏ下面的算法来计算q接定w估计。如果流量控制快启动阶段Q?.7Q一直l,q回0Q否则计最q?6个对包间隔(PIQ,q些值在对包H口中,那么q接定w是1/PIQ每U包的数量)?/p>
GQ打包应{序列号Q应{号QRTTQRTT 变量Q有效的量H口大小q估计连接,他们放入ACK包中Q然后发送出厅R?/p>
HQ?记录ACK序列P应答号和q个应答的开始时_q放入历史窗口中?/p>
3.5.4. 处理NAK定时器到?br>Ø 查找接受方的丢失链表Q找到所有上ơ反馈时间是Qk*QRTT+4*RTTVar ) Q前的包Qk当前q个包的反馈ơ数?Q如果没有反馈丢失,停止?/p>
Ø 压羃W一步中得到的序P?.9Q,然后在一个NAK包中发送他们到发送方?/p>
Ø 如果不是停止量控制快启动阶Dc?/p>
3.5.5. 处理EXP定时?br>AQ?nbsp; 如果发送端的丢失链表不是空的,停止 BQ?nbsp; 所有未应答的包攑ֈ发送端的丢失链表中 CQ?如果(exp-count>16)q且自上ơ从Ҏ接收C个包以来的L间超q?U,或者这个时间已l超q?分钟了,q被认ؓ是连接已l断开Q关闭UDTq接?/p>
DQ如果没有数据,也就没有应答Q发送一个保zdl对端,否则所有未应答包的序号攑օ发送丢失列表中?/p>
EQ?nbsp; 更新exp-count为:exp-count= exp-count+1 FQ?nbsp; 更新ETP为:ETP=exp-count*QRTT+4*RTTVarQ?ATP?/p>
3.5.6. 收到应答?br>AQ?nbsp; 更新最大的应答序号 BQ?更新RTT和RTTVar为:RTT = rttQ?RTTVar = rvQrtt和rv是ACK包中的RTT和RTTVar倹{?/p>
CQ?nbsp; 更新NTP和ETP为:NTP=RTT+4*RTTVarQETP=exp-count*QRTT+4*RTTVarQ?ATP?/p>
DQ?nbsp; 更新q接定w估计QB=QB*7+bQ?8Qb是ACK包带的倹{?/p>
EQ?nbsp; 更新量H口大小为ACK中的倹{?/p>
FQ?nbsp; 发送ACK2包,q设|与ACK序号相同的应{号到对?/p>
GQ?nbsp; 复位EXP定时?/p>
3.5.7. 当收到NAK包的时?br>AQ?所有NAK包中带的序号攑օ发送方的丢失列表中 BQ?通过速率控制来更新STPQ见3.6Q?/p>
CQ?复位EXP定时?/p>
3.5.8. 当收到ACK2?br>Ø 在ACK历史H口中根据接收到的ACK2序列h找行营的ACK包?/p>
Ø 更新曄被应{的最大应{号 Ø ҎACK2的到达时间和ACKd旉计算新的rtt|q且更新RTT和RTTVargؓQ?/p>
RTTVar = (RTTVar *3 +abs(rtt-RTT)/4 RTT = (RTT *7+rtt)/8 RTT和RTTVar的初始值是0.1U和0.05U?/p>
Ø 更新NTP和ETP为: NTP = RTTQ?/p>
ETP = (exp-count +1)* RTT+ATP 3.5.9. 当收Czd的时?br>什么也不做 3.5.10. 当收到连接握手和关闭包的时?br>?.8?/p>
3.6. 速度控制法 快启动阶D仅仅在开始一个UDTq接的时候发生,且不会在UDTq接的以后再出现。在快启动阶D之后,下面的算法就要工作了?/p>
3.6.2. 当RC定时器时间到 2Q?nbsp; 计算在上个RCTP旉内的丢失率,计算Ҏ是根据d发送的包与NAK反馈中d丢失包的数量。如果丢q大于0.1%Q停止?/p>
3Q?nbsp; 下个RCTP旉内发送包的增加数量如下计:(inc) If (B<=C) inc = 1/MSS Else inc = max (10^(ceil(log10((B-C)*MSS*8)))*Beta/MSS,1/MSS) B是连接容量估计,C是当前的发送速度。两个都计算为每U多个包。MSS是以字节计算的;Beta是gؓ0.0000015的常量?/p>
4Q?nbsp; 更新STPQSTP=QSTP*RCTPQ?QSTP*inc + RCTPQ?/p>
5Q?nbsp; 计算真正的数据发送周期(rspQ,从SND PKT历史H口中得刎ͼ如果QSTP<0.5 *rspQ设|STP为(0.5 * rspQ?/p>
6Q?nbsp; 如果QSTP<1.0Q,讄STP?.0?/p>
3.6.3. 当收到NAK包时 2Q?nbsp; NumNAKQ自上次LSD更新以后的NAK数量 3Q?nbsp; AvgNAKQ当最大序号大于LSD时两ơ事件之间的NAKUd的^均数?/p>
4Q?nbsp; DRQ在1到AvgNAK之间的随机^均数?/p>
3.6.3.2. 法 增加STP为:STP=STP*Q?+1/8Q?/p>
更新AvgNAK为:AvgNAK = QAvgNAK *7 +NumNAKQ?8 更新DR 复位 NumNAK = 0 记录LSD 2Q?nbsp; 否则Q增加NumNAK按照1个步骤增加;如果NumNAK % DR = 0Q增加STP为:STP=STP*Q?+1/8Q;记录LSD?/p>
3.7. 量控制法 3.7.1. 当ACK定时器到的时?br>1Q?nbsp; 量控制快启动:如果没有NAK产生或者W没有到达或超q?5个包Qƈ且AS>0Q流量窗口大更Cؓ应答包的L量?/p>
2Q?nbsp; 否则Q如果(AS>0Q,W更新为:QAS是包的到N度Q?/p>
W= ceil (W *0.875+AS* (RTT +ATP) *0.125) 3Q?nbsp; 限制W到对Ҏ大流量窗口大?/p>
3.8. q接建立和关?br>一个UDT实体首先作ؓ一个SERVER启动Q当一个客L需要连接的时候其发送握手包。客L在从服务端接收到一个握手响应包或时间溢Z前,应该每隔一D|间发送一个握手包Q时间间隔由响应旉和系loverhead来权衡)?/p>
握手包有如下信息Q?/p>
1Q?nbsp; UDT版本Q这个值是兼容的目的。当前的版本? 2Q?nbsp; 初始序号Q这是发送这个UDT实体来用于发送数据包的v始序受它必须是一个在1刎ͼ2^31-1Q之间的随机倹{另外,q个值在合理的时间历史窗口中不应该重复?/p>
3Q?nbsp; MSSQ数据包的大(通过IP有效负蝲来度量) 4Q?nbsp; 最大的量H口大小Q这是接收到握手信息的UDT实体允许的最大流量窗口大,H口大小通常限制为接收端的数据结构大?/p>
服务器接收到一个握手包之后Q比较MSS值和他自q值ƈ讄它自qgؓ较小的倹{结果g在握手响应中被发送到客户端,另外q有服务器的版本信息Q初始序列号Q最大流量窗口大?/p>
版本字段用来查两端的兼容性。初始序列号和最大流量窗口大用于初始化接收到这个握手包的UDT实体参数?/p>
服务器在W一步完成以后就准备发送或接收数据。然而,只要从同一个客L接收M握手包,其应该发送响应包?/p>
客户端一旦得到服务器的一个握手响应其p入发送和接收数据状态。设|它自己的MSS为握手响应包中的值ƈ初始化相应的参数为包中的|序列受最大流量窗口)。如果收CQ何其他的握手信息Q丢掉它?/p>
如果其中的UDT实体要关闭,它将发送一个关闭信息到对端Q对Ҏ到这个信息以后将自己关闭。这个关闭信息通过UDP传输Q仅仅发送一ơ,q不保证一定收到。如果消息没有收刎ͼҎ根据时间溢出机制来关闭q接?/p>
3.9. 丢失信息的压~方?br>NAK包中携带的丢׃息是一?2-bit整数的数l。如果数l的中数字是一个正常的序号Q第1位是0Q,q意味着q个序号的包丢失了,如果W?位是1Q意味着从这个号码开始(包括该号码)C一个数l中的元素(包括q个元素|之间的包Q它的第1位必L0Q都丢失?/p>
例如Q下面的NAK中携带的信息Q?/p>
0x00000002, 0x80000006, 0x0000000B, 0x0000000E 上面的信息表明序号ؓQ?Q?Q?Q?Q?Q?0Q?1Q?4的包都丢了?/p>
4. 效率和公qx?br>UDT能够充分利用当前有线|络的独立于q接定w的可用带?、RTT、后台共存流、给定的q接比特错误率。UDT在没有数据包丢失的情况下?bits/s?0%带宽需要一个常量时_q个旉?.5U。UDTq不适合无线|络?/p>
UDT的确满单瓶劲网l拓扑的最?最公qx。在多个瓶劲情况下,Ҏ最大最原则它能保证较瓶劲连接或者至一半的q等׃n(it guarantees that flows over smaller bottleneck links obtain at least half of their fair share according to max-min rule)。RTT对公qx都一点媄响?/p>
当和大块的TCP共存的时候,TCP能占用比UDT更多的带宽,除了三种情况Q?/p>
1Q?nbsp; |络BDP非常大,TCP不能利用他们的公q_享带宽。这U情况下QUDT占用TCP不能利用的带宽?/p>
2Q?nbsp; q接定w是如此的,从而导致UDT的带宽估计技术不能最有的工作Q模拟显C个极限连接容量大U是100kb/s?/p>
3Q?nbsp; 在用FIFO队列作ؓ|络路径的网l中Q如果队列大大于BDPQTCP的共享带宽随着队列大小的增加而降低。然而,抵达UDT的共享带宽是Q队列大通常过实际路由?交换机提供的数量?/p>
当短QtimewiseQ类似web的TCP和的q发UDT共存的时候,UDT在TCP上的效果非常小?/p>
更多的分析在[GHG03]?/p>
5. 安全考虑 然而,׃UDP是无q接的,UDT实现应该查所有达到的包是否是预期的来源。这是从socket的APIq接概念中承而来Q其q接只是接收指定来源的数据?/p>
2.1.3 最大传输报文大(MSSQ?br> TCP报文D在q接建立旉要通报MSSQ在TDP的实C也进行通报Q默认通报?460字节Q符合以太网标准Q这个默认值允?0字节的IP首部?字节的UDP首部?2字节的TDP首部Q以适合 1500字节的IP数据报)默认值可以由用户E序讄?br> TCP在对端地址为非本地IPӞ默认通报?36字节。TDP之所以默认通报?460Q是因ؓTDP在数据传输过E中Q实C路径MTU发现技术,通过实际发现的MTUQ进行MSS的动态调_以尽量避免报文段在网l中的传输生分片的情况。\径MTU发现技术在传输数据一节中q行描述?br>
std::placeholders::_1
改用
std::error_code
O?img src ="http://www.shnenglu.com/jack-wang/aggbug/229784.html" width = "1" height = "1" />
]]>
]]>
1. 介绍
随着|络带宽时g产品(BDP)的增加,通常的TCP协议开始变的低效。这是因为它的AIMDQadditive increase multiplicative decreaseQ算法彻底减了TCP拥塞H口Q但不能快速的恢复可用带宽。理Z的流量分析表明TCP在BDP增加到很高的时候比较容易受包损失攻凅R?/p>
UDT主要用在数量的bulk源共享富裕带宽的情况下,最典型的例子就是徏立在光纤q域|上的网D,一些研I所在这L|络上运行他们的分布式的数据密集E序Q例如,q程讉K仪器、分布式数据挖掘和高分L率的多媒体流?/p>
3.1. 概述
UDT是双工的Q每个UDT实体有两个部分:发送和接收。发送者根据流量控制和速率控制来发送(和重传)应用E序数据。接收者接收数据包和控制包QƈҎ接收到的包发送控制包。发送和接收E序׃n同一个UDP端口来发送和接收?/p>
3.2. 包结?br>UDT有两U包Q数据包和控制包。他们通过包头的第一位来区分Q标志位Q。如果是0Q表C是数据包,1表示是控制包?/p>
数据包结构如下显C:
包序?br>
应用数据
cd
保留
ACK序号
控制信息字段
说明
控制信息
000
协议q接握手
1Q?2?UDT版本
001
保活
没有
010
应答Q位16-31是应{序?br> 1Q?2位包序号Q先前接收到的包序号
011
Negative应答QNAKQ?br> 丢失信息?2位整数数l,?.9?br>
100
保留
q种cd的控制信息保留作为拥塞警告用,从接收到发送端。一个拥塞警告能被ECN或包延迟增加势的度量方法触发?br>
101
关闭
110
应答一个应{(ACK2Q?br> 16-31位,应答序号?br>
111
4-15的解?br> 保留来使用
3.4.1. 数据l构和变?br>AQ?SND PKT历史H口Q一个@环数l记录每个数据包的开始时?/p>
AQ?查询pȝ旉来检查RC、ACK、NAK、或EXP定时器是否过期。如果Q一定时器过期,处理事gQ本节下面介l)q复位过期的定时器?/p>
通过速率控制法来更新STPQ见3.6节)?/p>
3.6.1. 速率控制快启?br>STP被初始ؓ最的旉_ֺQ?个CPU周期?毫秒Q。这是在快启动阶D,一般收C个ACK包其携带的估计带宽大?q个阶段停止了。包的发送周期被讄?/WQW是ACK携带的流量窗口的大小?/p>
1Q?nbsp; 如果在上一个RCTP旉内,没有收到一个ACKQ停?/p>
3.6.3.1. 数据l构和变?br>1Q?nbsp; LSDQ自上次速率降低后发送的最大序?/p>
1Q?nbsp; 如果NAK中最大的丢失序列号大于LSDQ?/p>
量控制H口大小QWQ初始值是16?/p>
UDTq没有用特定的安全机制Q相反,它依赖于应用E序提供的授权和底层提供的安全机制?/p>
6.UDT SOURCE CODE LINK
http://sourceforge.net/projects/dataspace
本文来自CSDN博客Q{载请标明出处Q?a >http://blog.csdn.net/windcsn/archive/2006/01/04/570242.aspx
]]>
随着互联|应用广泛推q,出现了越来越多的|络应用Q其中基于p2p思想的各U网l技术的产品也越来越多的出现在我们的视野当中。从最早闻名的Napster到现在的Bittorrent、eMule、skype{品,P2Pq种|络应用模式已经从各个方面深入h心。这些品在各自的网l实现技术上Q都以各自的Ҏ解决着同样面的一个问题,如何让他们的软g产品在各异的|络拓扑l构中顺利的q行P2P通信?br> 众所周知Q在当今的网l拓扑结构中Q普遍存在用NAT讑֤来进行网l地址转换Q而让应用E序能跨这些NAT讑֤q行全双工的通信Q就成ؓ非常重要的一个问题。对于实现跨NAT通信可以采取很多U办法(对于能够直接q接、反向连接的情况不在此列Q:首先是通过服务器进行{发,q是比较_暴的方法,而且在用户量大的时候,转发服务器需要付出相当大的代PW二Q可以用NATIK技术。而大家知道关于NATIK中QUDPIK的成功率比起TCPIK要高出许多Q这一点这里将不做多述Q可以参考Bryan Ford的文章《Peer-to-Peer Communication Across Network Address Translators》(http://www.brynosaurus.com/pub/net/p2pnat/Q。因此在UDP协议上构Z些大型的|络应用E序可能会成为很多h的需求?br> 当然也可能基于更多的原因Q会有很多h希望能在UDP协议上进行大型应用程序的构徏。然而UDP协议本n存在着不通信不可靠的~点Q于是对于基于UDPq行可靠通信的需求就现出来了。目前在|络上有许多人正做着q一工作QUDT、RakNet、eNet{都是构建在UDP之后|络可靠通信开发库。然后这些库开发时都针对了一些特D应用来q行设计的,不具备通用性。比如RakNet是ؓ游戏应用而设计,对于实时性等游戏相关的网l需求有很好的支持,对于大批量数据传输却有点力所不及。而UDTZ一U基于带宽速率控制的拥塞控制算法进行设计(http://udt.sourceforge.net/doc/draft-gg-udt-01.txtQ,主要用在数量的bulk源共享富裕带宽的情况下,最典型的例子就是徏立在光纤q域|上的网D,而在ISP提供带宽有限的情况下q行却显得消耗资源ƈ性能不。甚臛_能被防火墙,或ISP服务商判断ؓ恶意带宽使用d。这些都使用得他们不能被q泛地用于各U网l应用程序。另外大家也陆箋发现目前的UDT实现版本存在的一些问题。比如UDT做服务端接收q接ӞL新开一个端口与客户端进行连接,q样会带来几个问题:1Q较多客Lq接上来Ӟ服务端新打开的众多端口中可能有的端口会被防火墙拦截而导致通信p|Q?Q如果客L处于Symmetric NAT和Port-Restricted Cone NAT后面Ӟ导致服务器端与客户端连接无法成功徏立,3Q由于udp端口数最大值有限,所以UDT服务器端可接收的q接C因些受限。再有就是不仅仅是UDT库,基本上所有的UDP-based可靠通信库,都未提供I越proxy代理的功能(socks5Q;再有是对UDP打洞技术有的支持得不完善或q不支持?br> Zq些原因Q得我需要开发一个基于UDP协议之上实现一个可靠、高效、通用的通信库,来满x目前所开发的目的需要。TCP协议法已经是经q多斚w及多q的验证Q是最具通用性,且可靠高效的。虽然UDT{各U库指出TCP在这h那样的网l环境下存在不Q但众多实现当中他仍然是最通用、可靠、高效的。相信有许多我一P需要这么一个开发库Q所以我打算在开发过E中Q陆l公开相关的文档及q个开发库?br>
二、设计目?br>
TDP主要的目标就是在UDP层之上实现TCP的协议算法,使得应用E序能够在UDP层之上获得通用、可靠、高效的通信能力?br> TDP|络开发库所实现的算法,都来自久l考验的TCP协议法Q网上有着非常多的参考资料。在实现当中Q参考最多的是Richard Stevens的《TCP/IP详解》?br> TDP提供的用于开发的应用E序接口与Socket API非常相像Q姑且称之ؓTDP Socket APIQ基本上的函数名与参数等都与Socket API怸_但是TDP Socket API的API接口都位于命名空间TDP当中。只要用过Socket APIq行开发过的朋友,都会用TDP库进行开发。下图ؓTDP及TDP Socket API所处在的协议栈应用中的位置Q以及与TCP协议栈应用的Ҏ?br>
三、协议说?br>
1Q协议格?br>
TDP的实现的法虽然与TCP实现的算法是大致相同?但TDP的协议格式只是从TCP协议格式获得参考,但ƈ不完全与他相同。TDP的协议格式如下:
接下来介l一下协议格式的各个字段含义?br> 4位首部长度:表示用户数据在数据包中的起始位置?br> LIVQ连接保zL志,用于表示TDPq接通\存活状态?br> ACKQ确认序h效?br> PSHQ接收方应该快这个报文段交给应用层?br> RSTQ重接?br> SYNQ同步序L来发起一个连接?br> FINQ发端完成发送Q务?br> 16位窗口大:接收端可接收数据的窗口大?br> 选项Q只有一个选项字段Qؓ最长报文大,即MSS。TDP选项格式与TCP选项格式一_kind=0时表C选项l束Qkind=1时表C无操作Qkind=2时表C最大报文段长度。如下图Q?br>
数据Q用户通过TDP传输的数据?br>
2QTDPq接建立与终?br>
TDP的连接徏立与l止可以参考TCP的状态变q图(此图的详l解释请参考《TCP/IP详解 卷一》第18?Q如下:
2.1q接建立
2.1.1三次握手
q接建立分要l过三次握手q程Q?)客户端发送一个SYND到指明客户打算q接的服务器的端口,报文D中要设|客L初始序号?Q服务器发回包含服务器的初始序号的SYN报文D作为应{。同Ӟ确认序可|ؓ客户的初始序号加1,q设|ACK位标志报文段为确认报文段?Q客L必须确认序可|ؓ服务器初始序号加1,Ҏ务器的SYN报文D进行确认?br> TDP在全局l护一个初始序L子,q个初始序号为随时生的32位整数?br> q接建立的超时和重传初始gؓ3U,时采用指数退避算法,3U超时后时gؓ6U,然后?2U,24U?#8230;…。连接徏立最长时间限制ؓ75U?br>
2.1.2 NAT UDP PUNCH模式
当TDP工作模式是NAT UDP PUNCHӞ在三ơ握手之前,向对端NAT端口及预端口间隔默?ms发送默认ؓ10个LIV报文D,一来用于打开自已的NAT端口Q二来是用于q入对端NAT端口。默认值可以由用户E序讄。这时的LIV报文D中初始序号及确认序号都??br> 当接收到对端LIV报文D后Q立卛_止LIV报文D发送,发出SYN报文D进行连接徏立。这时有两种可能Q其一是另一端直到接收到该SYN报文D之前,都没有接收到LIV报文D,或是刚接收到但没有来得及发送SYN报文D,此时会如上文描q的正常模式下连接徏立的q程一_经历三ơ握手。基二是另一端在接收到该SYN报文D之前,也已l发送出SYN报文D,此时双方都需要对SYN报文D进行确认,可以UC为四ơ握手?
2.1.4 半打开q接及连接保z?br> 半打开q接是指对端异常关闭Q如|线拔掉、突然断늭情况引发一端导演关闭,而另一端的q接却仍然认接处于打开当中Q这U情늧之ؓ半打开q接。TDP中的一个TDP SOCKET描述W由本地IP、本地端口、远端IP、远端端口唯一定。当q端客户端连接请求到来时Q服务端接收到一个新的TDP SOCKET描述W,当这一个描q符唯一定信息已经存在ӞҎ的连接请求发送RST报文D,通知光|连接请求。对于旧的连接,׃zL制自动发现是否ؓ半打开q接Q如果是半打开q接Q则自动关闭该连接。这里RST报文D与TCP中的RST报文D|些不一PTCP的RST报文D工作描q请参考《TCP/IP详解 卷一》?br> q接建立之后QTDPq接需要启动保zL制。TCPq接在没有数据通信的情况下也能保持q接Q但TDPq接不行。TDPq接在一定时间段内如果没有数据交互的话,主动发送保zLIV报文Dc这个时间段ҎTDPq接工作模块不同有所差异Q在NAT UDP PUNCH模式下,q个旉D默认gؓ1分钟Q大多数的NAT中,UDP会话时旉?Q?分钟左右Q;而在常规模块下这个时间段默认gؓ5分钟。默认值可以由用户E序讄Q用L序需要指明两U模块下的保zL间周期。这里TDP的保zL制与TCP中的保活机制完全不一PTCP的保zL制描q请参考《TCP/IP详解 卷一》?br>
2.2q接关闭
TDPq接与TCPq接一h全双工的Q因此每个方向必d独地q行关闭。客hl服务器一个FIN报文D,然后服务器返回给客户端一个确认ACK报文Qƈ且发送一个FIN报文D,当客h回复ACK报文后(四次握手Q,q接q束了?br> TDPq接的一端接收到FIN报文D|Q如果还有数据要发送,需要l将数据q行发送完成,然后才发出FIN报文D;如果q有数据未从~存中取出,取出数据,q进行确认,直到所有确认完成之后,然后才发出FIN报文D(此时如果有ؕ序的报文D|况不q行处理Q。上面的描述也表现出QTDP是支持半关闭的,当一端发出FIN报文D|Q仍然允许接收另一端数据。但是半关闭可能Dq接永远停留在状态图中FIN_WAIT_2状态中Q此时保zL制仍然在工作当中Q如果对端已l关闭,那么保活机制在到时立卛_闭这一q接?br>
下图是一个典型的q接建立与连接关闭的C意图,此图摘自《TCP/IP详解 卷一》?br>
四、TDP传输数据?br>
1Q传输的报文D?br>
在TDP工作q程中传输的所有报文段Q只有SYN报文DcFIN报文Dc数据报文段是可靠的之外Q其它报文段如ACK报文DcLIV报文DcRST报文D늭都不是可靠的。SYN报文D与FIN报文D传输中都占用一个序P数据报文D在传输中根据传输的数据字节数占用相应的序号Q其它报文段不占用传输序受?br> 成功接收数据报文D,应当按序对下一个期望的数据报文D늚序号作ؓ认序号发送ACK报文D进行确认。当出现接收Cؕ序的数据报文D|Q将乱序数据报文D|序缓存,q发送期望报文段的ACK报文D进行确认。ACK报文D늚发送ƈ非即时的Q也q是对应接收数据报q行一对一认发送。ACK报文D는200ms定时触发发送,也就是说ACK报文D要l受最?00ms的时延进行发送。ACK报文D对此时期望的数据序可行确认,因此q不是与接收数据报相对应。ACK报文D|不可靠的Q当丢失时对端将无法了解接收情况Q因此发送方会有一个超时机Ӟ如果发现认的ACK报文D超Ӟ发送方重发该数据报,q一点在W五节进行详l描q?br>
2Q\径MTU发现及MSS通告
前面已经提到要在q接建立q程中会通告初始MSSQ这个值可以由用户E序q行讄。但q个初始值是一个静态的。当通信的两个端点之间跨多个网l时Q用设|的MSSq行报文D发送时Q可能导致传输的IP报文分片情况的生。ؓ了避免分片情늚产生QTDP在数据传输过E中q行动态的路径MTU发现Qƈq行MSS的更新及通告?br> TDP创徏UDP SOCKETӞ卛_描述W设|IP选项Z允许q行分片Qsetsockopt (clientSock, IPPROTO_IP, IP_DONTFRAGMENT,(char*)&dwFlags, sizeof(dwFlags))Q。在发送数据时以当前MSS大小D行数据发送,如果q回gؓ错误码WSAEMSGSIZEQ?0040Q表CZؓ报文D尽寸大于MTUQ需要进行IP分片传输。此Ӟ~减MSS大小再次q行报文D发送,直至不再q回错误码WSAEMSGSIZEQ?0040Q。当MSS变更q能成功发送报文段后,需要向对端通报新的MSS倹{每ơMSS~小后,默认?0U,TDP默认扩大MSS大小Q以查是否\径MTU增大了(默认值可以由用户E序讄Q,之后?0*2U?0*2*2U进行检,如果三次都未发现MTU增大则停止进行检。见RFC1191描述Q网l中MTU值的个数是有限的Q如下图描述Q摘自RFC1191Q。因此MSS的扩大及~减Q可依据一些由q似值按序构成的表,依照此表索引q行MSS值的扩大与羃减计?br>
TDP中MSS与MTU之间关系的计公式如下:
MSS = MTU – 20(IP首部) – 8(UDP首部) – 12(TDP首部)?br>
3QNagle法
有些认ؓl受时g的捎带ACK发送是Nagle法Q其实不是。经受时延的捎带ACK发送是TCP的通常实现Q在TDP中也是如此。而Nagle法是要求一个TCPQTDP也是如此Q连接上最多只能有一个未被确认的未完成的报文D,在该报文D늚认到达之前不能发送其他的报文Dc相反,TCPQTDP也是如此Q在q个时候收集这些报文段Q关在确认到来时合ƈ作ؓ一个报文段发送出厅RNagle法对于处理应用E序产生大量报文段的情况,有利于避免网l中׃发送太多的包而过载(q便是发送端的糊涂窗口综合症Q关于糊涂窗口综合症在下文将做更详细描述Q?br> Nagle法适用于生大量小报文D늚情况Q但有时我们需要关闭Nagle法。一个典型的例子是XH口pȝ服务器:消息(鼠标UdQ必L时g地发送,以便行某U操作的交互用户提供实时的反馈?br> 默认的TDP实现中Nagle法是关闭的Q用L序可以设|打开它?br>
4Q窗口大通告与滑动窗?br>
双方接收模块需要依据各自的~冲区大,怺通告q能接受Ҏ数据的尺寸。双方发送模块则必须ҎҎ通告的接收窗口大,q行数据发送。这U机制称之谓滑动H口Q它是TDP接收方的量控制Ҏ。它允许发送方在停止ƈ{待认前可以连l发送多个分l(依据滑动H口的大)Q由于发送方不必每发一个分l就停下来等待确认,因此可以加速数据的传输?br> 参照《TCP/IP详解 卷一 20.3滑动H口》一节,滑动H口在排序数据流上不时的向右UdQ窗口两个边沿的相对q动增加或减了H口的大,关于H口Ҏ的运动有三个术语Q窗口合拢(当左Ҏ向右Ҏ靠近Q、窗口张开Q当双沿向右移动)、窗口收~(当右Ҏ向左UdQ。RFC文强烈不要在实现当中出现窗口收~的情况出现Q在我们的实C也将不会出现?br> 当遇到快的发送方与慢的接收方的情冉|Q接收方的窗口会很快被发送方的数据填满,此时接收方将通告H口大小?,发送方则停止发送数据。直到接收方用户E序取走数据后更新窗口大,发送方可以l箋发送数据;另外Q因为ACK报文D|可能丢失Q发送方可能没有成功接收到更新的H口大小Q因此发送方启动一个坚持定时器Q当坚持定时器超Ӟ发送方发送一个字节的数据到接收方Q尝试检查窗口大的更新?br> 在Nagle法中接到过p涂H口l合症,在这里要q一步进行描q。糊涂窗口综合症是指众多量数据的报文段通过q接q行交换Q而不是满长度的报文段Q这导致连接占用过多带宽,降低传输速率。糊涂窗口综合症产生是分两端的,接收方可以通告一个小的窗口(而不是一直等到有大的H口时才通告Q,发送方也可以发送少量的数据Q而不是等待其他的数据以便发送一个大的报文段Q。要以采用如下方法避免这一现象Q?br> 1Q接收方不通告窗口。通常的算法是接收方不通告一个比当前H口大的H口Q可以ؓ0Q,除非H口可以增加一个报文段大小(也就是将要接收的MSS)或者可以增加缓存空间的一半,不论实际有多?br> 2Q发送方避免出现p涂H口l合症的措施是只有以下条件之一满时才发送数据:(a)可以发送一个满长度的报文段Q?b)可以发送至是接收斚w告H口大小一半的报文D;(c)可以发送Q何数据ƈ且不希望接收ACKQ也是_我们没有q未被确认的数据Q或者该q接上不能用Nagle法?br>
5QPUSH标志
PSUH标志的作用是发送方使用PUSH标志通知接收方将所收到的数据全部提交给接收q程。在TDP实现中,用户E序q不需要关心PUSH标志。因为TDP实现从不接收到的数据推q交付给用户E序Q因此这个标志在TDP的实C是被忽略的?br>
五、TDP时与重?br>
1Q带宽时延乘U与拥塞
每个|络通道都有一定的定wQ可以计通道的容量大:
Capacity(bit) = bandwidth(b/s) * round-trip time(s)
q个g般称之ؓ带宽时g乘积。这个g赖于|络速度和两端的RTTQ可以有很大的变动。不论是带宽q是时g均会影响发送方与接收方之间通\的容量?br> 当数据到达一个大的网l通道q向一个小的网l通道发送,发生拥塞现象。另外当多个输入到达一个\由器Q而\由器的输出流于q些输入的d时也会发生拥塞。TDP时与重传机制刚采用TCP的拥塞控制算法来q行发送端的流量控制?br>
2Q往q时间与重传时旉量
时与重传中最重要的部分就是对一个给定连接的往q时_RTTQ的量。由于\由器和网l流量均会发生变化,因此一般认为RTT可能l常会发生变化,TDP应该跟踪q些变化q相应地改变相应的超时时间?br> 首先是必L量在发送一个带有特别序L字节和接收到包含字节的确认之间的RTT。由于数据报文段与ACK之间通常没有一一对应的关p,如下图(摘自《TCP/IP详解 卷一》图20.1Q中Q这意味着发送方可以量到的一个RTTQ是在发送报文段4和接收报文段7之间的时_用M表示所量到的RTT?br> Ҏ[Jacobson 1988]描述Q见《TCP/IP详解 卷一》参考文献)Q用A表示被^滑的RTTQ均g计器Q,用D表示被^滑的均值偏差,用Err表示刚得到的量l果M与当前RTT估计器之差,则可以计下一个超旉传时_用RTO表示下一个超旉传时_?br> A = 0 Q未q行量往q时间之前,A的初始|
D = 3 Q未q行量往q时间之前,D的初始|
RTO = A + 2D = 6 Q未q行量往q时间之前,RTO的初始|
A = M + 0.5 (W一ơ测量到往q时间结果,对RTT估计器计初始?
D = A / 2 Q第一ơ测量到往q时间结果,对均值偏差D计算初始|
RTO = A + 4D Q第一ơ测量到往q时间结果,对均值偏差RTO计算初始|
之后的计方法如下:
Err = M – A
A <- A + gErr
D <- D + h(|Err| - D)
RTO = A + 4D
其中g是常量增量,取gؓ1/8(0.125)Qh也是帔R增量Q取gؓ1/4(0.25)?br>
Karn法QKarn法是解x谓的重传多义性问题的。[Karn and Partridge 1987]规定Q见《TCP/IP详解 卷一》参考文献)Q当一个超时和重传发生Ӟ在重传数据的认最后到达之前,不能更新RTT估计器,因ؓ我们q不知道ACK对应哪次传输Q也许第一ơ传输被延迟而ƈ没有被丢弃,也有可能是第一ơ传输的ACK被gq丢弃)。ƈ且,׃数据被重传,RTO已经得到了一个指数退避,我们在下一ơ传输时使用q个退避后的RTO。对一个没有被重传的报文段而言Q除非收C一个确认,否则不要计算新的RTO?br> 在Q何时候对每个q接q行仅测量一ơRTT|在发送一个报文段Ӟ如果l定q接的定时器已经被用,则该报文D不被计Ӟ反之如果l定q接的定时器未被使用Q则开始计时以量RTT倹{即q每个发出报文D都q行量RTT|同一旉D里只能有一个RTT值测量行行,不会q行q行多个RTT值测量?br>
3Q慢启动
如果发送方一开始便向网l发送多个报文段Q直臌到接收方通告H口大小为止。当发送方与接收方在同一局域网Ӟq种方式是可以的。但如果在发送方与接收方之间存在多个路由器和速率较慢的链路时Q就可能出现问题。一些中间\由器必须~存分组Qƈ有可能耗尽存储器的I间Q将来得降低TCPq接的吞吐量。于是需要一U叫“慢启?#8221;的拥塞控制算法?br> 慢启动ؓ发送方增加一个拥塞窗口,CؓcwndQ当与另一个网l的L建立q接Ӟ拥塞H口被初始化?个报文段。每收到一个ACKQ拥塞窗口就增加一个报文段Qcwnd以字节ؓ单位Q但慢启动以报文D大ؓ单位q行增加Q。发送方取拥塞窗口与通告H口中的最g为发送上限。拥塞窗口是发送方使用的流量控Ӟ而通告H口是接收方使用的流量控制?br> 发送方开始时发送一个报文段Q然后等待ACK。当收到该ACKӞ拥塞H口?增加?,卛_以发送两个报文段。当收到q两个报文段的ACKӞ拥塞H口增加ؓ4。这是一U指数增加的关系?br>
4Q拥塞避?br>
慢启动算法增加拥塞窗口大到某些点上可能辑ֈ了互联网的容量,于是中间路由器开始丢弃分l。这通知发送方它的拥塞H口开得太大。拥塞避免算法是一U处理丢失分l的Ҏ。该法假定׃分组受到损坏引v的丢失是非常的Q远于1Q)Q因此分l丢失就意味着在源L和目标主Z间的某处|络上发生了拥塞。有两种分组丢失的指C:发生时和接收到重复的确认。拥塞避免算法与慢启动算法是两个独立的算法,但实际中q两个算法通常在一起实现?br> 拥塞避免法和慢启动法需要对每个q接l持两个变量Q一个拥塞窗口cwnd和一个慢启动门限ssthresh。算法的工作q程如下Q?br> 1) 对一个给定的q接Q初始化cwnd?个报文段Qssthresh?5535个字节?br> 2) TCP输出例程的输Z能超qcwnd和接收方通告H口的大。拥塞避免是发送方使用的流量控Ӟ而通告H口则是接收方进行的量控制。前者是发送方感受到的|络拥塞的估计,而后者则与接收方在该q接上的可用~存大小有关?br> 3) 当拥塞发生时Q超时或收到重复认Q,ssthresh被设|ؓ当前H口大小的一半(cwnd和接收方通告H口大小的最|但最ؓ2个报文段Q。此外,如果是超时引起了拥塞Q则cwnd被设|ؓ1个报文段Q这是慢启动)?br> 4) 当新的数据被Ҏ认Ӟ增加cwndQ但增加的方法依赖于我们是否正在q行慢启动或拥塞避免。如果cwnd于或等于ssthreshQ则正在q行慢启动,否则正在q行拥塞避免。慢启动一直持l到我们回到当拥塞发生时所处位|的半时候才停止Q因为我们记录了在步?中给我们刉麻烦的H口大小的一半)Q然后{为执行拥塞避免?br> 慢启动算法初始设|cwnd?个报文段Q此后每收到一个确认就?。这会ɽH口按指数方式增长:发?个报文段Q然后是2个,接着??#8230;…。拥塞避免算法要求每ơ收C个确认时cwnd增加1/cwnd。与慢启动的指数增加比v来,q是一U加性增ѝ我们希望在一个往q时间内最多ؓcwnd增加1个报文段Q不在q个RT T中收C多少个ACKQ,然而慢启动根据这个往q时间中所收到的确认的个数增加cwnd?br> 处于拥塞避免状态时Q拥塞窗口的计算公式如下Q引公式参照BSD的实玎ͼsegsize/8的值是一个匹配补充量Q不在算法描q当中)Q?br> cwnd <- cwnd + segsize * segsize / cwnd + segsize / 8
5Q快速重传与快速恢?br>
׃我们不知道一个重复的ACK是由一个丢q报文D引LQ还是由于仅仅出C几个报文D늚重新排序Q因此我们等待少量重复的ACK到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理ƈ产生一个新的ACK之前Q只可能产生1 ~ 2个重复的ACK。如果一q串收到3个或3个以上的重复ACKQ就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段Q而无需{待时定时器溢出。这是快速重传算法。接下来执行的不是慢启动法而是拥塞避免法。这是快速恢复算法?br> q个法通常按如下过E进行实玎ͼ
1) 当收到第3个重复的ACKӞssthresh讄为当前拥塞窗口cwnd的一半。重传丢q报文Dc设|cwnd为ssthresh加上3倍的报文D大?br> 2) 每次收到另一个重复的ACKӞcwnd增加1个报文段大小q发?个分l(如果新的cwnd允许发送)?br> 3) 当下一个确认新数据的ACK到达Ӟ讄cwnd为ssthreshQ在W?步中讄的|。这个ACK应该是在q行重传后的一个往q时间内Ҏ?中重传的认。另外,q个ACK也应该是对丢q分组和收到的W?个重复的A C K之间的所有中间报文段的确认。这一步采用的是拥塞避免,因ؓ当分l丢失时我们当前的速率减半?br>
六、代理socks5支持
参照RFC1928、RFC1929Q在TDP实现中,支持匿名通过socks5代理以及用户?密码验证方式通过socks5代理?br> ׃socks5代理是工作于q输层上Q因此连接当中对IP层选项的设|都没有效果。socks5代理起到的作用只是应用数据的转发Q但q已l基本上能支持大部分用户E序的应用需求。在使用socks5代理q行工作中,路径MTU的发现机Ӟ无法有效工作,此时MSS默认?36QMTU默认?76Q?用户E序可以修改使用的MSS倹{?br>
七、安全考虑
TDP协议及算法方面ƈ不对数据的安全性做M考虑Q用L序在传输数据时如果对安全性有要求Q可以自行在应用数据层做相应的工作。但TDP实现中,会提供一个简单的AES256位加解密ҎQ提供给用户E序使用。用L序可以调用该加解密方法,Ҏ据进行加密然后再通过|络q行发送,接收时将加密数据进行解密再会用户E序数据逻辑处理模块q行处理?br>
八、定时器
如BSD的TCP实现cMQTDP也ؓ每条q接建立了六个定时器Q简要介l如下:
1Q?#8220;q接建立”定时器,在发送SYN报文D徏立一条新的连接时启动。如果没有在75U内收到响应Q连接徏立将中止?br> 2Q?#8220;重传”定时器,在发送数据时讑֮。如果定时器已超时而对端的认q未到达Q将重传数据。重传定时器的值是动态计的Q取xRTT与该报文D被重传的次数?br> 3Q?#8220;延迟ACK”定时器,收到必须认但无需马上发出认的数据时讑֮。等?00ms后发送确认响应。如果,在这200ms内,有数据要在该q接上发送,延迟的ACK响应可随数据一起发送回对端Q称为捎带确认?br> 4Q?#8220;坚持”定时器,在连接对端通告接收H口?,Ll箋发送数据时讑֮。坚持定时器在超时后向对端发?字节的数据,判定对端接收H口是否已经打开。坚持定时器的值是动态的计算的,取决于RTT|?U与60U之间取倹{?br> 5Q?#8220;保活”定时器。TDPq接在一定时间段内如果没有数据交互的话,主动发送保zLIV报文Dc即?#8220;保活”定时器超Ӟ说明没有数据交互Q则发送保zL据包。保zd时器默认旉?分钟Q用L序可以进行设|?br> 6QTIME_WAIT定时?也可UCؓ2MSL定时器(实现中,一个MSL?分钟Q。当q接状态{UdTIME_WAITӞ卌接主动关闭时Q定时器启动?br>
九、开发接?br>
使用TDPq行|络E序开发是非常Ҏ的,它的开发接口(APIQ与socket API是非常相似的Q尤其是对应功能的函数名U都是一致的Q需要注意的是TDP的所有API都处于名U空间TDP之下。开发接口见下表Q?br>
函数 描述
TDP::accept 接受一个链?nbsp;
TDP::bind l定本地地址C个TDP::SOCKET句柄
TDP::cleanup 清除TDP全局资源Q一个进E中只需要调用一?nbsp;
TDP::close 关闭已打开的TDP::SOCKET句柄Qƈ关闭q接
TDP::connect q接到服务器?nbsp;
TDP::getlasterror 获得TDP最后的一个错?nbsp;
TDP::getpeername dq接的对端的地址信息
TDP::getsockname dq接的本地的地址信息
TDP::getsockopt dTDP的选项信息
TDP::listen {待客户端来q接
TDP::recv 接收数据
TDP::select {待集合中的TDP SOCKET改变状?nbsp;
TDP::send 发送数?nbsp;
TDP::setsockopt 修改TDP的选项信息
TDP::shutdown 指定关闭q接上双工通信的部分或全部
TDP::socket 创徏一个TDP SOCKET
TDP::startup 初始化TDP全局信息Q一个进E中只需要调用一?nbsp;
]]>
刚接触TCP/IP通信设计的hҎ范例可以很快~出一个通信E?
序,据此一些h可能会认为TCP/IP~程很简单。其实不Ӟ
TCP/IP~程h较ؓ丰富的内宏V其~程的丰富性主要体现在
通信方式和报文格式的多样性上?
一。通信方式
主要有以下三大类:
(一)SERVER/CLIENT方式
1.一个Client方连接一个Server方,或称点对?peer to peer)Q?
2.多个Client方连接一个Server方,q也是通常的ƈ发服务器方式?
3.一个Client方连接多个Server方,q种方式很少见,主要
用于一个客户向多个服务器发送请求情c?
(?q接方式
1.长连?
Client方与Server方先建立通讯q接Q连接徏立后不断开Q?
然后再进行报文发送和接收。这U方式下׃通讯q接一?
存在Q可以用下面命o查看q接是否建立Q?
netstat –f inet|grep 端口??678)?
此种方式常用于点对点通讯?
2.短连?
Client方与Server每进行一ơ报文收发交易时才进行通讯q?
接,交易完毕后立x开q接。此U方式常用于一点对多点
通讯Q比如多个Clientq接一个Server.
(?发送接收方?
1.异步
报文发送和接收是分开的,怺独立的,互不影响。这U方
式又分两U情况:
(1)异步双工Q接收和发送在同一个程序中Q有两个不同?
子进E分别负责发送和接收
(2)异步单工Q接收和发送是用两个不同的E序来完成?
2.同步
报文发送和接收是同步进行,既报文发送后{待接收q回报文?
同步方式一般需要考虑时问题Q即报文发上d不能无限{?
待,需要设定超时时_过该时间发送方不再{待读返回报
文,直接通知时q回?nbsp;
实际通信方式是这三类通信方式的组合。比如一般书上提供的
TCP/IP范例E序大都是同步短q接的SERVER/CLIENTE序。有?
l合是基本不用的Q比较常用的有h值的l合是以下几U:
同步短连接Server/Client
同步长连接Server/Client
异步短连接Server/Client
异步长连接双工Server/Client
异步长连接单工Server/Client
其中异步长连接双工是最为复杂的一U通信方式Q有时候经
怼出现在不同银行或不同城市之间的两套系l之间的通信?
比如金卡工程。由于这几种通信方式比较固定Q所以可以预
先编制这几种通信方式的模板程序?
?报文格式
通信报文格式多样性更多,相应地就必须设计对应的读写报文的?
收和发送报文函数?
(一)d与非d方式
1.非阻塞方?
dC停地q行d作,如果没有报文接收刎ͼ{待一D|间后
时q回Q这U情况一般需要指定超时时间?
2.d方式
如果没有报文接收刎ͼ则读函数一直处于等待状态,直到有报文到达?
(?循环d方式
1.一ơ直接读写报?
在一ơ接收或发送报文动作中一ơ性不加分别地全部d或全?
发送报文字节?
2.不指定长度@环读?
q一般发生在短连接进E中Q受|络路由{限Ӟ一ơ较长的?
文可能在|络传输q程中被分解成了好几个包。一ơ读取可能不
能全部读完一ơ报文,q就需要@环读报文Q直到读完ؓ止?
3.带长度报文头循环d
q种情况一般是在长q接q程中,׃在长q接中没有条件能?
判断循环d什么时候结束,所以必要加长度报文头。读函数
先是d报文头的长度Q再Ҏq个长度去读报文.实际情况中,
报头的码制格式还l常不一P如果是非ASCII码的报文_q必?
转换成ASCII,常见的报文头码制有:
(1)n个字节的ASCII?
(2)n个字节的BCD?
(3)n个字节的|络整型?
以上是几U比较典型的d报文方式Q可以与通信方式模板一?
预先提供一些典型的APId函数。当然在实际问题中,可能q?
必须~写与对Ҏ文格式配套的dAPI.
在实际情况中Q往往需要把我们自己的系l与别h的系l进行连接,
有了以上模板与API,可以说连接Q何方式的通信E序都不存在问题?
本文来自CSDN博客Q{载请标明出处Q?a >http://blog.csdn.net/wgl_suc102/archive/2008/01/23/2060828.aspx
常见的网l服务器Q基本上?*24时q{的,对于|游来说Q至要求服务器要能q箋工作一周以上的旉q保证不出现服务器崩溃这LN性事件。事实上Q要求一个服务器在连l的满负药转下不出M异常Q要求它设计的近乎完,q几乎是不太现实的。服务器本n可以出异常(但要可能少得出Q,但是Q服务器本n应该被设计得以健壮Q?#8220;病灾”打不垮它Q这p求服务器在异常处理方面要下很多功夫?/p>
服务器的异常处理包括的内定w常广泛,本文仅就在网l封包方面出现的异常作一讨论Q希望能Ҏ从事相关工作的朋友有所帮助?/p>
关于|络包斚w的异常,M来说Q可以分Z大类Q一是封包格式出现异常;二是包内容Q即包数据Q出现异常。在包格式的异常处理方面,我们在最底端的网l数据包接收模块便可以加以处理。而对于封包数据内容出现的异常Q只有依靠游戏本w的逻辑d以判定和验。游戏逻辑斚w的异常处理,是随每个游戏的不同而不同的Q所以,本文随后的内容将重点阐述在网l数据包接收模块中的异常处理?/p>
为方便以下的讨论Q先明确两个概念Q这两个概念是ؓ了叙q方面,W者自行取的,q无标准可言Q:
1、逻辑包:指的是在应用层提交的数据包,一个完整的逻辑包可以表CZ个确切的逻辑意义。比如登录包Q它里面可以含有用户名字段和密码字Dc尽它看上M是一D늼冲区数据Q但q个~冲区里的各个区间是代表一定的逻辑意义的?br> 2、物理包Q指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)从网l底层接收到的数据包Q这h到的一个数据包Q能不能表示一个完整的逻辑意义Q要取决于它是通过UDPcȝ“数据报协?#8221;发的包还是通过TCPcȝ“协?#8221;发的包?/p>
我们知道QTCP是流协议Q?#8220;协?#8221;?#8220;数据报协?#8221;的不同点在于Q?#8220;数据报协?#8221;中的一个网l包本n是一个完整的逻辑包,也就是说Q在应用层用sendto发送了一个逻辑包之后,在接收端通过recvfrom接收到的是刚才使用sendto发送的那个逻辑包,q个包不会被分开发送,也不会与其它的包攑֜一起发送。但对于TCP而言QTCP会根据网l状况和neagle法Q或者将一个逻辑包单独发送,或者将一个逻辑包分成若q次发送,或者会若q个逻辑包合在一起发送出厅R正因ؓTCP在逻辑包处理方面的q种_合性,要求我们在作ZTCP的应用时Q一般都要编写相应的拼包、解包代码?/p>
因此Q基于TCP的上层应用,一般都要定义自q包格式。TCP的封包定义中Q除了具体的数据内容所代表的逻辑意义之外Q第一步就是要定以何U方式表C当前包的开始和l束。通常情况下,表示一个TCP逻辑包的开始和l束有两U方式:
1、以Ҏ的开始和l束标志表示Q比如FF00表示开始,00FF表示l束?br> 2、直接以包长度来表示。比如可以用W一个字节表C包总长度,如果觉得q样的话包比较小Q也可以用两个字节表C包长度?/p>
下面要l出的代码是以第2U方式定义的数据包,包长度以每个包的前两个字节表示。我结合着代码l出相关的解释和说明?/p>
函数中用到的变量说明Q?/p>
CLIENT_BUFFER_SIZEQ缓冲区的长度,定义为:Const int CLIENT_BUFFER_SIZE=4096?br> m_ClientDataBufQ数据整理缓冲区Q每ơ收到的数据Q都会先被复制到q个~冲区的末尾Q然后由下面的整理函数对q个~冲行整理。它的定义是Qchar m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]?br> m_DataBufByteCountQ数据整理缓冲区中当前剩余的未整理字节数?br> GetPacketLen(const char*)Q函敎ͼ可以Ҏ传入的缓冲区首址按照应用层协议取出当前逻辑包的长度?br> GetGamePacket(const char*, int)Q函敎ͼ可以Ҏ传入的缓冲区生成相应的游戏逻辑数据包?br> AddToExeList(PBaseGamePacket)Q函敎ͼ指定的游戏逻辑数据包加入待处理的游戏逻辑数据包队列中Q等待逻辑处理U程对其q行处理?br> DATA_POSQ指的是除了包长度、包cd{这些标志型字段之外Q真正的数据包内容的起始位置?/p>
Bool SplitFun(const char* pData,const int &len)
{
PBaseGamePacket pGamePacket=NULL;
__int64 startPos=0, prePos=0, i=0;
int packetLen=0;
//先将本次收到的数据复制到整理~冲区尾?br> startPos = m_DataBufByteCount;
memcpy( m_ClientDataBuf+startPos, pData, len );
m_DataBufByteCount += len;
//当整理缓冲区内的字节数少于DATA_POS字节Ӟ取不到长度信息则退?br> //注意Q退出时q不|m_DataBufByteCount?
if (m_DataBufByteCount < DATA_POS+1)
return false;
//Ҏ正常逻辑Q下面的情况不可能出玎ͼ为稳妥v见,q是加上
if (m_DataBufByteCount > 2*CLIENT_BUFFER_SIZE)
{
//讄m_DataBufByteCount?Q意味着丢弃~冲Z的现有数?br> m_DataBufByteCount = 0;
//可以考虑开N误格式数据包的处理接口,处理逻辑交给上层
//OnPacketError()
return false;
}
//q原起始指针
startPos = 0;
//只有当m_ClientDataBuf中的字节个数大于最包长度时才能执行此语句
packetLen = GetPacketLen( pIOCPClient->m_ClientDataBuf );
//当逻辑层的包长度不合法Ӟ则直接丢弃该?br> if ((packetLen < DATA_POS+1) || (packetLen > 2*CLIENT_BUFFER_SIZE))
{
m_DataBufByteCount = 0;
//OnPacketError()
return false;
}
//保留整理~冲区的末尾指针
__int64 oldlen = m_DataBufByteCount;
while ((packetLen <= m_DataBufByteCount) && (m_DataBufByteCount>0))
{
//调用拼包逻辑Q获取该~冲区数据对应的数据?br> pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen);
if (pGamePacket!=NULL)
{
//数据包加入执行队列
AddToExeList(pGamePacket);
}
pGamePacket = NULL;
//整理~冲区的剩余字节数和新逻辑包的起始位置q行调整
m_DataBufByteCount -= packetLen;
startPos += packetLen;
//D留~冲区的字节数少于一个正常包大小Ӟ只向前复制该包随后退?br> if (m_DataBufByteCount < DATA_POS+1)
{
for(i=startPos; i<startPos+m_DataBufByteCount; ++i)
m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];
return true;
}
packetLen = GetPacketLen(m_ClientDataBuf + startPos );
//当逻辑层的包长度不合法Ӟ丢弃该包及缓冲区以后的包
if ((packetLen<DATA_POS+1) || (packetLen>2*CLIENT_BUFFER_SIZE))
{
m_DataBufByteCount = 0;
//OnPacketError()
return false;
}
if (startPos+packetLen>=oldlen)
{
for(i=startPos; i<startPos+m_DataBufByteCount; ++i)
m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];
return true;
}
}//取所有完整的?/p>
return true;
}
以上便是数据接收模块的处理函敎ͼ下面是几点简要说明:
1、用于拼包整理的~冲?m_ClientDataBuf)应该比recv中指定的接收~冲?pData)长度(CLIENT_BUFFER_SIZE)要大Q通常前者是后者的2?2*CLIENT_BUFFER_SIZE)或更大?/p>
2、ؓ避免因ؓ剩余数据前移而导致的额外开销Q徏议m_ClientDataBuf使用环Ş~冲区实现?/p>
3、ؓ了避免出现无法拼装的包,我们U定每次发送的逻辑包,其单个逻辑包最大长度不可以过CLIENT_BUFFER_SIZE?倍。因为我们的整理~冲区只?*CLIENT_BUFFER_SIZEq么长,更长的数据,我们无法整理。这p求在协议的设计上以及最l的发送函数的处理上要加上q样的异常处理机制?/p>
4、对于数据包q短或过长的包,我们通常的情冉||m_DataBufByteCount?Q即舍弃当前包的处理。如果此处不讄m_DataBufByteCount?也可Q但该客L只要发了一ơ格式错误的包,则其后发过来的包则也将q带着产生格式错误Q如果设|m_DataBufByteCount?Q则可以比较好的避免后的包受此包的格式错误影响。更好的作法是,在此处开放一个封包格式异常的处理接口(OnPacketError)Q由上层逻辑军_对这U异常如何处|。比如上层逻辑可以对封包格式方面出现的异常q行计数Q如果错误的ơ数过一定的|则可以断开该客L的连接?/p>
5、徏议不要在recv或wsarecv的函数后Q就紧接着作以上的处理。当recv收到一D|据后Q生成一个结构体或对?它主要含有data和len两个内容Q前者是数据~冲区,后者是数据长度)Q将q样的一个结构体或对象放C个队列中由后面的U程对其使用SplitFun函数q行整理。这P可以最大限度地提高|络数据的接攉度Q不臛_为数据整理的原因而在此处费旉?/p>
代码中,我已l作了比较详l的注释Q可以作为拼包函数的参考,代码是从偶的应用中提取、修改而来Q本w只为演CZ用,所以未作调试,应用旉要你自己d善。如有疑问,可以我的blog上留a提出?/p>
本文来自CSDN博客Q{载请标明出处Q?a >http://blog.csdn.net/clever101/archive/2008/10/12/3061679.aspx
~写WinSockE序Ӟ如果不包含WinSock2.h文g很多pȝcd无法识别。可是如果包含了WinSock2.h文g则报N多系l类型重定义的错误?br>例如 Q?br> mswsock.h(69) : error C2065: 'SOCKET' : undeclared identifier Windows|络~程臛_需要两个头文gQwinsock2.h和windows.hQ而在WinSock2.0之前q存在一个老版本的winsock.h。正是这三个头文件的包含序Q导致了上述问题的出现?/p> 先看看winsock2.h的内容,在文件开头有如下宏定义: #ifndef _WINSOCK2API_
#define _WINSOCK2API_ #define _WINSOCKAPI_ /* Prevent inclusion of winsock.h in windows.h */ _WINSOCK2API_很容易理解,q是最常见的防止头文g重复包含的保护措施。_WINSOCKAPI_的定义则是ؓ了阻止对老文件winsock.h的包含,x_如果用户先包含了winsock2.h׃允许再包含winsock.h了,否则会导致类型重复定义。这是怎样做到的呢Q很单,因ؓwinsock.h的头部同样存在如下的保护措施Q?/p> #ifndef _WINSOCKAPI_
#define _WINSOCKAPI_ 再回q头来看winsock2.hQ在上述内容之后紧跟着如下宏指令: /*
* Pull in WINDOWS.H if necessary */ #ifndef _INC_WINDOWS #include <windows.h> #endif /* _INC_WINDOWS */ 其作用是如果用户没有包含windows.hQ_INC_WINDOWS在windows.h中定义)p动包含它Q以定义WinSock2.0所需的类型和帔R{?/p> 现在切换到windows.hQ查找winsockQ我们会惊奇的发C下内容: #ifndef WIN32_LEAN_AND_MEAN
#include <cderr.h> #include <dde.h> #include <ddeml.h> #include <dlgs.h> #ifndef _MAC #include <lzexpand.h> #include <mmsystem.h> #include <nb30.h> #include <rpc.h> #endif #include <shellapi.h> #ifndef _MAC #include <winperf.h> #if(_WIN32_WINNT >= 0x0400) #endif 看到没?windows.h会反向包含winsock2.h或者winsock.hQ相互间的包含便是万恶之源! 下面具体分析一下问题是怎么发生的?/p> 错误情Ş1Q?/strong>我们在自q工程中先包含winsock2.h再包含windows.hQ如果WIN32_LEAN_AND_MEAN未定义且_WIN32_WINNT大于或等?x400Q那么windows.h会在winsock2.h开头被自动引入Q而windows.h又会自动引入mswsock.hQ此Ӟmswsock.h里所用的socketcdq尚未定义,因此会出现类型未定义错误?/p>
错误情Ş2Q?/strong>先包含windows.h再包含winsock2.hQ如果WIN32_LEAN_AND_MEAN未定义且_WIN32_WINNT未定义或者其版本号小?x400Q那么windows.h会自动导入旧有的winsock.hQ这样再当winsock2.h被包含时便会引v重定义?/p>
q里要说明的是,宏WIN32_LEAN_AND_MEAN的作用是减小win32头文件尺总加快~译速度Q一般由AppWizard在stdafx.h中自动定义。_WIN32_WINNT的作用是开启高版本操作pȝ下的Ҏ函数Q比如要使用可等待定时器QWaitableTimerQ,得要求_WIN32_WINNT的值大于或{于0x400。因此,如果你没有遇CqC个问题,很可能是你没有在q些条g下进行网l编E?/p>
问题q没有结束,要知道除了VC自带windows库文件外QMS的Platform SDK也含有这些头文g。我们很可能发现在之前能够好好编译的E序在改变了windows头文件包含\径后又出了问题。原因很单,Platform SDK中的windows.h与VC自带的文件存在差异,其相同位|的代码如下Q?/p>
#ifndef NOGDI 唉,我们不禁要问MSZ么要搞这么多花样Q更让h气愤的是Q既然代码不一Pwindows.h里却没有M一个宏定义能够帮助E序辨别当前使用的文件是VC自带的还是PSDK里的?/p>
后来Q我写了一个头文g专门处理winsock2.h的包含问题,名ؓwinsock2i.hQ只需在要使用WinSock2.0的源文g里第一个包含此文g卛_Q不q由于前面提到的问题Q当使用PSDKӞ需要手工定义一下USING_WIN_PSDKQ源码如下: #ifndef _WINSOCK2API_ // Prevent inclusion of winsock.h // NOTE: If you use Windows Platform SDK, you should enable following definition: #if !defined(WIN32_LEAN_AND_MEAN) && (_WIN32_WINNT >= 0x0400) && !defined(USING_WIN_PSDK) |
Causes:
讄了DF标志的ip包当遇到路由器的MTU比包的时候,不会被\由器拆包。而\由器发送icmp消息到发送端Q通知它应该拆包?/p>
但icmp消息被防火墙拦截下来?/p>
环境和现象:
q个例子中,MTU在client和server都是1500.
dump出来的包如下:
客户端看到的:
发送了2个包Q后1个包成功Q第1个过大而不停的被发?
id 57558, offset 0, flags [DF], proto: TCP (6), length: 1500) 10.54.40.43.43145 > 10.29.14.74.http: ., cksum 0×5096 (incorrect (-> 0×5c4e), 0:1448(1448) ack 1 win 46
17:23:06.933580 IP (tos 0×0, ttl 64, id 57559, offset 0, flags [DF], proto: TCP (6), length: 730) 10.54.40.43.43145 > 10.29.14.74.http: P, cksum 0×4d94 (incorrect (-> 0×3933), 1448:2126(678) ack 1 win 46
17:23:07.167049 IP (tos 0×0, ttl 64, id 57560, offset 0, flags [DF], proto: TCP (6), length: 1500) 10.54.40.43.43145 > 10.29.14.74.http: ., cksum 0×5096 (incorrect (-> 0×5b5b), 0:1448(1448) ack 1 win 46
17:23:07.634922 IP (tos 0×0, ttl 64, id 57561, offset 0, flags [DF], proto: TCP (6), length: 1500) 10.54.40.43.43145 > 10.29.14.74.http: ., cksum 0×5096 (incorrect (-> 0×5987), 0:1448(1448) ack 1 win 46
接受端看到的:
只有730大小的包接受成功
17:23:08.605622 IP (tos 0×0, ttl 59, id 57559, offset 0, flags [DF], proto: TCP (6), length: 730) 202.108.3.204.43145 > 10.29.14.74.http: P, cksum 0×9d5b (correct), 1448:2126(678) ack 1 win 46
解决Ҏ:
调整发送端机器的配|?QQ?个)
在网l层?
Decrease mtu on network adapter:
ifconfig eth* mtu 1400
操作pȝ配置:
Clear the default ‘MTU discovery’ flag with sysctl:
net.ipv4.ip_no_pmtu_disc = 1
或在应用E序?
Set socket option ‘IP_MTU_DISCOVER’ with setsockopt(2) to clear ‘DF’ flag of IP package.
Reference:
ThanksQ?/strong>
esx kobe steve
来自Q?a >http://blog.developers.api.sina.com.cn/?p=672
原文Q?a >http://drdr-xp-tech.blogspot.com/2009/04/black-hole-socket-problem.html
现在来讨Zơ提到的q发FIFO,其实现需要一些特D的技巧。我上次说要实现单线E读单线E写的FIFO,但是q里我们先来讨论一般的q发FIFO?br>
我们知道,传统的生产者——消费者问?通常是用一个共享的~冲区来交换数据?生者和消费者各自有对应的指?在生产或者消费的时候相应地Ud。如果达C~冲区的边界则回l。如果生产者指针追上消费者指?则表明缓冲区满了;如果消费者指针追上生产者指?则表明缓冲区IZ。问题在?Z防止在缓冲区满的时候插入数?或者在~冲区空的时候删除数?生者或者消费者的每一ơ插入或者删除数据操?都必d时访问这两个指针,q就带来了不必要的同步?br>
在单核处理器?׃n~冲区方式非帔R?q且h固定的空间开销(有时候你需要保守地估计一个比较大的数?。但是在多核处理器上(或者SMPpȝ?,如果要实现ƈ发的FIFO,必L弃这U方式。用单链表而不是共享缓冲区可以避开q个问题,q是W一个技巧?br>
W二个技巧关pd链表的用方向。一般用链?其插入或者删除节点的位置是Q意的。但是把链表作ؓFIFO使用,则只能也只需要在两端操作。需要注意的是这时候必MNTAIL插入新的节点,而从头部HEAD删除节点。否则从N删除节点之后,无从得知新的N在哪?除非从头部遍历。这样做的好处是,插入或者删除都只涉及到一个节炏V插入的时?只要让新创徏的节点包含所需要插入的数据,q且其后l?下一个节?为NULL;再让当前N的节点的后从NULL变成q个新节?q个新节点也变成了新的N节点(q里的操作顺序很关键)。删除的时?则检查当前头部节点的后NEXT是否NULL。若?表明FIFO是空?否则,取NEXT所包含的数据来使用(是的,是NEXT而不是当前头部节Ҏ包含的数?参看下一个技巧和不变?,q把该数据从NEXT中删?而NEXT也成为新的头部节炏V?没有配图,各位误己想象一?
最后一个技?Z隔离对头部和N的访?我们需要一个空节点N(不包含数据的有效节点),其下一个节点ؓNULL;q且引入HEAD和TAIL。在开始的时?HEAD和TAIL都等于N。插入和删除数据的过E上面已l讲q了,q里讲一下不变式?br>
W一个不变式:头部节点LI的(不包含数?。在FIFO初始化的时候这是成立的。之后的插入操作不改变头部节?因此对不变式没有影响。而对于删除操?则每一个新头部节点的数据都已经在它成ؓ新的头部节点的时候被删除(取用)了?br>
W二个不变式:插入和删除操作没有数据冲H?也就是说,插入U程和删除线E不会同时读写同一Ҏ?不是节点)。我们只需要考虑FIFO为空,即相当于刚刚完成初始化之后的情况。对于空节点N,插入操作改变其后l?删除操作则检查其后。只要插入线E保证先让新节点包含数据再把新节Ҏ入链?也就是不能先插入I?再往节点中填入数?,那么删除U程׃会拿到空的节炏V我们看?唯一可能发生争用的地方就是N的后l指?插入U程只要在更新N的后l指针之前准备好其它相关数据和设|即可?br>
q意味着,如果能够做到:1)一个线E对数据的更新能够被另外一个线E即ȝ?2)Ҏ据的L者写(更新和读取N的后l指?都是原子?3)指o没有被ؕ序执行。那么在单线E读单线E写的情况下,甚至不需要用锁可以安全地完成q发FIFO;如果有多个生产者线E?则增加一个生产者锁;如果有多个消费者线E?则可以增加一个消费者锁。也是?可以有四U组合?br>
但是实际情况q非如此。对?)是容易满的,因ؓC通用处理器上32位数据的L者写通常都是原子的。对?),则取决于pȝ的内存模?在强内存模型如C/C++中是满?在弱内存模型如Java中则不然。但是主要的问题q在?)。由于指令的乱序执行,W二个不变式所需要的保证很可能被破坏,即代码实是那样写的。因此锁是必不可的,因ؓ加锁的同时还会插入内存屏障?br>
q样看来,上次说的SRSWq发FIFO没有特别的意义了。干脆就用两个锁分别对应生者和消费?而ƈ不限制生产者或者消费者的数量:T_LOCK和H_LOCK。在插入新徏节点到链表尾部的时候用T_LOCK,而在对头部操作的时候用H_LOCK?br>
具体的代码这里先不给了。这里的法不是我发明的,而是来自Maged M. Michael ?nbsp;Michael L. Scott的Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms。请参考其双锁法的伪码?br>
现在来讨论游戏消息的传送。在一个网游的q营成本?带宽费用应该是很大的一块。因此如何高效编码以及收发消息就成ؓ节省q营成本的关键。这里面能做很多文章?br>
首先是一个基本的判断:随着处理器的计算能力不断提高,以及多核的日益普?在消息的~码以及收发环节,CPU资源不会成为瓶颈。相对的,应该千方百计考虑如何在保证游戏正常运行的前提?降低不必要的通信开销。也是?可以Ҏ戏中的消息进行一些比较复杂的~码?br>
那么游戏中都有哪些消?我们知道聊天和语x息优先比较?而且可以通过专门的服务器来处理。真正比较关键、能够媄响玩家的游戏体验?是那些状态变更、动作、玩家之间或者玩家和服务?NPC之间的实时交互的消息。尤其是,q些消息的传送有严格的时序要求。如果一个玩家先看到自己的角色被砍死,然后才看到对方发出来的攻d?甚至Ҏ没有看到Ҏ有什么动??她肯定会愤愤不^。因?消息pȝ必须保证每一条消息的及时传?q且不能打ؕ它们之间的顺序?br>
q意味着,每一条消息必L明确的边界。也是?收到一条消息之?接收方必能够明这条消息有多少个字节。这是一条显而易见的要求。但是大概是Z惯?在实践中它常常变为消息编码中的长度字Dc?br>
q无疑是一U浪贏V很多消息的长度是固定的,仅仅靠检查其消息cd可以了解其边界。变长消息的处理后面会讨论。我q里q不是说要把具体的游戏逻辑与网l代码在一赗通过使用元数据就可以有效的把|络代码跟具体的游戏逻辑有效隔离开来。关于元数据的用后面也会详加探讨。今天时间不多了,下面讨论消息cd的编码作为结束?br>
通常一个字节会被用来编码消息的cd,以方便接收方的解码。但是我们知?游戏中ƈ不是每种cd的消息的传送频率都是一L。事实上,我们知道哪些消息会被大量发?哪些消息的频率会低很?而另外一些消?一天也不会有几条。明乎此,可以采用非对称的编码方式来~码消息的类型。这是Huffman~码。对于占据了l大部分通信量的状态变更消息而言,即每条消息节省下半个字?也是非常划算的。以我的l验,一台普通PC可以作ؓ服务器支?000人同时在U的实时动作cL?消息通量是每U?0000?如果一个服务集有5台处理器,那么q当于节省?00kbps的带宽。这q仅仅是从消息类型编码方面榨取的。当?Huffman~码的解码是比较ȝ?效率也会低一些。但是正如前面所指出?q部分的q行开销q不会造成性能瓉?br>