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

Posted arm7star

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)相关的知识,希望对你有一定的参考价值。

1、tcp通告窗口/接收窗口/发送窗口

接收端有一个接收窗口大小,接收端只能接收这么多数据,接收窗口的数据需要被上层接收后才释放更大接收空间,才可以接收更多数据;接收窗口之前的数据已经被接收,再次接收到接收窗口之前的数据可以认为是重复发送的,不处理,接收窗口之后的数据不能接收,超出接收范围直接丢弃。

接收端通过tcp首部通告窗口字段告诉对端本地可以可以接收多少数据,用于控制发送端的发送窗口大小。

发送端有一个发送窗口,发送窗口大小即为对端通告窗口的大小,只有发送窗口内的tcp报文才可以发送。发送窗口之前的数据为已经发送并且被确认的数据,不需要再次确认,如果收到再次确认可能是网络延迟等造成的,发送窗口之后的数据不能发送,需要等发送窗口数据被确认后,释放更多发送窗口空间才可以继续往发送窗口发送数据。

 

1.1、tcp接收窗口/通告窗口大小发送

在客户连接服务器时,connect调用tcp_connect发起第一次握手时,设置接收窗口/通告窗口大小(连接过程请参考https://blog.csdn.net/arm7star/article/details/116560454);

调用tcp_output发送报文时,调用tcp_output_set_header设置tcp首部,设置报文通告窗口大小,建立连接时就是接收窗口大小。

第一次握手设置接收/通告窗口大小。

  iss = tcp_next_iss(); // 初始化发送序号
  pcb->rcv_nxt = 0;
  pcb->snd_nxt = iss;
  pcb->lastack = iss - 1;
  pcb->snd_lbb = iss - 1;
  pcb->rcv_wnd = TCP_WND; // 设置接收窗口大小
  pcb->rcv_ann_wnd = TCP_WND; // 设置通告窗口大小
  pcb->rcv_ann_right_edge = pcb->rcv_nxt;
  pcb->snd_wnd = TCP_WND; // 设置默认发送窗口大小(收到对端通告窗口大小后会更新发送窗口)

tcp首部内容如下:

tcp报文首部结构体如下:

struct tcp_hdr {
  PACK_STRUCT_FIELD(u16_t src); // 16位源端口号
  PACK_STRUCT_FIELD(u16_t dest); // 16位目的端口号
  PACK_STRUCT_FIELD(u32_t seqno); // 32位序号(发送端数据的序号)
  PACK_STRUCT_FIELD(u32_t ackno); // 32位确认序号(对端下一个报文的序号,ackno之前不包括ackno的数据已经被本地接收,对端可以释放ackno之前不包括ackno的发送数据,对于tcp协议,可以发送的报文存放在unacked队列里面,unacked队列里面的报文可能已经通过网卡发送出去了没有得到应答,也可能还没通过网卡发送出去)
  PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); // 4位首部长度、6位保留位、6位标志位(URG/ACK/PSH/RST/SYN/FIN),共16位
  PACK_STRUCT_FIELD(u16_t wnd); // 16位窗口大小,即通告窗口,通告对端本地能接收多少数据
  PACK_STRUCT_FIELD(u16_t chksum); // 16位校验和
  PACK_STRUCT_FIELD(u16_t urgp); // 16位紧急指针
} PACK_STRUCT_STRUCT;

1.2、tcp发送窗口大小设置

客户发起连接后,tcp_listen_input处理连接的SYN报文,新建一个tcp_pcb用于客户与服务器之间的连接(accept会返回一个新的socket,新的tcp_pcb即用于accept返回的socket),客户的SYN报文带有客户的通告窗口大小,使用客户通告窗口大小作为服务器的发送窗口大小。

第一次握手更新服务器发送窗口。

    ip_addr_set(&(npcb->local_ip), &(iphdr->dest));
    npcb->local_port = pcb->local_port;
    ip_addr_set(&(npcb->remote_ip), &(iphdr->src));
    npcb->remote_port = tcphdr->src;
    npcb->state = SYN_RCVD;
    npcb->rcv_nxt = seqno + 1; // seqno为客户SYN数据的sequence number,SYN报文虽然没有数据,但是占用一个sequence number,rcv_nxt为期望收到的下一个sequence number
    npcb->rcv_ann_right_edge = npcb->rcv_nxt;
    npcb->snd_wnd = tcphdr->wnd; // 设置发送窗口大小(发送窗口大小即为对端通告窗口大小)
    npcb->ssthresh = npcb->snd_wnd; // 慢启动门限设置为发送窗口大小
    npcb->snd_wl1 = seqno - 1;/* initialise to seqno-1 to force window update */ // 用于强制更新发送窗口
    npcb->callback_arg = pcb->callback_arg;

1.3、tcp发送窗口更新

