不能直接用 io.Copy 传文件,因为 TCP 是字节流无消息边界,会导致粘包、丢帧、断连后无法续传;必须自定义协议,包含长度头、分段读写、偏移记录与校验机制。

Go 用 Socket 传文件,不能直接 io.Copy 完事——TCP 是字节流,没有消息边界,大文件会粘包、丢帧、断连后重传无从下手。必须自己定义协议、处理偏移、校验完整性。
为什么 io.Copy 直接传文件会出问题
常见错误现象:read tcp 127.0.0.1:12345->127.0.0.1:56789: i/o timeout 或接收端只拿到半截文件,os.Stat 显示大小对不上,但没报错。
TCP 不保证一次 Write 对应一次 Read;网络栈可能把 1MB 文件拆成几十个 TCP 段发出去,接收端 conn.Read(buf) 可能只读到前 4KB 就返回,后续数据还在内核缓冲区里。
所以不能依赖「发完就完」,必须显式约定:怎么标识长度、怎么分段读、怎么确认收全。
立即学习“go语言免费学习笔记(深入)”;
关键点:
- 发送端不能只写文件内容,得先写长度头(如
binary.Write写 4 字节uint32) - 接收端必须先调用
io.ReadFull(conn, lengthBytes)读满 4 字节,再按该长度循环读够 - 任何环节中断(如客户端崩溃),服务端无法感知已收多少,除非额外记录断点
net.Conn 上实现断点续传的最小可行逻辑
断点续传不是靠重连自动恢复,而是靠双方协商「上次写到哪」。核心是让服务端持久化已收字节数,并在新连接时返回该值。
典型流程:
- 客户端连接后,先发一个带文件名的查询请求,比如
"GET_OFFSET:myfile.zip" - 服务端查本地状态文件或内存 map,返回已收字节数(如
"OFFSET:1234567") - 客户端
os.OpenFile(..., os.O_RDONLY)后,f.Seek(1234567, io.SeekStart)跳过已传部分 - 后续传输仍需带长度头,且每次写完要更新服务端 offset 记录(建议用原子操作或加锁)
注意:offset 文件必须同步刷盘(f.Sync()),否则断电后丢失;不要用内存变量存 offset,进程重启就归零。
文件名、元数据和内容怎么一起传
单纯传二进制内容不够,接收端不知道存成什么名字、是否需要校验。推荐分三阶段握手:
- 第一阶段:发送固定结构头,含文件名长度(
uint16)、文件名 UTF-8 字节、文件总大小(uint64)、可选 CRC32(uint32) - 第二阶段:服务端解析头,创建空文件(
os.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0644)),返回 OK 或拒绝(如磁盘满) - 第三阶段:开始传输正文,仍用「长度头 + 数据块」方式分片,每块建议 64KB–1MB,避免单次读写过大阻塞协程
示例头结构(共 15 字节):
0-1: filename_len (uint16, big-endian) 2-10: filename (max 9 bytes, padded) 11-18: file_size (uint64) 19-22: crc32 (optional)
别用 JSON 或 Protobuf 做头——序列化开销大,且需完整解码才能知道文件名,而你往往想先建文件再流式写入。
容易被忽略的底层细节
很多人写了逻辑能跑通,但一压测就丢数据或卡死,问题常出在:
-
conn.SetDeadline必须设,否则超时连接不释放,fd 耗尽;建议读写各设 30 秒,比文件大小预估时间略长 - 接收端用
bufio.NewReader(conn)会破坏流式边界——bufio自动预读,导致长度头被吞掉一半;必须裸用conn.Read或封装自己的 buffer - Windows 上
net.Listen("tcp", ":12345")默认 backlog 很小,高并发连接会拒绝;加net.ListenConfig{Control: func(fd uintptr) { syscall.SetsockoptInt( fd, syscall.SOL_SOCKET, syscall.SO_BACKLOG, 1024 ) }} - 关闭连接前,服务端最好发个结束标记(如
"FINISHED"),客户端收到才os.File.Close(),否则可能最后几 KB 滞留在 socket 发送缓冲区未落地
真正的难点不在协议设计,而在状态一致性:客户端认为「已发完」和服务端「已收全」之间永远存在网络不可靠带来的 gap。要么接受重传(简单),要么引入确认机制+本地 checkpoint(复杂但可靠)。


















