
简介这是一套基于Visual Studio 2015开发的TCP文件传输服务器完整工程面向网络编程初学者及C/S架构学习者演示了利用TCP协议在客户端与服务器之间高效、可靠传输文件的实现思路。项目核心包括Socket连接创建、文件名与文件大小预传输、文件内容接收及确认机制并通过TCP确认与重传策略保障数据完整性同时涉及路径处理、错误检查、文件打开模式设置等实操细节后续还可扩展断点续传、多线程并发、加密传输等功能。资源共42个文件压缩包67.33MB包含C源码.cpp/.h、VS工程文件.sln/.vcxproj、资源脚本.rc、编译生成的exe程序及调试符号.pdb等可直接打开工程阅读源码或运行体验。已有1520人学习下载适合想深入理解Socket编程、TCP粘包处理或文件传输流程的开发者参考。1. TCP文件传输服务器为什么不用 HTTP 硬扛大文件生产环境里往另一台机器传几个 GB 的日志包最省事的想法是开个 HTTP 服务拿浏览器下但真到传输阶段就开始闹幺蛾子传到一半断线没有续传、网盘限速到几十 KB、临时机器上连 Python 环境都得现场装。反观直接基于 socket 写的 TCP 文件传输服务器反而更稳——它要解决的就一件事把二进制流可靠地从 A 搬到 B有分块、有确认、有重发传输进度可控。这套资源把服务端、客户端、协议头定义和测试脚本都整理好了适合做内网传输、日志收集、或者想真正搞懂 TCP 可靠传输原理的开发与运维。2. 先把传输协议定下来包格式、ACK 与重传策略2.1 包格式设计魔数、偏移量与校验字段缺一不可TCP 本身是流协议它不是按“包”给你数据的recv 拿到的字节流天然没有边界。裸写 socket 传文件最容易翻车的点就在这接收方不知道一段数据到哪结束、属于文件的哪个位置。这个资源给的方案是在每个文件块前面加一个固定长度的二进制头部把文件块切分成“头部 负载”的帧结构。头部用 Python 的 struct 定义字段如下import struct # ! 表示网络字节序大端Huint16Iuint32 # 协议头魔数(2) 块长度(4) 偏移量(4) 校验值(2) 12 字节 HEADER struct.Struct(!HIIH) MAGIC 0xAB12 # 魔数用于快速判断是否为本协议的包 END_MAGIC 0xABFF # 结束包魔数客户端传完最后一帧后发送字段类型位数作用magicH16 bit帧类型标识能过滤脏数据chunk_lenI32 bit本帧负载的字节数上限 4 GBoffsetI32 bit该块在文件中的起始偏移量crcH16 bit负载的 MD5 前 2 字节用于块级校验魔数看起来多余但实际很管用。如果你把 TCP 端口误接到了别的客户端或者对方发来一堆乱码靠 magic 判断能在前 2 字节就丢弃整个帧不用硬解析。chunk_len 解决边界问题告诉接收方“这个帧的负载读多少字节才算完”。offset 是给断点续传留的口子也是服务端 seek 写文件的依据。crc 用负载 MD5 的前 2 字节碰撞概率不算低但对付网络传输的随机错位足够要更稳就在生产上改成完整 16 字节 MD5代价是每块多 14 字节的头部开销。2.2 可靠传输的两个锚点ACK 确认与超时重传TCP 底层有 ACK 和重传机制但在文件传输这个场景里你还要一层应用层确认。原因很直接TCP 的 ACK 只说明“数据进了对端内核缓冲区”不代表你的业务逻辑判断过这段数据完整、校验通过、正确落盘了。像磁盘写满、进程处理不过来这种问题TCP 完全感知不到。这个资源的做法是逐块确认。客户端每发完一个帧必须等服务端回一个 2 字节的结果OK表示该块校验通过并已写入NG表示校验失败需要重发。这一来一回看着多了一次 RTT但对于大文件传输吞吐的关键在流水线并发而不是单块确认逐块确认换来的正确性完全值得。超时重传放在客户端一侧。socket 连接设一个timeout一旦超过阈值没收到响应就重发当前块。重发不是无线循环资源里约定的是最多重试 3 次3 次之后直接报错退出。重传超时设多少有讲究局域网推荐 3 秒跨公网建议 10 秒起步设太短容易在丢包抖动时误判设太长又会拖慢失败反馈。2.3 三个核心参数Buffer Size、接收窗口与重传超时块大小Buffer Size是文件传输里最关键的参数。它影响两层东西一是 TCP 包大小是否匹配路径 MTU二是应用层确认频率。资源里默认给的是 64 KB也就是BLOCK 64 * 1024这个值是我反复试下来最稳的起点。参数推荐值影响BLOCK64 KB太小编码开销高太大单块重传代价大发送缓冲区256 KB ~ 1 MB小于块大小会阻塞 sendall接收缓冲区256 KB ~ 1 MB影响 TCP 窗口扩张上限重传超时局域网 3s / 跨网 10s设太短误判设太长拖慢反馈如果走跨互联网传输把块调到 128 KB 或 256 KB 也能跑但前提是发送缓冲区必须跟着调否则 sendall 会把一次逻辑发送拆成多次系统调用反而触发 Nagle 合并。TCP 窗口本身有系统级上限之后第 6 章会讲怎么用sysctl去调。总之这组参数是先保证“单块传输时间 超时时间”的原则定出来的改的时候别只动一个字段。3. 服务端实现从 Accept 循环到文件落盘的完整代码3.1 服务端主循环接收线程与解析资源里的服务端用单线程模型先把功能跑通主循环 accept 后直接处理当前连接逻辑清晰、方便调试。生产上要支持多客户端并发后面把handle_conn丢进线程池即可。下面是服务端核心代码# server.py —— 单线程版先保证功能正确 import os import socket import struct import hashlib HEADER struct.Struct(!HIIH) MAGIC 0xAB12 END_MAGIC 0xABFF def recv_until(sock, length): 循环接收直到读满 length 字节解决半包问题 data b while len(data) length: part sock.recv(length - len(data)) if part b: raise ConnectionError(对端关闭连接) data part return data def handle_conn(conn, final_path): tmp_path final_path .part ok False with open(tmp_path, wb) as tmp, conn: while True: try: head recv_until(conn, HEADER.size) except (ConnectionError, socket.timeout): break magic, chunk_len, offset, crc HEADER.unpack(head) if magic END_MAGIC: # 结束包客户端已全部确认对比文件总大小 if offset tmp.tell(): conn.sendall(bOK) ok True else: conn.sendall(bNG) break if magic ! MAGIC: # 未知数据直接断开避免把垃圾写入文件 break chunk recv_until(conn, chunk_len) if hashlib.md5(chunk).digest()[:2] ! crc: conn.sendall(bNG) continue tmp.seek(offset) tmp.write(chunk) conn.sendall(bOK) if ok: os.replace(tmp_path, final_path) def main(): srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((0.0.0.0, 9000)) srv.listen(8) print(server listening on 9000) while True: conn, addr srv.accept() print(accept:, addr) handle_conn(conn, ./recv.bin) if __name__ __main__: main()这段代码的关键点是recv_until函数它用循环保证了无论接收缓冲区里是一个块、半个块还是三四个块都能完整取出一帧再解析。tmp.seek(offset)让每个块写到文件的正确位置顺序乱着来也能正确落盘。文件先写.part临时文件全部确认完成后再用os.replace原子重命名期间任何断线都不会污染最终文件这是我从生产环境里学到的习惯。3.2 客户端实现分块发送与 ACK 等待客户端逻辑是服务端的镜像读取本地文件、按块切分、逐块发送并等待OK。注意客户端要处理一个服务端容易忽略的细节sendall之后必须recv如果直接关连接服务端可能还在处理上一块的数据最终文件大小对不上。# client.py import hashlib import os import socket import struct HEADER struct.Struct(!HIIH) MAGIC 0xAB12 END_MAGIC 0xABFF BLOCK 64 * 1024 def recv_until(sock, length): data b while len(data) length: part sock.recv(length - len(data)) if part b: raise ConnectionError(连接中断) data part return data def send_file(ip, port, filepath): total os.path.getsize(filepath) sent 0 with socket.create_connection((ip, port), timeout10) as sock: with open(filepath, rb) as f: offset 0 while True: chunk f.read(BLOCK) if not chunk: break crc hashlib.md5(chunk).digest()[:2] header HEADER.pack(MAGIC, len(chunk), offset, crc) sock.sendall(header chunk) resp recv_until(sock, 2) if resp ! bOK: raise RuntimeError(foffset{offset} 校验不通过) offset len(chunk) sent len(chunk) print(fprogress: {sent}/{total}) # 发送结束包offset 字段携带文件总大小 end HEADER.pack(END_MAGIC, 0, total, 0) sock.sendall(end) resp recv_until(sock, 2) if resp ! bOK: raise RuntimeError(结束包未确认) if __name__ __main__: send_file(127.0.0.1, 9000, ./test.bin)HEADER.pack(MAGIC, len(chunk), offset, crc)按大端序把 12 字节头部拼好紧接负载一起sendall保证了头部和负载在同一个 TCP 段里发出去。这里的超时设的是 10 秒局域网内足够跨公网则按延迟调。打印进度的sent/total是给用户看的真正决定进度可靠性的是每一块收到OK之后才递增的offset。3.3 并发权衡多线程、多进程与 epoll 的选择单线程版本代码短、易排查适合第一步跑通。但实际生产里常有多台机器同时推文件的情况这时就得考虑并发。按这个资源的使用场景最优先推荐的是线程池连接数不多几十以内每个连接又是阻塞读写线程池能把代码改动压缩到最小把handle_conn丢进ThreadPoolExecutor就行。如果单机连接数上千那才是 epoll 的舞台。Python 里用asyncio重写收发循环把 recv_until 改成 await本质是把非阻塞 socket 的事件轮询交给内核。但这里有个血泪教训异步模型下别在协程里做磁盘同步写尤其大文件落盘时必须用独立的写线程或loop.run_in_executor否则一个慢磁盘会把整个事件循环拖死。换句话说连接少就线程池连接多且每个连接数据量大先想想磁盘 IO 是不是瓶颈再上异步。4. TCP 文件传输避坑粘包、半包、Nagle 与 TIME_WAIT4.1 粘包一次 recv 读到多个数据块现象服务端连续收到两个文件块的内容recv一次返回的长度超过预期解析第一帧后发现缓冲区里还残留下一帧的数据顺序一乱文件就整个废掉。原因TCP 是字节流协议不保留应用层的消息边界。发端两次sendall的数据很可能一次到达接收端内核缓冲区recv一次全取出来这就是常说的粘包。解决不要裸 recv严格按照“先读固定长度头部再按头部声明长度读取负载”来消费数据。这个资源里recv_until的意义就在这它始终从缓冲区取走当前帧需要的字节数多出来的数据留在 socket 内核缓冲区下一轮循环继续读。粘包在业务上不是 bug处理好了它反而能减少系统调用次数。4.2 半包recv 返回长度不够一个块现象客户端发了 64 KB 的块服务端recv(64 * 1024)只返回了 20 KB后面再读才拿到剩余部分如果直接按一次 recv 结果解析头部残缺、长度对不上。原因TCP 报文段会被路径 MTU、内核缓冲区大小和拥塞窗口限制拆成多个小段传输。即使数据总量不大对端也可能分多次到达。这是 TCP 的常态不是异常。解决所有读取都必须包一层循环直到读满所需长度。代码里recv_until的while len(data) length就是标准解法。我见过不少人在半包这个问题上临时加sleep(0.1)去碰运气这是典型的玄学调试局域网里可能碰巧能过一上公网立刻现原形千万别这么写。4.3 Nagle 算法小包传输忽然变慢现象文件传输后期客户端发送速度突然掉到几十 KB/s服务端的 CPU 和网络带宽都没占满看起来像是连接被卡住了。原因TCP 默认开启 Nagle 算法它会合并小包再发送要求“前一个包 ACK 后才能发下一个小包”。如果对端同时开了 Delayed ACK小包传输就会产生 200 ms 级别的等待吞吐直接崩。解决对传输类 socket 设置TCP_NODELAY关闭 Nagle。注意一点本资源每个块是头部加负载一次性sendall块大小 64 KB 远超小包阈值Nagle 影响不大但如果你把块切小或者将来做交互式命令通道就一定要加conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)。这个开关只在需要低延迟时开大块传输场景开了也感知不到区别。4.4 TIME_WAIT服务端重启后 bind 失败现象服务端 CtrlC 杀掉进程后立刻重启报Address already in use等几十秒又能重启成功气得想砸键盘。原因客户端主动断开连接时服务端侧会进入 TIME_WAIT 状态持续约 2 个 MSL通常 60 秒。这段时间内 socket 的四元组还占着默认不允许重新 bind 同样的端口。解决服务端在 bind 前加srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。这个选项告诉内核允许重用处于 TIME_WAIT 状态的端口。代码里已经写上了。生产环境重启服务是家常便饭不写这一行的服务端程序我建议直接不验收。4.5 校验失败后重发必须有重试上限现象某一块 CRC 校验不过客户端收到NG后重发服务端又返回NG双方陷入无限重发日志刷屏文件卡在一个进度上不动。原因如果传输途经的链路存在固定干扰或 MTU 黑洞同一块数据每次到达都损坏重发多少次结果都一样。没有重试上限的循环本质上是个死循环。解决客户端对单块重试设上限资源里约定 3 次超过直接抛异常退出。同时服务端在连续NG超过 5 次时也主动断开连接不再响应重发。把重试和断连策略写进协议文档比写在代码注释里更重要这是能让运维半夜少接电话的那种细节。5. 断点续传与 MD5 校验大文件传输的必要扩展5.1 断点续传用已确认块号恢复传输几十 GB 的文件传一半断网从头再来谁也受不了。断点续传的核心思路很简单客户端把“已收到 OK 的块偏移量”持久化到本地重连服务端后从该偏移量继续读文件而不是从头开始。实现上要注意一个细节服务端启动时要检查.part临时文件是否存在存在就取其大小作为初始偏移量。代码如下方示意# 服务端续传从已有 .part 文件大小继续写 resume_offset 0 if os.path.exists(tmp_path): resume_offset os.path.getsize(tmp_path) with open(tmp_path, ab) as tmp: tmp.seek(resume_offset)服务端打开文件时要改成ab模式并seek到现有偏移量客户端则维护一个本地状态文件记录已确认偏移量。断线重连后客户端发送第一帧时把起始偏移量告诉服务端双方都定位到同一个点继续。这里最隐蔽的坑是文件读取要跳过已确认的块还是顺序读再让服务端按 offset 覆盖写我一般选后者——继续顺序读服务端继续seek(offset)覆盖写省去客户端随机读的复杂度逻辑也更容易证明正确。5.2 校验策略MD5 放分块层传输结束再做全量校验块级校验解决的是“单块数据在传输过程中是否被破坏”但有两个盲区一是 2 字节 CRC 的碰撞概率二是服务端磁盘写入时段的静默损坏这种损坏可能让每块分别校验都通过最终文件却和源文件不一致。所以这个资源推荐的策略是双层校验传输过程中逐块用完整 MD5 校验传输结束后再对两端整个文件做一次全量 MD5 比对。全量比对代码很短# 在源机器和接收机器上分别执行对比输出的 md5 值 md5sum test.bin全量 MD5 适合作为最终裁决但不要替代块级校验。因为全量校验只能告诉你“文件坏了”不能告诉你“坏在哪一块”网络传输该不该重发、从哪个偏移量重发还得靠块级信息。顺序是先块级校验保证边界正确再全量校验消除碰撞风险两条腿走路。5.3 原子落盘临时文件加重命名避免半成品被读取生产中流传的文件经常被下游程序监听如果接收方把数据直接写最终文件写着写着下游就打开了读到的必然是不完整的文件。这个资源对每个接收任务维护一个.part文件所有块都确认完成后再os.replace成最终名字。os.replace在同一个文件系统内是原子操作进程崩溃也不会出现“目标文件写到一半”的状态。这个模式同时解决了另一个问题断点续传时.part文件本身就是断点偏移量的记录载体不需要额外设计状态文件。实际部署中我会再加一条规矩服务端对超过 24 小时没有更新的.part文件做清理回收不然磁盘会被半成品占满这是运维最容易忽略的存储风险。6. 验证与调优把吞吐和数据完整性一起测出来6.1 三个能直接跑的验证手段拿到这份资源后我习惯按三步验证先测小文件确认协议正确再测大文件确认吞吐最后做断连测试确认重传和原子落盘生效。# 生成一个 1GB 测试文件 dd if/dev/urandom oftest.bin bs1M count1024 # 终端 A启动服务端 python3 server.py # 终端 B启动客户端 python3 client.py 127.0.0.1 9000 ./test.bin # 传输完成后对比完整性 md5sum test.bin recv.bin小文件用dd if/dev/zero生成全零文件测不出校验逻辑一定要用/dev/urandom。全零文件块之间没有差异CRC 碰撞概率会异常升高误导你低估校验开销。断连测试就是在客户端传输过程中用CtrlC杀掉客户端再重跑续传命令重点观察.part文件是否正确保留、服务端是否从断点继续。6.2 内核参数与 Buffer Size 调优传输速度上不去先别怀疑代码九成是内核 TCP 缓冲区上限压着。Linux 默认的发送和接收缓冲区不大传大文件要放开# 查看当前上限 sysctl net.core.wmem_max net.core.rmem_max # 临时放开到 8MB重启后失效 sysctl -w net.core.wmem_max8388608 sysctl -w net.core.rmem_max8388608内核参数放开后再把应用层的 socket 缓冲区调上去sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 262144)。块大小从 64 KB 提到 256 KB 时这两层必须同步调否则 sendall 内部会被拆成多次小发送先触发半包再触发 Nagle传输速度不升反降。调整的顺序一定是先确认内核上限再调 socket 缓冲最后调块大小。6.3 一个值得养成的验证习惯从那以后我每次上线这类 TCP 传输工具都会强制走一遍完整流程生成随机大文件、启动服务端、跑客户端、传输后立即md5sum对比、然后主动断连一次验证.part续传。整套操作一分钟不到但能同时把粘包处理、校验逻辑、原子落盘三条链路全测到位。文件传输这类工具功能看着简单真正偷懒一次生产环境就会用一次数据损坏来教育你。希望这套步骤和这份资源里的实现能帮你把坑都提前踩平。本文还有配套的精品资源点击获取