(参考《TCP-IP详解卷 1:协议》“第22章 TCP的坚持定时器”)

1、糊涂窗口综合症

《TCP-IP详解卷 1:协议》"22.3 糊涂窗口综合症"

基于窗口的流量控制方案,如TCP所使用的,会导致一种被称为“糊涂窗口综合症SWS(Silly Window Syndrome)”的状况。如果发生这种情况,则少量的数据将通过连接进行交换,而不是满长度的报文段 [Clark 1982]。
该现象可发生在两端中的任何一端:接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告) ,而发送方也可以发送少量的数据(而不是等待其他的数据以便发送一个大的报文段)。

2、避免出现糊涂窗口综合症

2.1、接收方不通告小窗口

接收窗口恢复时,调用tcp_update_rcv_ann_wnd更新通告窗口。

如果新的通告窗口比旧的通告窗口大于等于一个报文,那么用新的接收窗口大小作为通告窗口大小。

  if (TCP_SEQ_GEQ(new_right_edge, pcb->rcv_ann_right_edge + pcb->mss)) { // rcv_ann_right_edge旧的发送窗口的右边沿,new_right_edge - pcb->rcv_ann_right_edge >= pcb->mss表示新的窗口增加大于等于一个mss的报文/* we can advertise more window */pcb->rcv_ann_wnd = pcb->rcv_wnd;return new_right_edge - pcb->rcv_ann_right_edge;} else {

如果新的通告窗口增加小于一个mss报文(new_right_edge - pcb->rcv_ann_right_edge < pcb->mss),并且接收窗口已满(下一个待接收的序号大于旧的通告窗口(恢复前的接收窗口)的右边沿,旧的通告窗口通常指向恢复前的接收窗口的右边沿,也就是接收窗口恢复前,可用接收窗口为0),那么,接收窗口恢复后,可用接收窗口大小为(new_right_edge - pcb->rcv_ann_right_edge) + 0,即new_right_edge - pcb->rcv_ann_right_edge,之前已经判断恢复的窗口大小小于一个mss报文,那么接收窗口恢复后,新的接收窗口仍小于一个mss报文,通告窗口设置为0。

    if (TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)) {/* Can happen due to other end sending out of advertised window,* but within actual available (but not yet advertised) window */pcb->rcv_ann_wnd = 0;} else {

如果新的通告窗口增加小于一个mss报文,并且接收窗口恢复前,接收窗口没有占满,那么使用恢复前的接收窗口的可用窗口大小作为新的通告窗口大小。(接收通告窗口右边沿不变,此时的通告窗口大小可能小于一个mss报文,因此需要在发送方采取措施避免出现糊涂窗口综合症)

    } else {/* keep the right edge of window constant */pcb->rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt;}

(在数据被应用层接收前,接收窗口的右边沿保持不变,可用接收窗口大小不保证大于等于一个mss报文,因此也需要在发送方采取措施避免出现糊涂窗口综合症)

2.2、发送方避免糊涂窗口综合症

发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据:

( a ) 可以发送一个满长度的报文段;

( b ) 可以发送至少是接收方通告窗口大小一半的报文段;(避免发送一个太小的报文,lwip没有实现这个条件,lwip都是按照一个满长度的报文段来发送)

( c ) 可以发送任何数据并且不希望接收ACK(也就是说,我们没有还未被确认的数据)或者该连接上不能使用Nagle算法。

lwip在调用tcp_output发送报文时,报文在发送窗口内,调用tcp_do_output_nagle检查报文是否可以发送(非TF_FIN|TF_NAGLEMEMERR,tcp_do_output_nagle返回1时才可以发送)。

#define tcp_do_output_nagle(tpcb) ((((tpcb)->unacked == NULL) || \((tpcb)->flags & TF_NODELAY) || \(((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || \((tpcb)->unsent->len >= (tpcb)->mss))) \) ? 1 : 0)

“(tpcb)->unacked == NULL”: 满足条件(c)中的“我们没有还未被确认的数据”;

"(tpcb)->flags & TF_NODELAY": 禁用了Nagle算法,"#define TF_NODELAY     ((u8_t)0x40U)   /* Disable Nagle algorithm */",lwip中TF_NODELAY标志禁用Nagle算法,满足条件(c)中的“该连接上不能使用Nagle算法”;

