嘉楠科技
直播中

lotusp

11年用户 1195经验值
擅长:电源/新能源
私信 关注
[问答]

socket缓冲区溢出的原因?怎么解决?

我在测试视频通话时 发现丢帧特别严重 进行了一些列的排查 发现socket本身似乎有问题
通过测试代码发现了大量的缓冲区溢出  我尝试换了不同的服务器
我还分别测试了wifi网卡和4G网卡 全都这样 无法正常的进行视频通话
不可能才四五百KB/s的速率就不行了啊  好久以前我测试过1M/s都是没问题的
服务器1配置如下 ↓

服务器2配置如下 ↓

期待结果和实际结果
软硬件版本信息
01科技K230开发板 + EC200A移远minipcie USB网卡 + RTOS_ONLY
错误日志
通过wifi往腾讯云上传日志如下 ↓

通过4G网卡移远 EC200A 往雨云上传日志如下 ↓

尝试解决过程
补充材料
测试代码如下
        #include #include #include #include #include #include #include #include #include #include #define TARGET_IP "27.25.142.58"// #define TARGET_IP        "162.14.198.185"#define TARGET_PORT      30100#define BUFFER_SIZE      1400#define TARGET_RATE_KB_S 500 // 目标发送速率(KB/s)#define SEND_tiMEOUT_SEC 5 // 发送超时时间(秒)volatile int keep_running = 1;void int_handler(int sig){    keep_running = 0;    printf("n接收到中断信号,准备退出...n");}// 创建非阻塞连接int connect_server(){    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);    if (sock_fd < 0) {        perror("socket创建失败");        return -1;    }    // 设置非阻塞    int flags = fcntl(sock_fd, F_GETFL, 0);    fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);    // 设置发送超时    struct timeval tv = { SEND_TIMEOUT_SEC, 0 };    setsockopt(sock_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));    // 连接服务器    struct sockaddr_in server_addr;    memset(&server_addr, 0, sizeof(server_addr));    server_addr.sin_family = AF_INET;    server_addr.sin_port   = htons(TARGET_PORT);    inet_pton(AF_INET, TARGET_IP, &server_addr.sin_addr);    printf("连接 %s:%d...n", TARGET_IP, TARGET_PORT);    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {        if (errno != EINPROGRESS) {            perror("连接失败");            close(sock_fd);            return -1;        }        // 等待连接完成        fd_set wfds;        FD_ZERO(&wfds);        FD_SET(sock_fd, &wfds);        tv.tv_sec  = 5;        tv.tv_usec = 0;        if (select(sock_fd + 1, NULL, &wfds, NULL, &tv) <= 0) {            perror("连接超时");            close(sock_fd);            return -1;        }    }    printf("已连接n");    return sock_fd;}int main(int argc, char* argv[]){    signal(SIGINT, int_handler);    // 初始化缓冲区    unsigned char buffer[BUFFER_SIZE];    memset(buffer, 12, BUFFER_SIZE);    // 连接服务器    int sock_fd = connect_server();    if (sock_fd < 0)        return -1;    printf("开始发送数据 (速率:%d KB/s, 按Ctrl+C停止)n", TARGET_RATE_KB_S);    // 计算发送间隔    long delay_us = (long)((double)BUFFER_SIZE / (TARGET_RATE_KB_S * 1024) * 1000000);    // 统计变量    unsigned long  total_bytes        = 0;    int            packets_sent       = 0;    int            consecutive_errors = 0;    struct timeval start_time, current_time, last_success;    gettimeofday(&start_time, NULL);    last_success = start_time;    // 发送循环    while (keep_running) {        int bytes_sent = send(sock_fd, buffer, BUFFER_SIZE, 0);        if (bytes_sent < 0) {            if (errno == EAGAIN || errno == EWOULDBLOCK) {                printf("缓冲区满n");                // 缓冲区满,检查是否需要重连                gettimeofday(¤t_time, NULL);                double idle_time = (current_time.tv_sec - last_success.tv_sec)                    + (current_time.tv_usec - last_success.tv_usec) / 1000000.0;                if (idle_time > SEND_TIMEOUT_SEC) {                    printf("发送超时(%.1f秒),重连...n", idle_time);                    close(sock_fd);                    sleep(1);                    sock_fd = connect_server();                    if (sock_fd < 0)                        break;                    gettimeofday(&last_success, NULL);                    consecutive_errors = 0;                    continue;                }                usleep(100000); // 等待100ms                consecutive_errors++;                if (consecutive_errors > 10) {                    sleep(1); // 连续失败时等待更久                    consecutive_errors = 0;                }                continue;            } else {                printf("发送错误: %sn", strerror(errno));                break;            }        } else if (bytes_sent == 0) {            printf("连接关闭n");            break;        }        // 发送成功        consecutive_errors = 0;        gettimeofday(&last_success, NULL);        total_bytes += bytes_sent;        packets_sent++;        // 每10个包更新状态        if (packets_sent % 100 == 0) {            gettimeofday(¤t_time, NULL);            double elapsed                = (current_time.tv_sec - start_time.tv_sec) + (current_time.tv_usec - start_time.tv_usec) / 1000000.0;            printf("已发送: %d包, %luB, 速率: %.2fKB/sn", packets_sent, total_bytes, (total_bytes / 1024.0) / elapsed);        }        // 控制发送速率        usleep(delay_us);    }    // 统计结果    gettimeofday(¤t_time, NULL);    double total_time        = (current_time.tv_sec - start_time.tv_sec) + (current_time.tv_usec - start_time.tv_usec) / 1000000.0;    printf("n总计: %d包, %luB, 时间: %.2f秒, 平均速率: %.2fKB/sn", packets_sent, total_bytes, total_time,           (total_bytes / 1024.0) / total_time);    close(sock_fd);    return 0;}        
