前言

在某次摸鱼的过程中,老大突然后面冒出来说要做一个拉取文件到本地的需求(写的时候疯狂回头),当时心想这简单,不就一个HttpClient或者RestTemplate的事情嘛,很快一两天就给整出来心满意足的提交了。
不出意外的话要出意外了,老大看了一眼我的代码就问:“你没有做断点续传吗”,我:“啊?”(好吧得加班了)
因为当时我还没有玩过断点续传,老大就和我提了一嘴可以考虑使用Range请求头来实现,在我巴拉巴拉的有一两天后,最终断点续传版本的网络文件下载功能就出来了(工具类加一)

省流:本文章除了断点续传的视线,还发散介绍了Range请求头和一些零零散散的其他小东西,不感兴趣的小伙伴可直接跳至“断点续传下载实现”。

1、Range请求头

1.1、概述

顾名思义,HTTP/1.1 Range请求头代表发送范围获取数据的请求,要求服务器仅向客户端回传HTTP消息的一部分,格式以及示例如下:

Range: <数据格式>=<数据开始的索引位置>-<数据结束的索引位置>
# 1. 请求从0至500的byte数据:
Range: bytes=0-500
# 2. 请求第500个byte以后的全部数据:
Range: bytes=501-
# 3. 请求最后500个byte的数据:
Range:bytes=-500
# 4. 请求多个分段时,各分段以,分割:
Range: bytes=0-100,101-200

1.2、使用限制

正是得益于Range请求头的这种特性,因此在很多断点续传的场景下都能看到它的身影,但在使用之前需要确定我们的请求中能否使用该请求头。
不知道大家伙有没有发现前面提及Range请求头的时候,我添加上了一个前缀“HTTP/1.1”,这是因为只在 HTTP/1.1(RFC2616) 之上,才支持范围请求。所以如果客户端或者服务端两端的某一端低于 HTTP/1.1,我们就不应该使用范围请求的功能。我们可以通过 curl -i命令来测试一下是否支持范围请求:

curl -i https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png

如果 HTTP 响应中存在 Accept-Ranges 标头,并且其值不是 none,那么该服务器支持范围请求。

1.3、范围请求

当我们确定可以使用范围请求后,我们便可愉快的开始发起请求啦。这里以获取前1024bytes数据为例:Range 还有几种不同的方式来限定范围,可以根据需要灵活定制:

  1. 500-1000:指定开始和结束的范围,一般用于多线程分片下载。
  2. 500-:指定开始区间,一直传递到结束。这个就比较适用于断点续传、或者在线播放等等。
  3. -500:无开始区间,只意思是需要最后 500 bytes 的内容实体。
  4. 100-300,1000-3000:指定多个范围,这种方式使用的场景很少,了解一下就好了。
curl -i https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png -H "Range: bytes=0-1023"

在响应中我们可以很明确看到206响应码和Content-Range范围响应数据:

  1. HTTP 206 Partial Content 成功状态响应代码表示请求已成功,进一步了解可查看206 Partial Content - HTTP | MDN
  2. Content-Range标记当前传递的内容实体范围和总长度,单位是bytes

1.4、预防资源变更

我们在网上偶尔也会发现一个现象:下载大尺寸资源的时候,偶尔中间暂停过再重新下载,资源又重头开始了下载。这看似断点续传功能失效了,但实际上并不一定,可能是在这期间该资源发生了变更。
针对以上情况,可以使用If-Range请求头标记创建具有条件的范围请求,条件没有得到满足,服务器将返回完整的资源以及 200 OK 状态。

进一步了解可查看If-Range - HTTP | MDN

2、断点续传下载实现

2.1、流程设计

基于前面对Range请求头及与其相关内容的分析,假设现在需要下载uTools工具(打钱Please),借助Range实现断点续传下载的流程设计如下:

  1. 获取下载地址链接;
  2. 获取文件的断点(起始点或续点);
  3. 设置Range及相关请求头;
  4. 下载完毕,标记文件完成(以重命名文件作标记)

叠甲:下面的代码实现是为了演示做了修改的代码,大家伙在实现时可根据具体情况具体实现,这里提供几个可更换的点:

  • 记录最新修改时间可使用其它方式存储替代Map;
  • 文件下载方式视情况而定;
  • 文件下载完毕标记等……

2.2、代码实现