客户收到第二次握手SYN|ACK报文,SYN_SENT状态下,tcp_process函数更新客户发送窗口大小为服务器通告窗口大小(客户发起连接请求时并没有服务器通告窗口大小信息,使用默认值作为发送窗口大小,收到服务器第二次握手SYN|ACK报文时,报文里面有服务器的通告窗口大小,即可更新发送窗口的大小),然后发送ACK第三次握手报文。

第二次握手时更新客户发送窗口大小。

  case SYN_SENT:
    LWIP_DEBUGF(TCP_INPUT_DEBUG, ("SYN-SENT: ackno %"U32_F" pcb->snd_nxt %"U32_F" unacked %"U32_F"\\n", ackno,
     pcb->snd_nxt, ntohl(pcb->unacked->tcphdr->seqno)));
    /* received SYN ACK with expected sequence number? */
    if ((flags & TCP_ACK) && (flags & TCP_SYN)
        && ackno == ntohl(pcb->unacked->tcphdr->seqno) + 1) {
      pcb->snd_buf++; // 发送缓存大小加1(SYN报文占一个字节,SYN被服务器确认后,增加1个字节发送缓存大小)
      pcb->rcv_nxt = seqno + 1; // 期望接收的服务器的下一个报文的sequence number
      pcb->rcv_ann_right_edge = pcb->rcv_nxt;
      pcb->lastack = ackno; // 等待被确认的下一个报文的seqno
      pcb->snd_wnd = tcphdr->wnd; // 用对端通告窗口大小设置为本地发送窗口大小
      pcb->snd_wl1 = seqno - 1; /* initialise to seqno - 1 to force window update */ // 用于强制更新发送窗口,seqno为SYN|ACK的sequence number,服务器下次发送报文的sequence number不小于seqno,设置snd_wl1小于seqno,下次接收到服务器的报文时,snd_wl1小于服务器报文的sequence number时,接收函数会更新snd_wnd,实际就是握手之后强制更新本地的发送窗口
      pcb->state = ESTABLISHED;

1.4、tcp服务器再次更新发送窗口

客户收到第二次握手的SYN|ACK报文后,发送第三次握手ACK报文,服务器收到ACK报文后,服务器在SYN_RCVD状态调用tcp_receive接收报文时更新发送窗口。

 

2、发送窗口更新

发送窗口更新涉及两个变量,snd_wl1、snd_wl2,snd_wl1记录的是之前用于更新发送窗口的报文的seqno,snd_wl2记录的是之前用于更新发送窗口的报文的ackno。

tcp_receive根据当前收到报文的seqno及ackno来确定是否更新发送窗口大小。

如果当前收到的报文的seqno大于等于之前用于更新发送窗口的报文的seqno,那么用该报文的通告窗口大小更新本地发送窗口大小;(seqno用于判断报文发送的先后顺序,seqno越大,发送时间越后)

如果当前收到的报文的seqno等于之前用于更新发送窗口的报文的seqno,那么用ackno大的报文的通过窗口大小更新本地发送窗口大小;(ackno用于判断ACK报文发送的先后顺序,ackno越大,发送时间越后)

如果当前收到的报文的seqno及ackno都相等,那么用通告窗口大的报文的通告窗口大小更新本地发送窗口大小;(ACK可能在网络中阻塞,对重复报文再次发ACK,两个ACK的seqno、ackno相同,通告窗口可能不一样,两个ACK可能都会被接收到,没办法区分哪个后发送)

tcp_receive更新发送窗口代码如下。

  if (flags & TCP_ACK) { // ACK报文
    right_wnd_edge = pcb->snd_wnd + pcb->snd_wl2;

    /* Update window. */
    if (TCP_SEQ_LT(pcb->snd_wl1, seqno) || // seqno大于收到的报文的最大seqno,该报文发送时间更后
       (pcb->snd_wl1 == seqno && TCP_SEQ_LT(pcb->snd_wl2, ackno)) || // seqno等于收到的报文的最大seqno,ackno大于之前收到的报文的最大ackno
       (pcb->snd_wl2 == ackno && tcphdr->wnd > pcb->snd_wnd)) { // seqno、ackno都等于收到的报文的最大seqno、ackno
      pcb->snd_wnd = tcphdr->wnd; // 更新发送窗口
      pcb->snd_wl1 = seqno; // snd_wl1记录收到的报文的最大seqno
      pcb->snd_wl2 = ackno; // snd_wl2记录收到的报文的最大ackno
      if (pcb->snd_wnd > 0 && pcb->persist_backoff > 0) { // 如果发送窗口大于0并且坚持定时器已经启动,那么关闭坚持定时器(坚持定时器用于发送报文去探测对端的接收窗口,对端收到报文会发送一个ACK,ACK带有对端通告窗口大小)
          pcb->persist_backoff = 0;
      }

 

3、接收窗口/接收通告窗口

接收窗口涉及rcv_nxt、rcv_wnd、rcv_ann_wnd、rcv_ann_right_edge几个变量,rcv_nxt为下一个等待接收报文的seqno,rcv_wnd为接收窗口大小(未被上层接收的、缓存在接收窗口里面的数据大小),rcv_ann_wnd为接收通告窗口的大小(receiver window to announce,告诉对端本地接收窗口还可以接收多少数据),rcv_ann_right_edge为接收通告窗口的右边缘(通常情况下指向接收窗口的右边缘,接收通告窗口更新后,还没发送给对端的情况下,rcv_ann_right_edge指向的是上一次发送通告窗口时的接收窗口右边缘,通告窗口发送后,rcv_ann_right_edge指向当前接收窗口的右边缘)。

3.1、接收窗口的更新

tcp接收到报文后,接收窗口减小。如下图所示,"已被接收的数据“表示已被上层应用程序接收的数据,"等待接收"为接收端期待接收的下一个数据,"out of sequence segments"为接收序列之外的数据(接收端按顺序接收1、2、3、4、5序号的数据,如果在没有收到1的时候收到其他报文,那么其他报文都是out of sequence segments,当”等待接收“的数据收到后,”等待接收“的数据与out of sequence segments组成一个连续的可接受的报文,发送数据给应用层,在被应用程序接收前,这些报文都属于接收窗口里面的报文)

当收到”等待接收“的报文时,这一部分数据是期望接收的,可接受的,那么接收窗口需要减去这段报文长度才是接收窗口当前的长度。tcp_receive处理过程如下。

收到的报文有数据。

  /* If the incoming segment contains data, we must process it
     further. */
  if (tcplen > 0) {

当前收到报文包含期望接收的序号。(当前报文序号为期望接收的序号,或者当前报文有部分已经接收的数据,部分是期望待接收的数据,那么丢弃已经接收的数据,获取等待接收的数据即可)

    /*    if (TCP_SEQ_LT(seqno, pcb->rcv_nxt)){
          if (TCP_SEQ_LT(pcb->rcv_nxt, seqno + tcplen)) {*/
    if (TCP_SEQ_BETWEEN(pcb->rcv_nxt, seqno + 1, seqno + tcplen - 1)){

释放报文中之前已经接收过了的数据。

      off = pcb->rcv_nxt - seqno; // 获取等待接收的数据在报文数据中的偏移
      p = inseg.p; // p指向报文数据
      LWIP_ASSERT("inseg.p != NULL", inseg.p);
      LWIP_ASSERT("insane offset!", (off < 0x7fff));
      if (inseg.p->len < off) { // inseg第一个pbuf节点长度小于off,已接收的数据占pbuf几个节点,inseg.p里面全是已经接收过的数据,需要完全丢弃该pbuf节点
        LWIP_ASSERT("pbuf too short!", (((s32_t)inseg.p->tot_len) >= off));
        new_tot_len = (u16_t)(inseg.p->tot_len - off); // 需要读取的全部数据的长度
        while (p->len < off) { // p->len < off表明该p里面的数据都是已经被接收了的数据,需要丢弃,循环直到丢弃off数据为止
          off -= p->len; // 减去p里面包含的已经接收的数据,off为inseg里面还剩余的已经被接收了的数据
          /* KJM following line changed (with addition of new_tot_len var)
             to fix bug #9076
             inseg.p->tot_len -= p->len; */
          p->tot_len = new_tot_len; // 更新p里面总数据长度
          p->len = 0; // p->len设置为0(没有释放内存,仅标记有效数据为0,遍历pbuf时直接跳过该节点)
          p = p->next; // 指向pbuf的下一个节点
        }
        if(pbuf_header(p, (s16_t)-off)) { // p里面包含部分需要接收的数据,部分需要丢弃的数据,释放需要丢弃的那部分数据
          /* Do we need to cope with this failing?  Assert for now */
          LWIP_ASSERT("pbuf_header failed", 0);
        }
      } else { // inseg已经接收了的数据都在第一个pbuf节点里面,直接释放第一个节点已经接收的数据即可(长度减少,有效数据偏移往后移,不需要真正释放内存)
        if(pbuf_header(inseg.p, (s16_t)-off)) { // ”释放“off个已经接收了的数据
          /* Do we need to cope with this failing?  Assert for now */
          LWIP_ASSERT("pbuf_header failed", 0);
        }
      }

更新报文序号数据等(删除部分已经被接收的数据后,报文的序号等需要更新)。

      /* KJM following line changed to use p->payload rather than inseg->p->payload
         to fix bug #9076 */
      inseg.dataptr = p->payload; // 报文数据指针
      inseg.len -= (u16_t)(pcb->rcv_nxt - seqno); // 报文有效数据长度
      inseg.tcphdr->seqno = seqno = pcb->rcv_nxt; // 报文起始序号

检查报文是否在接收窗口内以及是否是期待接收的报文(之前是删除已接收的数据,out of sequence segments以及更新后的报文都走下面代码处理)。

    /* The sequence number must be within the window (above rcv_nxt
       and below rcv_nxt + rcv_wnd) in order to be further
       processed. */
    if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt, 
                        pcb->rcv_nxt + pcb->rcv_wnd - 1)){ // 接收到的报文的序号是否在接收窗口里面
      if (pcb->rcv_nxt == seqno) { // 接收的报文的seqno是其期待收的下一个序号,可接受的报文,可以提交给上层处理

报文可接受,获取报文长度。

        accepted_inseq = 1; // 报文可接受
        /* The incoming segment is the next in sequence. We check if
           we have to trim the end of the segment and update rcv_nxt
           and pass the data to the application. */
        tcplen = TCP_TCPLEN(&inseg); // 获取报文长度

报文长度超过接收窗口大小,释放超出接收窗口的数据。

        if (tcplen > pcb->rcv_wnd) {
          LWIP_DEBUGF(TCP_INPUT_DEBUG, 
                      ("tcp_receive: other end overran receive window"
                       "seqno %"U32_F" len %"U32_F" right edge %"U32_F"\\n",
                       seqno, tcplen, pcb->rcv_nxt + pcb->rcv_wnd));
          if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN) {
            /* Must remove the FIN from the header as we're trimming 
             * that byte of sequence-space from the packet */
            TCPH_FLAGS_SET(inseg.tcphdr, TCPH_FLAGS(inseg.tcphdr) &~ TCP_FIN);
          }
          /* Adjust length of segment to fit in the window. */
          inseg.len = pcb->rcv_wnd; // 报文长度设置为接收窗口大小,超出的将被丢弃
          if (TCPH_FLAGS(inseg.tcphdr) & TCP_SYN) {
            inseg.len -= 1;
          }
          pbuf_realloc(inseg.p, inseg.len); // 重新申请报文内存(pbuf_realloc把超出长度的丢弃了,接收窗口内的数据还在)
          tcplen = TCP_TCPLEN(&inseg); // 重新设置报文长度
          LWIP_ASSERT("tcp_receive: segment not trimmed correctly to rcv_wnd\\n",
                      (seqno + tcplen) == (pcb->rcv_nxt + pcb->rcv_wnd));
        }

out of sequence segments报文处理(乱序收到的报文被缓存在ooseq里面),检查当前报文是否与out of sequence segments有重叠,只需要判断out of sequence segments的第一个报文即可,释放当前报文重叠部分数据。

        if (pcb->ooseq != NULL) { // 有out of sequence segments报文
          if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN) { // 当前收到的是FIN报文,FIN之后的out of sequence segments报文全部丢弃
            LWIP_DEBUGF(TCP_INPUT_DEBUG, 
                        ("tcp_receive: received in-order FIN, binning ooseq queue\\n"));
            /* Received in-order FIN means anything that was received
             * out of order must now have been received in-order, so
             * bin the ooseq queue */
            while (pcb->ooseq != NULL) {
              struct tcp_seg *old_ooseq = pcb->ooseq;
              pcb->ooseq = pcb->ooseq->next;
              memp_free(MEMP_TCP_SEG, old_ooseq);
            }               
          } else if (TCP_SEQ_LEQ(pcb->ooseq->tcphdr->seqno, seqno + tcplen)) { // out of sequence segments与当前报文有重叠,需要删除inseg里面重叠的数据,保留更早收到的out of sequence segments里面的数据
            if (pcb->ooseq->len > 0) {
              /* We have to trim the second edge of the incoming segment. */
              LWIP_ASSERT("tcp_receive: trimmed segment would have zero length\\n",
                          TCP_SEQ_GT(pcb->ooseq->tcphdr->seqno, seqno));
              /* FIN in inseg already handled by dropping whole ooseq queue */
              inseg.len = (u16_t)(pcb->ooseq->tcphdr->seqno - seqno); // out of sequence segments没有等待接收的部分数据,inseg保留out of sequence segments缺少的那部分数据即可
              if (TCPH_FLAGS(inseg.tcphdr) & TCP_SYN) {
                inseg.len -= 1;
              }
              pbuf_realloc(inseg.p, inseg.len); // 重新分配内存,释放重叠数据
              tcplen = TCP_TCPLEN(&inseg);
              LWIP_ASSERT("tcp_receive: segment not trimmed correctly to ooseq queue\\n",
                          (seqno + tcplen) == pcb->ooseq->tcphdr->seqno);
            } else { // out of sequence segments没有数据
              /* does the ooseq segment contain only flags that are in inseg also? */
              if ((TCPH_FLAGS(inseg.tcphdr) & (TCP_FIN|TCP_SYN)) ==
                  (TCPH_FLAGS(pcb->ooseq->tcphdr) & (TCP_FIN|TCP_SYN))) { // 如果当前报文的flags与out of sequence segments的flags相等,那么丢掉out of sequence segments里面的报文,out of sequence segments指向下一个out of sequence segments报文
                struct tcp_seg *old_ooseq = pcb->ooseq; // ooseq指向下一个out of sequence segments报文
                pcb->ooseq = pcb->ooseq->next;
                memp_free(MEMP_TCP_SEG, old_ooseq); // 释放没有数据的报文
              }
            }
          }
        }

接收窗口更新(已接收发送端的数据,接下来会发送ACK给对端,对端的发送窗口释放部分已经被确认的报文,对方发送窗口实际已经恢复了,但是本地接收窗口里面的数据还没被处理,还在接收窗口,需要减小接收端的接收通告窗口大小(对端发送窗口大小))。

        pcb->rcv_nxt = seqno + tcplen; // 接收窗口左边缘右移(接收窗口右边缘没有移动,此处缩小了接收窗口)

        /* Update the receiver's (our) window. */
        LWIP_ASSERT("tcp_receive: tcplen > rcv_wnd\\n", pcb->rcv_wnd >= tcplen);
        pcb->rcv_wnd -= tcplen; // 接收窗口边缘移动后更新接收窗口大小,减去已经接收的数据长度

        tcp_update_rcv_ann_wnd(pcb); // 更新接收通告窗口

接收窗口的恢复是在调用recv后,数据被上层接收后,接收窗口恢复已经被接收的部分数据大小,由tcp_recved函数恢复。该函数同样会更新通告接收窗口。后面处理out of sequence segments代码时,out of sequence segments跟当前报文组成一个大的连续的可接受的报文时,同样会更新接收窗口。

3.2、接收通告窗口更新

在接收端有收到可接受的数据时以及数据被上层接收后,lwip调用tcp_update_rcv_ann_wnd更新通告接收窗口。

u32_t tcp_update_rcv_ann_wnd(struct tcp_pcb *pcb)
{
  u32_t new_right_edge = pcb->rcv_nxt + pcb->rcv_wnd; // 新的接收窗口右边缘

  if (TCP_SEQ_GEQ(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 {
    if (TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)) { // 接收报文时,接收窗口已满,接收通告窗口设置为0;上层接收报文数据,恢复接收窗口后,接收窗口不足一个mss报文,接收通告窗口设置为0
      /* 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; // 通告窗口设置为0,不允许再发送数据到本地,需要等接收通告窗口滑动后,有足够接收通告窗口才行
    } else { // 接收报文时,接收窗口没有满,以当前接收窗口大小设置接口通告窗口大小;恢复接收窗口时,原来的接收接窗口没有满,用恢复后的接收窗口大小设置接收通告窗口大小(这里并没有判断是否够一个mss报文)
      /* keep the right edge of window constant */
      pcb->rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt; // 更新接收通告窗口大小为当前接收窗口大小
    }
    return 0;
  }
}

发送请求时,pcb->rcv_ann_right_edge = pcb->rcv_nxt + pcb->rcv_ann_wnd,初始化情况下,接收通告窗口的右边缘与接收窗口的右边缘对齐。

tcp接收到报文时,接收窗口左边缘右移,接收窗口大小减小,调用tcp_update_rcv_ann_wnd更新接收通告窗口,pcb->rcv_nxt右移与rcv_wnd减小正好抵消,tcp_update_rcv_ann_wnd计算的new_right_edge与之前的rcv_ann_right_edge相等,执行“TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)”比较,检查下一个期望接收的数据的序号是否超过接收通告窗口右边缘(默认情况下,接收通告窗口右边缘与接收窗口对齐,这里直接可以理解为下一个期望接收的序号是否超过接收窗口),如果超过,则接收窗口已满,接收通告窗口设置为0,否则接收通告窗口设置为当前接收窗口的大小。

 

4、发送窗口

发送端的发送窗口大小是以对端通告窗口大小为准的,发送窗口大小即为对端通告窗口大小。

发送窗口涉及snd_nxt、snd_wnd、snd_lbb、unacked、lastack等几个变量,snd_nxt为下一个可发送报文的序号(未发送/待发送的第一个报文的seqno),snd_wnd为发送窗口大小(未发送可以发送的、未被确认的数据的大小),snd_lbb为下一个缓存报文的seqno(待缓存的报文,tcp_enqueue时使用),unacked为正在发送的、没有被确认的报文队列,lastack为被确认的序号(确认已被对端接收的数据)。

4.1、报文发送/加入发送队列

lwip调用tcp_output发送unsent报文并加入unacked发送队列。

发送队列窗口大小获取(发送窗口/拥塞窗口取较小值),获取已发送未被确认的报文队列的队尾,获取未发送的报文队列。


  wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd); // 取发送窗口和拥塞窗口的较小值,发送报文的总大小不能超过发送窗口大小及拥塞窗口大小;发送窗口用于控制发送报文到对端(对端还没处理完报文,等待对端处理报文),拥塞窗口用于控制报文发送到网络(网络拥塞不能发送太多报文到网络,发送多了也只会造成网络拥塞,不能加快发送报文,也不能增加发送带宽,网络拥塞情况下,报文可能被丢弃)

  seg = pcb->unsent; // 获取未发送的报文队列

  /* useg should point to last segment on unacked queue */
  useg = pcb->unacked; // 获取已经发送未被确认的报文队列
  if (useg != NULL) {
    for (; useg->next != NULL; useg = useg->next); // useg指向最后一个已发送未被确认的报文
  }

TF_ACK_NOW报文发送,如果未发送队列为空或者报文序号超出了窗口,发送一个snd_nxt序号的报文,不经过unacked队列,不保证传达目的地。

  if (pcb->flags & TF_ACK_NOW &&
     (seg == NULL || // 没有未发送报文(不在正在发送的报文里面加ACK标志;lwip unsent队列里面会存在unacked重传的报文以及没有发送的报文,但是unacked队列里面的报文一定是被发送过的)
      ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd)) { // lastack可以理解为发送窗口的左边缘,“ntohl(seg->tcphdr->seqno) - pcb->lastack”即为当前报文之前还有多少正在发送的报文,“seg->tcphdr->seqno) - pcb->lastack + seg->len”即为包括当前报文在内,有多少数据需要发送(正在发送/未发送,当前报文未发送),如果大于窗口,那么需要创建一个ACK报文
#if LWIP_TCP_TIMESTAMPS
    if (pcb->flags & TF_TIMESTAMP)
      optlen = LWIP_TCP_OPT_LENGTH(TF_SEG_OPTS_TS);
#endif
    p = pbuf_alloc(PBUF_IP, TCP_HLEN + optlen, PBUF_RAM); // 申请一个tcp的ACK报文的内存
    if (p == NULL) {
      LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output: (ACK) could not allocate pbuf\\n"));
      return ERR_BUF;
    }
    LWIP_DEBUGF(TCP_OUTPUT_DEBUG, 
                ("tcp_output: sending ACK for %"U32_F"\\n", pcb->rcv_nxt));
    /* remove ACK flags from the PCB, as we send an empty ACK now */
    pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); // 本次将发送ACK报文,清除TF_ACK_NOW标记,清除TF_ACK_DELAY标记(收到数据时,不马上发送ACK报文,如果有收到更多数据,将多个数据一起ACK,减少发送ACK报文的次数)

    tcphdr = tcp_output_set_header(pcb, p, optlen, htonl(pcb->snd_nxt)); // tcp首部设置(seqno为pcb->snd_nxt,这里并没有对pcb->snd_nxt加1操作,下一个报文的序号仍为pcb->snd_nxt,不带数据的ACK报文不占用seqno)

    /* NB. MSS option is only sent on SYNs, so ignore it here */
#if LWIP_TCP_TIMESTAMPS
    pcb->ts_lastacksent = pcb->rcv_nxt;

    if (pcb->flags & TF_TIMESTAMP)
      tcp_build_timestamp_option(pcb, (u32_t *)(tcphdr + 1));
#endif 

#if CHECKSUM_GEN_TCP
    tcphdr->chksum = inet_chksum_pseudo(p, &(pcb->local_ip), &(pcb->remote_ip),
          IP_PROTO_TCP, p->tot_len);
#endif
#if LWIP_NETIF_HWADDRHINT
    ip_output_hinted(p, &(pcb->local_ip), &(pcb->remote_ip), pcb->ttl, pcb->tos,
        IP_PROTO_TCP, &(pcb->addr_hint));
#else /* LWIP_NETIF_HWADDRHINT*/
    ip_output(p, &(pcb->local_ip), &(pcb->remote_ip), pcb->ttl, pcb->tos,
        IP_PROTO_TCP); // 调用ip_output发送ACK报文,不经过缓存,因此ACK报文没有超时重传机制
#endif /* LWIP_NETIF_HWADDRHINT*/
    pbuf_free(p);

    return ERR_OK;
  }

检查未发送报文是否超过窗口大小(发送窗口/拥塞窗口),如果没有超过,则可以发送。

  /* data available and window allows it to be sent? */
  while (seg != NULL && // 未发送报文不为空
         ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) { // 待发送报文的数据没有超过窗口大小wnd

不能发送的报文直接返回,不需要立即发送的可延迟发送的报文延迟发送。

    if((tcp_do_output_nagle(pcb) == 0) && // 延迟发送、小报文等判断,不需要立即发送的、不能发送的报文等直接返回
      ((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)){ // 非TF_NAGLEMEMERR、TF_FIN报文可以暂缓发送
      break;

从未发送unsent队列移除当前报文,unsent指针移动到下一个未发送报文,非SYN_SENT状态的报文设置ACK标志,清除pcb的TF_ACK_DELAY、TF_ACK_NOW标志。

    pcb->unsent = seg->next;

    if (pcb->state != SYN_SENT) {
      TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
      pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); // 当前报文已经有ACK,不再需要TF_ACK_DELAY、 TF_ACK_NOW(TF_ACK_DELAY、 TF_ACK_NOW都由当前报文发送)
    }

发送报文,更新pcb->snd_nxt。

    tcp_output_segment(seg, pcb);
    snd_nxt = ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg);
    if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {
      pcb->snd_nxt = snd_nxt; // 如果当前发送的报文的下一个报文的seqno大于pcb->snd_nxt,更新pcb->snd_nxt
    }

带数据的tcp报文需要加入unacked队列里面等待应答。(需要保证数据传输到对端,如果一定时间没没有应答,需要超时重传,tcp_rexmit_rto将所有没有被确认的数据插入到unsent队列前面,再次发送超时报文)

    /* put segment on unacknowledged list if length > 0 */
    if (TCP_TCPLEN(seg) > 0) { // 报文数据长度大于0
      seg->next = NULL; // 报文的next设置为NULL(之前指向未发送队列里面的报文)
      /* unacked list is empty? */
      if (pcb->unacked == NULL) {
        pcb->unacked = seg; // unacked队列为空,当前报文即为第一个unacked报文
        useg = seg; // useg指向unacked队列
      /* unacked list is not empty? */
      } else {
        /* In the case of fast retransmit, the packet should not go to the tail
         * of the unacked queue, but rather somewhere before it. We need to check for
         * this case. -STJ Jul 27, 2004 */
        if (TCP_SEQ_LT(ntohl(seg->tcphdr->seqno), ntohl(useg->tcphdr->seqno))){ // 如果当前发送报文的序号小于unacked报文的序号,需要将当前报文按顺序插入unacked队列里面
          /* add segment to before tail of unacked list, keeping the list sorted */
          struct tcp_seg **cur_seg = &(pcb->unacked); // unacked队列头部
          while (*cur_seg &&
            TCP_SEQ_LT(ntohl((*cur_seg)->tcphdr->seqno), ntohl(seg->tcphdr->seqno))) { // cur_seg序号小于当前报文的序号
              cur_seg = &((*cur_seg)->next ); // 查找下一个unacked报文
          }
          seg->next = (*cur_seg); // 当前报文插入cur_seg之前
          (*cur_seg) = seg; // 当前报文替换cur_seg
        } else { // 当前报文插入unacked队列末尾
          /* add segment to tail of unacked list */
          useg->next = seg;
          useg = useg->next;
        }
      }
    /* do not queue empty segments on the unacked list */
    } else { // 没有数据的报文直接释放,不保证传输到对端

不考虑拥塞窗口的情况下,发送窗口有多大就能发送多少数据。

4.2、收到应答(ACK)

发送窗口除了在通告窗口代码中会更新外,其他地方都不更新;数据被确认接收时,只是更新lastack,lastack ~ lastack + snd_wnd即可理解为发送窗口,lastack ~ lastack + snd_wnd内的数据即可发送。

收到报文时,调用tcp_receive处理报文,如果带有ACK,检查ackno是否有确认unacked队列里面的报文。

检查ackno是否在unacked报文之间(ackno大于等于第一个需要被确认的报文的序号,小于下一个待发送报文的序号),在之间,那么就有unacked报文被对端确认已经接收;重置超时重传次数、超时重传时间等,计算已经确认的数据大小及下一个待确认数据的序号,dupacks清零。

    } else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)){ // ackno判断,检查是否有数据被确认接收
      /* We come here when the ACK acknowledges new data. */
      
      /* Reset the "IN Fast Retransmit" flag, since we are no longer
         in fast retransmit. Also reset the congestion window to the
         slow start threshold. */
      if (pcb->flags & TF_INFR) {
        pcb->flags &= ~TF_INFR;
        pcb->cwnd = pcb->ssthresh;
      }

      /* Reset the number of retransmissions. */
      pcb->nrtx = 0; // 超时重传次数设置为0

      /* Reset the retransmission time-out. */
      pcb->rto = (pcb->sa >> 3) + pcb->sv; // 重新设置超时重传时间

      /* Update the send buffer space. Diff between the two can never exceed 64K? */
      pcb->acked = (u16_t)(ackno - pcb->lastack); // 被确认已经接收的数据长度

      pcb->snd_buf += pcb->acked; // 发送缓存增加已经确认的数据长度

      /* Reset the fast retransmit variables. */
      pcb->dupacks = 0; // 收到非重复的ACK,dupacks清零
      pcb->lastack = ackno; // 下一个待确认的序号

拥塞窗口更新。开始时启用慢启动,拥塞窗口渐渐加大(线性增加),超过慢启动门限后,启用拥塞避免,拥塞窗口大小增加减缓(每次增加大小减少)。

      /* Update the congestion control variables (cwnd and
         ssthresh). */
      if (pcb->state >= ESTABLISHED) {
        if (pcb->cwnd < pcb->ssthresh) { // 拥塞窗口小于慢启动门限ssthresh,《TCP-IP详解卷 1:协议.pdf》 21.6 拥塞避免算法,当拥塞发生时(超时或收到重复确认) , ssthresh被设置为当前窗口大小的一半,如果是超时引起了拥塞,则cwnd被设置为1个报文段
          if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd) { // 加mss没有越界
            pcb->cwnd += pcb->mss; // 《TCP-IP详解卷 1:协议.pdf》 20.6 慢启动,每收到一个ACK拥塞窗口cwnd增加一个报文段mss大小(lwip没有按指数增加拥塞窗口)
          }
          LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: slow start cwnd %"U16_F"\\n", pcb->cwnd));
        } else { // 《TCP-IP详解卷 1:协议.pdf》 21.6 拥塞避免算法,拥塞窗口大于等于慢启动门限ssthresh,开始执行拥塞避免算法
          u16_t new_cwnd = (pcb->cwnd + pcb->mss * pcb->mss / pcb->cwnd); // 每次收到一个确认时,cwnd增加pcb->mss * pcb->mss / pcb->cwnd,cwnd在增加,pcb->mss * pcb->mss不变,因此每次收到ACK,cwnd增加越来越慢
          if (new_cwnd > pcb->cwnd) {
            pcb->cwnd = new_cwnd;
          }
          LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: congestion avoidance cwnd %"U16_F"\\n", pcb->cwnd));
        }
      }

