ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

自研TCP文件传输服务器:Socket编程、拆包策略与断点续传实践

自研TCP文件传输服务器:Socket编程、拆包策略与断点续传实践 简介基于TCP协议的文件传输服务器项目由开发者使用Visual Studio 2015编写完成面向网络编程初学者、课程设计人员以及需要实现可靠文件传输功能的开发者。项目以MFC对话框作为交互界面展示了从服务器监听、客户端连接、预传文件名与文件大小到创建目标文件、接收数据并确认完成的完整流程。压缩包共42个文件主要包含cpp/h/rc源码、sln/vcxproj工程配置、res资源文件以及exe可执行程序、pdb调试信息、ipch预编译缓存和log日志等整体67.33MB可直接打开工程查看细节或运行程序体验效果。已有1520人浏览学习。源码中涵盖了Socket套接字编程、路径与权限检查、文件打开模式设置、TCP确认重传机制和四次挥手等关键知识点同时MFC界面设计也体现了网络逻辑与图形化操作相结合的方法。这些内容对深入理解TCP通信原理、练习C网络编程、开展文件传输类课设或毕设都有实际参考价值。1. TCP文件传输服务器为什么自己写一个比直接用FTP更可控当你需要在两台机器之间搬一个几个GB的日志包、模型权重或者数据库备份时第一反应往往是开个FTP或者扔到网盘。但FTP在跨网段、弱网环境下经常出现连接被重置、断点续传要另配软件、传输过程没有任何进度反馈的问题而这些恰恰是TCP文件传输服务器能解决的。所谓TCP文件传输服务器就是基于TCP协议自己实现一套文件收发服务客户端连上来按约定好的帧格式把文件名、长度、内容分块发过去服务端落盘并回执。它的优势在于你能精确控制每个tcp连接的缓冲、重传和校验行为也能把传输逻辑嵌进自动化脚本里而不是依赖一个带图形界面的FTP工具。这篇笔记适合两类人一类是刚接触tcp socket编程想搞明白文件传输到底怎么设计报文格式的初学者另一类是已经在用FTP但被性能或稳定性折磨的运维和开发想换成可控的自研传输通道。我会从TCP协议栈的行为讲起直接给出一套能跑通的最小实现再聊参数调优和那些让人翻车的坑。最后你会得到一份可以直接抄走的、支持断点续传的服务器骨架。2. 传输模型与协议设计从TCP协议栈到自定义帧格式2.1 TCP和UDP在文件传输上的本质差别做文件传输时很多人会纠结udp和tcp协议的区别。UDP的优势是延迟低、开销小但它不保证数据顺序也不保证送达丢包后应用层要自己做重传和排序复杂度瞬间上升。TCP则把可靠性做进了协议栈里序号、确认应答、超时重传、流量控制和拥塞控制都由内核处理。对文件传输来说可靠性远比微小的延迟重要所以生产环境里绝大多数方案都基于TCP。但TCP的可靠性也带来一个副作用它是一个“字节流”协议不维护消息边界。你一次write发送的内容对端read时可能被拆成多次取走也可能把两次write的内容合并成一次返回这就是经典的粘包和半包问题。所以自研TCP文件传输服务器第一件事不是写收发循环而是设计好帧格式让接收方能从字节流里正确切出每一笔完整的传输请求。2.2 一个够用的自定义帧格式我常用的帧格式是“固定头 变长负载”。固定头用二进制结构体包含魔数、版本、命令类型、负载长度和可选校验值。魔数用来快速识别新连接是否在说我们的协议避免把乱七八糟的字节流当合法请求解析负载长度则精确告诉接收方要读多少字节才算一个完整的帧。命令类型至少要有“上传文件头”“上传数据块”“上传结束”“下载请求”“下载数据块”“下载结束”“错误回执”这七种。上传文件头里包含文件名、文件大小、每块大小下载请求则只需文件名。把命令类型放在帧头接收端就能通过一个switch分支决定后续解析逻辑而不是靠猜。这里给出Python的struct定义后续所有收发代码都基于它import struct # 帧头格式魔数(2字节) 版本(1字节) 命令(1字节) 负载长度(4字节) # 大端序避免不同平台字节序问题 HEADER_FMT !HBBI HEADER_LEN struct.calcsize(HEADER_FMT) MAGIC 0x5A5A def build_frame(cmd, payload: bytes) - bytes: header struct.pack(HEADER_FMT, MAGIC, 1, cmd, len(payload)) return header payload def parse_frame(data: bytes): # 返回 (cmd, payload) 或 None数据不足一个完整帧头 if len(data) HEADER_LEN: return None magic, version, cmd, length struct.unpack(HEADER_FMT, data[:HEADER_LEN]) if magic ! MAGIC: raise ValueError(fbad magic: {hex(magic)}) return cmd, data[HEADER_LEN:HEADER_LEN length]说明一下参数!表示网络字节序大端H是2字节无符号整数B是1字节无符号整数I是4字节无符号整数。魔数0x5A5A是随便选的只要不和常见二进制格式撞车就行。版本字段保留是为了以后协议升级时不至于连帧头都推倒重来。负载长度用4字节意味着单帧最大能表示4GB实际传输中我们会把文件切成小块所以不会真用到那么大。2.3 粘包与半包的拆包策略有了帧头接收端就必须维护一个接收缓冲区。每次从socket读到数据先追加进缓冲区然后循环尝试解析如果缓冲区长度大于等于帧头长度就解析帧头拿到负载长度再判断缓冲区中是否已经包含完整的负载如果不够就等下一次read如果够就切出这一帧交给业务逻辑剩下的字节继续循环解析。这个“先攒后拆”的模式是TCP拆包的标准做法。实际代码里我会给recv指定一个合理的单次读取长度比如64KB。不要一次读太少否则频繁的系统调用会拉低吞吐也不要一次读太大因为应用层缓冲区通常是堆上分配的一块固定内存超过后反而触发拷贝开销。拆包时要注意帧头里的负载长度必须是可信的否则恶意客户端就能用超大长度声明把服务端拖死所以服务端要限制单帧最大长度超过直接断开。2.4 为什么不用现成协议库有现成的协议为什么要自己画帧常见做法是直接上HTTP或者WebSocket。HTTP的文件上传走multipart或application/octet-stream优点是穿透性好缺点是传输大文件时没有进度与块级校验断点续传要依赖Range头配合服务端实现复杂度也不低。WebSocket则偏实时双向通信不适合做大批量文件搬运。自研TCP文件传输服务器的核心收益是每一块数据的确认时机、重传粒度、校验方式都完全透明。比如你可以把文件切成1MB的块每块带CRC32或MD5接收方每收到一块就回执一个确认帧发送方只重发失败的那一块而不像TCP底层失败时整条连接都受影响。这种设计对弱网传输非常友好也是这个标题值得投入的原因。3. 用Python写一个最小可用的TCP文件传输服务器多客户端与收发流程3.1 服务端架构记住连接状态文件传输和普通Echo服务最大的区别是服务端要保存“会话状态”当前连接的客户端正在传哪个文件、已经写了多少字节、按什么块大小收。如果每个连接只用一个线程处理直接用dict以socket对象为key存状态就行。python的socket文件对象本身可哈希正好当key。多客户端处理我一般用threading模块起线程每个连接一个线程。Python的GIL对文件传输这种I/O密集场景影响不大因为瓶颈在磁盘和网络不在CPU计算。如果要更高的并发后续可以换asyncio或用selectors做事件驱动但线程模型最容易看懂、也最好排查问题。生产环境里几百个并发连接用线程完全够别一上来就上异步。状态机设计如下每个连接有phase字段初始为IDLE收到上传文件头后变为RECEIVING并记录目标文件句柄和剩余字节收到数据块时校验块序号并写入文件文件写完后回到IDLE。客户端断开时如果状态还在RECEIVING要清理未完成的临时文件。3.2 服务端核心代码下面这段代码实现了上传文件的完整流程下载逻辑代码对称就不再重复贴。注意每一步的异常处理都很关键。import os import socket import threading import struct HEADER_FMT !HBBI HEADER_LEN struct.calcsize(HEADER_FMT) MAGIC 0x5A5A CMD_UPLOAD_HEAD 1 CMD_UPLOAD_DATA 2 CMD_UPLOAD_END 3 CMD_DOWNLOAD_REQ 4 CMD_DOWNLOAD_DATA 5 CMD_DOWNLOAD_END 6 CMD_ERROR 7 MAX_FRAME_SIZE 8 * 1024 * 1024 # 单帧最大8MB STORE_DIR ./received_files def recv_exact(conn, n): 从连接中读取n字节拆包时用 buf b while len(buf) n: chunk conn.recv(n - len(buf)) if not chunk: raise ConnectionError(socket closed) buf chunk return buf def recv_frame(conn): 阻塞式读一个完整帧返回(cmd, payload) header recv_exact(conn, HEADER_LEN) magic, version, cmd, length struct.unpack(HEADER_FMT, header) if magic ! MAGIC: raise ValueError(bad magic) payload recv_exact(conn, length) return cmd, payload def handle_upload(conn, payload, state): # payload: 文件名字段(2字节长度) 文件名 文件大小(8字节) 块大小(4字节) name_len struct.unpack(!H, payload[:2])[0] filename payload[2:2 name_len].decode(utf-8, errorsignore) file_size, block_size struct.unpack(!QI, payload[2 name_len:2 name_len 12]) save_path os.path.join(STORE_DIR, os.path.basename(filename)) fh open(save_path, wb) state.update({phase: RECEIVING, fh: fh, remaining: file_size, block_size: block_size, filename: filename}) def handle_upload_data(conn, payload, state): # payload前4字节为块序号后续为文件内容 seq struct.unpack(!I, payload[:4])[0] data payload[4:] fh state[fh] fh.write(data) state[remaining] - len(data) # 回执一个确认帧客户端可据此知道本块已落盘 ack_payload struct.pack(!I, seq) conn.sendall(build_frame(CMD_UPLOAD_HEAD, ack_payload)) # 借用一下命令字段 def handle_upload_end(conn, state): if state.get(fh): state[fh].close() state[remaining] 0 state[phase] IDLE print(fupload done: {state[filename]}) def client_thread(conn, addr): print(ftcp连接已建立: {addr}) state {} try: while True: cmd, payload recv_frame(conn) if cmd CMD_UPLOAD_HEAD: handle_upload(conn, payload, state) elif cmd CMD_UPLOAD_DATA: handle_upload_data(conn, payload, state) elif cmd CMD_UPLOAD_END: handle_upload_end(conn, state) break except Exception as e: print(f连接异常: {addr} {e}) if state.get(fh): state[fh].close() finally: conn.close() def main(): os.makedirs(STORE_DIR, exist_okTrue) server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 7890)) # tcp端口号按需修改 server.listen(128) print(TCP文件传输服务器已启动端口7890) while True: conn, addr server.accept() threading.Thread(targetclient_thread, args(conn, addr), daemonTrue).start() if __name__ __main__: main()这段代码的逻辑说明recv_exact解决半包它循环读取直到读满指定字节数保证拆包的基础。recv_frame先读帧头再根据帧头里的长度读负载把读帧这件事封装成阻塞调用让业务代码不用自己维护缓冲区。handle_upload解析文件头后立即创建文件对象并在state里记录剩余字节数。handle_upload_data模拟了块确认发送方可以根据确认帧知道哪些块发送成功。连接异常时关闭文件句柄避免残留脏数据。参数说明监听端口7890只是示例生产环境建议选高位端口避开常见的21、80、443减少被扫描的概率。listen(128)表示内核中未完成握手队列的长度并发量大的话可以调大。recv_exact里每次recv的缓冲大小来自n - len(buf)这个值会随剩余量变化但不会超过单帧最大长度。3.3 客户端核心代码客户端逻辑比服务端简单就是建连、发帧、读回执但要特别注意大文件的分块读取。不要一次把整个文件读进内存否则几GB的文件直接把客户端内存打爆。正确做法是固定一个chunk_size比如1MB用read循环读每个chunk封装成一帧发送然后等待确认。import socket import sys import os import struct HOST 127.0.0.1 PORT 7890 CHUNK_SIZE 1024 * 1024 # 1MB def recv_exact(conn, n): buf b while len(buf) n: chunk conn.recv(n - len(buf)) if not chunk: raise ConnectionError(socket closed) buf chunk return buf def send_upload(conn, filepath): filename os.path.basename(filepath) file_size os.path.getsize(filepath) name_bytes filename.encode(utf-8) head_payload struct.pack(!H, len(name_bytes)) name_bytes \ struct.pack(!QI, file_size, CHUNK_SIZE) conn.sendall(build_frame(CMD_UPLOAD_HEAD, head_payload)) seq 0 with open(filepath, rb) as f: while True: data f.read(CHUNK_SIZE) if not data: break frame build_frame(CMD_UPLOAD_DATA, struct.pack(!I, seq) data) conn.sendall(frame) # 等待服务端确认防止客户端发送过快把内核缓冲塞满 cmd, ack_payload recv_frame(conn) if cmd ! CMD_UPLOAD_HEAD: raise RuntimeError(unexpected ack) seq 1 conn.sendall(build_frame(CMD_UPLOAD_END, b)) print(f发送完成共 {seq} 块{file_size} 字节) def main(): if len(sys.argv) 2: print(用法: client.py 文件路径) return with socket.create_connection((HOST, PORT), timeout30) as conn: send_upload(conn, sys.argv[1]) if __name__ __main__: main()逻辑说明客户端逐块读文件每块带序号。序号的作用是让服务端能检测块丢失或乱序虽然TCP保证顺序但业务层的序号也能帮助后续的断点续传实现。每次发送后recv_frame等待确认这是一种简单的流控避免数据大量堆积在socket缓冲区导致内存无谓占用。参数说明timeout30表示建立连接的超时时间超过就抛异常。CHUNK_SIZE是1MB实际调优时建议测试4KB到4MB之间的不同值。块太小帧头占比高吞吐上不去块太大单帧内存开销高且出错重传的粒度粗。常见做法是先用1MB起步弱网环境降到128KB局域网可以升到4MB。4. Windows/Linux下的TCP参数调优端口号、缓冲区、Nagle与netsh int tcp4.1 端口号与连接状态排查服务器监听端口选好后经常遇到的问题就是端口被占用。用netstat -ano | findstr 7890Windows或ss -lntp | grep 7890Linux查看端口状态。如果出现大量TIME_WAIT连接说明客户端频繁短连接关闭这时候要么让连接复用要么调整系统参数。TIME_WAIT多不是坏事它刚好好处是保证旧连接的数据不会串到新连接上。但量大时会占用本地端口和少量内核资源。常见做法是在客户端开启SO_REUSEADDR并在服务端设置SO_REUSEADDR避免重启时端口被残留连接卡住。4.2 TCP缓冲区大小怎么设TCP发送和接收缓冲区的大小直接影响吞吐。缓冲区太小吞吐受限于窗口缓冲区太大内存浪费且可能增加延迟。Linux下默认值通常在几十KB到几百KB大文件传输时建议调大。用socket.setsockopt设置# 服务端与客户端都要设置单位是字节 s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 1024) # 发送缓冲1MB s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024) # 接收缓冲1MB但Linux有个规则内核会把你设置的值乘以2并且不小于一个下限。你设1MB实际可能变成2MB。所以更靠谱的做法是设置完之后用getsockopt读回来确认实际值避免误判。Windows下设置逻辑类似但底层实现略有差异以实际读回为准。4.3 Nagle算法与延迟确认的矛盾Nagle算法会把小包合并成大包发送减少网络中的小报文数量但对需要低延迟的场景是灾难。文件传输如果频繁发送小块Nagle会等一下后续数据凑包而对端的延迟确认机制又会等待数据填满才回ACK双方互相等待可能造成几十毫秒甚至更久的“确认延迟”。对文件传输这种需要高吞吐、但不需要低延迟交互的场景可以关闭Nagles.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)这样每个sendall都会立即发送配合我们前面设计的1MB块大小实际上不会产生大量小包所以关闭Nagle几乎没有副作用。如果你用Windows并跑大文件传输还可以尝试一条内核参数netsh int tcp set global timestampsenabled这条命令的作用是开启TCP时间戳选项在长肥网络高带宽、高延迟下提供更精确的RTT采样帮助拥塞控制算法更好地估算窗口。注意它只影响新的tcp连接已经建立的连接不受影响所以改完参数后要重启传输进程或系统。netsh int tcp show global可以确认当前状态。在Linux下对等操作是调整net.ipv4.tcp_timestamps默认是开启的一般不用动。重点是tcp_sack、tcp_window_scaling要保持开启否则大窗口传输会受限。用sysctl net.ipv4.tcp_sack查看。4.4 最大段大小和窗口缩放TCP握手时会协商最大段大小MSS通常由网卡MTU决定。以太网MTU是1500字节去掉IP和TCP头MSS常见为1460字节。应用层即使一次write 1MB协议栈也会按MSS切片。如果MTU配置成9000巨型帧吞吐会明显提升但需要交换机支持。这个在跨机房或云环境里通常不可控所以应用层最大的收益还是来自调整缓冲区和关闭Nagle。窗口缩放window scaling是TCP扩展选项用于支持大于64KB的窗口。Linux默认开启Windows在netsh int tcp set global autotuninglevelnormal下也会自动开启。如果传输吞吐上不去可以用netsh int tcp show global检查接收窗口自动调谐级别千万别设成disabled。4.5 传输参数对吞吐的影响测试方法调优不能靠感觉。我的做法是写一个只测吞吐的小脚本客户端发固定大小的数据块服务端只读取不落盘比较不同参数组合下的速率。用time命令计时或者直接打印每秒字节数。测试时先测本机回环127.0.0.1再测局域网最后测跨网段逐层定位瓶颈。如果本机回环就慢问题在协议栈或代码如果回环快但局域网慢问题在网络设备或网卡配置。一个容易被忽略的坑是CPU频率和网卡中断绑核。在大量tcp连接下多队列网卡会把中断分散到多个CPU但如果只开单队列所有中断都打在一个核上吞吐会被锁死。Linux下用ethtool -L eth0 combined 4调整队列数Windows下一般在网卡高级属性里改。这些不是必须但值得了解。5. TCP文件传输常见的5个坑粘包、半包、断线、防火墙与踩坑记录5.1 粘包导致文件头解析错误现象服务端收到的文件名变成乱码或者文件大小字段巨大直接内存异常。原因客户端连续发送文件头和数据块TCP把两次write的数据合并成一个包到达服务端没有按帧边界切分而是把文件头后面的数据块内容误当作文件身的一部分。解决必须按“固定头 负载长度”的协议拆包。如果用的是阻塞式recv_exact要求每次读够帧头长度再读负载长度就不会粘包。如果自己维护缓冲区一定要循环解析不能假设一次recv正好是一个完整帧。5.2 半包导致连接假死现象客户端发了文件头服务端一直卡在recv不返回看起来像死锁。原因文件头帧总长度超过一次recv返回的字节数服务端只读到半个帧头然后继续尝试解析但帧头不完整抛异常或进入等待。如果代码里没有用recv_exact就会出现这个问题。解决统一使用recv_exact读固定长度不要用单个recv硬解析。卡住时用netstat -an看连接状态如果Recv-Q一直有数据但应用不消费基本就是半包处理没写对。顺带检查是否因为忘记调用recv读取剩余数据导致服务端缓冲区被占满客户端发送被阻塞。5.3 客户端发送过快导致内存暴涨现象客户端已经全部发送完毕服务端还在慢慢写盘客户端进程内存占用却持续上升。原因这是发送端没有做块级确认导致的。发送方不断sendall内核socket发送缓冲区满后应用层数据继续堆积在用户态内存里我们代码里用的是文件流本来不会全部读入内存但如果被迫重试发送逻辑没写好就可能把整个待发送数据缓存下来。解决采用每块确认机制。客户端每发送一块就等服务端回一个ACK再发下一块。这个做法虽然让RTT成为吞吐上限但能精确控制内存占用。要提速就改用滑动窗口允许同时在途N个块N根据RTT和带宽估算比如局域网可以放4个跨网段放16个。5.4 防火墙或安全组把连接静默丢弃现象本机能连局域网能连换到云服务器上就永远连接超时。原因云平台安全组默认只放行少数端口或者系统防火墙拦截了非标准端口。TCP握手SYN包被丢弃客户端表现为超时而不是连接被拒绝。解决先看防火墙规则Windows下用netsh advfirewall firewall add rule nametcp file dirin actionallow protocolTCP localport7890Linux下用firewall-cmd --add-port7890/tcp或iptables放行。云服务器还要在控制台的安全组入方向增加对应端口规则。验证是否被防火墙拦截可以在服务端用tcpdump -i any port 7890看是否有SYN到达如果没到就是安全组/云策略问题到了但没响应就是服务端进程问题。5.5 服务端重启后大量端口处于TIME_WAIT现象服务端崩溃后立即重启却报Address already in use。原因之前接受过大量短连接连接关闭后进入TIME_WAIT状态默认持续60秒Linux或240秒Windows并占用监听端口。解决服务端监听socket设置SO_REUSEADDR后可以立即复用端口。但注意这只对监听socket有效已建立的连接不可复用。如果确实想缩短TIME_WAITLinux下可调整net.ipv4.tcp_fin_timeout30Windows下没这么细的公开参数不建议改。另外客户端应尽量复用同一个连接传多个文件而不是每次新建tcp连接这样也减少了TIME_WAIT。补充一个真事有次我遇到客户端和服务端都在内网但传输10%后速度从500MB/s掉到10MB/s查了半天发现是服务端写盘时用了默认文件打开方式没有带O_DIRECT导致页缓存反复换入换出。这个问题和TCP无关但属于文件传输服务器里最容易忽略的一个坑先把磁盘I/O搞明白再去碰网络参数。写文件时建议用buffering参数调大Python文件对象的缓冲或者直接以二进制模式顺序写避免随机写。6. 让传输更可靠断点续传与校验的进阶实现断点续传是文件传输服务器最常被要求的进阶功能。实现思路不复杂客户端在发送文件头时附带上一次传输的上下文服务端根据上下文决定是从头写还是从偏移量继续写。关键是要设计一个可恢复的会话记录。我建议服务端为每个传输任务生成一个唯一ID并把已完成的块序号记录在一个临时元数据文件里。客户端因为断网或人为取消而中断时重新发起传输并带上任务ID和最后确认的序号。服务端则根据项目状态跳过已经完整收到的块只接受序号连续的后续数据。这样即使传输过程中TCP协议栈报错也不需要重传整个文件。校验方面单靠TCP的校验码不够。TCP的CRC校验只覆盖每个报文段无法发现应用层数据被应用程序错误处理导致的损坏比如写盘时的指针错位。常见做法是每块附带CRC32服务端每收一块就校验一次。整个文件完成后再对文件整体计算一次SHA256客户端发送时也计算一次双方比对。CRC32快但碰撞概率略高SHA256慢但安全两者结合正好。import hashlib import zlib def calc_crc32(data: bytes) - int: return zlib.crc32(data) 0xffffffff def calc_sha256_file(filepath, chunk_size1024*1024): h hashlib.sha256() with open(filepath, rb) as f: while True: chunk f.read(chunk_size) if not chunk: break h.update(chunk) return h.hexdigest()参数说明chunk_size在计算文件大文件SHA256时建议和传输块大小一致减少内存分配次数。如果你用CRC32做块校验注意zlib.crc32返回的是无符号32位整数在Python里可能显示为负数要按位与0xffffffff转换成无符号值。发到网络时用struct.pack(!I, crc)即可。验证传输是否正确的第一步不是直接跑业务而是用dd或pv传输一个固定内容的文件然后比对两边的SHA256。我会用以下命令快速验证# 生成一个1GB随机文件用于测试 dd if/dev/urandom of/tmp/test.bin bs1M count1024 # 计算原始文件校验值 sha256sum /tmp/test.bin # 传输完成后对接收到的文件再算一次 sha256sum /tmp/received/test.bin两边输出一致说明传输链路没问题。如果实验网络环境跑10G以上大文件时注意硬盘剩余空间和文件系统上限。断点续传还有一个容易翻车的细节文件名撞车。如果两个任务上传到同一个目录且文件名相同服务端必须区分是覆盖还是续传。我的做法是在元数据里记录原始文件名和当前临时文件名比如{task_id}.part只有全部传输完成后才重命名为正式文件名。这样即使传输中断临时文件也不会被误用下次续传时直接追加到part文件上。关于传输效率还有一个经验千万别在应用层再做一层数据压缩。如果文件本身是压缩包、视频或模型权重压缩率很低且白白消耗CPU。TCP协议栈核心追求的是把数据尽快搬完压缩交给业务方决定。如果文件是文本或JSON日志可以在客户端预先压缩服务端解压后落盘但这已经超出TCP文件传输服务器的范畴。最后分享一个我的教训有一次我把TCP的SO_RCVBUF设得很大却忘了同步调整内存限制导致服务器在高并发时内存被打满。从那以后调整参数时我只看实际生效值和系统当前空闲内存不盲目堆数字。文件传输服务器表面上是网络应用真正决定上限的往往是磁盘I/O、内存分配和内核参数这三者的配合。你把帧格式、拆包逻辑、块确认和断点续传做扎实无论是传日志、传模型还是传备份都能应付自如。希望这篇笔记能帮你在自研TCP文件传输服务器的路上少走几步弯路。先从最小可跑通的版本开始再逐步加上校验和断点续传坑踩完一遍后你会发现它远比想象中可靠。本文还有配套的精品资源点击获取
返回列表