http实现文件分片下载

news2025/1/12 23:01:10

文章目录

    • 检测是否支持
    • HTTP Range 语法
    • Range请求cURL示例
      • 单一范围
      • 多重范围
      • 条件式分片请求
    • Range分片请求的响应
    • 文件整体下载
    • 文件分片下载
      • 文本下载
      • 图片下载
      • 封装下载方法

HTTP分片异步下载是一种下载文件的技术,它允许将一个大文件分成多个小块(分片),然后分别下载这些分片,从而实现更快速、稳定的下载过程。这种技术常用于大文件的下载,例如视频、游戏、软件等。或者与文件下载的断点续传功能搭配使用时非常有用。

比如当你正在看大片时,网络断了,你需要继续看的时候,文件服务器不支持断点的话,则你需要重新等待下载这个大片,才能继续观看。而支持HTTP Range的话,客户端就会记录了之前已经看过的视频文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个视频文件发送回客户端,以此节省网络带宽,带来更流畅的用户体验。

检测是否支持

检测服务器端是否支持分片请求。

假如在响应头中存在 Accept-Ranges(并且它的值不为none),那么表示该服务器支持分片请求。例如,你可以使用 cURL 发送一个 HEAD 请求来进行检测。

curl -I https://xxx.jpg

HTTP/1.1 200 OK
...
Accept-Ranges: bytes
Content-Length: 146515

在上面的响应中, Accept-Ranges: bytes 表示界定范围的单位是 bytes。这里 Content-Length 也是有效信息,因为它提供了要检索的图片的完整大小。

如果站点未响应 Accept-Ranges,那么它们有可能不支持分片请求。一些站点会明确将其值设置为 none,以此来表明不支持。在这种情况下,某些应用的下载管理器会将暂停按钮禁用。

curl -I https://xxx.movie

HTTP/1.1 200 OK
...
Accept-Ranges: none

HTTP Range 语法

Range是一个HTTP请求头,告知服务器要返回文件的哪一部分,即:哪个区间范围(字节)的数据。

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
  • <unit>:范围所采用的单位,通常是字节(bytes)

  • <range-start>:一个整数,表示在特定单位下,范围的起始值。(下标从0开始)

  • <range-end>:一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。

Range: bytes=200-1000 就是下载 200-1000 字节的内容(两边都是闭区间),服务端返回 206 的状态码,并带上这部分内容。

可以省略右边部分,代表一直到结束:Range: bytes=200-

也可以省略左边部分,代表从头开始:Range: bytes=-1000

而且可以请求多段 range,服务端会返回多段内容:Range: bytes=200-1000, 2000-6576, 19000-

Range请求cURL示例

从服务器端请求特定的范围。

单一范围

我们可以请求资源的某一部分。这次我们依然用 cURL 来进行测试。-H 选项可以在请求中追加一个首部行,在这个例子中,是用 Range 首部来请求图片文件的前 1024 个字节。

curl http://xxx.jpg -i -H "Range: bytes=0-1023"

这样生成的请求如下:

GET /xxx.jpg HTTP/1.1
Host: i.imgur.com
Range: bytes=0-1023

服务器端会返回状态码为 206 Partial Content 的响应:

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
...
(binary content)

在这里,Content-Length 首部现在用来表示先前请求范围的大小(而不是整张图片的大小)。Content-Range 响应首部则表示这一部分内容在整个资源中所处的位置。

多重范围

Range 头部也支持一次请求文档的多个部分。请求范围用一个逗号分隔开。

curl https://xxx.jpg -i -H "Range: bytes=0-50, 100-150"

服务器返回 206 Partial Content 状态码,Content-Type:multipart/byteranges,boundary=3d6b6a416f9b5 头部。

  • Content-Type:multipart/byteranges 表示这个响应有多个 byterange。
  • 每一部分 byterange 都有他自己的 Content-type 头部和 Content-Range,并且使用 boundary 参数对 body 进行划分。
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 282

--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 0-50/1270

<!doctype html>
<html>
<head>
    <title>Example Do
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-150/1270

eta http-equiv="Content-type" content="text/html; c
--3d6b6a416f9b5--

条件式分片请求

当(中断之后)重新开始请求更多资源片段的时候,必须确保自从上一个片段被接收之后该资源没有进行过修改。

