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

TCP建立流程《LwIP协议栈源码详解——TCP/IP协议的实现》

(2012-08-23 20:43:05)
标签:

lwip

tcp

建立流程

链表

字段

分类: 嵌入式网络那些事

前面说了一大堆虚无缥缈的东西,而且大部分都借鉴于标准协议里面的内容,让人有点晕!这一节我们就看看如何在我们的LWIP上实现一个http服务器的过程,结合连接建立过程来理解TCP状态转换图和TCP控制块中各个字段的意义。这里先讲解一些与TCP相关的最基础的函数,至于是怎样将这些函数合理高效的组织起来以方便实际应用,这里先不涉及。

第一个函数是tcp_new函数,该函数简单的调用tcp_alloc函数为一个连接分配一个TCP控制块tcp_pcb。tcp_alloc函数首先为新的tcp_pcb分配内存空间,若内存空间不够,则函数会释放处于TIME-WAIT状态的TCP或者优先级更低的PCB(在PCB控制块的prio字段)以为新的PCB分配空间。当内存空间成功分配后,函数会初始化新的tcp_pcb的内容,源码如下:

if (pcb != NULL) {

memset(pcb, 0, sizeof(struct tcp_pcb));  // 清0所有字段的值

pcb->prio = TCP_PRIO_NORMAL; // 设置PCB的优先级为64,优先级在1~127之间

pcb->snd_buf = TCP_SND_BUF; // TCP发送数据缓冲区剩余大小

pcb->snd_queuelen = 0;   // 发送缓冲中的数据包pbuf个数

pcb->rcv_wnd = TCP_WND;  // 接收窗口大小

pcb->rcv_ann_wnd = TCP_WND; // 通告窗口大小

pcb->tos = 0;  // IP报头部TOS字段

pcb->ttl = TCP_TTL; // IP报头部TTL字段

pcb->mss = (TCP_MSS > 536) ? 536 : TCP_MSS; // 设置最大段大小,不能超过536字节

pcb->rto = 3000 / TCP_SLOW_INTERVAL;// 初始超时时间值,为6s

pcb->sa = 0;    // 估计出的RTT平均值??

pcb->sv = 3000 / TCP_SLOW_INTERVAL;  // 估计出的RTT方差??

pcb->rtime = -1;  //重传定时器,当该值大于rto时则重传发生

pcb->cwnd = 1;  // 阻塞窗口

iss = tcp_next_iss();  // iss为一个临时变量,保存该连接的初始数据序列号

pcb->snd_wl2 = iss;  // 上一个窗口更新时收到的ACK号

pcb->snd_nxt = iss;  // 下一个将要发送的数据编号

pcb->snd_max = iss;  // 发送了的最大数据编号

pcb->lastack = iss;   // 上一个ACK编号

pcb->snd_lbb = iss;  // 下一个将要缓冲的数据编号

 

pcb->tmr = tcp_ticks;  // tcp_ticks是一个全局变量,记录了当前协议时钟滴答

pcb->polltmr = 0;   // 未解???

#if LWIP_CALLBACK_API

pcb->recv = tcp_recv_null;  // 注册默认的接收回调函数

#endif

pcb->keep_idle  = TCP_KEEPIDLE_DEFAULT;

#if LWIP_TCP_KEEPALIVE    //保活定时器相关设置。。未解??

pcb->keep_intvl = TCP_KEEPINTVL_DEFAULT;

pcb->keep_cnt   = TCP_KEEPCNT_DEFAULT;

#endif

pcb->keep_cnt_sent = 0;

}

上面有很多晕的地方,这些将在后续一一讲解。PCB中的还有一些函数字段如发送、接收函数等是在具体应用中初始化的。

当一个新建的PCB被初始化好后,tcp_bind函数将会被调用,用来将IP地址及端口号与该TCP控制块绑定。该函数的输入参数很明显有三个,即TCP控制块、IP地址和端口号。tcp_bind函数的工作也很简单,就是将两个参数的值赋值给TCP控制块中local_ip和local_port的字段。但这里有个前提,就是这个对没有被使用。所以,函数需要先遍历各个pcb链表,以保证这个对没有被其他PCB使用,这里的pcb链表有好几种:处于侦听状态的链表tcp_listen_pcbs、处于稳定状态的链表tcp_active_pcbs、已经绑定完毕的PCB链表tcp_bound_pcbs、处于TIME-WAIT状态的PCB链表tcp_tw_pcbs。如果遍历完这些链表后,都没有找到相应的对,则说明该对可用,则可进行上面说的赋值操作,最后,函数将这个PCB加入绑定完毕的PCB链表tcp_bound_pcbs。

上面一共说了四种PCB链表,现在看看它们各自用来链接了处于哪种状态的PCB控制块。tcp_bound_pcbs链表用来连接新创建的控制块,可以认为新建的控制块处于closed状态。tcp_listen_pcbs链表用来连接处于LISTEN状态的控制块,tcp_tw_pcbs链表用来连接处于TIME_WAIT状态的控制块,tcp_active_pcbs链表用来连接处于TCP状态转换图中其他所有状态的控制块。

