lwip源码分析之 TCP协议 数据输出(二)

一,简介

上一章提到的tcp_write()函数的作用是将数据插入unsent队列,其并无真正的把数据交给ip层发送。tco_output()的功能就是将unsent队列中的tcp报文交给ip层发送。

一般情况下,tcp_output()函数会在500ms定时任务中被循环调用。所以当用户使用tcp_write()将数据写入unsent队列后,会在定时任务中将数据传递给ip层发送。

二,源码分析

首先需要先判断能不能发送unsent队列,若当前的PCB正在接收数据或者当前发送窗口小于要发送的报文长度,都不能发送数据。代码如下:

//发送控制块中的所有unsents报文段
err_t
tcp_output(struct tcp_pcb *pcb)
{
  struct tcp_seg *seg, *useg;//seg:要发送的报文段,useg:未确认的报文段
  u32_t wnd, snd_nxt; //wnd:真实发送窗口,snd_nxt:下一个发送的序号
  err_t err;
  struct netif *netif;  //发送的网络接口

  LWIP_ASSERT("don't call tcp_output for listen-pcbs",
    pcb->state != LISTEN);

  //如果该tcp正在接收数据,则不发送
  if (tcp_input_pcb == pcb) {
    return ERR_OK;
  }
  //真实发送窗口是发送窗口和拥塞窗口的最小
  wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd);

  //取第一个报文段
  seg = pcb->unsent;

  //如果tcp使用TF_ACK_NOW标志,或者本地不能发送数据(unsent为null或者当前窗口无法发送seg),则发送一个不带任何数据的ack
  if (pcb->flags & TF_ACK_NOW &&
     (seg == NULL ||
      lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd)) {
     return tcp_send_empty_ack(pcb);
  }

  //将useg指向unacked队尾
  useg = pcb->unacked;
  if (useg != NULL) {
    for (; useg->next != NULL; useg = useg->next);
  }
  //找到最佳的网络接口
  netif = ip_route(&pcb->local_ip, &pcb->remote_ip);
  if (netif == NULL) {
    return ERR_RTE;
  }

  //检查本地ip地址
  if (ip_addr_isany(&pcb->local_ip)) {
    const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, &pcb->remote_ip);
    if (local_ip == NULL) {
      return ERR_RTE;
    }
    ip_addr_copy(pcb->local_ip, *local_ip);
  }
   //发送窗口太小而不能发送当前报文,等待对方接收窗口变大,所以启动零窗口探测,坚持定时器开启
  if (seg != NULL &&
      lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd &&
      wnd > 0 && wnd == pcb->snd_wnd && pcb->unacked == NULL) {
    /* Start the persist timer */
    //启动坚持定时器
    if (pcb->persist_backoff == 0) {
      pcb->persist_cnt = 0;
      pcb->persist_backoff = 1;
    }
    //由于发送窗口太小,函数实际上无输出
    goto output_done;
  }

接下来就是遍历unsent队列,将tcp报文发送,直到发送窗口不满足发送调节

  while (seg != NULL &&
         lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) {
    LWIP_ASSERT("RST not expected here!",
                (TCPH_FLAGS(seg->tcphdr) & TCP_RST) == 0);

    //如果nagle算法有效或者缓存有错误(这是由write导致的内存错误),停止发送
    if ((tcp_do_output_nagle(pcb) == 0) &&
      ((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
      break;
    }

    //!SYN_SENT状态下发送的报文的ack位无效
    if (pcb->state != SYN_SENT) {
      TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
    }

    //发送一个tcp报文
    err = tcp_output_segment(seg, pcb, netif);
    if (err != ERR_OK) {
      /* segment could not be sent, for whatever reason */
      pcb->flags |= TF_NAGLEMEMERR;
      return err;
    }
    pcb->unsent = seg->next;  //队列头部已经发送,更新
    //!SYN_SENT状态下发送的报文的ack位无效
    if (pcb->state != SYN_SENT) {
      pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); //清除应答标志位
    }
    snd_nxt = lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg); //计算下一个要发送的序号

    //如果pcb->snd_nxt比snd_nxt小,则更新下一个要发送的序号
    if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {
      pcb->snd_nxt = snd_nxt;
    }

    //将已发送报文插入unacked队列
    if (TCP_TCPLEN(seg) > 0) {
      seg->next = NULL;

      //unacked为空,seg则放入队首
      if (pcb->unacked == NULL) {
        pcb->unacked = seg;
        useg = seg;
      } else {
        //unacked队列的排队是小号在前,大号在后的模式
        //若seg<useg则在循环中中找到合适的位置
        if (TCP_SEQ_LT(lwip_ntohl(seg->tcphdr->seqno), lwip_ntohl(useg->tcphdr->seqno))) {

          //如果当前报文序号比较低,则查找适合的位置插入,序号越大越靠后
          struct tcp_seg **cur_seg = &(pcb->unacked); 
          //找到cur_seg>seg的的报文,并在该报文前插入seg
          while (*cur_seg &&
            TCP_SEQ_LT(lwip_ntohl((*cur_seg)->tcphdr->seqno), lwip_ntohl(seg->tcphdr->seqno))) {
              cur_seg = &((*cur_seg)->next );
          }
          seg->next = (*cur_seg);
          (*cur_seg) = seg;
        } else {  //
          
          //seg>useg,seg插入unacked队尾
          useg->next = seg;
          useg = useg->next;  //更新unacked
        }
      }

    } else {
      //对于空的报文段直接删除不需要插入unacked
      tcp_seg_free(seg);
    }
    seg = pcb->unsent;  //发送下一个报文
  }

