TCP ACK 方式

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

 

 

 

 在CODE上查看代码片派生到我的代码片

  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.   
  13.         /* Scheduled number of quick acks. 
  14.          * 快速确认模式下,最多能够发送多少个ACK,额度用完以后就退出快速确认模式。 
  15.          */  
  16.         __u8 quick;   
  17.   
  18.         /* The session is interactive. 
  19.          * 值为1时,为延迟确认模式;值为0时,为快速确认模式。 
  20.          * 注意这个标志是不是永久性的,而是动态变更的。 
  21.          */  
  22.         __u8 pingpong;  
  23.   
  24.         /* Delayed ACK was blocked by socket lock. 
  25.          * 如果延迟确认定时器触发时,发现socket被用户进程锁住,就把blocked置为1。 
  26.          * 之后在接收到新数据、或者将数据复制到用户空间之后、或者再次超时时,会马上发送ACK。 
  27.          */  
  28.         __u8 blocked;  
  29.   
  30.         /* Predicted tick of soft clock. 
  31.          * ACK的超时时间,是一个中间变量,根据接收到数据包的时间间隔来动态调整。 
  32.          * 用来计算延迟确认定时器的超时时间timeout。 
  33.          */  
  34.         __u32 ato;   
  35.   
  36.         /* Currently scheduled timeout. 
  37.          * 延迟确认定时器的超时时刻。 
  38.          */  
  39.         unsigned long timeout;   
  40.   
  41.         /* timestamp of last incoming segment. 
  42.          * 最后一次收到带负荷的报文的时间点。 
  43.          */  
  44.         __u32 lrcvtime;  
  45.         __u16 last_seg_size; /* Size of last incoming segment */  
  46.         __u16 rcv_mss; /* MSS used for delayed ACK decisions */  
  47.     } icsk_ack;  
  48.     ...  
  49. };  

 

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

  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

 

 

 

 在CODE上查看代码片派生到我的代码片

  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

 

 

 

 在CODE上查看代码片派生到我的代码片

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

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

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

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

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

[java] view plain copy

 

 

 

 在CODE上查看代码片派生到我的代码片

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

 

 

 

 在CODE上查看代码片派生到我的代码片

  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

 

 

 

 在CODE上查看代码片派生到我的代码片

  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

 

 

 

 在CODE上查看代码片派生到我的代码片

  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.   
  5.     if (icsk->icsk_ack.quick) { /* 如果额度不为0 */  
  6.         if (pkts >= icsk->icsk_ack.quick) {  
  7.             icsk->icsk_ack.quick = 0;  
  8.             /* Leaving quickack mode we deflate ATO. */  
  9.             icsk->icsk_ack.ato = TCP_ATO_MIN;  
  10.         } else  
  11.             icsk->icsk_ack.quick -= pkts;  
  12.     }  
  13. }  

 

 

TCP ACK (Acknowledgement) 和 SEQ (Sequence Number) 是 TCP 协议中用于实现可靠数据传输和流控制的重要字段。 1. ACK (Acknowledgement): ACK 字段用于确认已经成功接收到的数据。在 TCP 通信中,接收方会向发送方发送 ACK 报文,其中的 ACK 字段表示下一个期望接收的字节序号。例如,如果 ACK 字段的值是 100,表示接收方已经成功接收到字节序号为 99 的数据,并期望接收序号为 100 的数据。 2. SEQ (Sequence Number): SEQ 字段用于标识 TCP 报文中数据的字节序号。在 TCP 通信中,每个 TCP 报文都会携带一个 SEQ 字段来指示该报文中数据的起始字节序号。接收方根据 SEQ 字段来按序接收和重组数据流。 通过 ACK 和 SEQ 字段的组合使用,TCP 协议可以实现可靠的数据传输和流控制。发送方发送数据时,接收方会发送带有 ACK 字段的确认报文,以告知发送方已成功接收到数据。发送方根据接收到的 ACK 确认来确认数据是否成功传输,如果没有收到 ACK 确认,发送方会进行重传。 同时,SEQ 字段的使用也可以帮助接收方按序接收和重组分片的数据。每个 TCP 报文都会携带 SEQ 字段,接收方根据 SEQ 字段来确定数据的顺序,并将它们按序交付给应用层。 总结:TCP 中的 ACK 和 SEQ 字段是用于实现可靠数据传输和流控制的重要字段。ACK 字段用于确认已经成功接收到的数据,SEQ 字段用于标识数据的字节序号。通过这两个字段的使用,TCP 可以实现可靠的数据传输和流控制机制。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值