The If-Range 请求首部可以用来生成条件式分片请求:

  • 假如条件满足的话,条件请求就会生效,服务器会返回状态码为 206 Partial 的响应,以及相应的消息主体。
  • 假如条件未能得到满足,那么就会返回状态码为 200 OK 的响应,同时返回整个资源。

该首部可以与 Last-Modified 验证器或者 ETag 一起使用,但是二者不能同时使用。

If-Range: Wed, 21 Oct 2015 07:28:00 GMT

Range分片请求的响应

与分片请求相关的有三种状态:

  • 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。请求多个部分,服务器会以 multipart 文件的形式将其返回。
  • 在请求的范围越界的情况下(范围值超过了资源的大小),服务器会返回 416 Requested Range Not Satisfiable (请求的范围无法满足)状态码。
  • 在不支持分片请求的情况下,服务器会返回 200 OK 状态码。

文件整体下载

以下实例采用node作为后端。

下载文件是一个常见的需求,只要服务端设置 Content-Dispositionattachment 就可以。

比如这样:

const express = require('express');
const app = express();

app.get('/download',(req, res, next) => {
    res.setHeader('Content-Disposition','attachment; filename="test.txt"')
    res.end('donwloadfileContent');
})

app.listen(3000, () => {
    console.log(`server is running at port 3000`)
})

设置 Cotent-Dispositionattachment,指定 filename

然后 html 里加一个 a 标签:

<!DOCTYPE html>
<html lang="en">
<body>
    <a href="http://localhost:3000/download">download</a>
</body>
</html>
  1. 下载http-server npm包,在当前文件目录运行http-server命令跑起静态服务器。
  2. 点击链接就可以下载。若需要在服务器端js文件修改后动态生效,则可以安装nodemon npm包,进而执行nodemon index.js命令。

当文件过大,则需要对文件进行分片来下载,下面使用案例进行讲解。

文件分片下载

文本下载

添加这样一个路由:

app.get('/downloadRange', (req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');

    res.download('downloadRange.txt', {
        acceptRanges: true
    })
})

downloadRange.txt与index.js在同级目录下。

设置允许跨域请求,因为前端起的静态服务为http://localhost:8080,而node服务为http://localhost:3000,非同域。

res.download 是读取文件内容返回,acceptRanges 选项为 true 就是会处理 range 请求(其实默认就是 true)。

文件 downloadRange.txt 的内容是这样的:

0123456789

然后在 html 里访问一下这个接口:

<!DOCTYPE html>
<html lang="en">
<body>
    <script>
        fetch('http://localhost:3000/downloadRange', {
            headers: {
                Range: 'bytes=0-4',
            }
        })
            .then(res => res.text())
            .then(res => {
                console.log(res) // 输出01234
            })
            .catch((err) => {
                console.log(err);
            })
    </script>
</body>
</html>

访问页面,可以看到返回的是 206 的状态码!

这时候 Content-Length 就代表返回的内容的长度。

还有个 Content-Range 代表当前 range 的长度以及总长度。

此时响应内容为01234

当然,你也可以访问 5 以后的内容

Range: 'bytes=5-'

响应头内容是这样的:

Content-Length: 5
Content-Range: bytes 5-9/10

返回的内容是这样的:

56789

这俩连接起来就是整个文件的内容。这样就实现了简易版的断点续传。

我们再来试试如果超出 range 会怎么样:

Range: 'bytes=50-60',

请求 50-60 字节的内容,这时候响应头是这样的:

Status Code: 416 Range Not Satisfiable

返回的是 416 状态码,代表 range 不合法。

Range 不是还可以设置多段么?多段内容是怎么返回的呢?

我们来试一下:

Range: 'bytes=0-1, 3-4, 6-'

重新访问一下,这时候报了一个跨域的错误,说是发送预检请求失败。

Access to fetch at 'http://localhost:3000/downloadRange' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

浏览器会在三种情况下发送预检(preflight)请求:

  • 用到了非 GET、POST 的请求方法,比如 PUT、DELETE 等,会发预检请求看看服务端是否支持
  • 用到了一些非常规请求头,比如用到了 Content-Type,会发预检请求看看服务端是否支持
  • 用到了自定义 header,会发预检请求

为啥 Range 头单个 range 不会触发预检请求,而多个 range 就触发了呢?