以上代码中,tcp_output_segment()是真正将报文传递给ip层的函数,该函数主要做的是,填充即将发送的tcp报文首部的通告窗口,确认序号,mss以及校验和。同时开启PCB的超时重传和rtt估算,最后移动payload指针到首部,并将报文的pbuf传递给ip层。

//通过调用ip层发送tcp报文
static err_t
tcp_output_segment(struct tcp_seg *seg, struct tcp_pcb *pcb, struct netif *netif)
{
  err_t err;
  u16_t len;
  u32_t *opts;

  //TODO ref为1才能发送
  if (seg->p->ref != 1) {
    return ERR_OK;
  }

  //填充tcp首部确认号字段
  seg->tcphdr->ackno = lwip_htonl(pcb->rcv_nxt);

  //填充tcp首部通告窗口
  seg->tcphdr->wnd = lwip_htons(TCPWND_MIN16(RCV_WND_SCALE(pcb, pcb->rcv_ann_wnd)));
  
  //pcb通告接收窗口右边界=下一个接收序号+通告接收窗口长度
  pcb->rcv_ann_right_edge = pcb->rcv_nxt + pcb->rcv_ann_wnd;
  
  opts = (u32_t *)(void *)(seg->tcphdr + 1);  //选项字段

  //填充mss选项
  if (seg->flags & TF_SEG_OPTS_MSS) {
    u16_t mss;
    mss = tcp_eff_send_mss(TCP_MSS, &pcb->local_ip, &pcb->remote_ip); //针对ip路径计算mss大小
    mss = TCP_MSS;
    *opts = TCP_BUILD_MSS_OPTION(mss);  //构建适用于tcp首部的mss
    opts += 1;  //opts移动
  }

  //如果超时重传定时器没开就给老资开
  if (pcb->rtime < 0) {
    pcb->rtime = 0;
  }
  //如果该tcp的rtt计时器关闭了则开启rtt估测
  if (pcb->rttest == 0) {
    pcb->rttest = tcp_ticks;  //记录当前时间
    pcb->rtseq = lwip_ntohl(seg->tcphdr->seqno);  //被估算的报文段的编号

    LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_output_segment: rtseq %"U32_F"\n", pcb->rtseq));
  }
  LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output_segment: %"U32_F":%"U32_F"\n",
          lwip_htonl(seg->tcphdr->seqno), lwip_htonl(seg->tcphdr->seqno) +
          seg->len));

  len = (u16_t)((u8_t *)seg->tcphdr - (u8_t *)seg->p->payload); //计算首部占用的字节空间

  //如果首部为0那肯定有问题
  if (len == 0) {
    MIB2_STATS_INC(mib2.tcpoutsegs);
  }

  //pbuf的len指的是tcp数据的长度
  seg->p->len -= len;
  seg->p->tot_len -= len;

  //移动payload到tcp首部,因为需要传递给ip层
  seg->p->payload = seg->tcphdr;

  seg->tcphdr->chksum = 0;

   //计算tcp校验和
  seg->tcphdr->chksum = ip_chksum_pseudo(seg->p, IP_PROTO_TCP,
      seg->p->tot_len, &pcb->local_ip, &pcb->remote_ip);
  
  TCP_STATS_INC(tcp.xmit);
  
  NETIF_SET_HWADDRHINT(netif, &(pcb->addr_hint));

  //调用ip层发送该报文的pbuf
  err = ip_output_if(seg->p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl,
    pcb->tos, IP_PROTO_TCP, netif);
  NETIF_SET_HWADDRHINT(netif, NULL);
  return err;
}

ip_output_if()函数就是将tcp报文传递给ip层,tcp报文传递给ip层时数据变化如下,tcp_hdr中被填入相应的参数,payload移动到tcp_hdr
在这里插入图片描述

三,小结

tcp_output();将unsent队列的报文填充完整,然后交给ip层,实现数据的发送
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值