http://blog.csdn.net/netnote/article/details/3212069
TCP的机制与实现
version 1.0
2008-11-3
一、 TCP的任务:
在不可靠的IP之上,实现可靠的端到端的字节流服务;
二、 要解决的问题:
l 数据分段,将合适大小的数据段交给IP传送;
l 确认与重发;
l 校验和以保证数据传输的正确性;
l 失序数据的排序,丢弃重复数据;
l 流量控制;
l 拥塞避免;
三、 问题与解决方案
1, 建立连接的需要——处理网络中的延迟、重复分组
问题:如果连接建立、数据发送、连接断开的分组都存储在网络中,并在后面某个时候有规律地到达接收端,则可能:
1) 如果连接还存在,这些数据会被接收方丢弃;
2) 如果连接已经断开,接收端可能会再次执行连接建立、数据处理、断开的过程;
解决思路:
1) 废弃使用过的传输地址;
2) 每个连接分配一个标识符;
3) 保证每个分组在网络中停留时间不超过某个已知值;
解决办法:
2MSL等待?对于服务器来说是一个连接4元组的等待还是监听端口的等待?如果是4元组的等待,OS应该允许socket被重用,但是需记录已经释放的4元组。
根据TCP的标准状态机实现连接建立与关闭。
2, 确认与顺序收发
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。
3, 检查数据合法性
带伪首部的Checksum,数据长度为奇数时补0;将所有16b字以补码形式相加,再对相加和取补;
如果出错,直接丢弃等待对方重发,或者发送一个ACK帮助对方快速重发;
4, 分段避免
避免IP对数据报分段,使用MSS在连接建立时协商;通过检查与目的端是否在同一个网络上来选择MSS;
路径MTU发现;
如果发生分段,每段都有自己的IP及TCP头——IP执行分段时需要对TCP头进行处理(校验和)!
5, 路径MTU发现
问题:大分段有利用于提供TCP的效率;
方法:使用输出接口MTU-40及对端声明的MSS的最小值作为初始MSS;所有被TCP发送的IP数据包都设置“不分片”标志;如果IP数据包大于链路MTU,中间路由器产生ICMP不能分片差错;此时,TCP减小段大小进行重传,不改变拥塞窗口大小,但启动慢启动;
因为路径的动态变化,在减小MTU获得最终值一段时间后,尝试使用一个较大值。
不管是本地目的地还是非本地目的地,建议的MSS初始值都去网卡的MTU-40;
6, 防止本端缓存溢出
滑动窗口,在TCP头中通告对方本端缓冲大小(窗口大小相对已经确认的数据),这样发送方就知道能够发送哪些数据。
另外:窗口大小为0时可发送紧急数据,或1字节的报文段要求通告接收方通告窗口大小。
7, 重传策略,指数退避
一个例子:第一次超时间隔是1.5秒,然后每次超时值为前一次的2倍,直到48秒——它的下一个值为64,然后是多个64秒,总共发送13次,最后还是失败,则64秒后发送一个RST,断开连接;
见“Karn方法”,发生超时时不更新RTT,但将RTO进行指数退避处理;
仿照lwip的方式,使用tcp_backoff[]及重发次数实现指数退避。
8, Push标志
要求TCP尽快将数据发出,而不是等待后续数据;TCP实体收到push数据时立即将数据提交给应用,而不是等待缓冲区满时才提交;
9, 紧急数据传输
如用于这种情况:用户用telnet远程启动了一个程序,然后ctrl+c希望马上终止它;
Tcp头中的URGENT标志及紧急指针——当前顺序号到紧急数据位置的偏移;
TCP立即停止为该连接积累数据,并将连接上已有的任何数据立即发出;
UNIX中使用signal——SIGURG(man 7 signal)通知应程紧急数据到达;
10, 网络拥塞避免
基本假设,分组传输超时是由拥塞,而非线路噪声引起;
慢启动:新分组进入网络的速度应与另一端返回确认的速度相同;
拥塞窗口:网络的容量;发送数据取拥塞窗口与接收方窗口的小值;
算法:
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;
11, 处理数据丢失——快速重传与快速恢复
发现报文段丢失时尽快重传数据,而不是缓慢地等待超时;因为超时是指数退避的,可能等待很长时间。
数据丢失的发现:接收方收到失序报文段(原因可能是报文段丢失或仅仅是重新排序)时会以非延迟方式马上发送ACK,通知对方该问题;如果发送方多次(3次)收到同一个重复的ACK,则假定重复原因是报文段丢失,于是马上重发丢失的报文段——而非等待超时重发(快速重传),之后执行拥塞避免(快速恢复)。
选择快速恢复而非慢启动的原因是:对方回ACK表明有数据成功抵达接收方,避免慢启动造成的突然减少数据流;
判定重复ACK的标准:收到的ack_seq重复但存在未确认的已发数据;
实现中如果发生丢失,则更改ssthresh,并将cwnd设置为ssthresh加3倍mss,这样正常收到ack后会执行拥塞避免而非慢启动(处理方式同lwip);
12, 重发超时时间的确定
问题:ACK与Segment没有固定的一一对应关系,路由路径、网络载荷等都会造成ACK返回时间RTT(Round-trip time)发生变化;RTT的概率曲线偏平滑,且可能变化迅速;
办法:不断调节超时时间的动态算法,并通过增加本次测量值的比重,使计算值能反映网络状态的急剧变化;
RTO:retransmission timeout value
RTT的计算方法:
《计算机网络》中的算式:
1, 测量某个byte的ack到达花费的时间M
2, RTT ß aRTT + (1-a)M,a取值7/8
3, 根据RTT选择重发超时时限,使用偏差值D ß aD + (1-a)| RTT-M | ,根据上次的偏差估计本次的偏差,此处a可与上式得a取值不同
4, 超时 ß RTT + 4D
为什么需要D——避免单次ACK的偏差
《TCP/IP详解》中的算式:
1, 获得M
2, A ß A + g(M-A),g取值1/8
3, D ß D + h(|M-A| - D),h取值1/4
4, RTO = A + 4D
A相当于上面的RTT,两种算式是一样的;
A和D的初始化为0、3秒;
13, 超时时间的确定——重传歧义性问题
如果发生重传,无法确定收到的ACK是对第一次发送的还是重发的。
Karn方法:如果发生超时重发,当被重传数据的ACK最终到达时,不更新RTT估计值;每次传输失败,将RTO加倍(指数退避);直到一个非重传数据的ACK到达时才重新计算RTO;
14, 窗口更新消息丢失
消息丢失产生死锁;
Persist Timer坚持定时器:在获得窗口通告为0时启动,周期性地向接收方发送窗口探查;探查消息带一个字节数据,并采取指数退避方式重发,直到窗口张开或连接断开;按TCP规范,必须携带数据,否则接收方不会发送ACK。
接收端仅仅根据接收窗口为0(及该segment数据为一字节)来判定这是一个探查消息,而不是普通的数据传输?其实接收方无需检查这是普通数据传输还是窗口探查,它在缓冲区有空闲时接收数据,否则不接收。
对接收方,对窗口探查的处理与对普通数据的处理完全一样。
15, 保活定时器
问题:两个TCP模块之间长期不交换任何数据,TCP连接仍能保持;这样,如果一端不发送任何数据,它将不能发现另一端的崩溃;使用保活(keeplive)定时器发现该情况;
主要用于服务端在客户崩溃时回收资源,如避免服务端的半开连接状态;
过程:如果2h无任何动作,开启该功能的一方发送一个探查报文段;如果
1) 对端正常运行:正常相应;
2) 对端已经崩溃而未作出相应,或其他原因造成的不可达,75秒后报文超时,本端共发送10个,每个间隔75秒;
3) 对端已经重启,将以RST相应该报文,本端收到后终止连接;
保活报文,Seq字段比下一个要发送的报文的Seq小1,TCP收到这样的Seq错误的Segment时,相应一个ACK告诉对方自己所期望的seq(不能直接丢掉?)。
16, 低性能的避免
3) 问题:网络上大量的无数据的ACK段;
延迟确认,数据携带ACK:ACK并不立即发送,而是使用一个200ms的定时器,在有数据需要发送、或定时器到时时才发送ACK;
“隔一个报文段确认”;上一个ACK未发出又收到了新的数据,此时发出ACK。
4) 问题:如果发送方每次都只发送很少量的数据,如1B,则严重影响性能。
Nagle算法,一个TCP连接上最多只允许有一个未经确认的小分组;每发出一个Segment,就等待对方确认,并积累后续待发数据;得到确认后再将积累的数据一次性发出,然后继续等待下一个确认;
自适应的算法,确认越快则发送越快;
某些应用需禁用Nagle算法;
5) 傻瓜窗口
问题:慢的接收方进程每次都只取少量数据,每取一次,接收方TCP就向对方发一次窗口更新消息,于是发送方TCP又发送少量数据,并再次等待窗口更新消息;
Clarle方法:接收方能处理min(MSS, 1/2 buffer_size)的报文段时才发送窗口更新消息,如果窗口过小,则仍然向对方通告窗口大小为0;或发送方不发送小数据块;
17, 长肥管道
长肥管道的问题:
1, 在高带宽、高延迟的网络中(连接的容量,带宽时延积),大的窗口能提高TCP的吞吐率,但是TCP头中16bit的Windows大小字段限制了窗口大小不能超过65535bytes;
2, 在发生分组丢失时会急剧减少吞吐量。
3, 需要更好的RTT测量方式;
4, 高带宽下,32bit的seq编号很快就会发生回绕;
18, 其他相关问题
i. 网卡选定,由IP层实现,每个连接都只能关联到一块网卡:
TCP服务器使用INADDR_ANY不指定网卡监听连接请求,IP层获得数据的同时能知道数据来自哪块网卡,并将相关数据报给TCP实体,这样连接就在这块网卡上建立;
外发TCP连接请求,将由IP层根据目的地址所处子网选择网卡。