TCP机制与实现

version 1.0

2008-11-3


<!--[if !supportLists]-->一、  <!--[endif]-->TCP的任务:
在不可靠的IP之上,实现可靠的端到端的字节流服务;

 

<!--[if !supportLists]-->二、  <!--[endif]-->要解决的问题:
<!--[if !supportLists]-->l         <!--[endif]-->数据分段,将合适大小的数据段交给IP传送;

<!--[if !supportLists]-->l         <!--[endif]-->确认与重发;

<!--[if !supportLists]-->l         <!--[endif]-->校验和以保证数据传输的正确性;

<!--[if !supportLists]-->l         <!--[endif]-->失序数据的排序,丢弃重复数据;

<!--[if !supportLists]-->l         <!--[endif]-->流量控制;

<!--[if !supportLists]-->l         <!--[endif]-->拥塞避免;

<!--[if !supportLists]-->三、  <!--[endif]-->问题与解决方案
<!--[if !supportLists]-->1,             <!--[endif]-->建立连接的需要——处理网络中的延迟、重复分组
问题:如果连接建立、数据发送、连接断开的分组都存储在网络中,并在后面某个时候有规律地到达接收端,则可能:

<!--[if !supportLists]-->1)  <!--[endif]-->如果连接还存在,这些数据会被接收方丢弃;

<!--[if !supportLists]-->2)  <!--[endif]-->如果连接已经断开,接收端可能会再次执行连接建立、数据处理、断开的过程;

解决思路:

<!--[if !supportLists]-->1)  <!--[endif]-->废弃使用过的传输地址;

<!--[if !supportLists]-->2)  <!--[endif]-->每个连接分配一个标识符;

<!--[if !supportLists]-->3)  <!--[endif]-->保证每个分组在网络中停留时间不超过某个已知值;

解决办法:

2MSL等待?对于服务器来说是一个连接4元组的等待还是监听端口的等待?如果是4元组的等待,OS应该允许socket被重用,但是需记录已经释放的4元组。

 

根据TCP的标准状态机实现连接建立与关闭。

 

<!--[if !supportLists]-->2,             <!--[endif]-->确认与顺序收发
TCP头中的Seq、ack_seq,对每个发送的字节进行编号,收到小序号的报文段直接丢弃,收到大序号的报文段,发送ack通知对方自己已成功收到的数据,要求对方重发;该报文段应该缓存起来,以处理仅仅是报文段到达接收方顺序不对的情况;

TCP不提供要求对方发送指定报文段的能力;

RFC1106引入NAK扩展,要求对方发送指定的报文段;缓存已获得的大序号的数据(可能是包丢失,或失序),在获得重发报文段时,一次性确认收到的所有数据,并将所有数据提交给应用;

接收方收到重复ACK时不应立即重发数据,见“快速重发与快速恢复”。

 

实现:

存储待发Segment、已发未确认Segment、已收Segment、收到的失序Segment;

在向网络发送数据时设置m_nSendTime为0,在每次重发计时器超时时将m_nSendTime加1,如果m_nSendTime大于RTO,则重发第一个未被确认的Segment(此时再次设置m_nSendTime为0),并将其他所有未经确认的Segment放到待发队列;

收到ack时检索未确认Segment队列,根据ack_seq删除其前端一个或数个Segment。

 

<!--[if !supportLists]-->3,             <!--[endif]-->检查数据合法性
带伪首部的Checksum,数据长度为奇数时补0;将所有16b字以补码形式相加,再对相加和取补;

如果出错,直接丢弃等待对方重发,或者发送一个ACK帮助对方快速重发;

 

<!--[if !supportLists]-->4,             <!--[endif]-->分段避免
避免IP对数据报分段,使用MSS在连接建立时协商;通过检查与目的端是否在同一个网络上来选择MSS;

路径MTU发现;

如果发生分段,每段都有自己的IP及TCP头——IP执行分段时需要对TCP头进行处理(校验和)!

 

<!--[if !supportLists]-->5,             <!--[endif]-->路径MTU发现
问题:大分段有利用于提供TCP的效率;

方法:使用输出接口MTU-40及对端声明的MSS的最小值作为初始MSS;所有被TCP发送的IP数据包都设置“不分片”标志;如果IP数据包大于链路MTU,中间路由器产生ICMP不能分片差错;此时,TCP减小段大小进行重传,不改变拥塞窗口大小,但启动慢启动;

因为路径的动态变化,在减小MTU获得最终值一段时间后,尝试使用一个较大值。

不管是本地目的地还是非本地目的地,建议的MSS初始值都去网卡的MTU-40;

 

<!--[if !supportLists]-->6,             <!--[endif]-->防止本端缓存溢出
滑动窗口,在TCP头中通告对方本端缓冲大小(窗口大小相对已经确认的数据),这样发送方就知道能够发送哪些数据。

另外:窗口大小为0时可发送紧急数据,或1字节的报文段要求通告接收方通告窗口大小。

 

