文件断点续传原理与实现

简介: 在网络状况不好的情况下,对于文件的传输,我们希望能够支持可以每次传部分数据。首先从文件传输协议FTP和TFTP开始分析,FTP是基于TCP的,一般情况下建立两个连接,一个负责指令,一个负责数据;而TFTP是基于UDP的,由于UDP传输是不可靠的,虽然传输速度很快,但对于普通的文件像PDF这种,少了一个字节都不行。

在网络状况不好的情况下,对于文件的传输,我们希望能够支持可以每次传部分数据。首先从文件传输协议FTP和TFTP开始分析,

FTP是基于TCP的,一般情况下建立两个连接,一个负责指令,一个负责数据;而TFTP是基于UDP的,由于UDP传输是不可靠的,虽然传输速度很快,但对于普通的文件像PDF这种,少了一个字节都不行。本次以IM中的文件下载场景为例,解析基于TCP的文件断点续传的原理,并用代码实现。

什么是断点续传?

断点续传其实正如字面意思,就是在下载的断开点继续开始传输,不用再从头开始。所以理解断点续传的核心后,发现其实和很简单,关键就在于对传输中断点的把握,我就自己的理解画了一个简单的示意图:

原理:

断点续传的关键是断点,所以在制定传输协议的时候要设计好,如上图,我自定义了一个交互协议,每次下载请求都会带上下载的起始点,这样就可以支持从断点下载了,其实HTTP里的断点续传也是这个原理,在HTTP的头里有个可选的字段RANGE,表示下载的范围,下面是我用JAVA语言实现的下载断点续传示例。

提供下载的服务端代码:

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.Socket;

// 断点续传服务端
public class FTPServer {

	// 文件发送线程
	class Sender extends Thread{
		// 网络输入流
		private InputStream in;
		// 网络输出流
		private OutputStream out;
		// 下载文件名
		private String filename;