"(((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || ((tpcb)->unsent->len >= (tpcb)->mss)))": 如果未发送队列不为空且未发送队列有多个报文("((tpcb)->unsent->next != NULL)"表示未发送队列有多个报文,通常两个小报文会合并成一个大的mss报文,未发送队列的第一个报文正常情况下是一个mss大小的报文,即一个满长度的报文段),或者未发送队列的第一个报文的长度大于等于一个报文段("((tpcb)->unsent->len >= (tpcb)->mss)",lwip数据缓存到未发送队列时基本不会超过mss,因此可以认为是一个满长度的报文段),那么满足条件(a)中的“可以发送一个满长度的报文段”。

3、坚持定时器

3.1、启动坚持定时器

(参考《TCP-IP详解卷 2:实现》"25.9 tcp_setpersist函数")

坚持定时器是在发送窗口已满并且还有待发送的数据时启动。

tcp_output会把发送窗口/拥塞窗口内能发送的数据都发送出去,如果发送出去后,还有未发送的报文(seg不为空),那么发送方认为接收方的接收窗口为0(接收方接收到这些数据后,接收窗口减少为0),启动坚持定时器。

  if (seg != NULL && pcb->persist_backoff == 0 && // 发送窗口外有待发送的报文且还没有启动坚持定时器ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > pcb->snd_wnd) {/* prepare for persist timer */pcb->persist_cnt = 0; // 复位坚持定时器计数(persist_cnt时间后发送探测报文)pcb->persist_backoff = 1; // 探测报文发送次数persist_backoff设置为1(persist_backoff有多个作用,persist_backoff大于0表示启用了坚持定时器,persist_backoff另外还记录坚持定时器的超时次数,坚持定时器超时时间采用退避算法,坚持定时器每次超时时间间隔不一样)}

3.2、坚持定时器超时

tcp_slowtmr每次对坚持定时器加1,如果坚持定时器超时,那么persist_backoff加1(persist_backoff直到加到sizeof(tcp_persist_backoff)后,就一直保持不变),调用tcp_zero_window_probe探测接收方的接收窗口。(坚持状态与重传超时之间一个不同的特点就是TCP从不放弃发送窗口探查。(persist_backoff没有次数限制(加到最大后,保持不变))

      if (pcb->persist_backoff > 0) { // 已启用坚持定时器/* If snd_wnd is zero, use persist timer to send 1 byte probes* instead of using the standard retransmission mechanism. */pcb->persist_cnt++; // 坚持定时器加1if (pcb->persist_cnt >= tcp_persist_backoff[pcb->persist_backoff-1]) { // 坚持定时器超时pcb->persist_cnt = 0; // 坚持定时器重新开始计数if (pcb->persist_backoff < sizeof(tcp_persist_backoff)) {pcb->persist_backoff++; // persist_backoff加1,persist_backoff永远不会大于sizeof(tcp_persist_backoff),因此TCP从不放弃发送窗口探查}tcp_zero_window_probe(pcb); // 发送窗口探测报文}} else { // else分支(超时重传),坚持定时器启动后,不走else分支;坚持定时器启动后,超时重传定时器并没有在计数(坚持定时器超时,调用tcp_zero_window_probe发送探测报文时,tcp_zero_window_probe会发送unacked队列里面的第一个字节,坚持定时器实际上在重传报文,因此不需要用到超时重传定时器;超时重传定时器与坚持定时器互斥)

tcp_zero_window_probe发送窗口探测报文。(《TCP-IP详解》解释的是0窗口探测,在lwip中是发送窗口已满时启用坚持定时器,发送窗口已满表示已经发送接收方接收窗口大小的数据,一定程度上可以理解为接下来接收方的可用接收窗口会变为0,接收方0通告窗口的ACK报文可能在网络中丢失(发送方可能接收不到0通告窗口的报文),因此,发送窗口满的时候,发送方开始启用坚持定时器)

tcp_zero_window_probe拷贝待确认报文的第一个字节组成一个新的重复的数据报文。

void
tcp_zero_window_probe(struct tcp_pcb *pcb)
{struct pbuf *p;struct tcp_hdr *tcphdr;struct tcp_seg *seg;LWIP_DEBUGF(TCP_DEBUG, ("tcp_zero_window_probe: sending ZERO WINDOW probe to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",ip4_addr1(&pcb->remote_ip), ip4_addr2(&pcb->remote_ip),ip4_addr3(&pcb->remote_ip), ip4_addr4(&pcb->remote_ip)));LWIP_DEBUGF(TCP_DEBUG, ("tcp_zero_window_probe: tcp_ticks %"U32_F"   pcb->tmr %"U32_F" pcb->keep_cnt_sent %"U16_F"\n", tcp_ticks, pcb->tmr, pcb->keep_cnt_sent));seg = pcb->unacked; // 获取unacked的第一个报文if(seg == NULL)seg = pcb->unsent; // 获取unsent的第一个报文if(seg == NULL) // 没有待确认的报文,也没有待发送的报文,直接返回,不需要探测return;p = pbuf_alloc(PBUF_IP, TCP_HLEN + 1, PBUF_RAM); // 申请一个TCP_HLEN + 1的内存(存储tcp首部以及1个数据)if(p == NULL) {LWIP_DEBUGF(TCP_DEBUG, ("tcp_zero_window_probe: no memory for pbuf\n"));return;}LWIP_ASSERT("check that first pbuf can hold struct tcp_hdr",(p->len >= sizeof(struct tcp_hdr)));tcphdr = tcp_output_set_header(pcb, p, 0, seg->tcphdr->seqno); // 探测报文的序号设置为unacked或者unsent第一个报文的seqno/* Copy in one byte from the head of the unacked queue */*((char *)p->payload + sizeof(struct tcp_hdr)) = *(char *)seg->dataptr; // 拷贝一个字节到探测报文里面#if CHECKSUM_GEN_TCPtcphdr->chksum = inet_chksum_pseudo(p, &pcb->local_ip, &pcb->remote_ip,IP_PROTO_TCP, p->tot_len);
#endifTCP_STATS_INC(tcp.xmit);/* Send output to IP */
#if LWIP_NETIF_HWADDRHINTip_output_hinted(p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl, 0, IP_PROTO_TCP,&(pcb->addr_hint));
#else /* LWIP_NETIF_HWADDRHINT*/ip_output(p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl, 0, IP_PROTO_TCP); // 调用ip_output直接发送报文(探测报文不会重发,只有坚持定时器每次超时后会发送一次)
#endif /* LWIP_NETIF_HWADDRHINT*/pbuf_free(p);LWIP_DEBUGF(TCP_DEBUG, ("tcp_zero_window_probe: seqno %"U32_F" ackno %"U32_F".\n",pcb->snd_nxt - 1, pcb->rcv_nxt));
}

3.3、关闭坚持定时器

persist_backoff 等于0时表示关闭坚持定时器。

tcp_receive更新发送窗口时,如果新的发送窗口snd_wnd大于0并且已经启动坚持定时器,那么关闭坚持定时器,否则不关闭坚持定时器。

    /* Update window. */if (TCP_SEQ_LT(pcb->snd_wl1, seqno) ||(pcb->snd_wl1 == seqno && TCP_SEQ_LT(pcb->snd_wl2, ackno)) ||(pcb->snd_wl2 == ackno && tcphdr->wnd > pcb->snd_wnd)) {pcb->snd_wnd = tcphdr->wnd;pcb->snd_wl1 = seqno;pcb->snd_wl2 = ackno;if (pcb->snd_wnd > 0 && pcb->persist_backoff > 0) {pcb->persist_backoff = 0; // 关闭坚持定时器}LWIP_DEBUGF(TCP_WND_DEBUG, ("tcp_receive: window update %"U16_F"\n", pcb->snd_wnd));
#if TCP_WND_DEBUG} else {

TCP/IP传输层协议实现 - TCP的坚持定时器(lwip)相关推荐

  1. TCP/IP传输层协议实现 - TCP连接的建立与终止(lwip)

    1.lwip tcp相关数据结构 1.1.tcp报文格式 <TCP-IP详解卷 1:协议>TCP包首部结构如下: 1.2.lwip tcp数据结构 tcp相关数据结构如下,tcp_pcb_ ...

  2. TCP/IP传输层协议实现 - TCP的超时与重传(lwip)

    (参考<TCP-IP详解卷 1:协议> 第21章 TCP的超时与重传) 1.往返时间测量(RTT) 1.1.分组交换和RTT测量示例 <TCP-IP详解卷 1:协议>中分组交换 ...

  3. TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)

    1.tcp通告窗口/接收窗口/发送窗口 接收端有一个接收窗口大小,接收端只能接收这么多数据,接收窗口的数据需要被上层接收后才释放更大接收空间,才可以接收更多数据:接收窗口之前的数据已经被接收,再次接收 ...

  4. 前端工程师如何理解 TCP/IP 传输层协议?| 技术头条

    作者 | 浪里行舟 责编 | 郭芮 网络协议是每个前端工程师都必须要掌握的知识,TCP/IP 中有两个具有代表性的传输层协议,分别是 TCP 和 UDP,本文将介绍下这两者以及它们之间的区别. TCP ...

  5. 简单理解TCP/IP传输层协议TCP和UDP

    TCP/IP模型中的传输层主要负责端到端通信,和数据链路层类似,数据链路层负责点到点的通信.TCP/IP模型的传输层主要协议有TCP (Transmission Control Protocol,传输 ...

  6. 传输层协议(TCP/UDP)介绍

    一,TCP/IP协议族的传输层协议概况:  1,TCP:传输控制协议  2,UDP:用户数据报协议  二,TCP/UDP协议详解:  1,TCP  a.TCP是面向连接的,可靠的进程到进程通信的协议 ...

  7. Linux网络编程(传输层协议 )—tcp三次握手/四次挥手

    传输层协议:负责应用程序之间数据传输-TCP/UDP UDP协议: 16位源端-对端端口:用于描述识别通信两端进程 16位数据报长度:能够存储最大数字 65535,(udp报文总大小不超过64k) 1 ...

  8. 面试热点|理解TCP/IP传输层拥塞控制算法

    来自:后端技术指南针 0x00.前言 这是TCP/IP协议栈系列的第二篇文章,之前的一篇理解TCP/IP协议栈之HTTP2.0感兴趣可以看下,今天一起来学习下一个热点问题. 通过本文你将了解到以下内容 ...

  9. TCP/IP 七层协议

    一.TCP/IP与OSI 二.七层参考模型概述 2.1.物理层 在OSI参考模型中,物理层(Physical Layer)是参考模型的最低层.物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可 ...

最新文章

  1. 扩展源_Ubuntu14版本下无法使用php7.2版本的bcmath扩展
  2. (原創) 我的Design Pattern之旅[3]:使用template改進Strategy Pattern (OO) (Design Pattern) (C/C++) (template)...
  3. word2vec安装以及使用
  4. android 数字时钟代码大全,Android自定义view实现数字时钟
  5. Windows7操作系统任务栏的相关技巧
  6. ABAP Development Tools的语法高亮实现原理
  7. 【实战 Ids4】║ 认证中心之内部加权
  8. PCL之点云分割算法概述
  9. 电机与拖动基础第四版_伺服电机控制
  10. 问题处理:VMware Workstation和Device / Credential Guard不兼容
  11. 区块链游戏——开发平台总览:EOSIO
  12. 以虎嗅网4W+文章的文本挖掘为例,展现数据分析的一整套流程
  13. vue创建的挂钩中出错_建立自己的Vue 3 SWR挂钩
  14. 最全阿里架构师P系列解读:P5-P8的技能要求和薪资结构
  15. HASH查找算法—JAVA实现
  16. Vue3 使用vant actionBar组件后对icon图标的点击切换效果/点击收藏/取消收藏
  17. Javascript迭代、递推、穷举、递归常用算法实例讲解
  18. 视频号跳转商品页面的教程
  19. shell 批量修改文件名
  20. 云端卫士实战录 | OFTest 的安装和使用

热门文章

  1. 云服务器的系统镜像怎么选,买云服务器镜像怎么选择
  2. CSDN博客大神汇总
  3. 22.1.11京东大数据实习面试
  4. 求 Fibonacci 数列的前 20 项
  5. 【正点原子FPGA连载】第三十九章OV7725摄像头RGB-LCD显示实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1
  6. tcp too many orphaned sockets 问题引发的思考
  7. herf(超链接、锚链接)
  8. php推送手机,PHP_解析php做推送服务端实现ios消息推送,准备工作1.获取手机注册应用 - phpStudy...
  9. HDMI RGB_TO_DVI模块
  10. 对视频文件进行简单的加密