HTTP 请求头 Range
请求资源的部分内容 (不包括响应头的大小), 单位是 byte, 即字节, 从 0 开始.
如果服务器能够正常响应的话, 服务器会返回 206 Partial Content 的状态码及说明.
如果不能处理这种 Range 的话, 就会返回整个资源以及响应状态码为 200 OK .
Range 请求头格式
Range: bytes=start-end
响应头
Conent-Length
表示这次服务器响应数据的字节数
一, 思路整理
用过迅雷等下载工具会发现: 文件在下载过程中, 会生成. downloading 后缀和. downloading.cfg 后缀的两个文件..downloading 后缀的文件跟文件已下载的大小是一致的, 而. downloading.cfg 后缀的文件特别小. 当文件下载完成后,.downloading 后缀及. downloading.cfg 文件均不存在, 只保留下载完成的文件.
通过网上了解知道, cfg 文件大多是配置文件. 那么可以 推测出:.downloading 文件是下载的临时文件, 接收下载文件流. 而. downloading.cfg 是下载的配置文件, 保存文件下载的相关信息.
配合断点续传的需求, 梳理出分片下载的方案: 文件下载, 首先判断当前目录有没有已下载的断点文件. 若有, 则创建一个'append'的文件流, 通过. downloading.cfg 文件读取已下载分片的相关信息, 续传下载; 若无, 则创建一个新文件流, 指定请求文件的部分内容 (分片). 传输过程中, 将文件流写入. downloading 文件, 并同步更新. downloading.cfg 文件, 记录下载文件的相关信息及分片信息. 每一片传输完成, 判断服务器相应数据的字节是否小于分片字节数. 若是, 表示为最后一个分片, 文件已下载完成, 将. downloading 文件重命名为原文件名并删除. downloading.cfg 文件.
二, 分解任务
将任务分解成几个子任务:
1, 递归创建文件夹.
2, 判断当前目录有没有已下载的断点文件, 创建文件流.
3, 设定 HTTP 请求头 Range, 分片请求文件 url.
4, 更新. downloading.cfg 文件.
5, 文件下载完成, 重命名. downloading 文件并删除. downloading.cfg 文件.
1, 递归创建文件夹
完整路径为 "D:/tmp / 新建文件夹 / 002.docx" 之类的文件在下载时需要先一级一级创建文件夹. 借助 Node 的 fs 及 path 模块, 完成递归创建文件夹任务.
- const fs = require("fs")
- const path = require("path")
- const mkdirs = (dirname, callback, errback) => {
- fs.stat(dirname, (err, stats) => {
- if (err) {
- mkdirs(path.dirname(dirname), () => {
- fs.mkdir(dirname, callback)
- }, errback)
- } else {
- if (stats.isDirectory()) {
- callback()
- } else {
- errback()
- }
- }
- })
- }
2, 父级文件夹创建好后, 判断当前目录有没有已下载的断点文件, 创建文件流.
fs.createWriteStream 返回 WriteSteam 对象, 用于创建文件写入流.
- fs.createWriteStream(path[, options])
- path <string> | <Buffer> | <URL>
- options <string> | <Object>
- flags <string> See support of file system flags. Default: 'w'.
- encoding <string> Default: 'utf8'
- fd <integer> Default: null
- mode <integer> Default:
- 0o666
- autoClose <boolean> Default: true
- start <integer>
- Returns: <fs.WriteStream> See Writable Stream.
还是借助 Node 的 fs 模块的 stat 方法, 检测当前目录有没有. downloading 文件. 若有, 则创建一个 flags 为'a'的文件流; 若无, 则创建一个默认的文件流.
- let statDir = function (flag) {
- fs.stat(file.path + '.downloading', function (err, stats) {
- if (flag) {
- if (err) {
- contents.send('download-error', file.path)
- stream.end()
- return
- }
- } else {
- stream = !err ? fs.createWriteStream(file.path + '.downloading', {flags: 'a'}) :
- fs.createWriteStream(file.path + '.downloading')
- streams.push(stream)
- if (!err) {
- receivedBytes += stats.size
- }
- }
- func()
- })
- }
3, 设定 HTTP 请求头 Range, 分片请求文件 url.
net
使用 Chromium 的原生网络库发出 HTTP / HTTPS 请求
net 模块是一个发送 HTTP(S) 请求的客户端 API. 它类似于 Node.JS 的 HTTP https://nodejs.org/api/http.html 和 HTTPS https://nodejs.org/api/https.html 模块 , 但它使用的是 Chromium 原生网络库来替代 Node.JS 的实现, 提供更好的网络代理支持.
receivedBytes 为. downloading 临时文件已下载的文件流大小, chunkSize 为分片大小. 所以每个分片的请求内容为 receivedBytes 至 receivedBytes + chunkSize - 1. 每个分片下载完成后, 更新 receivedBytes 大小.
- const request = net.request(url)
- let start = receivedBytes
- let end = receivedBytes + chunkSize - 1
- request.setHeader('Range', 'bytes=' + start + '-' + end)
- request.on('response', (response) => {
- response.on('data', chunk => {
- if (response.statusCode == 206) {
- try {
- stream.write(chunk)
- } catch(e) {}
- }
- })
- let contentLength = response.headers['content-length'][0]
- response.on('end', () => {
- receivedBytes += parseInt(contentLength)
- }
4, 更新. downloading.cfg 文件, 记录下载文件的相关信息及分片信息.
.downloading 文件保存文件的进度, 大小, 路径等信息. 用于启动应用时, 读取并渲染续传列表, 显示文件名, 文件大小, 进度条等信息.
- let JSON = {
- percent: percent,
- filesize: file.filesize,
- md5: file.md5,
- uid: file.uid ,
- bucketName: file.bucketName,
- path: file.path,
- }
- try {
- !stream.closed && fs.writeFileSync(file.path + '.downloading.cfg', JSON.stringify(JSON))
- } catch(e) {}
5, 最后一个分片下载完成 , 将. downloading 文件重命名为原文件名并删除. downloading.cfg 文件.
getList
获取当前目录下的文件列表.
获取文件列表后, 算出重命名后的文件名 (如果当前目录有重名文件, 则需要将文件重命名. 重命名算法见系列文章 (一)). 将. downloading 文件重命名为算出的文件名并删除. downloading.cfg 文件.
- if (contentLength <chunkSize) {
- stream.end()
- endStream(file.path)
- try {
- getList(dirname).then(fileList => {
- let newName = fileRename(fileList, filename, 'filename')
- setTimeout(() => {
- fs.rename(file.path + '.downloading', path.join(dirname, newName), (err) => {
- if (err) {
- return console.error(err)
- }
- })
- }, 500)
- })
- fs.unlink(file.path + '.downloading.cfg', function (er) {
- if (er) {
- return console.error(er);
- }
- })
- } catch (e) { console.log(e) }
- } else {
- !stream.closed && statDir(true)
- }
至此, 文件分片下载完成.
来源: https://www.cnblogs.com/shawnyung/p/10097959.html