很简单的Java断点续传实现原理

原理解析

在开发当中,“断点续传”这种功能很实用和常见,听上去也是比较有“逼格”的感觉。所以通常我们都有兴趣去研究研究这种功能是如何实现的?
以Java来说,网络上也能找到不少关于实现类似功能的资料。但是呢,大多数都是举个Demo然后贴出源码,真正对其实现原理有详细的说明很少。
于是我们在最初接触的时候,很可能就是直接Crtl + C/V代码,然后捣鼓捣鼓,然而最终也能把效果弄出来。但初学时这样做其实很显然是有好有坏的。
好处在于,源码很多,解释很少;如果我们肯下功夫,针对于别人贴出的代码里那些自己不明白的东西去查资料,去钻研。最终多半会收获颇丰。
坏处也很明显:作为初学者,面对一大堆的源码,感觉好多东西都很陌生,就很容易望而生畏。即使最终大致了解了用法,但也不一定明白实现原理。

我们今天就一起从最基本的角度切入,来看看所谓的“断点续传”这个东西是不是真的如此“高逼格”。
其实在接触一件新的“事物”的时候,将它拟化成一些我们本身比较熟悉的事物,来参照和对比着学习。通常会事半功倍。
如果我们刚接触“断点续传”这个概念,肯定很难说清楚个一二三。那么,“玩游戏”我们肯定不会陌生。

OK,那就假设我们现在有一款“通关制的RPG游戏”。想想我们在玩这类游戏时通常会怎么做?
很明显,第一天我们浴血奋战,大杀四方,假设终于来到了第四关。虽然激战正酣,但一看墙上的时钟,已经凌晨12点,该睡觉了。
这个时候就很尴尬了,为了能够在下一次玩的时候,顺利接轨上我们本次游戏的进度,我们应该怎么办呢?
很简单,我们不关掉游戏,直接去睡觉,第二天再接着玩呗。这样是可以,但似乎总觉着有哪里让人不爽。
那么,这个时候,如果这个游戏有一个功能叫做“存档”,就很关键了。我们直接选择存档,输入存档名“第四关”,然后就可以关闭游戏了。
等到下次进行游戏时,我们直接找到“第四关”这个存档,然后进行读档,就可以接着进行游戏了。

这个时候,所谓的“断点续传”就很好理解了。我们顺着我们之前“玩游戏”的思路来理一下:
假设,现在有一个文件需要我们进行下载,当我们下载了一部分的时候,出现情况了,比如:电脑死机、没电、网络中断等等。
其实这就好比我们之前玩游戏玩着玩着,突然12点需要去睡觉休息了是一个道理。OK,那么这个时候的情况是:

 • 如果游戏不能存档,那么则意味着我们下次游戏的时候,这次已经通过的4关的进度将会丢失,无法接档。
 • 对应的,如果“下载”的行为无法记录本次下载的一个进度。那么,当我们再次下载这个文件也就只能从头来过。
 话到这里,其实我们已经发现了,对于我们以上所说的行为,关键就在于一个字“”!
而我们要实现让一种断开的行为“续”起来的目的,关键就在于要有“介质”能够记录和读取行为出现”中断”的这个节点的信息。

转化到编程世界

实际上这就是“断点续传”最最基础的原理,用大白话说就是:我们要在下载行为出现中断的时候,记录下中断的位置信息,然后在下次行为中读取。
有了这个位置信息之后,想想我们该怎么做。是的,很简单,在新的下载行为开始的时候,直接从记录的这个位置开始下载内容,而不再从头开始。
好吧,我们用大白话掰扯了这么久的原理,开始觉得无聊了。那么我们现在最后总结一下,然后就来看看我们应该怎么把原理转换到编程世界中去。

 • 当“上传(下载)的行为”出现中断,我们需要记录本次上传(下载)的位置(position)。
 • 当“续”这一行为开始,我们直接跳转到postion处继续上传(下载)的行为。

显然问题的关键就在于所谓的“position”,以我们举的“通关游戏来说”,可以用“第几关”来作为这个position的单位。
那么转换到所谓的“断点续传”,我们该使用什么来衡量“position”呢?很显然,回归二进制,因为这里的本质无非就是文件的读写。

那么剩下的工作就很简单了,先是记录position,这似乎都没什么值得说的,因为只是数据的持久化而已(内存,文件,数据库),我们有很多方式。

另一个关键在于当“续传”的行为开始,我们需要需要从上次记录的position位置开始读写操作,所以我们需要一个类似于“指针”功能的东西。
我们当然也可以自己想办法去实现这样一个“指针”,但高兴地是,Java已经为我们提供了这样的一个类,那就是RandomAccessFile。
这个类的功能从名字就很直观的体现了,能够随机的去访问文件。我们看一下API文档中对该类的说明:

此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。

如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。

写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。

看完API说明,我们笑了,是的,这不正是我们要的吗?那好吧,我们磨刀磨了这么久了,还不去砍砍柴吗?

实例演示

既然是针对于文件的“断点续传”,那么很明显,我们先搞一个文件出来。也许音频文件,图像文件什么的看上去会更上档次一点。
但我们已经说了,在计算机大兄弟眼中,它们最终都将回归“二进制”。所以我们这里就创建一个简单的”txt”文件,因为txt更利于理解。

我们在D盘的根目录下创建一个名为”test.txt”的文件,文件内容很简单,如图所示:

没错,我们输入的内容就是简单的6个英语字母。然后我们右键→属性:

我们看到,文件现在的大小是6个字节。这也就是为什么我们说,所有的东西到最后还是离不开“二进制”。
是的,我们都明白,因为我们输入了6个英文字母,而1个英文字母将占据的存储空间是1个字节(即8个比特位)。
目前为止,我们看到的都很无聊,因为这基本等于废话,稍微有计算机常识的人都知道这些知识。别着急,我们继续。

在Java中对一个文件进行读写操作很简单。假设现在的需求如果是“把D盘的这个文件写入到E盘”,那么我们会提起键盘,啪啪啪啪,搞定!
但其实所谓的文件的“上传(下载)”不是也没什么不同吗?区别就仅仅在于行为由“仅仅在本机之间”转变成了”本机与服务器之间”的文件读写。
这时我们会说,“别逼逼了,这些谁都知道,‘断点续传'呢?“,其实到了这里也已经很简单了,我们再次明确,断点续传要做的无非就是:
前一次读写行为如果出现中断,请记录下此次读写完成的文件内容的位置信息;当“续传开始”则直接将指针移到此处,开始继续读写操作。

反复的强调原理,实际上是因为只要弄明白了原理,剩下的就只是招式而已了。这就就像武侠小说里的“九九归一”大法一样,最高境界就是回归本源。
任何复杂的事物,只要明白其原理,我们就能将其剥离,还原为一个个简单的事物。同理,一系列简单的事物,经过逻辑组合,就形成了复杂的事物。

下面,我们马上就将回归混沌,以最基本的形式模拟一次“断点续传”。在这里我们连服务器的代码都不去写了,直接通过一个本地测试类搞定。
我们要实现的效果很简单:将在D盘的”test.txt”文件写入到E盘当中,但中途我们会模拟一次”中断”行为,然后在重新继续上传,最终完成整个过程。
也就是说,我们这里将会把“D盘”视作一台电脑,并且直接将”E盘”视作一台服务器。那么这样我们甚至都不再与http协议扯上半毛钱关系了,(当然实际开发我们肯定是还是得与它扯上关系的 ^<^),从而只关心最基本的文件读写的”断”和”续”的原理是怎么样的。

为了通过对比加深理解,我们先来写一段正常的代码,即正常读写,不发生中断:

public class Test {

 public static void main(String[] args) {
  // 源文件与目标文件
  File sourceFile = new File("D:/", "test.txt");
  File targetFile = new File("E:/", "test.txt");
  // 输入输出流
  FileInputStream fis = null;
  FileOutputStream fos = null;
  // 数据缓冲区
  byte[] buf = new byte[1];

  try {
   fis = new FileInputStream(sourceFile);
   fos = new FileOutputStream(targetFile);
   // 数据读写
   while (fis.read(buf) != -1) {
    System.out.println("write data...");
    fos.write(buf);
   }
  } catch (FileNotFoundException e) {
   System.out.println("指定文件不存在");
  } catch (IOException e) {
   // TODO: handle exception
  } finally {
   try {
    // 关闭输入输出流
    if (fis != null)
     fis.close();

    if (fos != null)
     fos.close();
   } catch (IOException e) {
    e.printStackTrace();
   }

  }
 }
}

该段代码运行,我们就会发现在E盘中已经成功拷贝了一份“test.txt”。这段代码很简单,唯一稍微说一下就是:
我们看到我们将buf,即缓冲区 设置的大小是1,这其实就代表我们每次read,是读取一个字节的数据(即1个英文字母)。

