本地主机TCP服务端/客户端传输大于32K文件时出错
TCP大文件传输后出现附加^@字符的问题
我在本地主机使用TCP协议传输文件,通过ifconfig查看lo接口得知MTU为65536。小文件(小于32K)传输正常,但大文件传输完成后会附加^@字符,用vbindiff工具检查发现差异从32K(0x8000)位置开始。
测试文件生成命令
base64 /dev/urandom | head -c $((1024 * 30)) > test-small.txt base64 /dev/urandom | head -c $((1024 * 180)) > test-large.txt
服务端代码(server.c)
#include <stdint.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <time.h> #include <stdbool.h> #include <ctype.h> #include <assert.h> #include <signal.h> #include <stdarg.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/prctl.h> #include <sys/mman.h> #include <sys/wait.h> #include <unistd.h> #include <fcntl.h> typedef int64_t s64; typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; void write_entire_file(char *file_name, void *buf, u64 buf_size) { int file_fd = open(file_name, O_CREAT | O_TRUNC | O_WRONLY, 0666); if (file_fd != -1) { u8 *byte_location = (u8 *)buf; u32 bytes_to_write = buf_size; while (bytes_to_write > 0) { int write_res = write(file_fd, byte_location, bytes_to_write); if (write_res != -1) { bytes_to_write -= write_res; byte_location += write_res; } else { fprintf(stderr, "Error: unable to write file %s (%s)\n", file_name, strerror(errno)); break; } } close(file_fd); } else { fprintf(stderr, "Error: unable to open file %s (%s)\n", file_name, strerror(errno)); } } void readx(int fd, void *buf, size_t count) { int bytes_read = read(fd, buf, count); if (bytes_read == -1) { fprintf(stderr, "Error: read failed (%s)\n", strerror(errno)); exit(1); } if (bytes_read != count) { fprintf(stderr, "Error: read failed to read all bytes in one go(%s)\n", strerror(errno)); exit(1); } } void * mallocx(size_t size) { void *result = NULL; result = malloc(size); if (result == NULL) { fprintf(stderr, "Error: malloc failed (%s)\n", strerror(errno)); exit(1); } return result; } #define MTU 16384 typedef struct { u32 file_size; u32 contents_size; char contents[MTU]; } FileMessage; int main(int argc, char *argv[]) { int server_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_sock != -1) { int opt_val = 1; if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt_val, sizeof(opt_val)) == -1) { fprintf(stderr, "Warning: unable to set resuable socket (%s)\n", strerror(errno)); } struct sockaddr_in server_addr = {0}; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(8000); if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1) { u32 max_num_connections = 100; if (listen(server_sock, max_num_connections) != -1) { while (true) { struct sockaddr_in client_addr = {0}; u32 client_size = sizeof(client_addr); int client_fd = accept(server_sock, (struct sockaddr *)&client_addr, &client_size); if (client_fd != -1) { FileMessage file_message = {0}; readx(client_fd, &file_message, sizeof(file_message)); u32 byte_counter = 0; void *file_mem = mallocx(file_message.file_size); u8 *file_cursor = file_mem; u32 file_size = file_message.file_size; memcpy(file_cursor, file_message.contents, file_message.contents_size); byte_counter += file_message.contents_size; fprintf(stderr, "file size left: %d, content size: %d \n", (file_message.file_size - byte_counter), file_message.contents_size); while (byte_counter != file_size) { file_cursor += byte_counter; readx(client_fd, &file_message, sizeof(file_message)); memcpy(file_cursor, file_message.contents, file_message.contents_size); byte_counter += file_message.contents_size; fprintf(stderr, "file size left: %d, content size: %d \n", (file_message.file_size - byte_counter), file_message.contents_size); } write_entire_file("recieved-tcp-file", file_mem, file_message.file_size); free(file_mem); } else { fprintf(stderr, "Error: unable to accept connection\n"); } } } else { fprintf(stderr, "Error: unable to listen on socket\n"); } } else { fprintf(stderr, "Error: unable to bind on socket\n"); } } else { fprintf(stderr, "Error: unable to open socket\n"); } return 0; }
客户端代码(client.c)
#include <stdint.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <time.h> #include <stdbool.h> #include <ctype.h> #include <assert.h> #include <signal.h> #include <stdarg.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/prctl.h> #include <sys/mman.h> #include <sys/wait.h> #include <unistd.h> #include <fcntl.h> typedef int64_t s64; typedef uint8_t u8; typedef uint32_t u32; typedef uint64_t u64; typedef int64_t s64; typedef uint8_t u8; typedef uint32_t u32; void sleep_ms(int ms) { struct timespec sleep_time = {0}; sleep_time.tv_nsec = ms * 1000000; struct timespec leftover_sleep_time = {0}; nanosleep(&sleep_time, &leftover_sleep_time); } void writex(int fd, void *buf, size_t count) { int bytes_written = write(fd, buf, count); if (bytes_written == -1) { fprintf(stderr, "Error: write failed (%s)\n", strerror(errno)); exit(1); } if (bytes_written != count) { fprintf(stderr, "Warning: write failed to write all bytes in one go(%s)\n", strerror(errno)); exit(1); } } typedef struct { void *contents; u32 size; } ReadFileResult; ReadFileResult read_entire_file(char *file_name) { ReadFileResult result = {0}; int file_fd = open(file_name, O_RDONLY); if (file_fd != -1) { struct stat file_stat = {0}; int fstat_res = fstat(file_fd, &file_stat); if (fstat_res != -1) { result.contents = malloc(file_stat.st_size); if (result.contents != NULL) { result.size = file_stat.st_size; size_t bytes_to_read = file_stat.st_size; u8 *byte_location = (u8 *)result.contents; while (bytes_to_read > 0) { int read_res = read(file_fd, byte_location, bytes_to_read); if (read_res != -1) { bytes_to_read -= read_res; byte_location += read_res; } else { fprintf(stderr, "Error: unable to read file %s (%s)\n", file_name, strerror(errno)); free(result.contents); break; } } } else { fprintf(stderr, "Error: unable to malloc memory for file %s (%s)\n", file_name, strerror(errno)); } } else { fprintf(stderr, "Error: unable to fstat file %s (%s)\n", file_name, strerror(errno)); } } else { fprintf(stderr, "Error: unable to open file %s (%s)\n", file_name, strerror(errno)); } return result; } #define MTU 16384 typedef struct { u32 file_size; u32 contents_size; char contents[MTU]; } FileMessage; int main(int argc, char *argv[]) { int server_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_sock != -1) { int opt_val = 1; if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void *)&opt_val, sizeof(opt_val)) == -1) { fprintf(stderr, "Warning: unable to set resuable socket (%s)\n", strerror(errno)); } struct sockaddr_in server_addr = {0}; if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) == 1) { server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8000); if (connect(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1) { char *file_name = "test-large.txt"; ReadFileResult file_res = read_entire_file(file_name); if (file_res.contents != NULL) { FileMessage file_message = {0}; file_message.file_size = file_res.size; s64 file_size_left = file_res.size; u8 *file_cursor = (u8 *)file_res.contents; while (file_size_left != 0) { if (file_size_left - MTU >= 0) { memcpy(file_message.contents, file_cursor, MTU); file_message.contents_size = MTU; file_cursor += MTU; file_size_left -= MTU; writex(server_sock, &file_message, sizeof(file_message)); // artificially throttle sending of bytes sleep_ms(100); fprintf(stderr, "file size left: %ld\n", file_size_left); } else { memcpy(file_message.contents, file_cursor, file_size_left); file_message.contents_size = file_size_left; writex(server_sock, &file_message, sizeof(file_message)); file_size_left = 0; fprintf(stderr, "file size left: %ld\n", file_size_left); } } free(file_res.contents); } else { fprintf(stderr, "Error: unable to read file %s\n", file_name); } } else { fprintf(stderr, "Error: failed to connect (%s)\n", strerror(errno)); } } else { fprintf(stderr, "Error: invalid IP address provided (%s)\n", strerror(errno)); } } else { fprintf(stderr, "Error: unable to create server socket (%s)\n", strerror(errno)); } return 0; }
问题分析与修复
核心错误
服务端main函数的循环中,file_cursor += byte_counter这行代码逻辑完全错误。每次循环时,byte_counter是已经累计的总字节数,这会导致指针直接跳到总字节数的位置,跳过了当前应该写入的区间,未被写入的内存空间默认填充空字符(^@即ASCII 0),同时后续写入的内容也会覆盖之前的有效数据。
比如第一次循环后byte_counter为16384,file_cursor跳到file_mem + 16384;第二次循环时byte_counter变为32768,file_cursor又跳到file_mem + 16384 + 32768,中间的16384字节空间未被写入,最终就会出现多余的空字符。
修复方案
有两种可靠的修复方式:
方式一:修正指针偏移量
将file_cursor += byte_counter改为file_cursor += file_message.contents_size,每次只移动当前收到的内容长度对应的偏移:
while (byte_counter != file_size) { // 修正指针偏移逻辑 file_cursor += file_message.contents_size; readx(client_fd, &file_message, sizeof(file_message)); memcpy(file_cursor, file_message.contents, file_message.contents_size); byte_counter += file_message.contents_size; fprintf(stderr, "file size left: %d, content size: %d \n", (file_size - byte_counter), file_message.contents_size); }
方式二:直接用累计值计算偏移
完全依赖byte_counter的累计值定位写入位置,避免指针操作错误:
while (byte_counter != file_size) { readx(client_fd, &file_message, sizeof(file_message)); // 用累计字节数直接计算写入位置 memcpy((u8*)file_mem + byte_counter, file_message.contents, file_message.contents_size); byte_counter += file_message.contents_size; fprintf(stderr, "file size left: %d, content size: %d \n", (file_size - byte_counter), file_message.contents_size); }
额外优化建议
- 字节序兼容性:当前代码中
file_size和contents_size直接以主机序传输,跨不同架构的主机时会出现解析错误,建议使用htonl(客户端发送时)和ntohl(服务端接收时)转换为网络字节序。 - 健壮的读取逻辑:
readx函数强制要求一次性读取完整的FileMessage,本地环境下大概率没问题,但生产环境中建议实现循环读取,直到获取到完整的结构体数据,避免因TCP分段导致读取失败。
内容的提问来源于stack exchange,提问作者Edward Chamberlain
相关产品推荐
相关产品推荐

