TCP建立流程《LwIP协议栈源码详解——TCP/IP协议的实现》
(2012-08-23 20:43:05)
标签:
lwiptcp建立流程链表字段 |
分类: 嵌入式网络那些事 |
前面说了一大堆虚无缥缈的东西,而且大部分都借鉴于标准协议里面的内容,让人有点晕!这一节我们就看看如何在我们的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));
pcb->prio = TCP_PRIO_NORMAL; // 设置PCB的优先级为64,优先级在1~127之间
pcb->snd_buf = TCP_SND_BUF; // TCP发送数据缓冲区剩余大小
pcb->snd_queuelen =
0;
pcb->rcv_wnd =
TCP_WND;
pcb->rcv_ann_wnd = TCP_WND; // 通告窗口大小
pcb->tos = 0;
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;
pcb->sv = 3000 /
TCP_SLOW_INTERVAL;
pcb->rtime = -1;
pcb->cwnd = 1;
iss = tcp_next_iss();
pcb->snd_wl2 =
iss;
pcb->snd_nxt =
iss;
pcb->snd_max =
iss;
pcb->lastack =
iss;
pcb->snd_lbb =
iss;
pcb->tmr =
tcp_ticks;
pcb->polltmr =
0;
#if LWIP_CALLBACK_API
pcb->recv =
tcp_recv_null;
#endif
pcb->keep_idle
#if
LWIP_TCP_KEEPALIVE
pcb->keep_intvl = TCP_KEEPINTVL_DEFAULT;
pcb->keep_cnt
#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;
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结构,还有很多不懂的地方,为啥仅仅拷贝保留了这几个字段,其他字段直接被忽略?