<!--[if !supportLists]-->7,             <!--[endif]-->重传策略,指数退避
一个例子:第一次超时间隔是1.5秒,然后每次超时值为前一次的2倍,直到48秒——它的下一个值为64,然后是多个64秒,总共发送13次,最后还是失败,则64秒后发送一个RST,断开连接;

见“Karn方法”,发生超时时不更新RTT,但将RTO进行指数退避处理;

仿照lwip的方式,使用tcp_backoff[]及重发次数实现指数退避。

 

<!--[if !supportLists]-->8,             <!--[endif]-->Push标志
要求TCP尽快将数据发出,而不是等待后续数据;TCP实体收到push数据时立即将数据提交给应用,而不是等待缓冲区满时才提交;

 

<!--[if !supportLists]-->9,             <!--[endif]-->紧急数据传输
如用于这种情况:用户用telnet远程启动了一个程序,然后ctrl+c希望马上终止它;

Tcp头中的URGENT标志及紧急指针——当前顺序号到紧急数据位置的偏移;

TCP立即停止为该连接积累数据,并将连接上已有的任何数据立即发出;

UNIX中使用signal——SIGURG(man 7 signal)通知应程紧急数据到达;

 

<!--[if !supportLists]-->10,        <!--[endif]-->网络拥塞避免
基本假设,分组传输超时是由拥塞,而非线路噪声引起;

慢启动:新分组进入网络的速度应与另一端返回确认的速度相同;

拥塞窗口:网络的容量;发送数据取拥塞窗口与接收方窗口的小值;

算法:

Cwnd初始化为MSS大小,每收到一个ACK则增加一个MSS,这样以指数规律增大,直到传输超时或达到接收方窗口大小;(出现超时则将Cwnd减半)

临界值ssthresh,初始化为64K,发生拥塞(超时或重复确认)时将ssthresh设为当前窗口大小的一半(ssthresh= MAX(2*mss,MIN(window,cwnd))),如果是超时则恢复cwnd为mss,之后执行慢启动,cwnd以指数规律增长,每收到一个ACK就增加一个mss;cwnd增长到ssthresh时,后续增长以线形规律进行——一个往返时间内为cwnd增加一个mss大小,即每次收到一个ack就将cwnd增加mss*mss/cwnd;

 

<!--[if !supportLists]-->11,        <!--[endif]-->处理数据丢失——快速重传与快速恢复
发现报文段丢失时尽快重传数据,而不是缓慢地等待超时;因为超时是指数退避的,可能等待很长时间。

数据丢失的发现:接收方收到失序报文段(原因可能是报文段丢失或仅仅是重新排序)时会以非延迟方式马上发送ACK,通知对方该问题;如果发送方多次(3次)收到同一个重复的ACK,则假定重复原因是报文段丢失,于是马上重发丢失的报文段——而非等待超时重发(快速重传),之后执行拥塞避免(快速恢复)。

选择快速恢复而非慢启动的原因是:对方回ACK表明有数据成功抵达接收方,避免慢启动造成的突然减少数据流;

判定重复ACK的标准:收到的ack_seq重复但存在未确认的已发数据;

实现中如果发生丢失,则更改ssthresh,并将cwnd设置为ssthresh加3倍mss,这样正常收到ack后会执行拥塞避免而非慢启动(处理方式同lwip);

 

<!--[if !supportLists]-->12,        <!--[endif]-->重发超时时间的确定
问题:ACK与Segment没有固定的一一对应关系,路由路径、网络载荷等都会造成ACK返回时间RTT(Round-trip time)发生变化;RTT的概率曲线偏平滑,且可能变化迅速;

办法:不断调节超时时间的动态算法,并通过增加本次测量值的比重,使计算值能反映网络状态的急剧变化;

RTO:retransmission timeout value

RTT的计算方法:

《计算机网络》中的算式:

<!--[if !supportLists]-->1,  <!--[endif]-->测量某个byte的ack到达花费的时间M

<!--[if !supportLists]-->2,  <!--[endif]-->RTT ß aRTT + (1-a)M,a取值7/8

<!--[if !supportLists]-->3,  <!--[endif]-->根据RTT选择重发超时时限,使用偏差值D ß aD + (1-a)| RTT-M | ,根据上次的偏差估计本次的偏差,此处a可与上式得a取值不同

<!--[if !supportLists]-->4,  <!--[endif]-->超时 ß RTT + 4D

为什么需要D——避免单次ACK的偏差

 

《TCP/IP详解》中的算式:

<!--[if !supportLists]-->1,  <!--[endif]-->获得M

<!--[if !supportLists]-->2,  <!--[endif]-->A ß A + g(M-A),g取值1/8

<!--[if !supportLists]-->3,  <!--[endif]-->D ß D + h(|M-A| - D),h取值1/4

<!--[if !supportLists]-->4,  <!--[endif]-->RTO = A + 4D

A相当于上面的RTT,两种算式是一样的;

