嵌入式上位机开发入门(十九):Socket 状态检测与断线重连

目录


一、前言

大家好,这里是 Hello_Embed。上篇修复了首次连接超时的竞态问题,本篇继续完善整个通信流程的健壮性。

实测中会遇到两类典型场景:

  1. 上位机主动断开连接:此时开发板的收/发接口会持续返回错误,若不处理,整个 Modbus 任务就会卡死。
  2. 网络波动导致丢包 :若 recvfrom 无限等待信号量,遇到断线就会永久阻塞。

本篇要解决的就是这两个问题------在收发接口 里增加 socket 状态检测,并在 Modbus 主循环里增加断线自动重连逻辑。


二、接收时的状态检测

原来 recvfrom 的逻辑是:先尝试从队列里读数据,读不到就等信号量。问题在于,如果 socket 已经断开,信号量永远不会被触发,函数就会无限阻塞。

原始逻辑(仅读数据,无状态检测):

c 复制代码
/* 先读取数据 */
for (i = 0; i < len; i++)
{
    if (pdPASS != xQueueReceive(recv_queue, &buf[i], 0))
        break;
}

/* 读取到数据的话设置 from */
if (i)
{
    if (from)
        *from = ptDev->sockets[socket].remote;
    if (fromlen)
        *fromlen = sizeof(*from);
}

改进:当队列为空时,不无限等待,而是定期检查 socket 状态:

c 复制代码
if (i == 0)
{
    /* 不要无限等待
     * 每隔 1000ms 检查一次 socket 状态
     * 如果 socket 已断开,立即返回错误
     */
    while (pdPASS != xSemaphoreTake(ptDev->sockets[socket].at_packet_sem, 1000))
    {
        status = w800_get_status(socket);
        if (status != 2)
        {
            closesocket(socket);
            return -1;
        }
    }
}

解析xSemaphoreTake 设置了 1000ms 超时,而不是 portMAX_DELAY。每次超时未拿到信号量,就主动查询一次 socket 状态(通过 AT 命令)。若状态不是 2(已连接),则关闭 socket 并返回 -1,让上层感知到连接已断开。


三、w800_get_status 实现

w800_get_status 是本篇新增的辅助函数,功能是向 W800 发送 AT+SKSTT 命令查询指定 socket 的连接状态。

c 复制代码
static int w800_get_status(int socket)
{
    int8_t buf[100];
    int err;
    PAT_Device ptDev = get_netdev();
    uint32_t local_port, remote_port;
    char remote_ip[20];
    uint32_t resp_len;
    int status;
    int hw_socket;
    int new_socket;
    int rx_data;
    int i;
    PAT_Socket ptSocket;
    struct sockaddr_in *sin;

    /* 构造 AT 命令(查询 socket 状态) */
    sprintf((char *)buf, "AT+SKSTT=%d\r", (int)ptDev->sockets[socket].user_data);

    /* 执行 AT 命令,等待响应 */
    err = at_exec_cmd(ptDev, (int8_t *)buf, (uint8_t *)buf, sizeof(buf), &resp_len, AT_TIMEOUT);
    if (err)
        return -1;

    /* 返回的数据格式:
     * +OK=<socket>,<status>,[host],[HostPort],[LocalPort],[rx_data]<CR><LF>
     *
     * status: 0 - 断开, 1 - 监听, 2 - 连接
     * host   : 对端 IP
     * HostPort : 对端端口
     * LocalPort: 本地端口
     * rx_data: 接收 buffer 中数据长度
     */
    sscanf((const char *)buf, "+OK=%d,%d,\"%[^\"]\",%d,%d,%d",
           &hw_socket, &status, remote_ip, &remote_port, &local_port, &rx_data);

    return status;
}

解析 :通过 sscanf 解析 +OK= 响应,提取第二个字段 status。返回值含义:0 = 断开,1 = 监听,2 = 已连接。只要返回值不是 2,就认为连接异常。


四、发送时的状态检测

同样的状态检测逻辑也要加到 sendto 中------在数据发送完毕后,立即检查 socket 是否仍然在线:

c 复制代码
int w800_sendto(int socket, const void *data, size_t size, int flags,
                const struct sockaddr *to, socklen_t tolen)
{
    PAT_Device ptDev = get_netdev();
    struct sockaddr_in *old_dest = (struct sockaddr_in *)&ptDev->sockets[socket].remote;
    struct sockaddr_in *new_dest = (struct sockaddr_in *)to;
    int err;
    int8_t buf[100];
    uint32_t resp_len;

    /* 对于 TCP 连接,参数 to 没有意义,必须为 NULL */
    if (ptDev->sockets[socket].type == SOCK_STREAM && to)
        return -1;

    /* 对于 UDP,必须指定参数 to */
    if (ptDev->sockets[socket].type == SOCK_DGRAM && !to)
        return -1;

    /* 对于 UDP,可能会多次调用 sendto 给不同目标发送数据
     * 目标发生变化时,需要先用 AT 命令重新创建 socket
     */
    if (ptDev->sockets[socket].type == SOCK_DGRAM)
    {
        if ((ptDev->sockets[socket].user_data == NULL) ||
            (old_dest->sin_addr.s_addr != new_dest->sin_addr.s_addr ||
             old_dest->sin_port != new_dest->sin_port))
        {
            if (ptDev->sockets[socket].user_data)
                w800_closesocket(socket);

            err = w800_connect(socket, to, tolen);
            if (err)
                return -1;
        }
    }

    /* 发送数据:
     * 先发命令:AT+SKSND=<socket>,<size><CR>
     * 再发送 size 个字节的数据
     */
    sprintf((char *)buf, "AT+SKSND=%d,%d\r", (int)ptDev->sockets[socket].user_data, size);
    err = at_exec_cmd(ptDev, (int8_t *)buf, NULL, 0, NULL, AT_TIMEOUT);
    if (err)
        return -1;

    err = at_send_datas(ptDev, (uint8_t *)data, size, AT_TIMEOUT);
    if (err)
        return -1;

    /* 发送完毕后检查 socket 状态 */
    {
        int status = w800_get_status(socket);
        if (status != 2)
        {
            closesocket(socket);
            return -1;
        }
    }

    return size;
}

解析 :发送成功后不能就此"高枕无忧"------对端可能在我们发完的瞬间断开了连接。通过 w800_get_status 进行一次确认,若状态异常则主动关闭并返回错误,避免上层误以为发送成功。


五、C 语言小技巧:块作用域

上面代码里用了一个不太常见的写法------单独的花括号 {}

c 复制代码
/* 写法一:使用块限制变量作用域 */
{
    int status = w800_get_status(socket);
    if (status != 2) { closesocket(socket); return -1; }
}

/* 写法二:直接声明变量 */
int status = w800_get_status(socket);
if (status != 2) { closesocket(socket); return -1; }

解析 :两种写法功能完全相同,但写法一中 status 的生命周期被限制在 {} 内,不会污染外部作用域。在大函数中,这个小技巧能有效减少变量名冲突,让代码意图更清晰。


六、断线重连机制

有了收/发时的状态检测,上层 Modbus 任务就能通过返回值感知到断线事件。modbus_receive 返回负值时,说明 socket 已经出错,此时的处理逻辑如下:

c 复制代码
if (rc < 0)
{
    /* Socket 出错,等待重连 */
    Draw_String(0, 80, "wait re-connect ...", 0xff0000, 0);

    while (1)
    {
        socket_client = modbus_tcp_accept(ctx, &socket_server);
        if (socket_client >= 0)
            break;
    }

    Draw_String(0, 96, "Modbus client re-connected", 0xff0000, 0);
    continue;
}

解析 :这里不重新调用 modbus_tcp_listen,是因为 socket_server(监听 socket)始终没有被关闭,仍在监听端口。只需重新调用 modbus_tcp_accept 等待新的客户端连入即可。整个循环是非阻塞友好的------只要上位机重新连接,就能立刻恢复通信。


七、完整任务函数

以下是加入断线重连后的完整 LibmodbusServerTask,供参考:

c 复制代码
void LibmodbusServerTask(void *pvParameters)
{
    uint8_t *query;
    modbus_t *ctx;
    int rc;
    modbus_mapping_t *mb_mapping;
    PChannelInfo ptChannelInfo;
    int channel;
    int err;
    int header_length;
    int socket_server, socket_client;

    /* 显示当前运行模式 */
    if (isBootloader())
        Draw_String(150, 0, "Bootloader", 0xff0000, 0);
    else
        Draw_String(150, 0, "Application", 0xff0000, 0);

    /* 初始化 AT 层 */
    at_init("uart1");

    /* 连接 WiFi 热点,失败则 1s 后重试 */
    while (1)
    {
        err = at_connect_ap("Programmers", "100asktech");
        if (!err)
            break;
        vTaskDelay(1000);
    }

    /* 创建 Modbus TCP 上下文 */
#if 0
    ctx = modbus_new_st_rtu("usb", 115200, 'N', 8, 1);
    modbus_set_slave(ctx, 1);
    query = pvPortMalloc(MODBUS_RTU_MAX_ADU_LENGTH);
#else
    ctx = modbus_new_tcp(NULL, 1502);
    query = pvPortMalloc(MODBUS_TCP_MAX_ADU_LENGTH);
#endif

    header_length = modbus_get_header_length(ctx);

    /* 分配寄存器映射表 */
    mb_mapping = modbus_mapping_new_start_address(0, 500, 0, 500, 0, 500, 0, 500);
    memset(mb_mapping->tab_bits, 0, mb_mapping->nb_bits);
    memset(mb_mapping->tab_registers, 0, mb_mapping->nb_registers * 2);

    /* 开始监听 */
    Draw_String(0, 48, "Waiting connect ...", 0xff0000, 0);
    while (1)
    {
        socket_server = modbus_tcp_listen(ctx, 1);
        if (socket_server >= 0)
            break;
    }

    /* 等待第一次客户端连接 */
    while (1)
    {
        socket_client = modbus_tcp_accept(ctx, &socket_server);
        if (socket_client >= 0)
            break;
    }
    Draw_String(0, 64, "Modbus client connected", 0xff0000, 0);

    /* 初始化通道信息,创建业务任务 */
    for (channel = 1; channel < MAX_CHANNEL_NUMBER; channel++)
    {
        ptChannelInfo = &g_tChannelInfos[channel];
        ptChannelInfo->mb_mapping = mb_mapping;
        ptChannelInfo->xMutex     = xSemaphoreCreateMutex();
    }

    if (!isBootloader())
    {
        xTaskCreate(CH0_Task, "CH0_Task", 400, mb_mapping, osPriorityNormal, NULL);

        for (channel = 1; channel < MAX_CHANNEL_NUMBER; channel++)
        {
            ptChannelInfo = &g_tChannelInfos[channel];
            xTaskCreate(CHn_Task, ptChannelInfo->uart_name, 400,
                        ptChannelInfo, osPriorityNormal, NULL);
        }
    }

    /* 主循环:接收请求 → 处理 → 回复 */
    for (;;)
    {
        do {
            rc = modbus_receive(ctx, query);
            /* 过滤查询返回 0,继续等待 */
        } while (rc == 0);

        if (rc < 0)
        {
            /* Socket 出错,等待重连 */
            Draw_String(0, 80, "wait re-connect ...", 0xff0000, 0);
            while (1)
            {
                socket_client = modbus_tcp_accept(ctx, &socket_server);
                if (socket_client >= 0)
                    break;
            }
            Draw_String(0, 96, "Modbus client re-connected", 0xff0000, 0);
            continue;
        }

        /* 处理紧急命令 */
        err = process_emergency_cmd(ctx, &query[header_length - 1], rc, mb_mapping);
        if (err)
        {
            modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
            continue;
        }

        /* 处理文件记录 */
        err = process_file_record(&query[header_length - 1], rc);
        if (err)
        {
            modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_SLAVE_OR_SERVER_BUSY);
            continue;
        }

        /* 正常回复 */
        rc = modbus_reply(ctx, query, rc, mb_mapping);
    }

    /* 清理资源(正常情况下不会执行到这里) */
    modbus_mapping_free(mb_mapping);
    vPortFree(query);
    modbus_close(ctx);
    modbus_free(ctx);
    vTaskDelete(NULL);
}

八、总结

改进点 位置 方案
接收阻塞问题 w800_recvfrom 信号量等待改为带超时的轮询,超时后检查 socket 状态
发送后状态确认 w800_sendto 发送完毕后主动调用 w800_get_status 验证连接
断线重连 LibmodbusServerTask rc < 0 时重新调用 modbus_tcp_accept 等待新连接

经过本篇的改进,系统已经能够:

  • 上位机主动断开时,开发板感知到断线并自动进入重连等待
  • 网络波动时,接收接口不会无限阻塞,而是定期"自检"

九、结尾

本篇完成了收发接口的 socket 状态检测与断线自动重连机制。至此,整个 Modbus TCP 通信已经具备了基本的容错能力。

下一篇将继续改进程序,实现写文件功能,敬请期待~

Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

相关推荐
foundbug9993 小时前
STM32 内部温度传感器测量程序(标准库函数版)
stm32·单片机·嵌入式硬件·算法
天狼IoT3 小时前
STM32-keil+CubeMX快速开发:新建项目
stm32·单片机·嵌入式硬件
cheems95273 小时前
[SpringMVC]Cookie 和Session 从无状态协议到状态保存实务
网络·http
Bruce_Liuxiaowei4 小时前
2026年4月第2周网络安全形势周报(3)
网络·安全·web安全
zl_dfq4 小时前
计算机网络 之 【IP协议】(IPv4报文格式、IP地址、公网IP VS 私网IP、路由VS转发)
网络·计算机网络·ip
大数据新鸟4 小时前
NIO 三大核心组件
服务器·网络·nio
添砖java‘’4 小时前
网络层IP
网络·网络协议·tcp/ip·ip
gihigo19984 小时前
量程自动切换数字电压表Proteus仿真+程序
单片机·嵌入式硬件·proteus
木燚垚4 小时前
基于STM32的智能衣柜系统设计与实现——温湿度调控+烟雾报警+远程监控
stm32·单片机·嵌入式硬件