夜鹰教程网-程序员的加油站
 当前位置:文章中心 >> Java_Jsp_Jdk_eclipse_tomcat
java实现断点续传功能
夜鹰教程网 来源:www.yyjcw.com 日期:2018-6-30 15:52:48
java实现断点续传功能

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

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

什么是断点续传?

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

20141206215834127.png

原理:

断点续传的关键是断点,所以在制定传输协议的时候要设计好,如上图,我自定义了一个交互协议,每次下载请求都会带上下载的起始点,这样就可以支持从断点下载了,其实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!

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

20141206222814964.jpg

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


20141206222835727.jpg

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

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


复制链接 网友评论 收藏本文 关闭此页
上一条: php实现大文件断点续传功能  下一条: Windows Server 2016新功能
夜鹰教程网成立于2008年,目前已经运营了将近 13 年,发布了大量关于 html5/css3/C#/asp.net/java/python/nodejs/mongodb/sql server/android/javascript/mysql/mvc/easyui/vue/echarts原创教程。 我们一直都在坚持的是:认证负责、一丝不苟、以工匠的精神来打磨每一套教程,让读者感受到作者的用心。我们默默投入的时间,确保每一套教程都是一件作品,而不是呆板的文字和视频! 目前我们推出在线辅导班试运营,模式为一对一辅导,教学工具为QQ。我们的辅导学科包括 java 、android原生开发、webapp开发、商城开发、C#和asp.net开发,winform和物联网开发、web前端开发,但不仅限于此。 普通班针对的是国内学员,例如想打好基础的大学生、想转行的有志青年、想深入学习的程序员、想开发软件的初学者或者业余爱好者等。 就业办针对即将毕业上岗的大四学生,或者打算转行的初级开发工程师。 留学生班针对的是在欧美、加拿大、澳洲、日本、韩国、新加坡等地留学的中国学子,目的是让大家熟练地掌握编程技能,按时完成老师布置的作业,并能顺利地通过考试。 详细咨询QQ:1416759661   夜鹰教程网  基于角色的权限管理系统(c-s/b-s)。
  夜鹰教程网  基于nodejs的聊天室开发视频教程
  夜鹰教程网  Git分布式版本管理视频教程
  夜鹰教程网  MVC+EasyUI视频教程
  夜鹰教程网  在线考试系统视频教程
  夜鹰教程网  MongoDB视频教程。
  夜鹰教程网 Canvas视频教程
  夜鹰教程网 报表开发视频教程
  热点推荐
初学教程:Java Socket网络编程浅…
Java随机数总结
java读取文本文件内容并获取文件大…
字节流和字符流Java
java常用图片读写程序
怎样成为一个优秀的Java程序员
C++程序员转Java容易吗?难点在哪…
思科培训与华为培训在教材和内容方…
Java注释的使用和定义
使用 Java 实现 Comet 风格的 Web…
利用Java实现zip压缩/解压缩
Java Web三层架构的配置详解
Java Socket编程之我见
使用 Java 实现 Comet 风格的 Web…
两种J2ME网络编程的方法之一
  最近更新
short、int、long、float、double…
二进制(原码、反码、补码)
python时间模块详解
java 解决split分割空值不能得到的…
如何打开eclipse安卓开发代码提示…
php实现大文件断点续传功能
java实现断点续传功能
Windows Server 2016新功能
MyEclipse 8.5 汉化方法
jquery实现无刷新分页
什么是JSON?
什么是回调函数
jQuery 1.7下载
jquery加载XML文档
什么是MVC(三层架构)

关于我们 | 网站建设 | 技术辅导 | 常见问题 | 联系我们 | 友情链接

夜鹰教程网 版权所有 www.yyjcw.com All rights reserved 备案号:蜀ICP备08011740号3