释放unacked队列中已经被确认接收的数据,如果unacked队列为空,停止超时重传定时器,否则超时次数重置为0(unacked队列仍有数据待确认,超时重传定时器重新开始)。(lwip unsent里面可能存在已经发送过的数据,unacked超时重传会把unacked的报文放到unsent队列里面,确认时需要检查unsent队列里面是否有数据被确认接收)

      /* Remove segment from the unacknowledged list if the incoming
         ACK acknowlegdes them. */
      while (pcb->unacked != NULL &&
             TCP_SEQ_LEQ(ntohl(pcb->unacked->tcphdr->seqno) +
                         TCP_TCPLEN(pcb->unacked), ackno)) {
        LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_receive: removing %"U32_F":%"U32_F" from pcb->unacked\\n",
                                      ntohl(pcb->unacked->tcphdr->seqno),
                                      ntohl(pcb->unacked->tcphdr->seqno) +
                                      TCP_TCPLEN(pcb->unacked)));

        next = pcb->unacked;
        pcb->unacked = pcb->unacked->next;

        LWIP_DEBUGF(TCP_QLEN_DEBUG, ("tcp_receive: queuelen %"U16_F" ... ", (u16_t)pcb->snd_queuelen));
        LWIP_ASSERT("pcb->snd_queuelen >= pbuf_clen(next->p)", (pcb->snd_queuelen >= pbuf_clen(next->p)));
        pcb->snd_queuelen -= pbuf_clen(next->p);
        tcp_seg_free(next);

        LWIP_DEBUGF(TCP_QLEN_DEBUG, ("%"U16_F" (after freeing unacked)\\n", (u16_t)pcb->snd_queuelen));
        if (pcb->snd_queuelen != 0) {
          LWIP_ASSERT("tcp_receive: valid queue length", pcb->unacked != NULL ||
                      pcb->unsent != NULL);
        }
      }

      /* If there's nothing left to acknowledge, stop the retransmit
         timer, otherwise reset it to start again */
      if(pcb->unacked == NULL)
        pcb->rtime = -1;
      else
        pcb->rtime = 0;

      pcb->polltmr = 0;

接下来的代码就是之前接收数据、接收窗口更新的内容了。

以上是关于TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)的主要内容,如果未能解决你的问题,请参考以下文章

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

TCP/IP 协议图--传输层中的 TCP 和 UDP

TCP/IP中的传输层协议TCPUDP

深入理解TCP/IP传输层

TCP/IP

TCP/IP