加载中…
个人资料
老衲五木
老衲五木
  • 博客等级:
  • 博客积分:0
  • 博客访问:352,549
  • 关注人气:330
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

TCP输入输出函数1《LwIP协议栈源码详解——TCP/IP协议的实现》

(2012-08-24 19:06:11)
标签:

lwip

tcp

字段

队列

链表

分类: 嵌入式网络那些事

这节从tcp_receive函数入手,逐步深入了解控制块各个字段的意义以及整个TCP层的运行机制,足足600行,神想吐血。源码注释的该函数功能为:检查收到的数据段是不是对已发数据段的确认,如果是,则释放相应发送缓冲中的数据;接下来,如果该数据段中有数据,应将数据挂接到控制块的接收队列上(pcb->ooseq)。如果数据段同时也是对正在进行RTT估计的数据段的确认,则RTT计算也在这个函数中进行。我晕,陷入了恶性循环。越看越难,越看越说不清,TCP的东西太多了。要讲清楚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是要发送的数据的长度,以字节为单位;flags是TCP数据段头部中的标识字段,主要用于连接建立或断开的握手;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控制块中的unsent、unacked、ooseq字段,这三个字段用于连接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上,每个控制块的三个指针unsent、unacked、ooseq连接了该连接相关的数据。unsent、unacked链表与ooseq链表上的tcp_seg结构描述数据段的方式不尽相同,从图上可知,unsent、unacked链表的tcp_seg结构dataptr和tcphdr字段都指向pbufs的数据起始位置,即TCP头部位置;而ooseq链表上的tcp_seg结构dataptr指向了TCP数据段的开始位置,tcphdr字段指向了TCP头部。且对于链表ooseq上的数据包pbuf,其payload指针也是指向TCP数据段的开始位置,而不是指向pbuf的数据开始位置。这是因为链表ooseq上的TCP数据段都是从IP层递交上来的,TCP层已经调用tcp_input函数将数据包的payload指针指向了TCP数据段的开始位置。

TCP输入输出函数1《LwIP协议栈源码详解——TCP/IP协议的实现》

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有