1.TCP协议简介
TCP协议,是一种面向连接、可靠的、基于字节流的传输层通信协议。
主要就是要知道,TCP协议是需要连接才可以互发数据的,连接需要三次挥手,而断开连接需要四次挥手。
2.TCP协议报文结构
TCP协议的头部一共有20字节,左边的结构体与右边的框图示意图一一对应;src就是16位表本地端口号;dest是16位目标端口号;seqno是32位序号,用来重组TCP的分包(因为TCP不能在网络层进行分片,也就是IP协议不能分片,只能在传输层层进行分包);ackno是32位确认序号;hdrlen_rsvd_flags是16位的flag;wnd是16位窗口大小;然后是16位校验和chksum;最后是16位紧急指针urgp。
这其中,关于标志位的具体解析如下:
- URG:为1时紧急指针有效;
- ACK:为1时,确认序号有效;
- PSH:为1时,接收方应该尽快将这个报文段交给应用层;
- RST:为1时,重建连接;
- SYN:为1时,同步程序,发起一个连接;
- FIN:为1时,发送端完成任务,释放一个连接。
3.TCP转换状态
进入连接,就是通过三次握手来确认客户端和服务器连接:
三次握手,就是首先由客户端发送SYN给服务器,这是第一次握手;然后服务器就会回复SYN+ACK信号给客户端,客户端进入SYN-SENT模式,而服务器切换到LISTEN模式,这是第二次握手;第三次握手,就是客户端发送ACK信号给服务器。至此,完成三次握手,客户端和服务器均进入ESTAB-LISHED状态,可以完成数据的互发。
四次挥手,客户端和服务器都可以发起,以此来完成断联。客户端在ESTAB-LISHED状态发送FIN给服务器,客户端进入FIN-WAIT-1状态,这就是第一次挥手;然后服务器在ESTAB-LISHED状态接受FIN,回应一个ACK信号给客户端,服务器进入CLOSE-WAIT状态,这就是第二次挥手;然后服务器发送一个FIN信号给客户端,客户端进入FIN-WAIT-2状态,服务器进入LAST-ACK状态,这就是第三次挥手;最后客户端发送ACK信号给服务器,自身进入TIME-WAIT状态(2s),然后进入CLOSED状态,服务器也进入CLOSED状态,这就是第四次挥手。
lwIP中,通过一个枚举类型的tcp_state来描述以上的状态,完成TCP协议中的三次握手和四次挥手。枚举类型如下所示:
4. RAW接口相关函数
TCP控制块
TCP的控制块类似UDP,定义在tcp.h中,如下所示:
TCP的控制块如上所示,主要会用到的已经罗列在上面了;操作的TCP首部的,就是pcb控制块,协议特定的TCP_PCB_COMMON控制块,远程端口号以及标志位flags(用于判断处于什么状态,完成三次握手和四次挥手);
之后还会用到发送和接收成功的两个回调函数,以及连接成功的回调函数;轮询查阅是否有信息的函数,以及发生致命错误时的函数。
具体的就不会多看,只需要知道有这些成员,然后之后学会调用就可以了,源码整个TCP占据lwIP的一半,可以直接看计算机网络的书进行学习。
lwIP将TCP的控制块连接成为单向链表如下所示:
通过TCP_PCB_COMMON里面的next指针进行链接,最后一个就是NULL,连接之后就可以遍历完成寻找。
TCP回调函数
RAW编程接口的TCP实验需要自行实现对应的回调函数,然后讲这些回调函数注册给指定的TCP控制块:
首先,如果是发送,就会调用tcp_write进行发送,把数据挂载到缓冲之中,也就是图中的enqueue,然后通过tcp_output发送出去,最终由ip_output发送到网络层;
tcp_process完成三次握手和四次挥手,因为这些操作无需发送回应用程序,而是可以直接处理。
查看源码以及讲义,TCP实现如下:
首先,先调用tcp_connect函数进行远程服务器的连接,客户端会发送一个SYN信号,并把pcb的状态改为SYN_SENT,通过tcp_output发送出去,这就是第一次握手;
服务器这边,会调用tcp_listen函数,其就是一个宏定义,实际调用tcp_listen_with_backlog函数,这里面会调用tcp_listen_with_backlog_and_err函数,在这里面会把state改为LISTEN;然后会调用tcp_listen_input,在接受到SYN信号后,发送一个SYN|ACK的信号给客户端,同时把服务器状态改为SYN_RCVD;
客户端在tcp_process函数中,判断pcb->state是SYN_SENT,就会处理,如果接收到了SYN+ACK的信号,会把状态改为ESTABLISHED,这里相当于第二次握手;
然后调用tcp_output函数,在这里面调用tcp_output_segment发送一个应答包的ACK信号;
服务器在tcp_process中,如果在SYN_RCVD状态接收到了ACK信号,就会把自身状态改为ESTABLISHED;这时就完成了第三次握手。
tcp_output有两个定时器:快定时器250ms,用来发送ACK包;慢定时器500ms,实现了TCP协议的超时部分,是否接受到ACK包。
5. RAW接口的TCP函数
- tcp_new()
就是调用了tcp_alloc函数;这个函数里面定义了tcp_pcb的结构体pcb,然后内存池的方式memp_malloc申请内存,然后设置控制块参数,完成后返回pcb; - tcp_bind()
一个tcp_pcb结构体pcb传参进来,通过ip_addr_set这是本地IP地址,然后把本地端口号port给到pcb->local_port;
实现与之前的UDP很类似,就不再赘述。
6. RAW接口的TCPClient实验
配置流程:
- tcp_new常见一个TCP控制块:描述当前TCP的端口号、IP地址等信息;
- tcp_connect设置目标IP地址和插入TCP PCB链表:把控制块插入TCP PCB链表;
- tcp_recv注册接受回调函数:接收回调函数由用户编写;
- tcp_write发送数据:网络搭建完成,可发数据。
与UDP实验类似,首先会进入lwip_tcp_client_set_remoteip()函数,也就是配置远程IP地址,也就是PC地址,因为是DHCP配置,所以前三个IP保持一致即可,然后可以通过按键修改最后一个IP地址;
然后tcp_new申请一个新的pcb;创建成功就通过IP4_ADDR来组合IP地址,传到rmtipaddr里面,然后tcp_connect来连接到目的地址的指定端口上;
这个函数中还包括了lwip_tcp_client_connected这个回调函数;会申请一个tcp_client_struct结构体es,(里面包含了TCP客户端的状态,是否连接成功;一个tcp_pcb结构体和一个pbuf结构体);申请成功就会更新状态,如果已经调用这个函数,说明已经完成三次握手,就连接成功了;这个函数中还要调用tcp的另外四个回调函数;
tcp_arg就是把tpcb和es传入就好了;
tcp_recv是接收回调函数,把我们自己实现的lwip_tcp_client_recv传入;这个函数定了一pbuf结构体q和tcp_client_struct结构体es,还定义了err_t结构体ret_err,es接上arg参数,也就是之前的es;如果es是连接成功的状态同时p非空,就需要遍历pcb的链表,调用memcpy把pbuf的数据拷贝到g_lwip_demo_recvbuf缓冲中,然后把flag位置1表示收到数据,然后调用tcp_recved通知lwIP内核可以更新获取新的数据,最后pbuf_free释放内存;如果不是以上的情况就可以直接释放内存;
tcp_err函数传入自己实现的lwip_tcp_client_error,但是不做任何处理;
tcp_sent函数传入lwip_tcp_client_sent;其中tcp_client_struct通过arg接到之前的es,然后调用lwip_tcp_client_senddata发送数据;这个函数中,循环遍历es->p(pbuf),把整个pbuf链表通过tcp_write写入发送缓冲区,然后调用tcp_output发送出去;
tcp_poll函数传入lwip_tcp_client_poll;其中检查es的state是否是关闭状态,如果是就调用lwip_tcp_client_connection_close关闭连接;这个函数中,移除所有的回调函数(传入tcp_pcb结构体tpcb,以及NULL清楚数据),然后把flag位清零;
以上均在tcp_connect中设置,如果设置成功res返回0,进入while循环;如果按下KEY0就会调用lwip_tcp_client_usersent发送数据;这个函数中,如果es有数据,就会申请pbuf(通过pbuf_alloc),然后pbuf_take把数据拷贝到pbuf中;通过lwip_tcp_client_senddata发送出去之后,把标识位置1;最后释放pbuf内存(通过pbuf_free)。
7. RAW接口的TCP Server实验
在lwip_demo中启动整个函数:
首先定义了连个tcp_pcb的tcp控制块,一个是tcppcbnew,还有一个是tcppcbconn用于监听;然后就跟之前的实验类似,通过tcp_new创建一个新的控制块给到tcppcbnew;创建成功后,通过tcp_bind把本地IP和指定端口绑定到tcppcbnew上;绑定完成,通过tcp_listen设置tcppcbnew到监听状态,返回值给到tcppcbconn;
tcp_listen就是一个宏定义,其就是指向了tcp_listen_with_backlog函数;然后这个函数return给了tcp_listen_with_backlog_and_err函数;这个函数会通过内存池的方式memp_malloc申请内存给到tcp_pcb_listen这个tcp控制块lpcb;然后初始化这个控制块的参数,其中state状态就是LISTEN监听状态;完成初始化后会把lpcb强转成tcp_pcb类型return出去;
完成后,调用tcp_accept,初始化lwIP这个回调函数,其实现就是自行定义lwip_tcp_server_accept;(这一步就是完成开发板作为server,然后网络调试助手作为client的初始化);进入之后,定义了tcp_server_struct结构体es(包括三个参数,一个是state状态,一个是tcp_pcb来只想当前控制块,最后一个就是传输的数据pbuf);然后es调用mem_malloc申请内存;内存分配成功后,进行接收连接,把es的参数进行初始化,状态state变成ES_TCPSERVER_ACCEPTED,pcb就是传入的newpcb,pbuf暂时没有是NULL;然后设置另外四个回调函数,把newpcb传入禁区,然后标记客户端连上(通过自定义的全局flag),并设置远程IP地址;
同样的,tcp_recv就是调用lwip_tcp_server_recv函数;如果是空数据就关闭连接(设置es的state为ES_TCPSERVER_CLOSING);如果err说明有错误,就直接释放pbuf内存;如果es连接成功且pbuf有数据,就会通过memset,然后遍历pbuf的链表进行数据拷贝,然后标记收到数据(全局的flag),设置远程IP的地址,并调用tcp_recved读取接收数据,然后释放掉当前的pbuf内存(数据缓冲区已经传输完成,就可以释放掉了);
tcp_err就是调用lwip_tcp_server_err来进行连接过程中的错误处理;这里的操作就是判断arg参数是否为空,不为空就直接mem_free释放掉arg的内存;
tcp_poll调用lwip_tcp_server_poll函数注册轮询;就是新建一个tcp_server_struct结构体es然后把传入的arg强转成该类型并赋值给es;然后不断轮询if判断es的state是否是需要关闭的状态,如果是就调用lwip_tcp_connection_close进行关闭连接;这个函数的关闭操作就是调用tcp_close,然后所有的五个回调函数全部给NULL,再mem_free释放掉es的内存,把对应的flag标志位清零;
最后是发送的回调函数tcp_sent,调用lwip_tcp_server_sent函数;这里面就是判断es是否有数据(pbuf是否存在),有数据就调用lwip_tcp_server_senddata进行发送;这个函数就是在有数据的情况下遍历pbuf链表,然后通过tcp_write把pbuf加入到发送缓冲队列,然后把当前的pbuf释放掉并tcp_recved,最后通过tcp_output发送出去。
以上任务全部完成后,判断是否成功完成,成功完成就会进入死循环进行任务执行;其中发送数据调用了lwip_tcp_server_usersent来发送数据;这个函数通过tcp_server_struct结构体es,es->p通过pbuf_alloc申请内存,然后pbuf_take把数据拷贝到申请的pbuf之中,最后调用lwip_tcp_server_senddata发送数据。
总结
这一章没有将太多TCP实现的源码,因为实在是太多了……把我文章里涉及到的几个回调函数,以及三次握手四次挥手理解一下就OK了。
主要就是要自己在raw接口中实现五个回调函数,然后就能完成TCP的客户端的搭建,与PC完成通讯。
TCP的Server实验很类似,都是实现五个回调函数,然后进行通讯。