以下内容为评论
          #include #include #include #include #include #include #include #include #include #include #define TARGET_IP "27.25.142.58"// #define TARGET_IP        "162.14.198.185"#define TARGET_PORT      30100#define BUFFER_SIZE      1400#define TARGET_RATE_KB_S 500 // 目标发送速率(KB/s)#define SEND_TIMEOUT_SEC 5 // 发送超时时间(秒)volatile int keep_running = 1;void int_handler(int sig){    keep_running = 0;    printf("n接收到中断信号,准备退出...n");}// 创建非阻塞连接int connect_server(){    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);    if (sock_fd < 0) {        perror("socket创建失败");        return -1;    }    // 设置非阻塞    int flags = fcntl(sock_fd, F_GETFL, 0);    fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);    // 设置发送超时    struct timeval tv = { SEND_TIMEOUT_SEC, 0 };    setsockopt(sock_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));    // 连接服务器    struct sockaddr_in server_addr;    memset(&server_addr, 0, sizeof(server_addr));    server_addr.sin_family = AF_INET;    server_addr.sin_port   = htons(TARGET_PORT);    inet_pton(AF_INET, TARGET_IP, &server_addr.sin_addr);    printf("连接 %s:%d...n", TARGET_IP, TARGET_PORT);    if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {        if (errno != EINPROGRESS) {            perror("连接失败");            close(sock_fd);            return -1;        }        // 等待连接完成        fd_set wfds;        FD_ZERO(&wfds);        FD_SET(sock_fd, &wfds);        tv.tv_sec  = 5;        tv.tv_usec = 0;        if (select(sock_fd + 1, NULL, &wfds, NULL, &tv) <= 0) {            perror("连接超时");            close(sock_fd);            return -1;        }    }    printf("已连接n");    return sock_fd;}int main(int argc, char* argv[]){    signal(SIGINT, int_handler);    // 初始化缓冲区    unsigned char buffer[BUFFER_SIZE];    memset(buffer, 12, BUFFER_SIZE);    // 连接服务器    int sock_fd = connect_server();    if (sock_fd < 0)        return -1;    printf("开始发送数据 (速率:%d KB/s, 按Ctrl+C停止)n", TARGET_RATE_KB_S);    // 计算发送间隔    long delay_us = (long)((double)BUFFER_SIZE / (TARGET_RATE_KB_S * 1024) * 1000000);    // 统计变量    unsigned long  total_bytes        = 0;    int            packets_sent       = 0;    int            consecutive_errors = 0;    struct timeval start_time, current_time, last_success;    gettimeofday(&start_time, NULL);    last_success = start_time;    // 发送循环    while (keep_running) {        int bytes_sent = send(sock_fd, buffer, BUFFER_SIZE, 0);        if (bytes_sent < 0) {            if (errno == EAGAIN || errno == EWOULDBLOCK) {                printf("缓冲区满n");                // 缓冲区满,检查是否需要重连                gettimeofday(¤t_time, NULL);                double idle_time = (current_time.tv_sec - last_success.tv_sec)                    + (current_time.tv_usec - last_success.tv_usec) / 1000000.0;                if (idle_time > SEND_TIMEOUT_SEC) {                    printf("发送超时(%.1f秒),重连...n", idle_time);                    close(sock_fd);                    sleep(1);                    sock_fd = connect_server();                    if (sock_fd < 0)                        break;                    gettimeofday(&last_success, NULL);                    consecutive_errors = 0;                    continue;                }                usleep(100000); // 等待100ms                consecutive_errors++;                if (consecutive_errors > 10) {                    sleep(1); // 连续失败时等待更久                    consecutive_errors = 0;                }                continue;            } else {                printf("发送错误: %sn", strerror(errno));                break;            }        } else if (bytes_sent == 0) {            printf("连接关闭n");            break;        }        // 发送成功        consecutive_errors = 0;        gettimeofday(&last_success, NULL);        total_bytes += bytes_sent;        packets_sent++;        // 每10个包更新状态        if (packets_sent % 100 == 0) {            gettimeofday(¤t_time, NULL);            double elapsed                = (current_time.tv_sec - start_time.tv_sec) + (current_time.tv_usec - start_time.tv_usec) / 1000000.0;            printf("已发送: %d包, %luB, 速率: %.2fKB/sn", packets_sent, total_bytes, (total_bytes / 1024.0) / elapsed);        }        // 控制发送速率        usleep(delay_us);    }    // 统计结果    gettimeofday(¤t_time, NULL);    double total_time        = (current_time.tv_sec - start_time.tv_sec) + (current_time.tv_usec - start_time.tv_usec) / 1000000.0;    printf("n总计: %d包, %luB, 时间: %.2f秒, 平均速率: %.2fKB/sn", packets_sent, total_bytes, total_time,           (total_bytes / 1024.0) / total_time);    close(sock_fd);    return 0;}               

                                                                                  0                                                                     
         
                  
        

                                 
         
                     