public class FileUtils {
public static void main(String[] args) {
FileUtils.processNetFile(
"https://open.u-tools.cn/download/uTools-5.0.0.exe",
"uTools-5.0.0.exe",
"F:\\download"
);
} // 用于存放文件对应的最新修改时间
private static final Map<String, String> fileRangeModifiedMap = new HashMap<>(); /**
* 处理网络文件下载操作
* @author xbaoziplus
* @param downloadUrl 网络文件下载地址
* @param filename 文件名,含后缀
* @param storageDirectory 文件存储目录路径
* @createTime 2024/5/7 15:39
*/
public static void processNetFile(String downloadUrl, String filename, String storageDirectory) {
if (StringUtils.isAnyBlank(downloadUrl, filename)) {
throw new RuntimeException("param is blank");
}
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(downloadUrl);
String filePath = storageDirectory + '/' + filename;
String tmpFilePath = storageDirectory + '/' + filename + ".downloading"; // 文件对象及断点位置初始化
File targetFile = new File(tmpFilePath);
long downloadedByte = 0;
if (targetFile.exists()) {
downloadedByte = targetFile.length();
} else {
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
targetFile.createNewFile();
} // 如果文件存在且大小不为0,添加Range请求头
if (downloadedByte > 0) {
httpGet.addHeader("Range", "bytes=" + downloadedByte + "-");
if (fileRangeModifiedMap.get(filename) != null) {
httpGet.addHeader("If-Range", fileRangeModifiedMap.get(filename));
}
} // 请求下载地址
HttpResponse response = httpClient.execute(httpGet); // 获取并保存last-modified头值
String lastModified = response.getFirstHeader("Last-Modified").getValue();
fileRangeModifiedMap.put(filename, lastModified); long contentLength = Long.parseLong(response.getFirstHeader("Content-Length").getValue()) + downloadedByte;
System.out.println("总大小:" + contentLength + " bytes");
try (InputStream is = response.getEntity().getContent();
RandomAccessFile raf = new RandomAccessFile(targetFile, "rwd")) {
// 将写文件指针移到文件尾。
raf.seek(downloadedByte); // 设置每次磁盘写入最大1M
byte[] buffer = new byte[1024*1024];
int len;
while ((len = is.read(buffer)) != -1) {
raf.write(buffer, 0, len);
System.out.println("下载进度:" + raf.length() * 100 / contentLength + "%, " + raf.length() + '/' + contentLength + " bytes");
}
} // 下载完成,重命名临时文件成正式文件
targetFile.renameTo(new File(filePath));
System.out.println("下载完成");
} catch (Exception e) {
e.printStackTrace();
}
}
}

2.3、运行结果

本地运行之后可自行打断重试,从而测试断点续传是否生效,以下是我这边断点续传的结果

图片做了裁剪拼接,左边是断点续传的起始截图,右边是程序结束截图,左右拼接得到的以下图片

3、RandomAccessFile

RandomAccessFileJava I/O库中的一个特殊类,不属于InputStream或OutputStream的子类,它支持对文件的随机访问读写,也就是说,可以访问文件的任意位置。正因为这一特性,在代码中采用该类进行实现文件写入,通过seek()方法移动文件指针,而后将续传的内容拼接写入文件中。
该类的构造函数中可传入不同的运行模式:

  • r代表以只读方式打开指定文件 。
  • rw以读写方式打开指定文件 。
  • rws读写方式打开,并对内容或元数据都同步写入底层存储设备 。
  • rwd读写方式打开,对文件内容的更新同步更新至底层存储设备 。

4、思维拓展

通过前面我们知道Range请求头可用于断点续传实现,这得益于Range请求头支持指定请求范围的特性,那么这个特性我们还能不能用于其它业务场景的视线呢?答案肯定是可以的。
比如现在有100w个静态资源文件链接需要检测该文件是否可用,即需要确定这些文件链接的可访问性,比较清晰明了的方法就是都请求一遍,若正常请求通了,那便是可访问的。
但这有一个问题,这些都是静态资源文件的链接,即他们的下载地址,那么当我们全部请求一遍之后,那就代表着我们对这100w个文件都进行了下载,这无疑是对服务器的带宽、内存和CPU等资源是一个巨大的开销。
这种情况下我们就可以使用Range请求头进行实现这种检测的需求,每次请求之前携带上Range请求头,内容为Range: bytes=0-1,这代表每一个链接我都只尝试获取1字节的内容,从而大大减少了网络流量的流通和性能的提高。

参考资料

  1. HTTP | MDN
  2. 图解:HTTP 范围请求,助力断点续传、多线程下载的核心原理 - 承香墨影 - 博客园