从状态转换图可以知,服务器端需进入LISTEN状态等待客户端的连接。因此,服务器端此时需要调用函数tcp_listen使相应TCP控制块进入LISTEN状态。可以直接的想象,要把一个控制块置为LISTEN状态很简单,先将其从tcp_bound_pcbs链表上取下来,将其state字段置为LISTEN,最后再将该PCB挂接到链表tcp_listen_pcbs上。但事实上,LWIP的实现有一定的区别,它引入了一个叫tcp_pcb_listen的结构,该结构与tcp_pcb结构相近,但是去掉了其中在LISTEN阶段用不到的传输控制字段,这样tcp_pcb_listen的结构更小,更可以节省内存空间。所以,其实tcp_listen是这样做的,先申请一个tcp_pcb_listen的结构,然后将tcp_pcb参数中的有用字段拷贝进来,然后将这个tcp_pcb_listen的结构挂接到链表tcp_listen_pcbs上。

到这里服务器就等待客户端发送来的SYN数据包进行连接了,要等待外面的数据包,这就和以前讨论过的ip_input函数相关了,ip_input函数会判断IP包头部的协议字段,并把TCP数数据包通过tcp_input函数传递到TCP层。SYN数据包当然是TCP层数据包,当然也要经过tcp_input函数进行处理并递交上层,现在就来看看tcp_input函数。

tcp_input函数开始会对IP层递交进来的数据包进行一些基础操作,如移动数据包的payload指针、丢弃广播或多播数据包、数据和校验、提取TCP头部各个字段的值等等。接下来,函数根据接收到的TCP包的对遍历tcp_active_pcbs链表,寻找匹配的PCB控制块,若找到,则调用tcp_process函数对该数据包进行处理。若找不到,则再分别到tcp_tw_pcbs链表和tcp_listen_pcbs中寻找,找到则调用各自的数据包处理函数tcp_timewait_input和tcp_listen_input对数据包进行处理,若到这里都还未找到匹配的TCP控制块,则tcp_input函数会调用函数tcp_rst向源主机发送一个TCP复位数据包。

这里我们的TCP控制块处于LISTEN状态,连接在tcp_listen_pcbs上,正在等待一个SYN数据包。因此,当等到该数据包后,函数tcp_listen_input应该被调用。从状态转换图上可以看出,处于LISTEN状态的TCP控制块只能响应SYN握手包,所以,tcp_listen_input函数对非SYN握手包返回一个TCP复位数据包,若一个数据包不是SYN包,则其TCP包头中的ACK字段通常会被置1,所以tcp_listen_input函数是通过检验该位来实现的。接下来,函数通过验证SYN位来确认该包是否为SYN握手包。若是,则需要新建一个tcp_pcb结构,因为处于tcp_listen_pcbs上的控制块结构是tcp_pcb_listen结构的,而其他链表上的控制块结构是tcp_pcb结构的,所以这里新建一个tcp_pcb结构,并将相应tcp_pcb_listen结构拷贝至其中,同时在tcp_active_pcbs链表中添加这个新的tcp_pcb结构。这样新的TCP控制块就处在tcp_active_pcbs中了,注意此时的这个tcp_pcb结构的state字段应该设置为SYN_RCVD,表示进入了收到SYN状态。注意tcp_listen_pcbs链表中的这个tcp_pcb_listen结构还一直存在,它并不会被删除,以等待其他客户端的连接,服务器正是需要这样的功能。

到这里,函数tcp_listen_input还没完。它应该从收到的SYN数据报中提取TCP头部中选项字段的值,并设置自己的TCP控制块。这里要被调到用的函数叫tcp_parseopt,它目前仅能够做的是提取选项中的MSS(最长报文大小)字段,在LWIP以后的更高版本中,该函数将被扩充,以支持更多的TCP选项。此后,函数还可以调用tcp_eff_send_mss来设置控制块中mss字段的值,该函数可直译为“有效发送最长报文大小”,所谓有效,就是指收到SYN数据包中的MSS值不能大于我的硬件支持的最大发送报文长度,即硬件的MTU。因此当收到的MSS值更大时,设置控制块中mss字段值会被设置为MTU,而不是MSS。

最后,函数需要向源端返回一个带SYN和ACK标志的握手数据包,并可以向源端通告自己的MSS大小。发送数据包是通过tcp_enqueue和tcp_output函数共同完成的。关于数据包的发送,将在以后介绍。

最最后,来看看函数tcp_listen_input内部的关键源代码部分,这几行代码涉及到TCP控制块内部各个字段值的设置,其中很重要的就是滑动窗口相关的字段。

ip_addr_set(&(npcb->local_ip), &(iphdr->dest)); //复制本地IP地址

npcb->local_port = pcb->local_port;  //复制本地端口

ip_addr_set(&(npcb->remote_ip), &(iphdr->src)); //复制源IP地址

npcb->remote_port = tcphdr->src;  //复制源端口

npcb->state = SYN_RCVD;  // 设置TCP状态

npcb->rcv_nxt = seqno + 1;  // 期望接收到的下一个字节序号

npcb->snd_wnd = tcphdr->wnd; // 设置发送窗口大小

npcb->ssthresh = npcb->snd_wnd;  //快速启动阈值设为和发送窗口大小相同??

npcb->snd_wl1 = seqno - 1;//该字段??

npcb->callback_arg = pcb->callback_arg; //该字段??

#if LWIP_CALLBACK_API

npcb->accept = pcb->accept;   //接收回调函数

#endif

其中npcb表示新建的tcp_pcb结构,还有很多不懂的地方,为啥仅仅拷贝保留了这几个字段,其他字段直接被忽略?


0

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

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

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

新浪公司 版权所有