回帖(1)

李鸿洋

2025-6-23 17:15:05

根据你描述的现象(丢帧严重、缓冲区溢出、多种网络环境下重现)和提供的系统信息(K230+RTOS_ONLY+EC200A),问题根源很可能是 嵌入式RTOS环境下的Socket缓冲区配置、数据发送策略与视频流高实时性要求不匹配。以下是详细分析和解决方案:




核心原因分析




  1. 默认缓冲区过小:



    • RTOS系统(尤其是为嵌入式设备优化的精简内核)默认的Socket发送缓冲区(SO_SNDBUF)通常较小(几十KB甚至几KB)。

    • 视频流需要持续高带宽(500KB/s+),小缓冲区瞬间就会被填满。如果发送端(你的开发板)填充缓冲区的速度快于网络底层实际发送的速度(可能因网络抖动、拥塞控制短暂变慢),就会触发 EWOULDBLOCK/EAGAIN 或直接的 ENOBUFS/EMSGSIZE 错误(如你的日志所示),导致应用程序 send() 调用失败,进而丢帧。




  2. RTOS网络栈差异:



    • 相比成熟的Linux/Windows网络栈,RTOS的网络协议栈可能更简单,性能优化不足,吞吐量上限较低

    • setsockopt 可能受限: 尝试增大 SO_SNDBUF 失败(如日志提示 Errno: 92),说明底层驱动或协议栈不支持设置大缓冲区,或者设置上限很低。




  3. 发送策略不当:



    • 大块数据发送: 如果尝试将整个视频帧(可能几十上百KB)在一个 send() 调用中发出,极易超过底层协议的 最大传输单元(MTU)(通常 ~1500字节)。IP层会自动分片(Fragmentation),但:

      • 非常消耗CPU资源。

      • 增大丢包概率(丢一个分片,整个包重传)。

      • 增加延迟。

      • 日志中的 EMSGSIZE 错误很可能与此相关。 说明发送的数据报超过了协议/设备支持的最大单次发送大小。





  4. 实时操作系统调度问题:



    • 如果网络发送线程/任务的优先级不够高,或任务切换开销大,无法及时处理发送缓冲区中的数据,可能导致缓冲区积压溢出。