因为多个 range 的时候返回的 Content-Type 是不一样的,是 multipart/byteranges 类型,比较特殊。

预检请求是 options 请求,那我们就支持一下:

app.options('/downloadRange', (req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Range')
    res.end('');
});

然后重新访问,这时候你会发现虽然状态码为200,且返回的是整个内容!

这是因为 express 只做了单 range 的支持,多段 range 可能它觉得没必要支持吧。

毕竟你发多个单 range 请求就能达到一样的效果。

图片下载

下面我们就用 range 来实现下文件的分片下载,最终合并成一个文件的功能。

我们来下载一个图片吧,分成两块下载,然后下载完合并起来。

就用这个图片好了:

node代码修改如下

app.options('/downloadPicRange', (req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Range')
    res.end('');
});

app.get('/downloadPicRange', (req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.download('1681859964629.png', {
        acceptRanges: true
    })
})

我们写下分片下载的代码,就分两段:这个图片是 56585 字节,也就是大概55.2k,那我们就分成 0-20000 和 20001- 两段:

<!DOCTYPE html>
<html lang="en">
<body>
    <script>
        const p1 = fetch('http://localhost:3000/downloadPicRange', {
            headers: {
                Range: 'bytes=0-20000',
            }
        })
        
        const p2 = fetch('http://localhost:3000/downloadPicRange', {
            headers: {
                Range: 'bytes=20001-',
            }
        })

        Promise.all([p1, p2])
        .then(res => {
           return Promise.all(res.map(resBody => resBody.blob()))
        })
        .then(res => {
            const completeBlob = new Blob(res, { type: res[0].type})
            console.log(completeBlob)
            console.log(URL.createObjectURL(completeBlob))
        })
        .catch(err => {
            console.log(err)
        })
    </script>
</body>
</html>

两个响应头Content-Range分别是这样的:

Content-Range: bytes 0-20000/56585
Content-Range: bytes 20001-56584/56585

第一个响应还能看到图片的预览,只能看到上部分:

然后我们要把两段给拼起来,怎么拼呢?

  1. 这里由于使用fetch进行请求,我们可以直接获取响应内容的文件对象为blob类型。
  2. 然后将两段blob类型文件对象,合并到新的blob文件对象中。
  3. 通过URL.createObjectURL获取文件对象的资源在本地的链接,将其粘贴到浏览器中,可以看到组合的图片正常显示。

封装下载方法

当然,一般不会这么写死来用,我们可以封装一个通用的文件分片下载工具。

但分片之前需要拿到文件的大小,所以要增加一个接口,调用这个接口返回文件大小:

const fs = require('fs');

app.get('/length',(req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.end('' + fs.statSync('./1681859964629.png').size);
})

然后我们来做分片:

async function fileDownloadRange(path, size, chunkSize) {
    let chunkNum = Math.ceil(size / chunkSize);

    const downloadTask = [];
    for (let i = 1; i <= chunkNum; i++) {
        const rangeStart = chunkSize * (i - 1);
        const rangeEnd = chunkSize * i - 1;

        downloadTask.push(
            fetch(path, {
                headers: {
                    Range: `bytes=${rangeStart}-${rangeEnd}`,
                },
            })
        )
    }
    return await Promise.all(downloadTask.map(task => task.then(res => res.blob())))
}

这部分代码不难理解:

首先根据 chunk 大小来计算一共几个 chunk,通过 Math.ceil 向上取整。

然后计算每个 chunk 的 range,构造下载任务的 promise。

Promise.all 等待所有下载任务完成,并获取下载内容为blob文件类型

我们来验证下:

async function fileDownloadRange(path, size, chunkSize) {
    let chunkNum = Math.ceil(size / chunkSize);

    const downloadTask = [];
    for (let i = 1; i <= chunkNum; i++) {
        const rangeStart = chunkSize * (i - 1);
        const rangeEnd = chunkSize * i - 1;

        downloadTask.push(
            fetch(path, {
                headers: {
                    Range: `bytes=${rangeStart}-${rangeEnd}`,
                },
            })
        )
    }
    return await Promise.all(downloadTask.map(task => task.then(res => res.blob())))
}