		public Sender(String filename, Socket socket){
			try {
				this.out = socket.getOutputStream();
				this.in = socket.getInputStream();
				this.filename = filename;
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		@Override
		public void run() {
			try {
				System.out.println("start to download file!");
				int temp = 0;
				StringWriter sw = new StringWriter();
				while((temp = in.read()) != 0){
					sw.write(temp);
					//sw.flush();
				}
				// 获取命令
				String cmds = sw.toString();
				System.out.println("cmd : " + cmds);
				if("get".equals(cmds)){
					// 初始化文件
					File file = new File(this.filename);
					RandomAccessFile access = new RandomAccessFile(file,"r");
					//
					StringWriter sw1 = new StringWriter();
					while((temp = in.read()) != 0){
						sw1.write(temp);
						sw1.flush();
					}
					System.out.println(sw1.toString());
					// 获取断点位置
					int startIndex = 0;
					if(!sw1.toString().isEmpty()){
						startIndex = Integer.parseInt(sw1.toString());
					}
					long length = file.length();
					byte[] filelength = String.valueOf(length).getBytes();
					out.write(filelength);
					out.write(0);
					out.flush();
					// 计划要读的文件长度
					//int length = (int) file.length();//Integer.parseInt(sw2.toString());
					System.out.println("file length : " + length);
					// 缓冲区10KB
					byte[] buffer = new byte[1024*10];
					// 剩余要读取的长度
					int tatol = (int) length;
					System.out.println("startIndex : " + startIndex);
					access.skipBytes(startIndex);
					while (true) {
						// 如果剩余长度为0则结束
						if(tatol == 0){
							break;
						}
						// 本次要读取的长度假设为剩余长度
						int len = tatol - startIndex;
						// 如果本次要读取的长度大于缓冲区的容量
						if(len > buffer.length){
							// 修改本次要读取的长度为缓冲区的容量
							len = buffer.length;
						}
						// 读取文件,返回真正读取的长度
						int rlength = access.read(buffer,0,len);
						// 将剩余要读取的长度减去本次已经读取的
						tatol -= rlength;
						// 如果本次读取个数不为0则写入输出流,否则结束
						if(rlength > 0){
							// 将本次读取的写入输出流中
							out.write(buffer,0,rlength);
							out.flush();
						} else {
							break;
						}
						// 输出读取进度
						//System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");
					}
					//System.out.println("receive file finished!");
					// 关闭流
					out.close();
					in.close();
					access.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			super.run();
		}
	}
	
	public void run(String filename, Socket socket){
		// 启动接收文件线程 
		new Sender(filename,socket).start();
	}
	
	public static void main(String[] args) throws Exception {
		// 创建服务器监听
		ServerSocket server = new ServerSocket(8888);
		// 接收文件的保存路径
		String filename = "E:\\ceshi\\mm.pdf";
		for(;;){
			Socket socket = server.accept();
			new FTPServer().run(filename, socket);
		}
	}

}

下载的客户端代码:

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.StringWriter;
import java.net.InetSocketAddress;
import java.net.Socket;

// 断点续传客户端
public class FTPClient {

	/**
	 *  request:get0startIndex0
	 *  response:fileLength0fileBinaryStream
	 *  
	 * @param filepath
	 * @throws Exception
	 */
	public void Get(String filepath) throws Exception {
		Socket socket = new Socket();
		// 建立连接
		socket.connect(new InetSocketAddress("127.0.0.1", 8888));
		// 获取网络流
		OutputStream out = socket.getOutputStream();
		InputStream in = socket.getInputStream();
		// 文件传输协定命令
		byte[] cmd = "get".getBytes();
		out.write(cmd);
		out.write(0);// 分隔符
		int startIndex = 0;
		// 要发送的文件
		File file = new File(filepath);
		if(file.exists()){
			startIndex = (int) file.length();
		}
		System.out.println("Client startIndex : " + startIndex);
		// 文件写出流
		RandomAccessFile access = new RandomAccessFile(file,"rw");
		// 断点
		out.write(String.valueOf(startIndex).getBytes());
		out.write(0);
		out.flush();
		// 文件长度
		int temp = 0;
		StringWriter sw = new StringWriter();
		while((temp = in.read()) != 0){
			sw.write(temp);
			sw.flush();
		}
		int length = Integer.parseInt(sw.toString());
		System.out.println("Client fileLength : " + length);
		// 二进制文件缓冲区
		byte[] buffer = new byte[1024*10];
		// 剩余要读取的长度
		int tatol = length - startIndex;
		//
		access.skipBytes(startIndex);
		while (true) {
			// 如果剩余长度为0则结束
			if (tatol == 0) {
				break;
			}
			// 本次要读取的长度假设为剩余长度
			int len = tatol;
			// 如果本次要读取的长度大于缓冲区的容量
			if (len > buffer.length) {
				// 修改本次要读取的长度为缓冲区的容量
				len = buffer.length;
			}
			// 读取文件,返回真正读取的长度
			int rlength = in.read(buffer, 0, len);
			// 将剩余要读取的长度减去本次已经读取的
			tatol -= rlength;
			// 如果本次读取个数不为0则写入输出流,否则结束
			if (rlength > 0) {
				// 将本次读取的写入输出流中
				access.write(buffer, 0, rlength);
			} else {
				break;
			}
			System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");
		}
		System.out.println("finished!");
		// 关闭流
		access.close();
		out.close();
		in.close();
	}

	public static void main(String[] args) {
		FTPClient client = new FTPClient();
		try {
			client.Get("E:\\ceshi\\test\\mm.pdf");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
测试
原文件、下载中途断开的文件和从断点下载后的文件分别从左至右如下:


断点前的传输进度如下(中途省略):

Client fileLength : 51086228
finish : 0.020044541 %
finish : 0.040089082 %
finish : 0.060133625 %
finish : 0.07430574 %
finish : 0.080178164 %
...
finish : 60.41171 %
finish : 60.421593 %
finish : 60.428936 %
finish : 60.448982 %
finish : 60.454338 %

断开的点计算:30883840 / 51086228 = 0.604543361471119 * 100% = 60.45433614%

从断点后开始传的进度(中途省略):
Client startIndex : 30883840
Client fileLength : 51086228
finish : 60.474377 %
finish : 60.494423 %
finish : 60.51447 %
finish : 60.53451 %
finish : 60.554558 %
...
finish : 99.922035 %
finish : 99.942085 %
finish : 99.95677 %
finish : 99.96213 %
finish : 99.98217 %
finish : 100.0 %
finished!

断点处前后的百分比计算如下:

============================下面是从断点开始的进度==============================

本方案是基于TCP,在本方案设计之初,我还探索了一下介于TCP与UDP之间的一个协议:UDT(基于UDP的可靠传输协议)。

我基于Netty写了相关的测试代码,用Wireshark拆包发现的确是UDP的包,而且是要建立连接的,与UDP不同的是需要建立连接,所说UDT的传输性能比TCP好,传输的可靠性比UDP好,属于两者的一个平衡的选择,感兴的可以深入研究一下。

目录
相关文章
|
存储 Go
Golang底层原理剖析之map
Golang底层原理剖析之map
533 1
|
3月前
|
存储 监控 Shell
四、Portainer图形化管理实战与Docker镜像原理
如果觉得命令行繁琐,可以试试Portainer这个图形化管理工具,让你在网页上点点鼠标就能轻松管理容器和镜像。安装它只需要一条docker run命令,非常方便。 同时,要理解Docker为何如此高效,关键在于它的镜像原理:镜像像洋-葱一样分层,启动容器时只在外面加一层可写的“外皮”。所有改动都发生在这层“外皮”上,这就是容器启动快、占用空间小的秘诀。
519 5
|
8月前
|
监控 Go 开发者
深入解析 go 语言中的 select 语句
在 Go 语言中,`select` 是专为并发编程设计的控制结构,用于在多个 channel 操作间进行非阻塞选择。它类似于 `switch`,但所有 case 分支必须是 channel 的 I/O 操作。本文通过通俗易懂的语言和丰富代码示例,详细讲解 `select` 的各种用法,包括基本语法、空 `select`、多分支选择、`default` 分支、特点总结及最佳实践,适合初学者和有经验的开发者学习参考。掌握 `select`,能有效提升 Go 并发编程的灵活性与效率。
284 6
|
11月前
|
机器学习/深度学习 人工智能 安全
魔搭社区模型速递(3.9-3.15)
魔搭ModelScope本期社区进展:1066个模型,153个数据集,125个创新应用,13篇内容
340 1
|
人工智能 安全 算法
多端融合,打造最优落地效果的多模态百炼
本次分享由阿里云智能集团专家介绍多端融合的多模态百炼,涵盖七个方面:1)如何打造最优效果的多模态百炼;2)最新版本在生产力和产品力建设上的进展;3)rap能力升级;4)终端大模型场景探索与实践;5)内容安全工作;6)模型能力增强及生态应用;7)终端模型结合的消费链及手机、PC、车机器人等能力。重点介绍了百炼在多模态模型、效果运营中心、终端AI结合云端AI等方面的创新与优化,以及在内容安全和生态建设中的努力。
639 18
|
消息中间件 中间件 Kafka
分布式事务最全详解 ,看这篇就够了!
本文详解分布式事务的一致性及实战解决方案,包括CAP理论、BASE理论及2PC、TCC、消息队列等常见方案,助你深入理解分布式系统的核心技术。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
分布式事务最全详解 ,看这篇就够了!
|
网络安全 Windows
备份SSH配置文件
备份SSH配置文件
440 1
|
安全 数据中心
|
传感器 自动驾驶 安全
计算机视觉在自动驾驶中的应用:技术解析与未来展望
【8月更文挑战第4天】自动驾驶依托计算机视觉实现环境感知与决策,通过目标检测、跟踪及车道识别等技术保障行车安全与效率。面对数据处理、场景理解等挑战,未来技术将持续优化,深化智能驾驶体验,引领交通行业变革。
1123 8

热门文章

最新文章