目录
实验平台
硬件:银杏科技GT7000双核心开发板-ARM-STM32H743XIH6,银杏科技iToolXE仿真器
软件:最新版本STM32CubeH7固件库,STM32CubeMX v6.10.0,开发板环境MDK v5.35,TCP&UDP测试工具
TCP_Server服务器
在 TCP 通讯场景中,TCP 客户端和 TCP 服务器端的角色可以看作网络传输中的两个关键节点,分别负责发起连接和处理请求。这种基于 TCP(传输控制协议)的通信方式,确保了数据的可靠性和顺序传输,使得应用程序能够在不需要关注底层网络传输细节的情况下,进行稳健的数据交换。
TCP 客户端 是负责发起通信的一方。它通过向服务器端发起连接请求,开始建立一个稳定的通信通道。在 TCP 通讯中,客户端的主要任务是根据特定的 IP 地址和端口号找到服务器,并与之建立连接。当连接建立后,客户端可以向服务器发送请求数据并接收服务器的响应。
TCP 服务器端 则是负责接收连接请求的实体。它会在特定的 IP 地址和端口号上进行监听,等待客户端的连接请求。当一个客户端发起连接时,服务器根据一定的协议规则(例如 TCP 的三次握手过程)来确认和建立连接。之后,服务器会处理来自客户端的请求,可能是发送数据、响应查询或其他应用逻辑的处理。
上一章节我们介绍了互联网到TCP相关知识,并且做了一个TCP客户端模式的实验,本章节我们来介绍下TCP的通信过程,并完成TCP作为服务器模式实验。
TCP通信过程
我们来详细解析 TCP 通信 的整个过程。TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议,它的核心在于建立连接、传输数据、断开连接这三个阶段。
为了让读者对整个过程有一个宏观的认识,下图清晰地展示了 TCP 通信的完整生命周期,即著名的三次握手、数据传输、四次挥手:

1. 建立连接:三次握手
这个过程确保了通信的双方都同意建立连接,并同步初始序列号。它解决了网络延迟导致的重复分组问题。
-
第一步:SYN
-
客户端发送一个 TCP 报文段,其中:
- SYN 标志位 置为 1,表示请求建立连接。
- 序列号 字段被设置为一个随机值 x。
-
客户端进入 SYN-SENT 状态。
-
-
第二步:SYN-ACK
-
服务器收到 SYN 报文后,如果同意连接,则会回复一个报文段,其中:
-
SYN 标志位 和 ACK 标志位 都置为 1。
-
序列号 字段被设置为一个随机值 y。
-
确认号 字段被设置为 x + 1,表示"我收到了你的序列号为 x 的包,我期望下一个从 x+1 开始的包"。
-
-
服务器进入 SYN-RCVD 状态。
-
-
第三步:ACK
-
客户端收到服务器的 SYN-ACK 报文后,会再次发送一个确认报文,其中:
-
ACK 标志位 置为 1。
-
序列号 为 x + 1(第一步的序列号+1)。
-
确认号 为 y + 1,表示"我收到了你的序列号为 y 的包,我期望下一个从 y+1 开始的包"。
-
-
客户端进入 ESTABLISHED 状态。
-
服务器收到这个 ACK 后,也进入 ESTABLISHED 状态。
-
至此,连接建立成功,双方可以开始数据传输。
2. 数据传输
在连接建立后,TCP 通过以下机制来保证数据的可靠传输:
-
序列号与确认应答
- 每个发送的字节都会被分配一个序列号。接收方在成功收到数据后,会回复一个 ACK 报文,其中的确认号指明了下一个期望接收的字节序列号。例如,发送方发送"序列号=1,数据长度=100",接收方成功接收后会回复"ACK=101"。
-
超时重传
- 发送方在发送一个数据段后会启动一个计时器。如果在规定时间内没有收到对应的 ACK 确认,就会认为数据丢失,并重新发送该数据段。
-
滑动窗口
- 为了提升效率,TCP 不是发送一个包就等待一个确认,而是采用"窗口"机制。窗口大小决定了在需要等待确认之前,可以连续发送多少数据包。这实现了流量控制,防止快的发送方淹没慢的接收方。
-
拥塞控制
- 为了防止过多的数据注入网络,导致路由器或链路过载,TCP 使用复杂的算法(如慢启动、拥塞避免、快速重传、快速恢复)来动态调整其发送速率。
3. 断开连接:四次挥手
当数据传输完毕,任何一方都可以发起关闭连接的请求。由于 TCP 连接是全双工的,每个方向必须单独关闭。
-
第一步:FIN
-
假设客户端主动关闭连接。它发送一个 TCP 报文段,其中:
-
FIN 标志位 置为 1,表示没有数据要发送了。
-
序列号 假设为 u。
-
-
客户端进入 FIN-WAIT-1 状态。
-
-
第二步:ACK
-
服务器收到 FIN 报文后,会立即回复一个 ACK 报文,其中:
-
ACK 标志位 置为 1。
-
确认号 为 u + 1。
-
-
服务器进入 CLOSE-WAIT 状态。此时,从客户端到服务器的连接已经关闭,客户端不能再发送数据,但服务器可能还有数据要发送给客户端。
-
客户端收到这个 ACK 后,进入 FIN-WAIT-2 状态。
-
-
第三步:FIN
-
当服务器也准备好关闭连接时,它会发送自己的 FIN 报文,其中:
-
FIN 标志位 和 ACK 标志位 置为 1。
-
序列号 假设为 v。
-
确认号 仍然为 u + 1。
-
-
服务器进入 LAST-ACK 状态。
-
-
第四步:ACK
-
客户端收到服务器的 FIN 报文后,会发送一个 ACK 报文作为确认,其中:
-
ACK 标志位 置为 1。
-
确认号 为 v + 1。
-
-
客户端进入 TIME-WAIT 状态,并等待一个 2MSL 的时间。
-
服务器收到这个 ACK 后,立即关闭连接,进入 CLOSED 状态。
-
客户端在等待 2MSL 时间后,也正式关闭连接,进入 CLOSED 状态。
-
TIME-WAIT 状态的作用:
确保最后的 ACK 能到达服务器:如果服务器没有收到第四个 ACK,它会重发第三个 FIN。客户端在 TIME-WAIT 状态下可以再次回复 ACK。
让本次连接的所有报文在网络中消失:防止旧的、延迟的报文段被之后新建的、具有相同四元组(源IP、源端口、目的IP、目的端口)的连接错误地接收。
STM32CubeMX生成工程
我们参考前面章节STM32H743-结合CubeMX新建HAL库MDK工程,打开CubeMX软件,重复步骤不再展示。我们来看配置MPU配置、以太网部分和Lwip部分配置如下图所示:
配置以太网。选用RMII(精简的独立于介质接口)模式。




