lwip --- (十六)TCP建立流程

  这一节我们就看看如何在我们的LWIP上实现一个http服务器的过程,结合连接建立过程来理解TCP状态转换图TCP控制块中各个字段的意义。这里先讲解一些与TCP相关的最基础的函数,至于是怎样将这些函数合理高效的组织起来以方便实际应用,这里先不涉及。

  第一个函数是tcp_new函数,该函数简单的调用tcp_alloc函数为一个连接分配一个TCP控制块tcp_pcbtcp_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_iplocal_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_inputtcp_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_enqueuetcp_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结构,还有很多不懂的地方,为啥仅仅拷贝保留了这几个字段,其他字段直接被忽略?

Guess you like

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