You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何提升TCP串口透传服务器吞吐量至80KB/s?

TCP串口回显服务器吞吐量优化问题

我正在开发一款TCP回显服务器,功能是接收TCP客户端数据写入串口,再从串口读取数据回传给客户端。测试时使用/dev/ttyUSB0回环连接器,同时作为烧录测试客户端向服务器发送数据并计算吞吐量。当前吞吐量约72KB/s,目标是提升至80KB/s。

服务器实现概述

  • 采用非阻塞Socket监听并处理TCP连接;
  • 将TCP客户端接收的数据写入串口;
  • 读取串口数据回传给TCP客户端;
  • 已实现高低水位标记(high/low watermarks)控制串口写入时机(解决TCP与串口吞吐量差异问题)。

但仍无法达到目标吞吐量,现提出以下问题:

  1. 可采用哪些策略或优化手段提升服务器吞吐量?
  2. 当前实现存在哪些潜在瓶颈?
  3. 是否有更高效的串口与TCP连接处理方式以提升性能?

附实现代码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/select.h>
#include <termios.h>
#include <sys/ioctl.h>
 
#define BACK_LOG 5
#define BUFFER_SIZE 8192
#define SERIAL_BUFFER_SIZE 2048
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
 
typedef struct sockaddr_in sockaddr_in;
typedef struct tcp_info
{
    int fd;
    sockaddr_in socket_address;
} tcp_info;
 
void tcp_destroy_server(tcp_info *tcp_server)
{
    if (tcp_server == NULL)
    {
        return;
    }
 
    close(tcp_server->fd);
    free(tcp_server);
}
 
