项目中遇到需要在STM32F767上创建一个TCP Server,并且允许偶尔有多个客户端同时连接。之前一直使用STM32CubeMX自动创建freeRTOS线程,也只使用过TCP Client模式,这次开发就遇到了问题,归根结底是自己对freeRTOS和LWIP不是太了解,为此利用周末时间专门研究了一下。
这次问题参考了《野火LwIP 应用开发实战指南:基于STM32》以及《嵌入式网络那些事LwIP协议深度剖析与实战演练》
一、实现TCP Server的并发处理的总体思想:
1、利用一个TCPServer主线程监控客户端接入,如果有客户端接入到服务器,那么分配连接句柄给第二个子任务处理数据接收,同时启动子任务线程。
2、子任务读取数据,如果出错或者需要关闭连接的时候,关闭连接,并osThreadExit()退出线程。
3、事先准备好允许的最大数量的存储空间,用来存储连接句柄和读数据的buffer等。
二、代码实现
本次demo允许最大三个线程。
1、准备好1各主线程和三个子线程的句柄和参数,这一部分还是参照STM32CubeMX自动创建的代码复制然后修改后来的,没办法,还是太懒。。。
// 定义对多连接的个数
#define DEF_TCP_SERVER_LISTEN_MAX 3
// TCP Server主任务句柄及参数
osThreadId_t TCPServerTaskHandle;
const osThreadAttr_t TCPServerTask_attributes = {
.name = "TCPServerTask",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityBelowNormal,
};
// 子任务1句柄及参数
osThreadId_t TCPClient1TaskHandle;
osThreadAttr_t TCPClient1Task_attributes = {
.name = "TCPClient1Task",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityLow,
};
// 子任务2句柄及参数
osThreadId_t TCPClient2TaskHandle;
osThreadAttr_t TCPClient2Task_attributes = {
.name = "TCPClient2Task",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityLow,
};
// 子任务3句柄及参数
osThreadId_t TCPClient3TaskHandle;
osThreadAttr_t TCPClient3Task_attributes = {
.name = "TCPClient3Task",
.stack_size = 256 * 4,
.priority = (osPriority_t) osPriorityLow,
};
2、准备好子线程的数据结构,这个数据结构里主要包含对应的线程函数、连接句柄、读数据缓存等。
// 接收数据的最大长度
#define DEF_TCP_RECV_MAX_LEN 256
// 各任务的接收buffer
typedef struct
{
osThreadId_t* pTaskHandle; // 任务句柄
osThreadAttr_t* pTaskAttributes; // 任务参数
void(*pTaskLoop)(void *argument); // 任务循环函数
int connectHandle; // 链接句柄
int recvLen; // 接收数据的长度
uint8_t recvBuf[DEF_TCP_RECV_MAX_LEN]; // 接收数据buffer
}t_TCPServerConnectStruct;
// 处理函数
static void Task_TCPServer_StartClient1Task(void *argument);
static void Task_TCPServer_StartClient2Task(void *argument);
static void Task_TCPServer_StartClient3Task(void *argument);
static t_TCPServerConnectStruct m_TCPServerConnectStruct[DEF_TCP_SERVER_LISTEN_MAX] = {
{.pTaskHandle = &TCPClient1TaskHandle,
.pTaskAttributes = &TCPClient1Task_attributes,
.pTaskLoop = Task_TCPServer_StartClient1Task,
.connectHandle = -1},
{.pTaskHandle = &TCPClient2TaskHandle,
.pTaskAttributes = &TCPClient2Task_attributes,
.pTaskLoop = Task_TCPServer_StartClient2Task,
.connectHandle = -1},
{.pTaskHandle = &TCPClient3TaskHandle,
.pTaskAttributes = &TCPClient3Task_attributes,
.pTaskLoop = Task_TCPServer_StartClient3Task,
.connectHandle = -1},};
3、写三个子线程,只贴出来一个,剩下的两个拷贝之后修改编号就行了。
// 第一个从机监听任务,其它的也一样
static void Task_TCPServer_StartClient1Task(void *argument)
{
// 接收数据监听主循环
for(;;)
{
if(m_TCPServerConnectStruct[0].connectHandle<0)
{
osThreadExit();
}
m_TCPServerConnectStruct[0].recvLen = recv(m_TCPServerConnectStruct[0].connectHandle, m_TCPServerConnectStruct[0].recvBuf, DEF_TCP_RECV_MAX_LEN, 0);
// 接收数据出错
if (m_TCPServerConnectStruct[0].recvLen <= 0)
break;
printf("Client 1 recv %d datas\n", m_TCPServerConnectStruct[0].recvLen);
// 写回去
write(m_TCPServerConnectStruct[0].connectHandle, m_TCPServerConnectStruct[0].recvBuf, m_TCPServerConnectStruct[0].recvLen);
osDelay(1);
}
// 走到这里的时候就出错了
if((m_TCPServerConnectStruct[0].connectHandle>=0))
{
closesocket(m_TCPServerConnectStruct[0].connectHandle);
m_TCPServerConnectStruct[0].connectHandle = -1;
}
m_TCPServerConnectStruct[0].connectHandle = -1;
printf("Exit task client 1\r\n");
osThreadExit();
}
4、写主线程
// TCP Server主任务, 负责监听客户端的接入
void Task_TCPServer_MainLoop(void *argument)
{
int sock = -1, connected;
struct sockaddr_in server_addr, client_addr;
socklen_t sin_size;
int recv_data_len;
// 打印服务器地址和端口
printf("TCP Server IP : %d:%d:%d:%d\r\n"
, m_Mem_DevParam.netParam.ip[0]
, m_Mem_DevParam.netParam.ip[1]
, m_Mem_DevParam.netParam.ip[2]
, m_Mem_DevParam.netParam.ip[3]);
printf("TCP Server port : %d\n\n", m_Mem_DevParam.netParam.serverPort);
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
printf("TCP Server Socket error\n");
osThreadExit();
return; // 到不了这里
}
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(m_Mem_DevParam.netParam.serverPort);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
// 绑定套接字
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
{
// 绑定套接字失败
printf("TCP Server Unable to bind\n");
if (sock >= 0)
{
closesocket(sock);
}
// 结束任务
osThreadExit();
return;
}
// 监听客户端的接入,指定最多监听DEF_TCP_SERVER_LISTEN_MAX个
if (listen(sock, DEF_TCP_SERVER_LISTEN_MAX) == -1)
{
// 监听失败
printf("TCP Server Listen error\n");
if (sock >= 0)
{
closesocket(sock);
}
osThreadExit();
}
// 服务器主任务的主循环,判断接入的客户端,参考《野火LwIP 应用开发实战指南》
while(1)
{
sin_size = sizeof(struct sockaddr_in);
connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);
printf("New client connected from (%s, %d)\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
{
int flag = 1;
setsockopt(connected,
IPPROTO_TCP, /* set option at TCP level */
TCP_NODELAY, /* name of option */
(void *) &flag, /* the cast is historical cruft */
sizeof(int)); /* length of option value */
}
// 查看空余的从机监听任务
for(int i=0;i<3;i++)
{
if(m_TCPServerConnectStruct[i].connectHandle == -1)
{
m_TCPServerConnectStruct[i].connectHandle = connected;
connected = -1;
m_TCPServerConnectStruct[i].pTaskHandle = osThreadNew(m_TCPServerConnectStruct[i].pTaskLoop, NULL, m_TCPServerConnectStruct[i].pTaskAttributes);
break;
}
}
// 如果没有空闲的
if ((connected>=0))
{
closesocket(connected);
connected = -1;
}
osDelay(10);
}
// 到不了这里
if (sock >= 0)
closesocket(sock);
osThreadExit();
}
5、整个服务器部分的入口函数,一定要放在LWIP初始化后。
// 启动TCP Server主任务
void task_TCPServer_start(void)
{
// 启动网络监听任务
TCPServerTaskHandle = osThreadNew(Task_TCPServer_MainLoop, NULL, &TCPServerTask_attributes);
}
6、程序的入口
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void *argument)
{
/* init code for LWIP */
MX_LWIP_Init();
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
// 启动网络监听任务
task_TCPServer_start();
// 退出启动任务
printf("to exit default task\r\n");
osThreadExit();
}
/* USER CODE END StartDefaultTask */
}
三、效果展示