前言
本文介绍代码在lwip的tcp_in.c
文件中,主要介绍TCP协议栈中数据的接收流程。
正文
1、一个正常的TCP数据,首先会传入到
tcp_input(struct pbuf *p, struct netif *inp)
函数,其中指针p
指向传入的数据流。
2、从数据流中获取TCP头部
tcphdr = (struct tcp_hdr *)p->payload;
TCP头部数据结构为:
3、获取TCP头部中重要信息:
tcphdr->src = lwip_ntohs(tcphdr->src);
tcphdr->dest = lwip_ntohs(tcphdr->dest);
seqno = tcphdr->seqno = lwip_ntohl(tcphdr->seqno);
ackno = tcphdr->ackno = lwip_ntohl(tcphdr->ackno);
tcphdr->wnd = lwip_ntohs(tcphdr->wnd);
flags = TCPH_FLAGS(tcphdr);
seqno
是序号,是发送方告诉接受方正在发送的报文段的序号。
ackno
是确认号,是接收方告诉发送方已经接受到的最后一个报文段序号。
flags
是6个控制位,其中的SYN与ACK在3次握手时使用如下:
客户端发起请求时,SYN为1,ACK为0。
服务器回复时,SYN为1,ACK为1。
客户端再次回复时,SYN为0,ACK为1。
ACK为0时,确认号无效(也就是ackno),建立连接后所有报文ACK置1。
flags中的FIN控制位置1,表示报文段的发送方数据已发送完毕,并要求释放运输连接。
释放TCP连接过程如下:
4、从 tcp_active_pcbs
链表中寻找对应的pcb
,寻找方式主要是看端口号和IP号是否能对应上:
if (pcb->remote_port == tcphdr->src &&
pcb->local_port == tcphdr->dest &&
ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr()) &&
ip_addr_cmp(&pcb->local_ip, ip_current_dest_addr()))
tcp_active_pcbs
链表上的pcb都是正在等待接受数据或者发送数据的。
5、将输入数据流放到输入报文段里:
inseg.next = NULL;
inseg.len = p->tot_len;
inseg.p = p;
inseg.tcphdr = tcphdr;
因为lwip每次只处理一个输入数据流,所以输入报文段inseg是一个全局变量,同时也不会出现访问冲突。
报文段的管理如下:
tcp_active_pcbs
链表上有很多活跃pcb,每个pcb有一个unsent
指针和一个unacked
指针,用于维护TCP协议中的发送窗口,unsent
指向发送窗口中还未发送的报文段,unacked
指向发送窗口中已经发送但是还没收到响应确认号的报文段。
发送窗口如下:
6、然后执行:
err = tcp_process(pcb);
进行TCP状态机处理。
TCP状态机转换过程如下:
7、一个已经建立连接的正常通信的pcb状态是ESTABLISHED
处理代码为:
case ESTABLISHED:
tcp_receive(pcb);
if (recv_flags & TF_GOT_FIN) { /* passive close */
tcp_ack_now(pcb);
pcb->state = CLOSE_WAIT;
}
break;
可以看到是调用tcp_receive()
函数对输入数据做进一步处理。这时输入数据仍然在inseg
输入报文段这个全局变量里。
8、接下来进入到tcp_receive()
函数,首先根据新发送来TCP报文段中的窗口大小更新本pcb的发送窗口大小。
pcb->snd_wnd = SND_WND_SCALE(pcb, tcphdr->wnd);
9、查看接收到的报文段中显示的报文序号是否在接收窗口内:
if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt,
pcb->rcv_nxt + pcb->rcv_wnd - 1))
pcb->rcv_nxt
表示下一个要接收的报文序号。
pcb->rcv_wnd
表示接收窗口大小。
其中pcb在初始化时,设置接收窗口大小代码如下:
pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND);
在lwipopts.h文件中设置
#define TCP_WND (2*TCP_MSS)
#define TCP_MSS (1500 - 40)
也就是窗口大小为两个报文段。
10、查看接收到的报文段序号是否就是期望的序号:
if (pcb->rcv_nxt == seqno)
如果是则更新期望报文段序号:
pcb->rcv_nxt = seqno + tcplen;
把收到的数据放到全局变量recv_data
里:
recv_data = inseg.p;
11、然后回到tcp_input()
函数
首先判断是否接受到了数据:
if (recv_data != NULL)
有数据的话将数据传给应用层:
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
然后再试试能不能发点数据出去:
tcp_output(pcb);