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
结构,还有很多不懂的地方,为啥仅仅拷贝保留了这几个字段,其他字段直接被忽略?
标签:lwip,函数,tcp,---,TCP,pcb,数据包,listen 来源: https://blog.csdn.net/qq_40390825/article/details/115700569