针对性解决方案(按优先级推荐)


⚙️ 1. 强制减少单个send()的数据包大小 (最可能见效)



  • 问题关键: 你正在发送的数据包大于底层MTU(通常是1500字节)。日志中的 EMSGSIZE 错误明确指出了这一点。

  • 解决方法:

    • 强制分片发送: 不要直接将整个编码后的视频帧传给 send()

    • 应用层分包: 在应用层主动将每帧视频数据拆分成小于MTU的小块(例如1200-1400字节)。推荐使用 1200字节

    • 循环发送小块: 在一个发送循环中,每次调用 send() 只发送一个小数据块。

    • 检查返回值/错误: 每次调用后都要检查发送的字节数和错误码(特别是 EWOULDBLOCK/EAGAIN)。如果遇到阻塞,需要等待(通过 select()/poll() 等待 socket 可写)或重试。这体现了非阻塞 I/O 的重要性。


  • 优点:

    • 显著减小发送失败概率。

    • 减少IP分片,降低CPU开销和传输延迟。

    • 更容易管理发送流控。



  • 代码示例 (思路):


      // 伪代码
      int frame_size = ...; // 一帧视频的大小
      char *frame_data = ...; // 指向视频帧数据的指针
      int packet_size = 1200; // 应用层分包大小
      int bytes_sent_total = 0;

      while (bytes_sent_total < frame_size) {
          int bytes_to_send = min(packet_size, frame_size - bytes_sent_total);
          int bytes_sent = send(sockfd, frame_data + bytes_sent_total, bytes_to_send, 0);
          if (bytes_sent < 0) {
              if (errno == EWOULDBLOCK || errno == EAGAIN) {
                  // 缓冲区满,等待可写(重要!)
                  fd_set write_fds;
                  FD_ZERO(&write_fds);
                  FD_SET(sockfd, &write_fds);
                  select(sockfd + 1, NULL, &write_fds, NULL, NULL); // 需处理超时或错误
                  continue; // 重试
              } else {
                  // 处理其他错误
                  break;
              }
          }
          bytes_sent_total += bytes_sent;
      }



⚡ 2. 设置非阻塞模式并使用事件驱动 (核心)



  • 问题: 阻塞式发送在缓冲区满时会使线程挂起,无法响应其他任务(比如处理新帧或控制信息)。

  • 解决方法:

    • 使用 fcntl(sockfd, F_SETFL, O_NONBLOCK)ioctl(sockfd, FIONBIO, &nonblocking) 将 socket 设为非阻塞模式

    • 使用 select(), poll() 或 RTOS 提供的事件机制(如果有)监听 socket 的可写事件(select() 的写集合 writefds) )

    • 只在 socket 可写时才尝试调用 send()。当 select()/poll() 指示 socket 可写,意味着当前缓冲区有空间容纳一定量的数据。

    • 结合方案1的小包发送,效果更佳。


  • 优点:

    • 避免线程在 send() 上无谓阻塞。

    • 更高效利用CPU。

    • 更容易实现发送流控。



? 3. 探索提升缓冲区大小 (RTOS下常受限)



  • 尽管日志显示 setsockopt(SO_SNDBUF) 失败,但仍需尝试:

    • 查看RTOS文档: 仔细查阅 RT-Thread / TencentOS Tiny 或 K230 SDK 关于 Socket 和网络栈的文档,确认是否支持设置 SO_SNDBUF,支持的取值范围是多少?失败的错误码 92 代表什么?(查头文件 errno.h)。

    • 内核配置: 某些RTOS允许在内核配置时修改默认的网络内存池大小或TCP发送窗口。检查是否有这样的配置项并增大它(可能需要重新编译内核或协议栈驱动)。注意内存消耗。

    • 硬件Offload: EC200A支持部分网络协议offload。确保相关offload功能(如TCP Segmentation Offload - TSO)在驱动中是开启的(如果底层支持)。它可以在网卡硬件层面做合理的分片,减轻CPU负担。但这通常是默认开启的,且不能替代应用层主动分包。



