STM32 + FreeRTOS + LWIP实现TCP服务器并同时监听多个客户端的方法

项目中遇到需要在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 */
}

三、效果展示

 

 

物联沃分享整理
物联沃-IOTWORD物联网 » STM32 + FreeRTOS + LWIP实现TCP服务器并同时监听多个客户端的方法

发表评论