我又学会了使用Range实现网络文件下载的断点续传的更多相关文章

  1. iOS开发之网络编程--4、NSURLSessionDataTask实现文件下载(离线断点续传下载) <进度值显示优化>

    前言:根据前篇<iOS开发之网络编程--2.NSURLSessionDownloadTask文件下载>或者<iOS开发之网络编程--3.NSURLSessionDataTask实现文 ...

  2. php实现远程网络文件下载到服务器指定目录(方法一)

    PHP实现远程网络文件下载到服务器指定目录(方法一) <?php function getFile($url, $save_dir = '', $filename = '', $type = 0 ...

  3. 初中级DBA必需要学会的9个Linux网络命令,看看你有哪些还没用过

    笔者不久前写了一篇文章<做DBA必须学会,不会会死的11个Linux基本命令>,博文地址为:http://blog.csdn.net/ljunjie82/article/details/4 ...

  4. 【SFTP】使用Jsch实现Sftp文件下载-支持断点续传和进程监控

    参考上篇文章: <[SFTP]使用Jsch实现Sftp文件下载-支持断点续传和进程监控>:http://www.cnblogs.com/ssslinppp/p/6248763.html  ...

  5. iOS开发之网络编程--3、NSURLSessionDataTask实现文件下载(离线断点续传下载)

    前言:使用NSURLSessionDownloadTask满足不这个需要离线断点续传的下载需求,所以这里就需要使用NSURLSessionDataTask的代理方法来处理下载大文件,并且实现离线断点续 ...

  6. DownloadURLFile网络文件下载

    import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; impo ...

  7. Linux网络文件下载

    wget 以网络下载 maven 包为例 wget -c http://mirrors.shu.edu.cn/apache/maven/maven-3/3.5.4/binaries/apache-ma ...

  8. Android网络文件下载模块整理

    一.知识基础 tomcat服务器配置 理解http协议 理解javaIO操作相关知识 SDcard操作知识 Android 权限配置 二.实现步骤 1.从网上获取资源 public String do ...

  9. JAVA实现网络文件下载

    HttpURLConnection conn = null; OutputStream outputStream = null; InputStream inputStream = null; try ...

  10. php实现远程网络文件下载到服务器指定目录(方法二)

    <?php // maximum execution time in seconds set_time_limit (24 * 60 * 60); //if (!isset($_POST['su ...

随机推荐

  1. 【项目学习】Timeswap:第一个完全去中心化的基于 AMM 的货币市场协议

    总览 Timeswap 是世界上第一个完全去中心化的基于 AMM 的货币市场协议,无需预言机或清算人即可工作. Timeswap 采用 3 变量来维持 AMM 的运作.它通过允许用户决定他们的风险状况 ...

  2. toLua中Lua调用C#中的类

    toLua中Lua调用C#: [7]Lua脚本调用C#中的class 准备工作:打算在Lua脚本中使用Debug,使用lua调用C#脚本,需要绑定LuaState和自定义添加Debug --- --- ...

  3. 密码学—重合指数法Python程序

    重合指数(Ic) 计算重合指数就是用来验证在Kasiski测试法中猜测出来的各种密钥长度哪一个才是最接近真实密钥长度的. 计算重合指数步骤 按照Kasiski测试法猜测的密钥长度分组 ↓ 分好组之后将 ...

  4. 基于webapi的websocket聊天室(三)

    上一篇处理了超长消息的问题.我们的应用到目前为止还是单聊天室,这一篇就要处理的多聊天室的问题. 思路 第一个问题,怎么访问不同聊天室 这个可以采用路由参数来解决.我把路由设计成这样/chat/{roo ...

  5. GROK 一个强大的调试工具

    GROK 在线工具 在线英文版地址 http://grokconstructor.appspot.com/ 中文翻译版 GitHub https://github.com/systemmin/Grok ...

  6. SASS 运算 (Operations)符的基本使用

    ​ sass 运算符虽然没有像那些编程语言那么强大,但为了更灵活的输出css,也增强了一些运算符的功能,例如赋值运算符.等号操作符.比较运算符.逻辑运算符.字符串运算符...等等,接下来就来详细介绍下 ...

  7. 当面试官问出“Unsafe”类时,我就知道这场面试废了,祖坟都能给你问出来!

    一.写在开头 依稀记得多年以前的一场面试中,面试官从Java并发编程问到了锁,从锁问到了原子性,从原子性问到了Atomic类库(对着JUC包进行了刨根问底),从Atomic问到了CAS算法,紧接着又有 ...

  8. 高分辨率食道测压(HRM)

    高分辨率测压(High resolution Manometry) HRM的优势 高分辨率食管测压不但实现了从咽部到胃部的全程功能监测,而且插管无需牵拉,操作十分方便.更为重要的是,临床医生经过简单的 ...

  9. 在唯一密钥属性“name”设置为“XXX”时,无法添加类型为“add”的重复集合项

    我是在调试时,更改了项目url出现的问题,没有改端口号,只是改了"/"后面的地址 这个是我是改哈端口号就好了,改了端口号就重新建立虚拟目录了. 感觉是因为端口号没变,但项目url变 ...

  10. Python OpenCV #1 - OpenCV介绍

    一.OpenCV介绍 1.1 OpenCV-Python教程简介 OpenCV由 Gary Bradsky 于1999年在英特尔创立,第一个版本于2000年发布. Vadim Pisarevsky 加 ...