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

本地主机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);
}

额外优化建议

  1. 字节序兼容性:当前代码中file_size和contents_size直接以主机序传输,跨不同架构的主机时会出现解析错误,建议使用htonl(客户端发送时)和ntohl(服务端接收时)转换为网络字节序。
  2. 健壮的读取逻辑:readx函数强制要求一次性读取完整的FileMessage,本地环境下大概率没问题,但生产环境中建议实现循环读取,直到获取到完整的结构体数据,避免因TCP分段导致读取失败。

内容的提问来源于stack exchange,提问作者Edward Chamberlain

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 00:50:20