文件断点续传原理与实现

文件断点续传原理与实现

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

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

什么是断点续传?

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

原理:

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

提供下载的服务端代码:

[java] view plaincopy

  1. import java.io.File;  
  2. import java.io.IOException;  
  3. import java.io.InputStream;  
  4. import java.io.OutputStream;  
  5. import java.io.RandomAccessFile;  
  6. import java.io.StringWriter;  
  7. import java.net.ServerSocket;  
  8. import java.net.Socket;  
  9.   
  10. // 断点续传服务端  
  11. public class FTPServer {  
  12.   
  13.     // 文件发送线程  
  14.     class Sender extends Thread{  
  15.         // 网络输入流  
  16.         private InputStream in;  
  17.         // 网络输出流  
  18.         private OutputStream out;  
  19.         // 下载文件名  
  20.         private String filename;  
  21.   
  22.         public Sender(String filename, Socket socket){  
  23.             try {  
  24.                 this.out = socket.getOutputStream();  
  25.                 this.in = socket.getInputStream();  
  26.                 this.filename = filename;  
  27.             } catch (IOException e) {  
  28.                 e.printStackTrace();  
  29.             }  
  30.         }  
  31.           
  32.         @Override  
  33.         public void run() {  
  34.             try {  
  35.                 System.out.println("start to download file!");  
  36.                 int temp = 0;  
  37.                 StringWriter sw = new StringWriter();  
  38.                 while((temp = in.read()) != 0){  
  39.                     sw.write(temp);  
  40.                     //sw.flush();  
  41.                 }  
  42.                 // 获取命令  
  43.                 String cmds = sw.toString();  
  44.                 System.out.println("cmd : " + cmds);  
  45.                 if("get".equals(cmds)){  
  46.                     // 初始化文件  
  47.                     File file = new File(this.filename);  
  48.                     RandomAccessFile access = new RandomAccessFile(file,"r");  
  49.                     //  
  50.                     StringWriter sw1 = new StringWriter();  
  51.                     while((temp = in.read()) != 0){  
  52.                         sw1.write(temp);  
  53.                         sw1.flush();  
  54.                     }  
  55.                     System.out.println(sw1.toString());  
  56.                     // 获取断点位置  
  57.                     int startIndex = 0;  
  58.                     if(!sw1.toString().isEmpty()){  
  59.                         startIndex = Integer.parseInt(sw1.toString());  
  60.                     }  
  61.                     long length = file.length();  
  62.                     byte[] filelength = String.valueOf(length).getBytes();  
  63.                     out.write(filelength);  
  64.                     out.write(0);  
  65.                     out.flush();  
  66.                     // 计划要读的文件长度  
  67.                     //int length = (int) file.length();//Integer.parseInt(sw2.toString());  
  68.                     System.out.println("file length : " + length);  
  69.                     // 缓冲区10KB  
  70.                     byte[] buffer = new byte[1024*10];  
  71.                     // 剩余要读取的长度  
  72.                     int tatol = (int) length;  
  73.                     System.out.println("startIndex : " + startIndex);  
  74.                     access.skipBytes(startIndex);  
  75.                     while (true) {  
  76.                         // 如果剩余长度为0则结束  
  77.                         if(tatol == 0){  
  78.                             break;  
  79.                         }  
  80.                         // 本次要读取的长度假设为剩余长度  
  81.                         int len = tatol - startIndex;  
  82.                         // 如果本次要读取的长度大于缓冲区的容量  
  83.                         if(len > buffer.length){  
  84.                             // 修改本次要读取的长度为缓冲区的容量  
  85.                             len = buffer.length;  
  86.                         }  
  87.                         // 读取文件,返回真正读取的长度  
  88.                         int rlength = access.read(buffer,0,len);  
  89.                         // 将剩余要读取的长度减去本次已经读取的  
  90.                         tatol -= rlength;  
  91.                         // 如果本次读取个数不为0则写入输出流,否则结束  
  92.                         if(rlength > 0){  
  93.                             // 将本次读取的写入输出流中  
  94.                             out.write(buffer,0,rlength);  
  95.                             out.flush();  
  96.                         } else {  
  97.                             break;  
  98.                         }  
  99.                         // 输出读取进度  
  100.                         //System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");  
  101.                     }  
  102.                     //System.out.println("receive file finished!");  
  103.                     // 关闭流  
  104.                     out.close();  
  105.                     in.close();  
  106.                     access.close();  
  107.                 }  
  108.             } catch (IOException e) {  
  109.                 e.printStackTrace();  
  110.             }  
  111.             super.run();  
  112.         }  
  113.     }  
  114.       
  115.     public void run(String filename, Socket socket){  
  116.         // 启动接收文件线程   
  117.         new Sender(filename,socket).start();  
  118.     }  
  119.       
  120.     public static void main(String[] args) throws Exception {  
  121.         // 创建服务器监听  
  122.         ServerSocket server = new ServerSocket(8888);  
  123.         // 接收文件的保存路径  
  124.         String filename = "E:\\ceshi\\mm.pdf";  
  125.         for(;;){  
  126.             Socket socket = server.accept();  
  127.             new FTPServer().run(filename, socket);  
  128.         }  
  129.     }  
  130.   
  131. }  