A和D的初始化为0、3秒;

 

<!--[if !supportLists]-->13,        <!--[endif]-->超时时间的确定——重传歧义性问题
如果发生重传,无法确定收到的ACK是对第一次发送的还是重发的。

Karn方法:如果发生超时重发,当被重传数据的ACK最终到达时,不更新RTT估计值;每次传输失败,将RTO加倍(指数退避);直到一个非重传数据的ACK到达时才重新计算RTO;

 

<!--[if !supportLists]-->14,        <!--[endif]-->窗口更新消息丢失
消息丢失产生死锁;

Persist Timer坚持定时器:在获得窗口通告为0时启动,周期性地向接收方发送窗口探查;探查消息带一个字节数据,并采取指数退避方式重发,直到窗口张开或连接断开;按TCP规范,必须携带数据,否则接收方不会发送ACK。

接收端仅仅根据接收窗口为0(及该segment数据为一字节)来判定这是一个探查消息,而不是普通的数据传输?其实接收方无需检查这是普通数据传输还是窗口探查,它在缓冲区有空闲时接收数据,否则不接收。

对接收方,对窗口探查的处理与对普通数据的处理完全一样。

 

<!--[if !supportLists]-->15,        <!--[endif]-->保活定时器
问题:两个TCP模块之间长期不交换任何数据,TCP连接仍能保持;这样,如果一端不发送任何数据,它将不能发现另一端的崩溃;使用保活(keeplive)定时器发现该情况;

主要用于服务端在客户崩溃时回收资源,如避免服务端的半开连接状态;

过程:如果2h无任何动作,开启该功能的一方发送一个探查报文段;如果

<!--[if !supportLists]-->1)  <!--[endif]-->对端正常运行:正常相应;

<!--[if !supportLists]-->2)  <!--[endif]-->对端已经崩溃而未作出相应,或其他原因造成的不可达,75秒后报文超时,本端共发送10个,每个间隔75秒;

<!--[if !supportLists]-->3)  <!--[endif]-->对端已经重启,将以RST相应该报文,本端收到后终止连接;

保活报文,Seq字段比下一个要发送的报文的Seq小1,TCP收到这样的Seq错误的Segment时,相应一个ACK告诉对方自己所期望的seq(不能直接丢掉?)。

<!--[if !supportLists]-->16,        <!--[endif]-->低性能的避免
<!--[if !supportLists]-->3)  <!--[endif]-->问题:网络上大量的无数据的ACK段;

延迟确认,数据携带ACK:ACK并不立即发送,而是使用一个200ms的定时器,在有数据需要发送、或定时器到时时才发送ACK;

“隔一个报文段确认”;上一个ACK未发出又收到了新的数据,此时发出ACK。

 

<!--[if !supportLists]-->4)  <!--[endif]-->问题:如果发送方每次都只发送很少量的数据,如1B,则严重影响性能。

Nagle算法,一个TCP连接上最多只允许有一个未经确认的小分组;每发出一个Segment,就等待对方确认,并积累后续待发数据;得到确认后再将积累的数据一次性发出,然后继续等待下一个确认;

自适应的算法,确认越快则发送越快;

某些应用需禁用Nagle算法;

<!--[if !supportLists]-->5)  <!--[endif]-->傻瓜窗口

问题:慢的接收方进程每次都只取少量数据,每取一次,接收方TCP就向对方发一次窗口更新消息,于是发送方TCP又发送少量数据,并再次等待窗口更新消息;

Clarle方法:接收方能处理min(MSS, 1/2 buffer_size)的报文段时才发送窗口更新消息,如果窗口过小,则仍然向对方通告窗口大小为0;或发送方不发送小数据块;

 

<!--[if !supportLists]-->17,      <!--[endif]-->长肥管道
长肥管道的问题:

<!--[if !supportLists]-->1,            <!--[endif]-->在高带宽、高延迟的网络中(连接的容量,带宽时延积),大的窗口能提高TCP的吞吐率,但是TCP头中16bit的Windows大小字段限制了窗口大小不能超过65535bytes;

<!--[if !supportLists]-->2,            <!--[endif]-->在发生分组丢失时会急剧减少吞吐量。

<!--[if !supportLists]-->3,            <!--[endif]-->需要更好的RTT测量方式;

<!--[if !supportLists]-->4,            <!--[endif]-->高带宽下,32bit的seq编号很快就会发生回绕;

 

<!--[if !supportLists]-->18,        <!--[endif]-->其他相关问题
<!--[if !supportLists]-->                         i.              <!--[endif]-->网卡选定,由IP层实现,每个连接都只能关联到一块网卡:

TCP服务器使用INADDR_ANY不指定网卡监听连接请求,IP层获得数据的同时能知道数据来自哪块网卡,并将相关数据报给TCP实体,这样连接就在这块网卡上建立;

外发TCP连接请求,将由IP层根据目的地址所处子网选择网卡。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/netnote/archive/2008/11/03/3212069.aspx

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值