TCP延迟确认

http://www.cnblogs.com/ggjucheng/archive/2012/01/15/2323081.html

简介

TCP延迟确认是由一些实现采用的技术,努力提高网络性能的传输控制协议 。从本质上讲,几个应答响应可能结合在一起,成一个响应,减少协议开销 。然而,在某些情况下,该技术可以降低应用程序的性能。

方法和优势

RFC 1122中描述,主机可能延迟发送ACK响应到500毫秒。此外,收到一个完整大小的TCP报文段,就要发送ACK响应

延迟ACK可以给应用程序的机会,一起发送更新的TCP接收窗口,ACK和应用程序的即时响应。如某些协议,远程登录,通过合并ACK,tcp窗口更新和应用程序的响应为一个报文段,延迟ACK可以减少服务器发送的响应的数据为3倍

问题

在某些应用程序和配置交互时,延迟ACK引入额外的等待时间可能会导致进一步延误。

如果Nagle算法是由发送方使用,数据将会排队,直到收到一个ACK确认 。如果发件人不发送足够的数据来填充最大的段大小(例如,如果它执行两个小写入一个阻塞读),然后发件人的应用程序将会暂停,直到ACK延迟超时

例如,考虑一个情况,其中鲍勃是将数据发送到卡罗尔,鲍勃的套接字层中,要发送的有效数据不够一个完整的数据包。根据Nagle算法,它不会被发送,直到收到一个ACK确认已发送的数据。同时,卡罗尔的应用层不会发送一个响应,直到它得到的所有数据。如果卡罗尔是使用延迟ACK,她套接字层将不会发送一个ACK,直到最后超时才会发送ACK。

如果应用程序是在较小的块中传输数据,并期望定期确认回复,可能会出现这种负面的效果。为了防止这种延迟,应用层需要不断发送数据,而无需等待确认回复。另外,发送端的应用程序可能会禁用Nagle算法。


“此外,收到一个完整大小的TCP报文段,就要发送ACK响应 ”

UNP中 对ACK延滞算法有一段描述:TCP期待在这一段延滞时间内有自身数据发送给对端,被延滞的ACK就可以由这些数据捎带。(与不使用nagle算法的差别?)

上面这两个描述还是有些不尽相同的。



http://hi.baidu.com/kwokwing0011/item/f946590bd47c9fe3349902fa

延迟ACK和nagle算法

nagle算法

从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的标题数据。这种情况转变成了4000%的消耗,这样的情况对于轻负载的网络来说还是可以接受的,但是重负载的福特网络就受不了了,它没有必要在经过节点和网关的时候重发,导致包丢失和妨碍传输速度。吞吐量可能会妨碍甚至在一定程度上会导致连接失败。Nagle的算法通常会在TCP程序里添加两行代码,在未确认数据发送的时候让发送器把数据送到缓存里。任何数

据随后继续直到得到明显的数据确认或者直到攒到了一定数量的数据了再发包。该算法的优越之处在于它是

自适应的:确认数据到达的越快,数据的发送越快。

延迟ACK

通常TCP在连接到数据时并不是立即发送ACK,相反,它推迟发送,以便ACK与需要沿该方向发送的数据一起发送。绝大多数采用的时延为200ms,也就是说,tcp将以最大时延200ms来等待是否有数据一起发送。

