lwip --- (十八)TCP输入输出函数1

  这节从tcp_receive函数入手,逐步深入了解控制块各个字段的意义以及整个TCP层的运行机制,足足600行。源码注释的该函数功能为:检查收到的数据段是不是对已发数据段的确认,如果是,则释放相应发送缓冲中的数据;接下来,如果该数据段中有数据,应将数据挂接到控制块的接收队列上(pcb->ooseq)。如果数据段同时也是对正在进行RTT估计的数据段的确认,则RTT计算也在这个函数中进行
  要讲清楚tcp_receive还得说清楚tcp_enqueue。源码注释对该函数的功能描述很简单:将数据包或者连接的控制握手包放到tcp控制块的发送队列上。这个函数的原型为:

err_t
tcp_enqueue( struct tcp_pcb *pcb, void *arg, u16_t len,u8_t flags,
			 u8_t apiflags,u8_t *optdata, u8_t optlen )

  其中有几个重要的输入参数:
  pcb是相应连接的TCP控制块
  arg是要发送的数据的指针
  len是要发送的数据的长度,以字节为单位;
  flagsTCP数据段头部中的标识字段,主要用于连接建立或断开的握手;
  apiflags表示要对该数据段做的操作,包括是否拷贝数据、是否设置PUSH标志;
  optdata表示TCP头部中的选项字段的值
  optlen表示选项字段的长度

  tcp_enqueue首先确认要发送的数据长度len是否小于当前连接能用的数据发送缓冲区大小,即pcb->snd_buf,若缓冲区不够,则不会对该数据进行任何处理(其实这个缓冲区并不存在,只是用snd_buf标识出连接还能缓存的数据量)。
  接着,将要发送的数据段的序号字段设置为pcb->snd_lbb,然后判断pcb->snd_queuelen值是否超过了所允许挂接的数据包的上限值TCP_SND_QUEUELEN,如果超过了该上限值,则函数也不会对这个要发送的数据段进行处理。
  接下来tcp_enqueue函数会将数据组装成为tcp_seg类型的数据段,根据数据长度的大小不同,可能需要几个tcp_seg类型结构才能描述完所有的数据,每个数据段中的TCP头部部分字段值要在这里都要被设置,包括数据序号、标志字段。
  最后,所有创建好的tcp_seg类型结构都是连接在queue队列上的,queue是函数的一个临时变量。
  接下来,函数tcp_enqueue需要将queue队列上的数据段挂接到TCP控制块的unsent队列上,这里又有好几种情况,即unsent队列是否为空的情况,若为空,则直接挂接,若不为空,则需要将queue挂接在unsent队列的最后一个tcp_seg之后,如果挂接点处相邻两个tcp_seg所包含的数据大小小于最长发送段大小pcb->mss,且相邻的两个段都不是FIN包或SYN包,则需要将两个段合并为一个段。
  最后,函数需要调整TCP控制块中的相关字段的值,这点也是我最关心的地方,


if ((flags & TCP_SYN) || (flags & TCP_FIN)) {
    
      //发送SYN或FIN包被认为数据长度为1
	++len;
}

if (flags & TCP_FIN) {
    
                             // 若为FIN包,则设置flags字段为相应值
	pcb->flags |= TF_FIN;
}

pcb->snd_lbb += len;                           // 下一个要被缓冲数据的序号,注意与snd_nxt不同
pcb->snd_buf -= len;                           // 减小空闲的发送缓冲数,注意这个缓冲区并不是真正存在的
pcb->snd_queuelen = queuelen;                  // 未发送队列中的pbuf个数

  因为在看滑动窗口时怎样实现的时候,这些字段是非常关键的。

  讲了tcp_enqueue函数,又不得不讲讲tcp_output函数。tcp_output函数有个唯一的参数,即某个链接的TCP控制块指针pcb,函数把该控制块的unsent队列上数据段发送出去或直接发送一个ACK数据段。如果调用该函数时,控制块的flags字段设置了TF_ACK_NOW标志,则函数必须马上发出去一个带有ACK标志。因此,如果此时unsent队列中无数据发送或者发送窗口此时不允许发送数据,则函数需要发出去一个不含任何数据的ACK数据报。当没有TF_ACK_NOW置位,或者TF_ACK_NOW置位但该ACK能和数据段一起发送出去时,则此时函数会取下unsent队列上的数据段发送出去(这里先暂时不考虑nagle算法)。发送一个具体的数据段是通过调用函数tcp_output_segment实现的,这个函数主要是填充待发送数据段的TCP头部中的确认序号为pcb->rcv_nxt,通告窗口大小为pcb->rcv_ann_wnd,校验和字段,最后tcp_output_segment将数据包递交给IP层发送。当然,tcp_output_segment还有许多其他操作,这里我们先不关心。

  好了,还是回到tcp_output这条正道上来,数据段被发送出去后,这个函数还需要设置控制块相关字段的值。这里我最关心的还是与滑动窗密切相关的字段,

pcb->snd_nxt = ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg); // 下一个要发送的字节序号

if (TCP_SEQ_LT(pcb->snd_max, pcb->snd_nxt)) {
    
    
	pcb->snd_max = pcb->snd_nxt;                            // 最大发送序号
}

  接下来,函数将发送出去的这个段挂接在控制块unacked链表上,以便后续的重发等操作。到这里,unsent队列上的第一个数据段就处理完了,tcp_output函数还会依次按照上述方法处理unsent队列上剩下的各个数据段,直到数据被全部发送出去或者发送窗口被填满。

  现在可以来看看tcp_receive这个庞然大物了。这个函数简单的来说就是操作TCP控制块中的unsentunackedooseq字段,这三个字段用于连接TCP的各种数据段。unsent用于连接还未被发送出去的数据段、unacked用于连接已经发送出去但是还未被确认的数据段、ooseq用于连接接收到的无序的数据段。这个三个字段都是tcp_seg类型的指针,结构体tcp_seg用于描述一个TCP数据段,源代码如下:

struct tcp_seg {
    
    
	struct tcp_seg *next;    // 用来建立链表的指针
	struct pbuf *p;          // 数据段pbuf指针
	void *dataptr;           // 指向TCP段的数据区
	u16_t len;               // TCP段的数据长度
	struct tcp_hdr *tcphdr;  // 指向TCP头部
};

  掌握这个结构体很重要,这是理解tcp_receive函数的关键。从下面的图中可以看出,tcp_seg结构时怎样描述一个TCP数据段的。能够进行数据段收发的TCP控制块都被连接在链表tcp_active_pcbs上,每个控制块的三个指针unsentunackedooseq连接了该连接相关的数据。unsentunacked链表与ooseq链表上的tcp_seg结构描述数据段的方式不尽相同,从图上可知,unsentunacked链表的tcp_seg结构dataptrtcphdr字段都指向pbufs的数据起始位置,即TCP头部位置;而ooseq链表上的tcp_seg结构dataptr指向了TCP数据段的开始位置,tcphdr字段指向了TCP头部。且对于链表ooseq上的数据包pbuf,其payload指针也是指向TCP数据段的开始位置,而不是指向pbuf的数据开始位置。这是因为链表ooseq上的TCP数据段都是从IP层递交上来的,TCP层已经调用tcp_input函数将数据包的payload指针指向了TCP数据段的开始位置。
在这里插入图片描述

Guess you like

Origin blog.csdn.net/qq_40390825/article/details/117066697