tcp_info *tcp_create_server(const char *ip_addr, int port)
{
    tcp_info *tcp_server;
    int yes = 1;
 
    if (ip_addr == NULL)
    {
        return NULL;
    }
 
    if ((tcp_server = calloc(1, sizeof(tcp_info))) == NULL)
    {
        return NULL;
    }
 
    tcp_server->socket_address.sin_port = htons(port);
    tcp_server->socket_address.sin_family = AF_INET;
    inet_pton(AF_INET, ip_addr, &(tcp_server->socket_address.sin_addr));
 
    if ((tcp_server->fd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("[ERROR] Failed to create socket!\n");
        return NULL;
    }
 
    if (setsockopt(tcp_server->fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
    {
        perror("Set socket option failed");
        close(tcp_server->fd);
        return NULL;
    }
 
    if (bind(tcp_server->fd, (const struct sockaddr *)&tcp_server->socket_address, sizeof(tcp_server->socket_address)) == -1)
    {
        printf("[ERROR] Failed to bind socket!\n");
        close(tcp_server->fd);
        return NULL;
    }
 
    if (listen(tcp_server->fd, BACK_LOG) == -1)
    {
        printf("[ERROR] Failed to listen!\n");
        close(tcp_server->fd);
        return NULL;
    }
 
    return tcp_server;
}
 
int serial_open_port(const char *serial_port)
{
    int serial_fd = -1;
    struct termios tty = {0};
 
    if (serial_port == NULL)
    {
        return -1;
    }
 
    if ((serial_fd = open(serial_port, O_RDWR | O_NOCTTY | O_SYNC | O_NDELAY)) == -1)
    {
        return -1;
    }
 
    if (tcgetattr(serial_fd, &tty) != 0)
    {
        perror("Error from tcgetattr");
        close(serial_fd);
        return -1;
    }
 
    cfsetospeed(&tty, B921600);
    cfsetispeed(&tty, B921600);
 
    tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;
    tty.c_iflag &= ~IGNBRK;
    tty.c_lflag = 0;
    tty.c_oflag = 0;
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);
    tty.c_cflag |= (CLOCAL | CREAD);
    tty.c_cflag &= ~(PARENB | PARODD);
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag |= CRTSCTS;
 
    if (tcsetattr(serial_fd, TCSANOW, &tty) != 0)
    {
        perror("Error from tcsetattr");
        close(serial_fd);
        return -1;
    }
 
    return serial_fd;
}
 
// receiving buffer
int get_serial_input_buffer_size(int fd)
{
    int bytes_available = -1;
 
    if (ioctl(fd, TIOCINQ, &bytes_available) == -1)
    {
        perror("ioctl TIOCINQ error");
        return -1;
    }
 
    return bytes_available;
}
 
// sending buffer
int get_serial_output_buffer_size(int fd)
{
    int bytes_available = -1;
 
    if (ioctl(fd, TIOCOUTQ, &bytes_available) == -1)
    {
        perror("ioctl TIOCOUTQ error");
        return -1;
    }
 
    return bytes_available;
}
 
int main(int argc, char *argv[])
{
    if (argc != 4)
    {
        printf("Usage: %s <IP> <PORT> <SERIAL PORT>\n", argv[0]);
        return EXIT_FAILURE;
    }
 
    /* Server configuration */
    const char *server_address = argv[1];
    int port = atoi(argv[2]);
    const char *serial_port = argv[3];
 
    /* Define variables */
    tcp_info *tcp_server;
    sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char socket_buffer[BUFFER_SIZE] = {0};
    char serial_buffer[SERIAL_BUFFER_SIZE] = {0};
    bool enable_write = true;
 
    int client_fd = -1;
    int serial_fd = -1;
    int serial_written = 0;
    int serial_offset = 0;
 
    if ((tcp_server = tcp_create_server(server_address, port)) == NULL)
    {
        perror("[ERROR] failed to create TCP server");
        return EXIT_FAILURE;
    }
 
    if ((serial_fd = serial_open_port(serial_port)) == -1)
    {
        perror("[ERROR] failed to open serial");
        tcp_destroy_server(tcp_server);
        return EXIT_FAILURE;
    }
 
    printf("[INFO] Server waiting for connections.........\n");
    memset(&client_addr, 0, sizeof(client_addr));
 
    if ((client_fd = accept(tcp_server->fd, (struct sockaddr *)&client_addr, &client_addr_len)) == -1)
    {
        perror("[ERROR] Failed to aceept a new socket connection");
        tcp_destroy_server(tcp_server);
        return EXIT_FAILURE;
    }
 
    printf("[INFO] Accept a connection from tcp \n");
 
    fd_set read_fds;
    fcntl(client_fd, F_SETFL, O_NONBLOCK);
 
    while (1)
    {
        FD_ZERO(&read_fds);
        FD_SET(client_fd, &read_fds);
        FD_SET(serial_fd, &read_fds);
 
        if (select(MAX(serial_fd, client_fd) + 1, &read_fds, NULL, NULL, NULL) == -1)
        {
            perror("select() error");
            break;
        }
 
        /* Read data from socket , then writing to the serial port */
        if (enable_write && FD_ISSET(client_fd, &read_fds))
        {
            int valread = recv(client_fd, socket_buffer, BUFFER_SIZE, 0);
 
            if (valread <= 0)
            {
                perror("[ERROR] Socket recv error");
                break;
            }
 
            int write_num = write(serial_fd, socket_buffer, valread);
           
            if (write_num < 0)
            {
                perror("[ERROR] Write to serial error");
                break;
            }
            else if(write_num != valread)
            {
                perror("write num != valread");
                break;
            }
           
        }
       
        /* high/low watermark */
        int val = get_serial_output_buffer_size(serial_fd);
        if (val >= 23000)
        {
            enable_write = false; // stop writing data to serial
        }
        else if (val <= 1000)
        {
            //printf("val size = %d", val);
            enable_write = true;
        }
 
        /* Read data from serial */
        if (FD_ISSET(serial_fd, &read_fds))
        {  
            int read_num = read(serial_fd, serial_buffer + serial_offset, SERIAL_BUFFER_SIZE - serial_offset);
 
            if (read_num <= 0)
            {
                perror("[ERROR] Read from serial");
                break;
            }
 
            serial_offset += read_num;
 
            if (serial_offset == SERIAL_BUFFER_SIZE)
            {
                if (send(client_fd, serial_buffer, SERIAL_BUFFER_SIZE, 0) == -1)
                {
                    perror("[ERROR] Socket send error");
                    break;
                }
                printf("socket send %d bytes\n", SERIAL_BUFFER_SIZE);
                serial_offset = 0;
            }
        }
    }
 
    tcp_destroy_server(tcp_server);
    return 0;
}

优化建议与瓶颈分析

一、当前实现的潜在瓶颈

  • 串口同步阻塞写入:串口打开时使用O_SYNC标志,导致每次write都阻塞直到数据完全发送到硬件,严重拖慢写入效率。
  • 水位控制时机不合理:每次循环仅在Socket读取后检查串口缓冲区水位,无法及时响应缓冲区状态变化,容易出现过早停写或过晚恢复的情况,影响数据流转。
  • 串口读取的延迟发送:只有当串口缓冲区填满2048字节才回传TCP,未填满时会一直缓存,增加整体延迟,进而降低吞吐量。
  • Socket发送无容错处理:send操作未设置非阻塞,当TCP接收窗口不足时会阻塞整个处理流程;且未处理send部分发送的情况,可能导致数据丢失或积压。
  • select仅监听读事件:未利用select的写事件监听来优化串口写入,而是用轮询检查缓冲区的方式,额外消耗CPU资源,且响应不及时。

二、提升吞吐量的策略与优化手段

  1. 修改串口打开模式:去掉O_SYNC,添加O_NONBLOCK使串口读写非阻塞,配合select监听写事件控制写入时机,避免阻塞等待硬件发送。
  2. 优化水位控制逻辑:
    • 将水位检查与select的写事件结合,仅当串口可写且缓冲区低于低水位时才执行写入;
    • 调整阈值:将高水位降至16384字节(接近USB串口硬件缓冲区常见上限),低水位提高到4096字节,减少停写/恢复的频繁切换。
  3. 调整串口读取的发送策略:
    • 取消满缓冲区才发送的限制,读取到数据后立即尝试发送,或设置最小发送阈值(如512字节),平衡单次发送效率与延迟;
    • 处理send的部分发送:在非阻塞模式下,循环调用send直到所有数据发送完成或遇到EAGAIN错误。
  4. 优化Socket参数与操作:
    • 给客户端Socket的send操作也设置非阻塞模式,避免TCP窗口不足时阻塞;
    • 通过setsockopt增大TCP的发送/接收缓冲区(如设置SO_SNDBUF和SO_RCVBUF为65536字节),提升TCP数据流转效率。
  5. 调整缓冲区大小:将SERIAL_BUFFER_SIZE从2048调整为4096或8192,减少系统调用次数。
  6. 减少不必要的系统调用:仅在需要判断写入时机时调用get_serial_output_buffer_size,避免每次循环都执行该操作。

三、更高效的处理方式

  • 用epoll替代select:epoll在频繁事件触发场景下效率远高于select,减少不必要的事件遍历和复制开销,适合长期运行的服务器程序。
  • 尝试零拷贝技术:若系统支持,使用splice或sendfile直接将Socket接收的数据转发到串口,减少用户态与内核态之间的数据拷贝。
  • 多线程分离处理:将TCP数据接收与串口读写分离到不同线程,避免单线程中某一环节阻塞影响整体流程,可使用环形缓冲区实现线程间数据传递。
  • 优化串口驱动参数:确认回环连接器是否需要CRTSCTS硬件流控,若不需要可关闭以减少流控开销;调整tcsetattr中的VMIN和VTIME参数,让串口读取更高效。

内容的提问来源于stack exchange,提问作者Eric Chiu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 13:37:03