tcp需要ack,可是为了效率,并不是每发送一个数据都要等待ack,而是尽可能利用窗口机制,积累发送ack的,当然在某些特殊情况下还是需要马上发送ack的:比如接收到乱序的数据,这种情况下,虽然接收端可以将乱序的数据包暂存,但是接收方必须发送一个ack号为按序的期望的序列号的ack给发送端,另外就是接收窗口需要调整,此时就要立刻发送ack,否则则可以延迟发送ack,看一下linux的这方面的代码:
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
struct tcp_sock *tp = tcp_sk(sk);
//rcv_mss是估算的对端的mss,它对本端接收窗口的计算也有很大意义
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss //如果接收到了大于一个的报文,那么就发送ack,一下子确认两个报文
&& __tcp_select_window(sk) >= tp->rcv_wnd) || //需要调整窗口,最大化吞吐量
tcp_in_quickack_mode(sk) ||
(ofo_possible && //收到乱序的包
skb_peek(&tp->out_of_order_queue))) {
tcp_send_ack(sk);
} else {
tcp_send_delayed_ack(sk);
}
}
大体上,上述的函数实现了RFC1122和RFC2581的关于ack的建议。
传输层的ack发送就是立即模式和延迟模式,正如上面的__tcp_ack_snd_check所展示的那样。
发送ack其实很简单,就是填写一个tcp数据,ack字段设置为接收窗口最左边的那个数据的序列号加1,延迟发送不怕和捎带发送重复,RFC2581规定每个到来的报文只能生成一个ack,除非需要发送端重传才会发送冗余ack,如果tcp进入了等待延迟发送ack的状态,当接收端有数据要发送的时候就会将ack捎带到发送端,同时清除延迟ack定时器的pendding,如此延迟ack定时器到期后就不会再发送ack了。
稳定的理想情况下,接收端的窗口也是稳定的,不需要调整的,如果接收端不发送数据只是接收数据,ack几乎全部以延迟的方式发送给发送端,如果接收端同时也发送数据,那么ack就会以捎带的方式发给发送端,立即ack只会在几种特殊的异常情况才发送。
一种是收到了一个以上的完整的tcp段,并且可能要放大窗口,为了使得吞吐量最大化,放大的窗口决不能浪费掉,于是需要立即发送ack给发送端,发送端接收到这个ack以后就会继续发送其发送窗口中后面的数据了。发送端的mss和接收端的窗口大小相关联,接收端的窗口设定为发送端mss的整数倍比较好,这样内存的利用率最高,确定好接收端可以承受的窗口大小之后,如果其比当前窗口大,那么立即发送ack使得发送端可以尽快发送数据。
另外一种是在所谓的quick模式,quick模式并不是经常的,只有在非交互的tcp连接才可能进入quick模式,因为交互的连接表明ack已经足够快了,没有必要立即发送ack了,一般都是捎带ack或者延迟发送ack的,那么如何判断是否是交互连接呢?内核中tcp_opt结构体中有一个ack子结构体,内部有一个quick和pingpong两个字段,其中pingpong就是判断交互连接的,内核会在很多地方进行抉择,根据很多参数,比如收发间隔或者用户配置等判断是否一个连接是交互的,如果不是交互的,那么就存在一系列问题:1.由于用户进程长期不取出收到的数据导致一系列的问题,于是需要协议栈瞬间回复ack,2.积压的ack没有回复,影响了发送端的发送速率。此时就会给quick赋予一定的数值,每发送一个ack就会消耗掉一些quick值,直到用尽了quick而进入延迟模式,quick的值和窗口相关,因为接收端最多只能确认接收窗口这么大的数据。
立即ack的最后一种可能就是收到乱序包,表明数据已经可能丢失了,那么应该尽快地进入补救阶段,就是说要尽快进入快速重传,此时ack也要立即发送(内核发现越界包[和乱序包有区别]后会调用tcp_send_dupack发送一个ack后丢次报文而返回),内核收到乱序报文后会在out_of_order_queue队列缓存该乱序报本,最后会调用tcp_ack_snd_check再次发送一个ack,这个ack确认的是按序报文的最后一个,以前应该发送过该ack,这样接收端收到乱序报文后就会发送一个冗余的ack,如果下次接收的数据仍然是乱序的,那么就再发送一个前两个相同的ack,这样发送端可能就会连续接收到三个一模一样的ack,在接收端,第三次接收到的仍然是乱序报文时,再次发送冗余ack,只有这第4个ack被发送端收到后才会进行快速重传。这里的一个细节就是发送端收到了4个相同的ack(3个冗余ack),从而作为进入快速重传的标志,linux是这么实现的,符合了rfc的建议,但是这种实现所依赖的是其背后的一个思想。
一个报文谈不上顺序,最少两个报文才有顺序的概念,正如字节序一样,utf8以一个字节为编码单位,因此就没有字节序的问题,同样的,仅仅来了一个报文也不能说它对于当前按序的报文来讲是乱序的,只有当第二个报文到来的时候,如果当前按序报文,第一个报文,第二个报文拼不成顺序才能说明后来的这两个报文是乱序的,当然这也是一种权衡的结果,正如三次握手为何是三次一样,即使接收端收到了第三个乱序报文,仍有可能被第四个填充后成为按序报文,没完没了等下去是不行的,必须在发送端接收到确定的,不是很大的数目冗余ack的时候进入快速重传,同时也不能频繁的快速重传,因此就选择了3个冗余ack,当然这个数字是可以配置的。
最后看一下乱序报文的重新调整。linux的协议栈实现中将乱序的报文按照序列号大小顺序插入到一个队列当中,此队列是基于连接的,如果该乱序队列有报文暂存的话,每接收到一个报文都会尝试调用tcp_ofo_queue函数,它的意义在于努力将乱序的报文顺序化,正如上述冗余ack相关的背后的思想,每个新的报文都有可能填补按序报文和乱序报文之间的缺口,换句话说,每一个新到的报文都可能直接拼接到按序报文队列最后一个的后面,同时也有可能完成这种拼接后,和后面的乱序队列的最前面一个或者几个或者全部的报文拼接,最终成为一系列按序的报文:
static void tcp_ofo_queue(struct sock *sk)
{
struct tcp_opt *tp = tcp_sk(sk);
__u32 dsack_high = tp->rcv_nxt;
struct sk_buff *skb;
while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
if (after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) //最前面的skb都拼不上
break;
...
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
__skb_unlink(skb, skb->list); //曾经接收的报文段,继续
__kfree_skb(skb);
continue;
}
__skb_unlink(skb, skb->list); //可以拼接,更新tp的rcv_next字段
__skb_queue_tail(&sk->sk_receive_queue, skb);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
...
}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值