文件断点续传原理与实现

您所在的位置:网站首页 文件断点续传实现原理 文件断点续传原理与实现

文件断点续传原理与实现

2024-07-12 10:54| 来源: 网络整理| 查看: 265

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

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

什么是断点续传?

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

原理:

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

提供下载的服务端代码:

[java]  view plain copy 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);           }       }      }   下载的客户端代码:

[java]  view plain copy 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好,属于两者的一个平衡的选择,感兴的可以深入研究一下。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3