当你使用手机畅快的看着视频、听着音乐,这时,你有想过这些东西是怎么传输到你的手机上的么?html
此次,就让咱们以nodejs
的koa2
为例,来一个大揭秘!node
咱们以mp3类型的音频为例子: 下图就是一个http请求mp3文件,git
Request Headers
中有个Range: bytes=0-,Range
表明指示服务器应该返回文件的哪一或哪几部分。end是一个整数(如:Range: bytes=0-136868
),表示在特定单位下,范围的结束值。这个值是可选的,若是不存在,表示此范围一直延伸到文档结束。Accept-Ranges
首部(而且它的值不为 “none”
),那么表示该服务器支持范围请求。 Accept-Ranges: bytes
表示界定范围的单位是 bytes
。这里 Content-Length
它提供了要检索的文件的完整大小。Response Headers
中的,Content-Length
首部如今用来表示先前请求范围的大小(而不是整个文件的大小)。Content-Range
响应首部则表示这一部份内容在整个资源中所处的位置。 对于以上的解释能够参考:HTTP请求范围nodejs
登场的时候。首先介绍两个咱们最经常使用的两个模块fs【文件系统】和path【模块提供用于处理文件路径和目录路径的实用工具】 ,咱们以koa
为例进行介绍github
引用的写法以下:web
const fs = require('fs')
const path = require('path')
复制代码
从上图中能够看出,在Response Headers
中Content-Type: audio/mpeg
,而经常使用的音视频格式有mp三、mp四、webm、ogg、ogv、flv、wav
等,在HTTP
中返回的Content-Type
各不相同,整理以下:api
const mime = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'ogg': 'application/ogg',
'ogv': 'video/ogg',
'mpg': 'video/mepg',
'flv': 'flv-application/octet-stream',
'mp3': 'audio/mpeg',
'wav': 'audio/x-wav'
}
复制代码
每次在客户端进行访问的时候,咱们首先须要肯定请求文件的类型,所以,咱们还须要以下的一个纯函数:数组
let getContentType = (type) => {
if (mine[type]) {
return mine[type]
} else {
reutrn null
}
}
复制代码
有了上面的准备咱们就能够开始读取相应的文件,并返回给客户端了。bash
let readFile = async(ctx, options) => {
// 咱们先确认客户端请求的文件的长度范围
let match = ctx.request.header['range']
// 获取文件的后缀名
let ext = path.extname(ctx.path).toLocaleLowerCase()
// 获取文件在磁盘上的路径
let diskPath = decodeURI(path.resolve(options.root + ctx.path))
// 获取文件的开始位置和结束位置
let bytes = match.split('=')[1]
// 有了文件路径以后,咱们就能够来读取文件啦
let stats = fs.statSync(diskPath)
// 在返回文件以前,咱们还要知道获取文件的范围(获取读取文件的开始位置和开始位置)
let start = Number.parseInt(bytes.split('-')[0]) // 开始位置
let end = Number.parseInt(bytes.split('-')[1]) || (stats.size - 1) // 结束位置
// 若是是文件类型
if (stats.isFile()) {
reture new Promise((resolve, reject) => {
// 读取所须要的文件
let stream = fs.createReadStream(diskPath, {start: start, end: end})
// 监听 ‘close’当读取完成时,将stream销毁
ctx.res.on('close', function () {
stream.distory()
})
// 设置 Response Headers
ctx.set('Content-Range': `bytes ${start}-${end}/${stats.size}`)
ctx.set('Accept-Range', `bytes`)
// 返回状态码
ctx.status = 206
// getContentType上场了,设置返回的Content-Type
ctx.type = getContentType(ext.replace('.','')
stream.on('open', function(length) {
if (ctx.res.socket.writeable) {
try {
stream.pipe(ctx.res)
} catch (e) {
stream.destroy()
}
} else {
stream.destroy()
}
})
stream.on('error', function(err) {
if (ctx.res.socket.writable) {
try {
ctx.body = err
} catch (e) {
stream.destroy()
}
}
reject()
})
// 传输完成
stream.on('end', function () {
resolve()
})
})
}
}
复制代码
此时咱们还须要将方法导出去,方便使用服务器
module.exports = function (opts) {
// 设置默认值
let options = Object.assign({}, {
extMatch: ['.mp4', '.flv', '.webm', '.ogv', '.mpg', '.wav', '.ogg'],
root: process.cwd()
}, opts)
return async (ctx, next) => {
// 获取文件的后缀名
let ext = path.extname(ctx.path).toLocaleLowerCase()
// 判断用户传入的extMath是否为数组类型,且访问的文件是否在此数组之中
let isMatchArr = options.extMatch instanceof Array && options.extMatch.indexOf(ext) > -1
// 判断用户传输的extMath是否为正则类型,且请求的文件路径包含相应的关键字
let isMatchReg = options.extMatch instanceof RegExp && options.extMatch.test(ctx.path)
if (isMatchArr || isMatchReg) {
if (ctx.request.header && ctx.request.header['range']) {
// readFile 上场
return await readFile(ctx, options)
}
}
await next()
}
}
复制代码
终于来到了咱们在项目中使用的关键时刻app
const Koa = require('koa')
const app = new Koa()
app.use(koaMedia({
extMatch: /\.mp[3-4]$/i
}))
复制代码
这样咱们就完成了从客户端请求到服务端返回的所有过程。
关于中间件原理能够看个人这篇文章nodejs中koa2中间件原理分析
注:使用到的API
Content-Range: <unit> <range-start>-<range-end>/<size>
<unit>
数据区间所采用的单位。一般是字节(byte)。<range-start>
一个整数,表示在给定单位下,区间的起始值。<range-end>
一个整数,表示在给定单位下,区间的结束值。<size>
整个文件的大小(若是大小未知则用"*"表示)。fs.stat
用于检查文件是否存在,读取文件状态
fs.statSync
同步的stat,返回stats类
stats.isFile()
判断获取的对象是否为常规文件,是则返回true
stats.size
获取文件大小(以字节为单位)
path.extname
方法返回 path
的扩展名,从最后一次出现 .(句点)字符到 path
最后一部分的字符串结束。 若是在 path
的最后一部分中没有 . ,或者若是 path
的基本名称(参阅 path.basename()
)除了第一个字符之外没有 .,则返回空字符串。
fs.createReadStream
,参数option
能够包括 start
和 end
值,以从文件中读取必定范围的字节而不是整个文件。start 和 end 都包含在内并从 0 开始计数,容许的值在 [0, Number.MAX_SAFE_INTEGER] 的范围内。若是指定了 fd 而且省略 start 或为 undefined,则 fs.createReadStream() 从当前的文件位置开始顺序地读取。 encoding 能够是 Buffer 接受的任何一种字符编码。
特别鸣谢:koa-video