下载的客户端代码:

[java] view plaincopy

  1. import java.io.File;  
  2. import java.io.InputStream;  
  3. import java.io.OutputStream;  
  4. import java.io.RandomAccessFile;  
  5. import java.io.StringWriter;  
  6. import java.net.InetSocketAddress;  
  7. import java.net.Socket;  
  8.   
  9. // 断点续传客户端  
  10. public class FTPClient {  
  11.   
  12.     /** 
  13.      *  request:get0startIndex0 
  14.      *  response:fileLength0fileBinaryStream 
  15.      *   
  16.      * @param filepath 
  17.      * @throws Exception 
  18.      */  
  19.     public void Get(String filepath) throws Exception {  
  20.         Socket socket = new Socket();  
  21.         // 建立连接  
  22.         socket.connect(new InetSocketAddress("127.0.0.1", 8888));  
  23.         // 获取网络流  
  24.         OutputStream out = socket.getOutputStream();  
  25.         InputStream in = socket.getInputStream();  
  26.         // 文件传输协定命令  
  27.         byte[] cmd = "get".getBytes();  
  28.         out.write(cmd);  
  29.         out.write(0);// 分隔符  
  30.         int startIndex = 0;  
  31.         // 要发送的文件  
  32.         File file = new File(filepath);  
  33.         if(file.exists()){  
  34.             startIndex = (int) file.length();  
  35.         }  
  36.         System.out.println("Client startIndex : " + startIndex);  
  37.         // 文件写出流  
  38.         RandomAccessFile access = new RandomAccessFile(file,"rw");  
  39.         // 断点  
  40.         out.write(String.valueOf(startIndex).getBytes());  
  41.         out.write(0);  
  42.         out.flush();  
  43.         // 文件长度  
  44.         int temp = 0;  
  45.         StringWriter sw = new StringWriter();  
  46.         while((temp = in.read()) != 0){  
  47.             sw.write(temp);  
  48.             sw.flush();  
  49.         }  
  50.         int length = Integer.parseInt(sw.toString());  
  51.         System.out.println("Client fileLength : " + length);  
  52.         // 二进制文件缓冲区  
  53.         byte[] buffer = new byte[1024*10];  
  54.         // 剩余要读取的长度  
  55.         int tatol = length - startIndex;  
  56.         //  
  57.         access.skipBytes(startIndex);  
  58.         while (true) {  
  59.             // 如果剩余长度为0则结束  
  60.             if (tatol == 0) {  
  61.                 break;  
  62.             }  
  63.             // 本次要读取的长度假设为剩余长度  
  64.             int len = tatol;  
  65.             // 如果本次要读取的长度大于缓冲区的容量  
  66.             if (len > buffer.length) {  
  67.                 // 修改本次要读取的长度为缓冲区的容量  
  68.                 len = buffer.length;  
  69.             }  
  70.             // 读取文件,返回真正读取的长度  
  71.             int rlength = in.read(buffer, 0, len);  
  72.             // 将剩余要读取的长度减去本次已经读取的  
  73.             tatol -= rlength;  
  74.             // 如果本次读取个数不为0则写入输出流,否则结束  
  75.             if (rlength > 0) {  
  76.                 // 将本次读取的写入输出流中  
  77.                 access.write(buffer, 0, rlength);  
  78.             } else {  
  79.                 break;  
  80.             }  
  81.             System.out.println("finish : " + ((float)(length -tatol) / length) *100 + " %");  
  82.         }  
  83.         System.out.println("finished!");  
  84.         // 关闭流  
  85.         access.close();  
  86.         out.close();  
  87.         in.close();  
  88.     }  
  89.   
  90.     public static void main(String[] args) {  
  91.         FTPClient client = new FTPClient();  
  92.         try {  
  93.             client.Get("E:\\ceshi\\test\\mm.pdf");  
  94.         } catch (Exception e) {  
  95.             e.printStackTrace();  
  96.         }  
  97.     }  
  98. }  

测试
原文件、下载中途断开的文件和从断点下载后的文件分别从左至右如下:

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

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

猜你喜欢

转载自blog.csdn.net/aryasei/article/details/87162324