TCP 有两种确认方式,Delay ACK和quick ACK

Quick ACK,本端接收到数据包后,会立即发送ACK给对端。

Dealy ACK ,本端接收到数据包后,不会立即发送ACK给对端,而是等待一段时间,如果在此期间:

1. 本端有数据包要发送给对端。就在发送数据包的时候捎带上此ACK,如此一来就节省了一个报文。

2. 本端没有数据包要发送给对端。延迟确认定时器会超时,然后发送纯ACK给对端。

所以连续收到两个数据包时,就会发送ACK.

在具体实现中,用pingpong来区分这两种模式:

icsk->icsk_ack.pingpong == 0,表示使用快速确认。

icsk->icsk_ack.pingpong == 1,表示使用延迟确认。

Q:那么问题来了,为什么用“乒乓球”来标志这两种模式?

A:我们知道打乒乓球时,球是双向来回跳动的。这比喻传输是双向的,你发送数据给我,我也发送数据给你,应用是交互型的。

在这种情况下,可以让数据包捎带ACK,以减少纯ACK的发送,降低不必要的流量开销。

如果不是“乒乓球模式”,即传输是单向的,一方只发送数据,另一方只接收数据。这种情况下,接收方因为没有数据要发送,

不能够捎带ACK,所以不能使用延迟确认,应该使用快速确认。

以上只是为了说明pingpong的含义,在实际中到底使用哪种模式,还会受到其它因素的影响。

总的来说,快速确认模式是用于比较紧急的场景,此时需要立即通知对端,比如收到异常的数据报、接收窗口显著增大了。

延迟确认模式则希望通过减少纯ACK的发送,来降低不必要的流量开销,所以此时要求数据的传输是双向的。

在实际的传输过程中,会根据当时的场景来判断是使用快速确认模式还是延迟确认模式,因此ACK的发送模式并不是

固定的,而是在这两种模式之间动态切换。

Q:什么时候进行快速确认?

(1) 接收到数据包,检查是否需要发送ACK时 (__tcp_ack_snd_check):

1. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed,并且接收窗口变大了。

所以一般收到了两个数据包后,会发送ACK,而不是对每个数据包都进行确认。

2.  接收到数据包时,处于快速确认模式中。

3. 接收到数据包时,乱序队列不为空。

(2) 当接收队列中有数据复制到用户空间时,会判断是否要立即发送ACK (tcp_clean_rbuf):

如果现在有ACK需要发送,满足以下条件之一,就可以立即发送:

1. icsk->icsk_ack.blocked为1,之前有Delayed ACK被用户进程阻塞了。

2. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed (所以经常是收到2个全尺寸段后发送ACK)

3. 本次复制到用户空间的数据量大于0,且满足以下条件之一:

3.1 设置了ICSK_ACK_PUSHED2标志

3.2 设置了ICSK_ACK_PUSHED标志,且处于快速确认模式中

如果原来没有ACK需要发送,但是现在的接收窗口显著增大了,也需要立即发送ACK通知对端。

这里的显著增大是指:新的接收窗口大小不为0,且比原来接收窗口的剩余量增大了一倍。

(3) 接收到数据包的事件处理 (tcp_event_data_recv):数据包含有路由器的显式拥塞通知,进入快速确认模式。

(4) 设置TCP_QUICKACK选项之后:进入快速确认模式,并立即发送一个ACK。

(5) 如果接收到的段有负荷,且其中一部分之前已经接收过了,则认为是Delayed ACK丢失,进入快速确认模式。

Q:什么时候进行延迟确认?

1. 快速确认模式中的ACK额度用完了,一般在快速确认了半个接收窗口的数据后,进入延迟确认模式。

2. 发送ACK时,因为内存分配失败,启动延迟确认定时器。

3. 接收到数据包,检查是否需要发送ACK时(__tcp_ack_snd_check),如果无法进行快速确认。

4. 使用TCP_QUICKACK选项禁用快速确认,设置的值为0。

数据结构

icsk->icsk_ack中的变量,用于控制快速确认和延迟确认。

