TPC服务器断点续传的核心写法是利用服务端和客户端各自记录已传输的字节偏移量,在连接恢复后通过HTTP Range头或Socket自定义协议通知对方从指定位置继续读写文件。如果直接重新传整个文件,浪费带宽不说,在弱网场景下根本传不完,下面从协议设计、核心代码、断点状态存储三个维度拆解,最后给出可直接落地的Java实现方案。
tpc服务器断点续传代码怎么写
先搞懂协议层如何支持断点续传
断点续传的根基在协议设计,如果你用的是HTTP协议,服务端必须支持Range头,行业共识认为,Range头是HTTP/1.1规范中定义的标准机制,服务端返回206 Partial Content状态码即可实现断点续传,客户端在重连时带上Range: bytes=1024-,服务端就从第1024字节开始读文件返回数据。
如果你们公司自己定TCP私有协议,那就必须在报文头里加一个字段来记录偏移量,我用过比较稳妥的方案是:自定义报文头包含magic number、command type、sequence id、offset、data length这几个字段,其中offset就是断点续传的关键,每次发送数据包时带上这个偏移量,接收方根据偏移量决定往文件的哪个位置写入。
Java实现服务端断点续传的核心代码
下面这套代码基于Netty框架实现,适用于TPC服务器文件上传场景,核心逻辑是记录每次请求的偏移量并支持从指定位置追加写入。
服务端核心处理逻辑:
public class FileUploadHandler extends SimpleChannelInboundHandler<FileUploadPacket> {
// 存储文件上传状态,key为客户端标识+文件名,value为已上传字节数
private ConcurrentHashMap<String, Long> uploadProgress = new ConcurrentHashMap<>();
private String uploadDir = "/data/tpc_uploads/";
@Override
protected void channelRead0(ChannelHandlerContext ctx, FileUploadPacket packet) {
String fileKey = packet.getClientId() + "_" + packet.getFileName();
long offset = packet.getOffset();
String filePath = uploadDir + packet.getFileName();
try {
// 打开RandomAccessFile,seek到指定偏移位置写入
RandomAccessFile raf = new RandomAccessFile(filePath, "rw");
raf.seek(offset);
raf.write(packet.getData());
raf.close();
// 更新进度
uploadProgress.put(fileKey, offset + packet.getData().length);
// 如果文件传完,做完整性校验
if (packet.isLastPacket()) {
String md5 = MD5Util.getFileMD5(filePath);
if (md5.equals(packet.getFileMd5())) {
ctx.writeAndFlush(new UploadResponse(true, "文件上传完成"));
} else {
ctx.writeAndFlush(new UploadResponse(false, "MD5校验失败,请重新上传"));
}
uploadProgress.remove(fileKey);
} else {
ctx.writeAndFlush(new UploadResponse(true, String.valueOf(offset + packet.getData().length)));
}
} catch (Exception e) {
ctx.writeAndFlush(new UploadResponse(false, "写入失败: " + e.getMessage()));
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
// 连接断开时保留uploadProgress中的记录,后续恢复时使用
System.out.println("连接断开,保留当前断点状态");
}
}
客户端断点续传代码:
public class FileUploadClient {
private String serverHost = "192.168.10.20";
private int serverPort = 8080;
private String clientId = "client-001";
private String baseDir = "/data/tpc_uploads/";
public void upload(String filePath, String fileName) throws Exception {
RandomAccessFile raf = new RandomAccessFile(filePath, "r");
long fileLength = raf.length();
// 查询服务端已经接收了多少字节
long uploadedBytes = queryUploadedBytes(fileName);
if (uploadedBytes >= fileLength) {
System.out.println("文件已经上传过,无需重复上传");
raf.close();
return;
}
raf.seek(uploadedBytes);
byte[] buffer = new byte[8192];
int bytesRead;
long offset = uploadedBytes;
while ((bytesRead = raf.read(buffer)) != -1) {
byte[] sendData = Arrays.copyOf(buffer, bytesRead);
sendPacket(fileName, offset, sendData, false);
offset += bytesRead;
Thread.sleep(10); // 避免发送过快导致服务端处理不过来
}
// 发送最后一个包,标记文件结束
sendPacket(fileName, offset, new byte[0], true);
raf.close();
System.out.println("文件上传完成");
}
private void sendPacket(String fileName, long offset, byte[] data, boolean last) {
FileUploadPacket packet = new FileUploadPacket();
packet.setClientId(clientId);
packet.setFileName(fileName);
packet.setOffset(offset);
packet.setData(data);
packet.setLastPacket(last);
// 通过已有的channel发送
Channel channel = getChannel();
ChannelFuture future = channel.writeAndFlush(packet).syncUninterruptibly();
}
}
断点状态存储选型
断点信息不能只放在内存里,应用重启就丢了,我建议区分场景:如果文件上传量不大,用Redis存储key-value对即可;大规模场景用MySQL或PostgreSQL的表来记录。
Redis存储方案:
# 存储客户端-文件的偏移量 SET tpc_upload_001_大型数据库备份.zip 52428800 # 设置过期时间,比如24小时 EXPIRE tpc_upload_001_大型数据库备份.zip 86400
数据库方案建表语句:
CREATE TABLE tpc_upload_progress (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
client_id VARCHAR(64) NOT NULL,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(512) NOT NULL,
uploaded_bytes BIGINT NOT NULL,
total_bytes BIGINT NOT NULL,
status TINYINT DEFAULT 0 COMMENT '0-上传中,1-已完成',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_client_file (client_id, file_name)
);
每次收到数据包就UPDATE tpc_upload_progress SET uploaded_bytes = ? WHERE client_id = ? AND file_name = ?,客户端重连后先查这张表,拿到已上传的字节数再继续传。
大文件断点续传的效率优化
多线程分块上传与断点续传结合
文件超过几个GB时,单线程传太慢了,业内专家指出,把文件切成多个块,每个块独立传输,单独记录断点,能大幅提升重传效率。
实现思路:把文件按固定大小切块(比如每块8MB),客户端为每块分配一个id,上传时带上块编号,服务端收到某个块后记录该块已完成,断线恢复后,只需重新传输未完成的块,已完成的块直接跳过,这样避免了从头传一遍的尴尬。
文件秒传逻辑的联想
断点续传往深了做,还能联动秒传功能,传输前先计算文件的MD5值,服务端查一下数据库里是否存在相同MD5的文件,如果存在,直接返回秒传成功,这能省掉大量不必要的传输流量。
据相关统计,在很多企业内部网络环境中,重复文件占比可达两成左右,秒传能明显降低带宽压力。
断点续传的性能瓶颈与调优
服务端写入磁盘时,RandomAccessFile的seek和write是线性操作的,高并发场景下性能会受限制,考虑引入队列,收到数据包后先放进内存队列,后台线程批量刷盘,但要注意刷盘策略,极端情况下掉电会丢失最后几秒的数据,对流式传输要求不高的场景,可以接受这个折中方案。
另一个调优点在于socket缓冲区,传输大文件时,把socket receive buffer调大一点,比如256KB,能减少网络拥塞导致的丢包和重传。
tpc客户端连接断开后如何续传
客户端自动重连与续传逻辑
代码里需要加一个重连机制,检测到连接断开后,使用指数退避策略重连,比如第一次1秒后重试,第二次2秒,第三次4秒,最大不超过30秒,重连成功后,调用查询进度的接口拿偏移量,继续传输。
public void uploadWithResume(String filePath, String fileName) throws Exception {
int retryCount = 0;
while (true) {
try {
connect();
long uploaded = queryUploadedBytes(fileName);
uploadFromOffset(filePath, fileName, uploaded);
break;
} catch (IOException e) {
retryCount++;
long waitTime = Math.min(30, (long) Math.pow(2, retryCount));
System.out.println("连接断开," + waitTime + "秒后重试...");
Thread.sleep(waitTime 1000);
closeChannel();
}
}
}
续传时文件校验与防重复写入
续传前要做两件事:查询服务端已有字节数,再对比本地文件长度,如果本地文件的字节数比服务端记录的偏移量还小,说明本地文件也被修改过,需要重建断点,从头传,如果服务端偏移量大于本地文件长度,说明服务端数据有问题,需要删除服务端文件重新传。
还有种情况,客户端和服务端记录的偏移量一致,但服务端实际存储的数据是坏的,比如前一个连接传了一部分错误数据进去,此时需要服务端额外提供校验接口,用MD5比对已传部分的完整性,不校验的话,文件传完发现损坏,浪费时间事小,数据错误导致业务故障就麻烦了。
四种断点续传实现方式对比
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HTTP Range | 标准协议,实现简单,各种工具支持 | 只适用于客户端主动拉取或上传场景 | 网页播放器、CDN分发、网盘上传 |
| 私有TCP + 偏移量字段 | 灵活控制,效率高 | 需要自己设计协议和状态管理 | 企业内部专用的传输工具 |
| Ftp断点续传(REST命令) | 传统FTP协议支持,运维熟悉 | 传输效率一般,吞吐量受限 | 传统制造业机房环境 |
| 基于对象存储的分片上传 | 依赖云服务商SDK,可靠性高 | 需引入云厂商依赖,有网络延迟开销 | 简米云OSS、酷番云COS等场景 |
tpc网关服务器断点续传常见问题排查
续传时进度丢失怎么办
优先检查Redis或数据库中的断点记录是否存在,Redis记录如果设了过期时间,比如24小时,超出时间后就没法续传了,需要调大过期时间,数据库记录如果写入失败,多半是并发写同一文件的记录产生了锁冲突,解决思路是给client_id + file_name设计唯一索引,用INSERT ... ON DUPLICATE KEY UPDATE做原子操作。
服务端返回偏移量不准确如何处理
这通常发生在多块数据包乱序到达时,比如第一个包还没写到磁盘,第二个包就把记录覆盖了,解决办法是保证同一文件写入的串行性,用ConcurrentHashMap按文件名加锁,确保同一时间只有一个线程在写同一个文件。
private ConcurrentHashMap<String, Object> fileLocks = new ConcurrentHashMap<>();
private Object getFileLock(String fileName) {
return fileLocks.computeIfAbsent(fileName, k -> new Object());
}
public void writeToFile(String fileName, byte[] data, long offset) {
Object lock = getFileLock(fileName);
synchronized (lock) {
try (RandomAccessFile raf = new RandomAccessFile(uploadDir + fileName, "rw")) {
raf.seek(offset);
raf.write(data);
} catch (Exception e) {
// 处理异常
}
}
}
断点续传对服务器磁盘空间有额外要求吗
有,断点续传会临时占用被传输文件大小约1.5倍的磁盘空间,例如要传100GB的文件,中途断了,已传的空间不会被释放,同时日志、临时文件和断点记录还需要额外空间,建议大文件上传目录预留至少文件大小两倍的空间,避免磁盘写满导致全部传输失败。
tpc服务器断点续传代码实现注意事项
处理网络异常时,记得区分临时抖动和永久失效,TCP长连接断开后,应该立刻停止当前线程的写操作,防止大量已发送但未被确认的数据堆积在socket缓冲区里,重连完成后,先检查双方约定的协议版本号是否一致,不一致则要升级客户端。
关于日志记录,每传输一个数据包都记录全量日志太占空间,按照经验,每传输满50MB记录一次日志即可,日志字段包括文件名、偏移量、时间戳、耗时。
代码发布时注意兼容性,服务端升级了断点续传协议,旧版本客户端还能不能连?如果协议不兼容,服务端要能识别旧客户端并返回升级提示。
tpc服务器断点续传代码常见问题解答
tpc服务器断点续传代码使用什么编程语言写最合适
Java和Go是目前最主流的选择,Java生态成熟,Netty在处理高并发网络IO方面表现稳定;Go语言自身标准库net包就能实现可靠的TCP服务,编译产物免部署,特别适合运维场景,选型依据是团队技术栈和业务规模,小规模传输用Python也可以。
断点续传期间服务端重启会导致已传数据丢失吗
如果断点状态存储与文件写入使用了内存映射或未刷盘的缓冲区,重启后数据必然丢失,稳妥的做法是每次写入成功就更新数据库记录并调用raf.getFD().sync()强制刷盘,虽然性能有点损失但数据安全,如果不要求强一致,每写一个块刷一次盘也是能接受的折中,重启后最多丢最后一个块的数据。
tp服务器文件上传断点续传会不会被防火墙阻断
TCP长连接在长时间无数据传输时,防火墙会断开空闲连接,解决方法是客户端定期发送心跳包,间隔建议30秒或60秒,如果服务端开启SO_KEEPALIVE,那只是操作系统层面的探测,不能完全替代自定义心跳,传输大文件期间,网络设备如果启用了超时限制,必须调整设备策略或缩短传输间隙。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/791461.html