MPU配置

LWIP配置

选择PHY芯片型号。LAN8742向下兼容LAN8720


实验代码
1. 主函数
cpp
int main(void)
{
MPU_Config();
SCB_EnableICache();
SCB_EnableDCache();
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_LWIP_Init();
MX_USART6_UART_Init();
uart6.initialize(115200);
uart6.printf("\x0c");
uart6.printf("GT7000 OK!\r\n");
HAL_GPIO_WritePin(PHYAD0_GPIO_Port,PHYAD0_Pin,GPIO_PIN_RESET);
eth_tcps_init();
while (1)
{
MX_LWIP_Process();
}
}
2. LwIP初始化
cpp
void MX_LWIP_Init(void)
{
/* IP addresses initialization */
IP_ADDRESS[0] = 192;
IP_ADDRESS[1] = 168;
IP_ADDRESS[2] = 0;
IP_ADDRESS[3] = 11;
NETMASK_ADDRESS[0] = 255;
NETMASK_ADDRESS[1] = 255;
NETMASK_ADDRESS[2] = 255;
NETMASK_ADDRESS[3] = 0;
GATEWAY_ADDRESS[0] = 192;
GATEWAY_ADDRESS[1] = 168;
GATEWAY_ADDRESS[2] = 0;
GATEWAY_ADDRESS[3] = 1;
/* Initilialize the LwIP stack without RTOS */
lwip_init();
/* IP addresses initialization without DHCP (IPv4) */
IP4_ADDR(&ipaddr, IP_ADDRESS[0], IP_ADDRESS[1], IP_ADDRESS[2], IP_ADDRESS[3]);
IP4_ADDR(&netmask, NETMASK_ADDRESS[0], NETMASK_ADDRESS[1] , NETMASK_ADDRESS[2], NETMASK_ADDRESS[3]);
IP4_ADDR(&gw, GATEWAY_ADDRESS[0], GATEWAY_ADDRESS[1], GATEWAY_ADDRESS[2], GATEWAY_ADDRESS[3]);
/* add the network interface (IPv4/IPv6) without RTOS */
netif_add(&gnetif, &ipaddr, &netmask, &gw, NULL, ðernetif_init, ðernet_input);
/* Registers the default network interface */
netif_set_default(&gnetif);
/* We must always bring the network interface up connection or not... */
netif_set_up(&gnetif);
/* Set the link callback function, this function is called on change of link status*/
netif_set_link_callback(&gnetif, ethernet_link_status_updated);
/* Create the Ethernet link handler thread */
}
3. 作为服务器端连接相关函数
cpp
//--------------------------- Include ---------------------------//
#include "eth_tcps.h"
#include "tcp.h"
#include <string.h>
//--------------------- Function Prototype ----------------------//
void eth_tcps_init(void);
err_t tcp_loopback_accept(void *arg, struct tcp_pcb *newpcb, err_t err);
err_t tcp_loopback_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
//TCP初始化
void eth_tcps_init(void)
{
struct tcp_pcb *pcb = NULL;
pcb = tcp_new(); //创建一个新的TCP协议控制块
tcp_bind(pcb, IP_ADDR_ANY, TCP_SERVER_PORT); //给TCP协议控制块绑定端口号和IP地址
pcb = tcp_listen(pcb); //将连接的状态设置为侦听
tcp_accept(pcb, tcp_loopback_accept); //指定LISTENing连接连接到其他主机时的回调函数
}
//TCP监听
err_t tcp_loopback_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{
tcp_recv(newpcb, tcp_loopback_recv); //初始化LwIP的tcp_recv回调功能
return ERR_OK;
}
//TCP接收
err_t tcp_loopback_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
if (p != NULL)
{
tcp_recved(tpcb, p->tot_len); //TCP数据接收
tcp_write(tpcb, p->payload, p->tot_len, 1); //将接收到的数据加入到发送缓冲队列中
memset(p->payload, 0, p->tot_len);
pbuf_free(p); //释放ptr
}
else if (err == ERR_OK)
{
return tcp_close(tpcb);
}
return ERR_OK;
}
实验现象
1、打开控制面板-->网络和Internet-->网络和共享中心-->更改适配器设置-->以太网属性

2、Internet协议版本4,选择使用下面的IP地址,然后更改IP地址,IP地址与代码中的设置的一致。

3、打开测试工具,点击创建连接,弹出设置端口的窗口,设置为60001