[java] view plain copy

  1. struct inet_connection_sock {
  2. ...
  3. struct {
  4. /* ACK is pending.
  5. * ACK的发送状态标志,可以表示四种情况:
  6. * 1. ICSK_ACK_SCHED:目前有ACK需要发送
  7. * 2. ICSK_ACK_TIMER:延迟确认定时器已经启动
  8. * 3. ICSK_ACK_PUSHED:如果处于快速确认模式,允许立即发送ACK
  9. * 4. ICSK_ACK_PUSHED2:无论是否处于快速确认模式,都可以立即发送ACK
  10. */
  11. __u8 pending;
  12. /* Scheduled number of quick acks.
  13. * 快速确认模式下,最多能够发送多少个ACK,额度用完以后就退出快速确认模式。
  14. */
  15. __u8 quick;
  16. /* The session is interactive.
  17. * 值为1时,为延迟确认模式;值为0时,为快速确认模式。
  18. * 注意这个标志是不是永久性的,而是动态变更的。
  19. */
  20. __u8 pingpong;
  21. /* Delayed ACK was blocked by socket lock.
  22. * 如果延迟确认定时器触发时,发现socket被用户进程锁住,就把blocked置为1。
  23. * 之后在接收到新数据、或者将数据复制到用户空间之后、或者再次超时时,会马上发送ACK。
  24. */
  25. __u8 blocked;
  26. /* Predicted tick of soft clock.
  27. * ACK的超时时间,是一个中间变量,根据接收到数据包的时间间隔来动态调整。
  28. * 用来计算延迟确认定时器的超时时间timeout。
  29. */
  30. __u32 ato;
  31. /* Currently scheduled timeout.
  32. * 延迟确认定时器的超时时刻。
  33. */
  34. unsigned long timeout;
  35. /* timestamp of last incoming segment.
  36. * 最后一次收到带负荷的报文的时间点。
  37. */
  38. __u32 lrcvtime;
  39. __u16 last_seg_size; /* Size of last incoming segment */
  40. __u16 rcv_mss; /* MSS used for delayed ACK decisions */
  41. } icsk_ack;
  42. ...
  43. };

icsk.icsk_ack.pending是ACK的发送状态标志,用于表示是否有ACK需要发送,以及发送的紧急程度。

[java] view plain copy

  1. enum inet_csk_ack_state_t {
  2. ICSK_ACK_SCHED = 1,    /* 有ACK需要发送 */
  3. ICSK_ACK_TIMER = 2,     /* 延迟确认定时器已经启动 */
  4. ICSK_ACK_PUSHED = 4,   /* 如果处于快速发送模式,允许立即发送ACK */
  5. ICSK_ACK_PUSHED2 = 9    /* 无论是否处于快速发送模式,都可以立即发送ACK */
  6. };

以下是ACK发送状态的转换图:

ACK的发送状态转换

接收到数据报后,会调用tcp_event_data_recv(),设置ICSK_ACK_SCHED标志来表明有ACK需要发送。

如果接收到了小包,说明对端很可能暂时没有数据需要发送了,此时会设置ICSK_ACK_PUSHED标志,

如果处于快速路径中,就允许马上发送ACK。如果不止一次接收到小包,就设置ICSK_ACK_PUSHED2

标志,不管是否处于快速路径中,都允许立即发送ACK,以强调发送ACK的紧急程度。

同时根据距离上次接收到数据报的时间间隔,来动态调整icsk->icsk_ack.ato:

1. delta <= TCP_ATO_MIN /2时,ato = ato / 2 + TCP_ATO_MIN / 2。

2. TCP_ATO_MIN / 2 < delta <= ato时,ato = min(ato / 2 + delta, rto)。

3. delta > ato时,ato值不变。

如果接收到的数据包的时间间隔变小,ato也会相应的变小。

如果接收到的数据包的时间间隔变大,ato也会相应的变大。

inet_csk_schedule_ack()用于设置ICSK_ACK_SCHED标志位,表示有ACK需要发送。

[java] view plain copy

  1. static inline void inet_csk_schedule_ack (struct sock *sk)
  2. {
  3. inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_SCHED;
  4. }

