原理解析java
在开发当中,“断点续传”这种功能很实用和常见,听上去也是比较有“逼格”的感受。因此一般咱们都有兴趣去研究研究这种功能是如何实现的?
以Java来讲,网络上也能找到很多关于实现相似功能的资料。可是呢,大多数都是举个Demo而后贴出源码,真正对其实现原理有详细的说明不多。
因而咱们在最初接触的时候,极可能就是直接Crtl + C/V代码,而后捣鼓捣鼓,然而最终也能把效果弄出来。但初学时这样作其实很显然是有好有坏的。
好处在于,源码不少,解释不多;若是咱们肯下功夫,针对于别人贴出的代码里那些本身不明白的东西去查资料,去钻研。最终多半会收获颇丰。
坏处也很明显:做为初学者,面对一大堆的源码,感受好多东西都很陌生,就很容易望而生畏。即便最终大体了解了用法,但也不必定明白实现原理。数据库
咱们今天就一块儿从最基本的角度切入,来看看所谓的“断点续传”这个东西是否是真的如此“高逼格”。
其实在接触一件新的“事物”的时候,将它拟化成一些咱们自己比较熟悉的事物,来参照和对比着学习。一般会事半功倍。
若是咱们刚接触“断点续传”这个概念,确定很难说清楚个一二三。那么,“玩游戏”咱们确定不会陌生。编程
OK,那就假设咱们如今有一款“通关制的RPG游戏”。想一想咱们在玩这类游戏时一般会怎么作?
很明显,第一天咱们浴血奋战,大杀四方,假设终于来到了第四关。虽然激战正酣,但一看墙上的时钟,已经凌晨12点,该睡觉了。
这个时候就很尴尬了,为了可以在下一次玩的时候,顺利接轨上咱们本次游戏的进度,咱们应该怎么办呢?
很简单,咱们不关掉游戏,直接去睡觉,次日再接着玩呗。这样是能够,但彷佛总觉着有哪里让人不爽。
那么,这个时候,若是这个游戏有一个功能叫作“存档”,就很关键了。咱们直接选择存档,输入存档名“第四关”,而后就能够关闭游戏了。
等到下次进行游戏时,咱们直接找到“第四关”这个存档,而后进行读档,就能够接着进行游戏了。数组
这个时候,所谓的“断点续传”就很好理解了。咱们顺着咱们以前“玩游戏”的思路来理一下:
假设,如今有一个文件须要咱们进行下载,当咱们下载了一部分的时候,出现状况了,好比:电脑死机、没电、网络中断等等。
其实这就比如咱们以前玩游戏玩着玩着,忽然12点须要去睡觉休息了是一个道理。OK,那么这个时候的状况是:服务器
• 若是游戏不能存档,那么则意味着咱们下次游戏的时候,此次已经经过的4关的进度将会丢失,没法接档。
• 对应的,若是“下载”的行为没法记录本次下载的一个进度。那么,当咱们再次下载这个文件也就只能从头来过。
话到这里,其实咱们已经发现了,对于咱们以上所说的行为,关键就在于一个字“续”!
而咱们要实现让一种断开的行为“续”起来的目的,关键就在于要有“介质”可以记录和读取行为出现”中断”的这个节点的信息。网络
转化到编程世界dom
实际上这就是“断点续传”最最基础的原理,用大白话说就是:咱们要在下载行为出现中断的时候,记录下中断的位置信息,而后在下次行为中读取。
有了这个位置信息以后,想一想咱们该怎么作。是的,很简单,在新的下载行为开始的时候,直接从记录的这个位置开始下载内容,而再也不从头开始。
好吧,咱们用大白话掰扯了这么久的原理,开始以为无聊了。那么咱们如今最后总结一下,而后就来看看咱们应该怎么把原理转换到编程世界中去。post
• 当“上传(下载)的行为”出现中断,咱们须要记录本次上传(下载)的位置(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”,由此也就完成了续传。
转载于http://www.jb51.net/article/88707.htm