(async function () {
    const fileLength = await fetch('http://localhost:3000/length').then(res => res.text())
    const blobArr = await fileDownloadRange('http://localhost:3000/downloadPicRange', fileLength, 15000);

    const fileCompleted = new Blob(blobArr, { type: blobArr[0].type });
    console.log(URL.createObjectURL(fileCompleted))
})();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/981209.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个新工具 nolyfill

名字的意思&#xff0c; 我自己的理解 no(po)lyfill 正如它的名字, 不要再用补丁了, 当然这里说的是过时的补丁。 polyfill 是补丁的意思 为什么要用这个插件 文档原文: 当您通过安装最新的 Node.js LTS 来接受最新的功能和安全修复时&#xff0c;像eslint-plugin-import、…

架构师如何做好需求分析

架构师如何做好需求分析 目录概述需求&#xff1a; 设计思路实现思路分析1.主要步骤 2.主要步骤2操作步骤 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,…

Android笔记(二十八):在雷电模拟器安卓7.0+上使用Charles抓包详细教程

背景 由于手头没有合适的真机,所有经常使用雷神模拟器来跑项目,模拟器也需要能够抓包看看接口返回的数据,以便自测调试。本文记录了如何在雷电模拟器安卓7.0+上使用Charles抓包,其他模拟器没试过。 最终效果 浏览器打开百度网页,能抓到百度页面数据 具体步骤 模拟器…

xinput1_3.dll丢失的解决方法,快速修复xinput1_3.dll文件

在使用电脑时&#xff0c;我们可能会遇到各种各样的问题&#xff0c;其中之一就是xinput1_3.dll文件丢失的情况。这个文件是DirectX的一部分&#xff0c;它对于许多游戏和其他应用程序的正常运行至关重要。当xinput1_3.dll文件丢失时&#xff0c;我们无法启动或运行依赖该文件的…

LeetCode刷题笔记【28】:贪心算法专题-6(单调递增的数字、监控二叉树)

文章目录 前置知识738.单调递增的数字题目描述解题思路代码 968.监控二叉树题目描述解题思路代码 总结 前置知识 参考前文 参考文章&#xff1a; LeetCode刷题笔记【23】&#xff1a;贪心算法专题-1&#xff08;分发饼干、摆动序列、最大子序和&#xff09; LeetCode刷题笔记【…

VMware虚拟机+Centos7 配置静态,动态IP

本章目录 一、查看网关&#xff1a; 编辑–>虚拟网络编辑器二、点击NAT设置三、记住网关IP待会要用四、配置静态ip地址1、进入存放修改IP地址的目录2、修改ip地址的文件3、编辑文件4、文件&#xff08;编辑好后退出&#xff09; 五、重启网络六、测试1、linux上查看IP地址的…

使用pyenv安装python缓慢或无法安装

使用pyenv安装python缓慢或无法安装 这一定程度上和网络情况有关&#xff0c;下面提供几个常见方法&#xff1a; 关闭 VPN 后重新安装使用管理员权限打开命令窗口后安装如下 手动安装 pyenv 在执行 pyenv install --- 命令的时候&#xff0c;会连接远程库&#xff0c;将要安…

格式工厂多个图片合并成一个PDF的报错

使用图片合并PDF功能时 当图片数量超过50会报错 找到imgconv.py文件&#xff0c;将50改为500&#xff0c;保存 现在可以支持100张图合并成一个PDF文件了&#xff01; 但是超过150张程序会直接闪退&#xff0c;正在解决中。。

基于任务队列的机器学习服务实现

将机器模型部署到生产环境的方法有很多。 常见的方法之一是将其实现为 Web 服务。 最流行的类型是 REST API。 它的作用是全天候&#xff08;24/7&#xff09;部署和运行&#xff0c;等待接收来自客户端的 JSON 请求&#xff0c;提取输入&#xff0c;并将其发送到 ML 模型以预测…

3D异常检测论文笔记 | Shape-Guided Dual-Memory Learning for 3D Anomaly Detection

文章目录 摘要一、介绍三、方法3.1. 形状引导专家学习3.2. Shape-Guided推理 摘要 我们提出了一个形状引导的专家学习框架来解决无监督的三维异常检测问题。我们的方法是建立在两个专门的专家模型的有效性和他们的协同从颜色和形状模态定位异常区域。第一个专家利用几何信息通…