[java] view plain copy

  1. static void tcp_event_data_recv (struct sock *sk, struct sk_buff *skb)
  2. {
  3. struct tcp_sock *tp = tcp_sk(sk);
  4. struct inet_connection_sock *icsk = inet_csk(sk);
  5. u32 now;
  6. inet_csk_schedule_ack(sk); /* 设置有ACK需要发送的标志 */
  7. /* 通过接收到的数据段,来估算对端的MSS。
  8. * 如果接收到了小包,则设置ICSK_ACK_PUSHED标志。
  9. * 如果之前接收过小包,本次又接收到了小包,则设置ICSK_ACK_PUSHED2标志。
  10. */
  11. tcp_measure_rcv_mss(sk, skb);
  12. tcp_rcv_rtt_measure(tp); /* 没有使用时间戳选项时的接收端RTT计算 */
  13. now = tcp_time_stamp;
  14. /* 如果是第一次接收到带负荷的报文 */
  15. /* The first data packet received, initialize delayed ACK engine. */
  16. if (! icsk->icsk_ack.ato) {
  17. tcp_incr_quickack(sk); /* 设置在快速确认模式中可以发送的ACK数量 */
  18. icsk->icsk_ato.ato = TCP_ATO_MIN; /* ato的初始值,为40ms */
  19. else {
  20. int m = now - icsk->icsk_ack.lrcvtime; /* 距离上次收到数据报的时间间隔 */
  21. /* The fastest case is the first. */
  22. if (m <= TCP_ATO_MIN / 2) {
  23. icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2;
  24. else if (m < icsk->icsk_ack.ato) {
  25. icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m;
  26. /* ato的值不能超过RTO */
  27. if (icsk->icsk_ack.ato > icsk->icsk_rto)
  28. icsk->icsk_ack.ato = icsk->icsk_rto;
  29. else if (m > icsk->icsk_rto) {
  30. /* Too long gap. Apparently sender failed to restart window,
  31. * so that we send ACKs quickly.
  32. */
  33. tcp_incr_quickack(sk); /* 更新在快速确认模式中可以发送的ACK数量 */
  34. sk_mem_reclaim(sk);
  35. }
  36. }
  37. icsk->icsk_ack.lrcvtime = now; /* 更新最后一次接收到数据报的时间 */
  38. TCP_ECN_check_ce(tp, skb); /* 如果发现显示拥塞了,就进入快速确认模式 */
  39. /* 当报文段的负荷不小于128字节时,考虑增大接收窗口当前阈值 */
  40. if (skb->len >= 128)
  41. tcp_grow_window(sk, skb); /* 根据接收到的数据段的大小,来调整接收窗口的阈值rcv_ssthresh */
  42. }

如果接收到路由器的显式拥塞通知,就进入快速确认模式。

[java] view plain copy

  1. static inline void TCP_ECN_check_ce (struct tcp_sock *tp, const struct sk_buff *skb)
  2. {
  3. /* 如果连接不支持ECN */
  4. if (! (tp->ecn_flags & TCP_ECN_OK))
  5. return;
  6. switch (TCP_SKB_CB(skb)->ip_dsfield & INET_ECN_MASK) {
  7. case INET_ECN_NOT_ECT: /* IP层不支持ECN */
  8. /* If ECT is not set on a segment, and we already seen ECT on a previous segment,
  9. * it is probably a retransmit.
  10. */
  11. if (tp->ecn_flags & TCP_ECN_SEEN)
  12. tcp_enter_quickack_mode((struct sock *tp); / 进入快速确认模式 */
  13. break;
  14. case INET_ECN_CE: /* 数据包携带拥塞标志 */
  15. if (! (tp->ecn_flags & TCP_ECN_DEMAND_CWR)) {
  16. /* Better not delay acks, sender can have a very low cwnd */
  17. tcp_enter_quickack_mode((struct sock *) tp); /* 进入快速确认模式 */
  18. tp->ecn_flags |= TCP_ECN_DEMAND_CWR; /* 用于让对端感知拥塞的标志 */
  19. }
  20. /* fallinto */
  21. default:
  22. tp->ecn_flags |= TCP_ECN_SEEN;
  23. }
  24. }

通过接收到的数据段长度,来估算对端的MSS。

如果接收到了小包,则设置ICSK_ACK_PUSHED标志。

如果之前接收过小包,本次又接收到了小包,则设置ICSK_ACK_PUSHED2标志。

[java] view plain copy

  1. static void tcp_measure_rcv_mss (struct sock *sk, const struct sk_buff *skb)
  2. {
  3. struct inet_connection_sock *icsk = inet_csk(sk);
  4. const unsigned int lss = icsk->icsk_ack.last_seg_size; /* 上次收到的数据段大小 */
  5. unsigned int len;
  6. icsk->icsk_ack.last_seg_size = 0;
  7. len = skb_shinfo(skb)->gso_size ?: skb->len; /* 本次接收到数据的长度 */
  8. /* 如果本次接收到数据的长度,大于当前发送方的MSS */
  9. if (len >= icsk->icsk_ack.rcv_mss) {
  10. icsk->icsk_ack.rcv_mss = len; /* 更新发送方的MSS */
  11. else {
  12. /* Otherwise, we make more careful check taking into account,
  13. * that SACKs block is variable.
  14. * "len" is invariant segment length, including TCP header.
  15. */
  16. /* 之前的len表示数据的长度,现在加上TCP首部的长度,这才是总的长度 */
  17. len += skb->data - skb_transport_header(skb);
  18. /* 满足以下条件时,说明接收到的数据段还是比较正常的,尝试更精确的计算MSS,
  19. * 排除SACK块的影响,更新last_seg_size和rcv_mss。
  20. */
  21. /* If PSH is not set, packet should be full sized, provided peer TCP is not badly broken.
  22. * This observation (if it is correct 8)) allows to handle super-low mtu links fairly.
  23. */
  24. if (len >= TCP_MSS_DEFAULT + sizeof(struct tcphdr) ||
  25. (len >= TCP_MIN_MSS + sizeof(struct tcphdr) &&
  26. ! (tcp_flag_word(tcp_hdr(skb)) & TCP_PEMNANT))) {
  27. /* Subtract also invariant (if peer is RFC compliant),
  28. * tcp header plus fixed timestamp option length.
  29. * Resulting len is MSS free of SACK jitter.
  30. */
  31. /* 减去报头和时间戳选项的长度,剩下的就是数据和SACK块(如果有的话) */
  32. len -= tcp_sk(sk)->tcp_header_len;
  33. icsk->icsk_ack.last_seg_size = len; /* 更新最近一次接收到的数据段的长度 */
  34. /* 说明这次收到的还是full-sized,而不是小包 */
  35. if (len == lss) {
  36. icsk->icsk_ack.rcv_mss = len;
  37. return;
  38. }
  39. }
  40. /* 如果之前已经收到了小包,则进入更紧急的ACK发送模式,接下来无论是否处于快速确认模式,
  41. * 都可以马上发送ACK。
  42. */
  43. if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED)
  44. icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2;
  45. /* 如果收到小包,就允许在快速确认模式中,直接发送ACK */
  46. icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;
  47. }
  48. }
  49. #define TCP_MSS_DEFAULT 536U
  50. #define TCP_MIN_MSS 88U /* Minimal accepted MSS. It is (60+60+8) - (20+20). */

ACK的发送状态清除

当成功发送ACK时,会删除延迟确认定时器,同时清零ACK的发送状态标志icsk->icsk_ack.pending。

[java] view plain copy

  1. static int tcp_transmit_skb (struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
  2. {
  3. ...
  4. if (likely(tcb->tcp_flags & TCPHDR_ACK))
  5. tcp_event_ack_sent(sk, tcp_skb_pcount(skb)); /* ACK发送事件的处理 */
  6. ...
  7. }

ACK发送事件主要做了:更新快速确认模式中的ACK额度,删除ACK延迟定时器,清零icsk->icsk_ack.pending。

[java] view plain copy

  1. /* Account for an ACK we sent. */
  2. static inline void tcp_event_ack_sent (struct sock *sk, unsigned int pkts)
  3. {
  4. tcp_dec_quickack_mode(sk, pkts); /* 更新快速确认模式的ACK额度 */
  5. inet_csk_clear_xmit_timer(sk, ICSK_TIME_DACK); /* 删除ACK延迟定时器 */
  6. }

在快速确认模式中,可以发送的ACK数量是有限制的,具体额度为icsk->icsk_ack.quick。

当额度用完时,就进入延迟确认模式。

[java] view plain copy

  1. static inline void tcp_dec_quickack_mode (struct sock *sk, const unsigned int pkts)
  2. {
  3. struct inet_connection_sock *icsk = inet_csk(sk);
  4. if (icsk->icsk_ack.quick) { /* 如果额度不为0 */
  5. if (pkts >= icsk->icsk_ack.quick) {
  6. icsk->icsk_ack.quick = 0;
  7. /* Leaving quickack mode we deflate ATO. */
  8. icsk->icsk_ack.ato = TCP_ATO_MIN;
  9. else
  10. icsk->icsk_ack.quick -= pkts;
  11. }
  12. }

TCP ACK 方式相关推荐

  1. MobileIMSDK怎样修改Server端和安卓端TCP连接方式时报文的的限制大小

    场景 MobileIMSDK怎样修改服务端核心jar包的源码并替换掉Java服务端的jar包: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/de ...

  2. 2022.9.07 TCP协议特点,TCP适用场合,TCP连接方式(三次握手,四次挥手)。

    传输层协议: TCP   <传输控制协议>    transport control protocol TCP协议特点: 是一种面向连接的传输层协议,它能够提供高可靠性通信,无数据丢失,无 ...

  3. jenkins开启web代理通过tcp端口方式使master与slave建立连接

    jenkins开启web代理通过tcp端口方式使master与slave建立连接 文章目录 jenkins开启web代理通过tcp端口方式使master与slave建立连接 1.jenkins web ...

  4. lwip连续发数据卡死_LwIP用TCP连接方式在数据量比较大协议栈卡死

    这段时间用STM32移植LwIP做语音传输.但是遇到一个问题困扰许久,在使用TCP方式做一个client去连接server,由于数据量比较大经常在连接一个多小时候就出现断线而 也ping不通.接下来我 ...

  5. linux tcp ack设置,linux关于tcp协议ack的实现--发送端对ack的处理

    前面的文章分析了接收端如何发送ack给发送端,总结一下就是立即ack,捎带ack和延迟ack,现在看一下tcp的发送端是如何处理ack的,本质上tcp所谓的有连接就是双方对于seq和ack的处理,对于 ...

  6. C#调用斑马打印机打印条码标签(支持COM、LPT、USB、TCP连接方式和ZPL、EPL、CPCL指令)

    在批量打印商品标签时一般都要加上条码或图片,而这类应用大多是使用斑马打印机,所以我也遇到了怎么打印的问题. 一种办法是用标签设计软件做好模板,在标签设计软件中打印,这种办法不用写代码,但对我来说觉得不 ...

  7. C#调用斑马打印机打印条码标签(支持COM、LPT、USB、TCP连接方式和ZPL、EPL、CPCL指令)...

    在批量打印商品标签时一般都要加上条码或图片,而这类应用大多是使用斑马打印机,所以我也遇到了怎么打印的问题. 一种办法是用标签设计软件做好模板,在标签设计软件中打印,这种办法不用写代码,但对我来说觉得不 ...

  8. C#调用斑马打印机打印条码标签(支持COM、LPT、USB、TCP连接方式和ZPL、EPL、CPCL指令)【转】...

    原文地址:http://blog.csdn.net/ldljlq/article/details/7338772 在批量打印商品标签时一般都要加上条码或图片,而这类应用大多是使用斑马打印机,所以我也遇 ...

  9. C#调用斑马打印机打印条码标签(含源码)(支持COM、LPT、USB、TCP连接方式和ZPL、EPL、CPCL指令)

    在批量打印商品标签时一般都要加上条码或图片,而这类应用大多是使用斑马打印机,所以我也遇到了怎么打印的问题. 一种办法是用标签设计软件做好模板,在标签设计软件中打印,这种办法不用写代码,但对我来说觉得不 ...

最新文章

  1. 计算机辅助设计工业产品cad竞赛试题,2017计算机辅助设计(工业产品CAD)赛项样题...
  2. 【BZOJ4559】【JLOI2016】—成绩比较(拉格朗日插值+dp)
  3. 优化Android应用内存的若干方法
  4. cv2 imwrite中文路径_python3下使用cv2.imwrite存储带有中文路径图片的方法
  5. ERROR: Could not open CONNECT tunnel
  6. Spark Structured : KuduException$OriginalException: Originalasynchronous stack trace
  7. sas软件连接Oracle数据库的办法
  8. python3数据结构菜鸟教程_Python3
  9. Python 爬虫--网站下载器
  10. 计算机网卡和交换机网卡以及交换机数据转发
  11. 【Unity项目实战】手把手教学:飞翔的小鸟(3)动画制作
  12. 网络技术学习:虚拟专用网络
  13. Python正则提取
  14. abaqus质量缩放系数取值_ABAQUS中的质量缩放
  15. 如何检测猥琐的私有SDWAN隧道协议
  16. 四旋翼飞行器5——各类方案设计及参考
  17. css3 图片旋转360度动画
  18. 常用网络测试软件,常用的网络故障检测工具有哪些
  19. 机器学习实战(三)朴素贝叶斯 (Peter Harrington著)
  20. Matlab Shannon编码

热门文章

  1. 最美妙的英文单词大排名
  2. 股票入门技巧-----股票开户
  3. 【只推荐一位Python大佬】 从程序员到创业者,再到自由职业
  4. java cors_JAVA用CORS实现跨域请求
  5. 廖雪峰<python> 练习 :实现杨辉三角。把每一行看做一个list,试写一个generator,不断输出下一行的list。
  6. Endnote导出Bibtex格式自定义文献的“@article{ 后的标签
  7. MySQL超详细安装教程 手把手教你安装MySQL到使用MySQL 最简单的MySQL安装方式,这种方式装,卸载也简单
  8. win7 下使用cygwin
  9. Lego MindStorms NXT 井字棋机器人算法讨论
  10. AutoCAD 版本