如何提升TCP串口透传服务器吞吐量至80KB/s?
TCP串口回显服务器吞吐量优化问题
我正在开发一款TCP回显服务器,功能是接收TCP客户端数据写入串口,再从串口读取数据回传给客户端。测试时使用/dev/ttyUSB0回环连接器,同时作为烧录测试客户端向服务器发送数据并计算吞吐量。当前吞吐量约72KB/s,目标是提升至80KB/s。
服务器实现概述
- 采用非阻塞Socket监听并处理TCP连接;
- 将TCP客户端接收的数据写入串口;
- 读取串口数据回传给TCP客户端;
- 已实现高低水位标记(high/low watermarks)控制串口写入时机(解决TCP与串口吞吐量差异问题)。
但仍无法达到目标吞吐量,现提出以下问题:
- 可采用哪些策略或优化手段提升服务器吞吐量?
- 当前实现存在哪些潜在瓶颈?
- 是否有更高效的串口与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资源,且响应不及时。
二、提升吞吐量的策略与优化手段
- 修改串口打开模式:去掉
O_SYNC,添加O_NONBLOCK使串口读写非阻塞,配合select监听写事件控制写入时机,避免阻塞等待硬件发送。 - 优化水位控制逻辑:
- 将水位检查与select的写事件结合,仅当串口可写且缓冲区低于低水位时才执行写入;
- 调整阈值:将高水位降至16384字节(接近USB串口硬件缓冲区常见上限),低水位提高到4096字节,减少停写/恢复的频繁切换。
- 调整串口读取的发送策略:
- 取消满缓冲区才发送的限制,读取到数据后立即尝试发送,或设置最小发送阈值(如512字节),平衡单次发送效率与延迟;
- 处理
send的部分发送:在非阻塞模式下,循环调用send直到所有数据发送完成或遇到EAGAIN错误。
- 优化Socket参数与操作:
- 给客户端Socket的
send操作也设置非阻塞模式,避免TCP窗口不足时阻塞; - 通过
setsockopt增大TCP的发送/接收缓冲区(如设置SO_SNDBUF和SO_RCVBUF为65536字节),提升TCP数据流转效率。
- 给客户端Socket的
- 调整缓冲区大小:将
SERIAL_BUFFER_SIZE从2048调整为4096或8192,减少系统调用次数。 - 减少不必要的系统调用:仅在需要判断写入时机时调用
get_serial_output_buffer_size,避免每次循环都执行该操作。
三、更高效的处理方式
- 用epoll替代select:epoll在频繁事件触发场景下效率远高于select,减少不必要的事件遍历和复制开销,适合长期运行的服务器程序。
- 尝试零拷贝技术:若系统支持,使用
splice或sendfile直接将Socket接收的数据转发到串口,减少用户态与内核态之间的数据拷贝。 - 多线程分离处理:将TCP数据接收与串口读写分离到不同线程,避免单线程中某一环节阻塞影响整体流程,可使用环形缓冲区实现线程间数据传递。
- 优化串口驱动参数:确认回环连接器是否需要CRTSCTS硬件流控,若不需要可关闭以减少流控开销;调整
tcsetattr中的VMIN和VTIME参数,让串口读取更高效。
内容的提问来源于stack exchange,提问作者Eric Chiu
相关产品推荐
相关产品推荐