? 4. 优化发送策略:双缓冲区 + 主动清空



  • 问题: 在非阻塞模式下,调用 send() 后,应用层并不知道底层实际发送了多少数据,直到报告 EWOULDBLOCK/EAGAIN

  • 解决方法:

    • 维护发送队列: 使用两个缓冲区或一个队列。

      • Buffer1: 存放正在被网络栈送出的数据(交给 send() 的数据)。

      • Buffer2: 用于准备下一帧或下一部分数据。


    • 尽量清空发送队列:

      • 每次准备发送前,先尝试将发送队列中所有待发送数据通过非阻塞 send() 尽量发出,直到遇到 EWOULDBLOCK/EAGAIN。确保在缓冲区有空间时,尽快将积压数据发送出去,避免新数据堆叠导致溢出。


    • 准备好新数据后,放入发送队列。


  • 优点: 更主动地管理发送缓冲区的占用状态,避免新数据堆积在已经满的缓冲区之上。


? 5. 升级网络驱动/固件/RTOS (最后手段)



  • 检查更新: 确认使用的RTOS版本、EC200A USB网卡驱动、K230 SDK是否是官方最新版。厂家可能会修复已知的网络吞吐量或稳定性问题。特别是在日志中看到 "RTOS on mpy",如果 mpy 指 MicroPython,确保使用的 MicroPython 网络库是针对你的K230平台和当前RTOS版本稳定构建的。


⏱ 6. 考虑网络任务的优先级 (RTOS特性)



  • 问题: 如果网络发送任务优先级太低,可能在需要发送数据时得不到及时调度,导致缓冲区堆积溢出。

  • 解决方法:

    • 适当提升负责网络发送的任务/线程的优先级。确保它能在缓冲区有数据可发送时,尽快获得CPU资源执行 send()



? 7. 深入诊断:启用更详细日志



  • 如果RTOS支持,启用底层网络协议栈(IP/TCP)更详细的调试日志或统计信息。观察是否有大量的丢包、重传、IP分片发生。具体方法需查K230/RTOS文档。




总结执行步骤建议




  1. 立即实施(最可能快速见效):



    • 强制应用层分包: 修改发送代码,保证每次 send() 的数据块严格限制在 1200字节 以内。

    • 开启非阻塞模式: 将视频传输的socket设置为非阻塞。

    • 事件驱动发送: 使用 select() / poll() 监听socket可写事件,只在可写时发送一个分块数据(如果分包了),或者发送尽可能多的现有数据(如果未分包)。发送后立即检查状态和处理 EWOULDBLOCK




  2. 进一步优化(提升稳定性):



    • 结合非阻塞+事件驱动,实现 双缓冲区或队列机制,并在每次需要发送新数据前主动尝试清空现有发送队列(直到遇到 EWOULDBLOCK)再添加新数据。




  3. 环境/配置检查:



    • 仔细阅读RTOS文档,确认 SO_SNDBUF 的限制和 setsockopt 错误码92的含义。

    • 检查RTOS内核配置或驱动源码,看是否有全局网络内存池大小等参数可以调整。

    • 确认EC200A驱动设置,确保TOE之类的硬件offload功能开启(如果支持且驱动实现了)。

    • 考虑提升网络发送任务的优先级。




  4. 最后手段:



    • 升级RTOS、SDK、网卡驱动、MicroPython库等至最新版。




重点在于应用层强制分包到小于MTU(如1200字节)并且采用非阻塞I/O+事件驱动(select/poll)来管理发送时机。 这能直接解决日志中明确的 EMSGSIZE 错误,也能通过非阻塞+事件驱动有效应对缓冲区不足的 EWOULDBLOCK 情况。坚持做下去,视频丢帧问题应该能够解决!

举报

更多回帖

发帖
×
20
完善资料,
赚取积分