涛然自得周刊(第 5 期):蝲蛄吟唱的地方

作者&#xff1a;何一涛 日期&#xff1a;2023 年 8 月 20 日 涛然自得周刊主要精选作者阅读过的书影音内容&#xff0c;不定期发。历史周刊内容可以看这里。 电影 《沼泽深处的女孩》 改编自小说《蝲蛄吟唱的地方》&#xff0c;主角是一位在沼泽地独自生活并长大的女孩&…

[VSCode] 替换掉/去掉空行

VSCode中使用快捷键CtrlH&#xff0c;出现替换功能&#xff0c;在上面的“查找”框中输入正则表达式&#xff1a; ^\s*(?\r?$)\n然后选择右侧的“使用正则表达式”&#xff1b;“替换”框内为空&#xff0c;点击右侧的“全部替换”&#xff0c;即可去除所有空行。 参考 [VS…

Apipost forEach控制器怎么用

最近&#xff0c;Apipost对自动化测试进行了优化&#xff0c;新增foreach控制器。这个新功能的引入为自动化测试带来了更高的效率和灵活性。本文将介绍Apipost的foreach控制器&#xff0c;解释其用途和优势&#xff0c;帮助您更好地利用这一功能提升自己的测试工作。 什么是fo…

Andorid项目源码(167套)

一、项目介绍 (精华)新浪微博图片缓冲技术_hyg.rar ActivityGroup GridView ViewFlipper 实现选项卡.zip Adroid UI 界面绘制原理分析.rar AnderWeb-android_packages_apps_Launcher-4458ee4.zip andorid 源码北京公交线路查询&#xff08;离线&#xff09;.zip android Gal…

【Java 基础篇】Java Date 类详解:日期和时间操作的利器

在 Java 编程中&#xff0c;处理日期和时间是一项常见但复杂的任务。Java 提供了许多用于日期和时间操作的类&#xff0c;其中 java.util.Date 类是最早的日期和时间类之一。然而&#xff0c;它存在一些问题&#xff0c;因此 Java 8 引入了 java.time 包&#xff0c;其中包含了…

宇凡微发布2.4G合封芯片YE08,融合高性能MCU与射频收发功能

宇凡微在2023年推出了全新的2.4G合封芯片YE08&#xff0c;该芯片结合了32位高性能MCU和强大的2.4GHz无线通信功能&#xff0c;为各种远程遥控应用提供卓越性能和广泛应用潜力。 深入了解YE08内部构造 YE08芯片内部融合了两颗强大的芯片&#xff1a;PY32F002B MCU和G350 2.4G通…

基于神经网络结合紫外差分光谱的二氧化硫浓度定量预测

基于神经网络结合紫外差分光谱的二氧化硫浓度定量预测 前言一、代码运行1. 解压数据2. 导包3. 读取数据4. 构建网络5. 设置优化器6. 模型训练7. 可视化loss8. 模型验证 二、结果展示三、总结作者简介 前言 二氧化硫&#xff08;SO2&#xff09;是一种常见的环境污染物&#xff…

CUDA小白 - NPP(4) 图像处理 Data Exchange and Initialization(2)

cuda小白 原始API链接 NPP GPU架构近些年也有不少的变化&#xff0c;具体的可以参考别的博主的介绍&#xff0c;都比较详细。还有一些cuda中的专有名词的含义&#xff0c;可以参考《详解CUDA的Context、Stream、Warp、SM、SP、Kernel、Block、Grid》 常见的NppStatus&#xf…

pytorch代码实现之空间通道重组卷积SCConv

空间通道重组卷积SCConv 空间通道重组卷积SCConv&#xff0c;全称Spatial and Channel Reconstruction Convolution&#xff0c;CPR2023年提出&#xff0c;可以即插即用&#xff0c;能够在减少参数的同时提升性能的模块。其核心思想是希望能够实现减少特征冗余从而提高算法的效…

C++信息学奥赛1190:上台阶

#include <iostream> using namespace std;long long arr[80]; // 用于存储斐波那契数列的数组int main() {int n;arr[1]1; // 初始化斐波那契数列的前三个元素arr[2]2;arr[3]4;for(int i4;i<71;i) { // 计算斐波那契数列的第4到第71个元素arr[i]arr[i-1]arr[i-2]…