现在,我们就来模拟这个读写中断的行为,我们将之前的代码完善如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class Test {

 private static int position = -1;

 public static void main(String[] args) {
  // 源文件与目标文件
  File sourceFile = new File("D:/", "test.txt");
  File targetFile = new File("E:/", "test.txt");
  // 输入输出流
  FileInputStream fis = null;
  FileOutputStream fos = null;
  // 数据缓冲区
  byte[] buf = new byte[1];

  try {
   fis = new FileInputStream(sourceFile);
   fos = new FileOutputStream(targetFile);
   // 数据读写
   while (fis.read(buf) != -1) {
    fos.write(buf);
    // 当已经上传了3字节的文件内容时,网络中断了,抛出异常
    if (targetFile.length() == 3) {
     position = 3;
     throw new FileAccessException();
    }
   }
  } catch (FileAccessException e) {
   keepGoing(sourceFile,targetFile, position);
  } catch (FileNotFoundException e) {
   System.out.println("指定文件不存在");
  } catch (IOException e) {
   // TODO: handle exception
  } finally {
   try {
    // 关闭输入输出流
    if (fis != null)
     fis.close();

    if (fos != null)
     fos.close();
   } catch (IOException e) {
    e.printStackTrace();
   }

  }
 }

 private static void keepGoing(File source,File target, int position) {
  try {
   Thread.sleep(10000);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }

  try {
   RandomAccessFile readFile = new RandomAccessFile(source, "rw");
   RandomAccessFile writeFile = new RandomAccessFile(target, "rw");
   readFile.seek(position);
   writeFile.seek(position);

   // 数据缓冲区
   byte[] buf = new byte[1];
   // 数据读写
   while (readFile.read(buf) != -1) {
    writeFile.write(buf);
   }
  } catch (FileNotFoundException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
 }

}

class FileAccessException extends Exception {

}

总结一下,我们在这次改动当中都做了什么工作:

 • 首先,我们定义了一个变量position,记录在发生中断的时候,已完成读写的位置。(这是为了方便,实际来说肯定应该讲这个值存到文件或者数据库等进行持久化)
 然后在文件读写的while循环中,我们去模拟一个中断行为的发生。这里是当targetFile的文件长度为3个字节则模拟抛出一个我们自定义的异常。(我们可以想象为实际下载中,已经上传(下载)了”x”个字节的内容,这个时候网络中断了,那么我们就在网络中断抛出的异常中将”x”记录下来)。
 • 剩下的就如果我们之前说的一样,在“续传”行为开始后,通过RandomAccessFile类来包装我们的文件,然后通过seek将指针指定到之前发生中断的位置进行读写就搞定了。
(实际的文件下载上传,我们当然需要将保存的中断值上传给服务器,这个方式通常为httpConnection.setRequestProperty(“RANGE”,”bytes=x”);)

在我们这段代码,开启”续传“行为,即keepGoing方法中:我们起头让线程休眠10秒钟,这正是为了让我们运行程序看到效果。
现在我们运行程序,那么文件就会开启“由D盘上传到E盘的过程”,我们首先点开E盘,会发现的确多了一个test.txt文件,打开它发现内容如下:

没错,这个时候我们发现内容只有“abc”。这是在我们预料以内的,因为我们的程序模拟在文件上传了3个字节的时候发生了中断。

Ok,我们静静的等待10秒钟过去,然后再点开该文件,看看是否能够成功:

通过截图我们发现内容的确已经变成了“abc”,由此也就完成了续传。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 基于断点续传下载原理的实现

    需求背景 动态创建的文件下载的时候希望浏览器显示下载进度 动态创建的文件希望能够分段下载 HTTP断点续传报文 要实现HTTP断点续传必须要简单了解以下几个报文. Accept-Ranges 告诉客户端(浏览器..)服务器端支持断点续传 服务器端返回 Range 客户端告诉服务器端从指定的的位置/范围(这里值字节数)下载资源 客户端发出 Content-Range 服务器端告诉客户端响应的数据信息,在整个返回体中本部分的字节位置 服务器端返回 ETag 资源标识 非必须 服务器端返回 Last-

  • Java如何实现HTTP断点续传功能

    (一)断点续传的原理 其实断点续传的原理很简单,就是在Http的请求上和一般的下载有所不同而已. 打个比方,浏览器请求服务器上的一个文时,所发出的请求如下: 假设服务器域名为wwww.sjtu.edu.cn,文件名为down.zip. GET /down.zip HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- excel, application/msword, a

  • 很简单的Java断点续传实现原理

    原理解析 在开发当中,"断点续传"这种功能很实用和常见,听上去也是比较有"逼格"的感觉.所以通常我们都有兴趣去研究研究这种功能是如何实现的? 以Java来说,网络上也能找到不少关于实现类似功能的资料.但是呢,大多数都是举个Demo然后贴出源码,真正对其实现原理有详细的说明很少. 于是我们在最初接触的时候,很可能就是直接Crtl + C/V代码,然后捣鼓捣鼓,然而最终也能把效果弄出来.但初学时这样做其实很显然是有好有坏的. 好处在于,源码很多,解释很少:如果我们肯下功

  • Java文件断点续传实现原理解析

    一.作用: 随机流(RandomAccessFile)不属于IO流,支持对文件的读取和写入随机访问. 二.随机访问文件原理: 首先把随机访问的文件对象看作存储在文件系统中的一个大型 byte 数组,然后通过指向该 byte 数组的光标或索引(即:文件指针 FilePointer)在该数组任意位置读取或写入任意数据. 三.相关方法说明: 1.对象声明:RandomAccessFile raf = newRandomAccessFile(File file, String mode); 其中参数 m

  • Java 队列实现原理及简单实现代码

    Java 队列实现原理 "队列"这个单词是英国人说的"排".在英国"排队"的意思就是站到一排当中去.计算机科学中,队列是一种数据结构,有点类似栈,只是在队列中第一个插入的数据项也会最先被移除,而在栈中,最后插入的数据项最先移除.队列的作用就像电影院前的人们站成的排一样:第一个进入附属的人将最先到达队头买票.最后排队的人最后才能买到票. 队列和栈一样也被用作程序员的工具.它也可以用于模拟真实世界的环境,例如模拟人们在银行里排队等待,飞机等待起飞,或

  • 简单了解java等待唤醒机制原理及使用

    这篇文章主要介绍了简单了解java等待唤醒机制原理及使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 这是一篇走心的填坑笔记,自学Java的几年总是在不断学习新的技术,一路走来发现自己踩坑无数,而填上的坑却屈指可数.突然发现,有时候真的不是几年工作经验的问题,有些东西即使工作十年,没有用心去学习过也不过是一个10年大坑罢了(真实感受). 刚开始接触多线程时,就知道有等待/唤醒这个东西,写过一个demo就再也没有看过了,至于它到底是个什么东西,

  • 简单了解Java多态向上转型相关原理

    这篇文章主要介绍了简单了解Java多态向上转型相关原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 多态性是面向对象的第三大特征. 多态的优点 改善代码的组织结构和可读性. 能够创建可扩展的程序.(随时可以加入新功能) 消除类型之间的耦合关系. 说实话,作为小白的我,并不太能够理解上面三个优点.随着深入学习,理解应该会越来越深吧,共勉. 向上转型 概念 Java允许把子类对象赋值给父类的引用变量,不用做任何强制转换,系统自动完成.向上转型来自

  • Java  队列实现原理及简单实现代码

    Java 队列实现原理 "队列"这个单词是英国人说的"排".在英国"排队"的意思就是站到一排当中去.计算机科学中,队列是一种数据结构,有点类似栈,只是在队列中第一个插入的数据项也会最先被移除,而在栈中,最后插入的数据项最先移除.队列的作用就像电影院前的人们站成的排一样:第一个进入附属的人将最先到达队头买票.最后排队的人最后才能买到票. 队列和栈一样也被用作程序员的工具.它也可以用于模拟真实世界的环境,例如模拟人们在银行里排队等待,飞机等待起飞,或

  • 简单分析Java的求值策略原理

    形参和实参 java在定义方法时可以设置参数,参数分为形参和实参,形参是指在定义函数时用于接收外部传入数据的参数,而实参是指在调用方法时主调函数向被调函数传递的数据参数. 例如: public class Main { public static void main(String[] args) { int a = 2; int b = 3; System.out.println(sum(a,b));//5 } private static int sum(int i1,int i2){ ret

  • 不能再简单的无闪刷新验证码原理很简单

    只有这一段事件触发语句全搞定 onclick='this.src="GifCode.asp#"+new Date().getTime()' // GifCode.asp 验证码的图片 事件: onclick='this.src="GifCode.asp#"+new Date().getTime()' 呵呵! 很简单很简单很简单,没想到吧 同理可以在ASP.NET PHP JSP 中运用

  • Android 断点续传的原理剖析与实例讲解

    本文所要讲的是Android断点续传的内容,以实例的形式进行了详细介绍.   一.断点续传的原理 其实断点续传的原理很简单,就是在http的请求上和一般的下载有所不同而已. 打个比方,浏览器请求服务器上的一个文时,所发出的请求如下: 假设服务器域名为www.jizhuomi.com/android,文件名为down.zip. get /down.zip http/1.1 accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, ap

  • Android  断点续传的原理剖析与实例讲解

    本文所要讲的是Android断点续传的内容,以实例的形式进行了详细介绍.   一.断点续传的原理 其实断点续传的原理很简单,就是在http的请求上和一般的下载有所不同而已. 打个比方,浏览器请求服务器上的一个文时,所发出的请求如下: 假设服务器域名为www.jizhuomi.com/android,文件名为down.zip. get /down.zip http/1.1 accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, ap

随机推荐