当你使用手机畅快的看着视频、听着音乐,这时,你有想过这些东西是怎么传输到你的手机上的么?
这次,就让我们以nodejs的koa2为例,来一个大揭秘!
我们以mp3类型的音频为例子: 下图就是一个http请求mp3文件,
- 在
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请求范围,HTTP协议范围请求允许服务器只发送HTTP消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时非常有用。
1. 了解完基础知识,就到了nodejs登场的时候。
首先介绍两个我们最常用的两个模块fs【文件系统】和path【模块提供用于处理文件路径和目录路径的实用工具】 ,我们以koa为例进行介绍
引用的写法如下:
const fs = require('fs')
const path = require('path')
2.音视频文件的类型
从上图中可以看出,在Response Headers中Content-Type: audio/mpeg,而常用的音视频格式有mp3、mp4、webm、ogg、ogv、flv、wav等,在HTTP中返回的Content-Type各不相同,整理如下:
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'
}
3.判断请求文件类型
每次在客户端进行访问的时候,我们首先需要确定请求文件的类型,因此,我们还需要如下的一个纯函数:
let getContentType = (type) => {
if (mine[type]) {
return mine[type]
} else {
reutrn null
}
}
4.读取文件
有了上面的准备我们就可以开始读取相应的文件,并返回给客户端了。
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()
})
})
}
}
5.导出文件
此时我们还需要将方法导出去,方便使用
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()
}
}
6.在app.js中使用
终于来到了我们在项目中使用的关键时刻
const Koa = require('koa')
const app = new Koa()
app.use(koaMedia({
extMatch: /\.mp[3-4]$/i
}))
这样我们就完成了从客户端请求到服务端返回的全部过程。
关于中间件原理可以看我的这篇文章nodejs中koa2中间件原理分析
注:使用到的API
1. Content-Range
Content-Range: <unit> <range-start>-<range-end>/<size>
<unit>数据区间所采用的单位。通常是字节(byte)。<range-start>一个整数,表示在给定单位下,区间的起始值。<range-end>一个整数,表示在给定单位下,区间的结束值。<size>整个文件的大小(如果大小未知则用"*"表示)。
2. fs.stat
fs.stat用于检查文件是否存在,读取文件状态。获取文件信息。
| 方法 | 描述 |
|---|---|
| stats.isFile() | 如果是文件返回 true,否则返回 false。 |
| stats.isDirectory() | 如果是目录返回 true,否则返回 false。 |
| stats.isBlockDevice() | 如果是块设备返回 true,否则返回 false。 |
| stats.isCharacterDevice() | 如果是字符设备返回 true,否则返回 false。 |
| stats.isSymbolicLink() | 如果是软链接返回 true,否则返回 false。 |
| stats.isFIFO() | 如果是FIFO,返回true,否则返回 false。FIFO是UNIX中的一种特殊类型的命令管道。 |
| stats.isSocket() | 如果是 Socket 返回 true,否则返回 false。 |
参考文档:Node.js 文件系统
3. fs.statSync
fs.statSync同步的stat,返回stats类
4. stats.isFile()
stats.isFile()判断获取的对象是否为常规文件,是则返回true
5. stats.size
stats.size获取文件大小(以字节为单位)
6. path.extname
path.extname方法返回 path 的扩展名,从最后一次出现 .(句点)字符到 path最后一部分的字符串结束。 如果在 path 的最后一部分中没有 . ,或者如果 path 的基本名称(参阅 path.basename())除了第一个字符以外没有 .,则返回空字符串。
7. fs.createReadStream
fs.createReadStream,参数option可以包括 start 和 end 值,以从文件中读取一定范围的字节而不是整个文件。start 和 end 都包含在内并从 0 开始计数,允许的值在 [0, Number.MAX_SAFE_INTEGER] 的范围内。如果指定了 fd 并且省略 start 或为 undefined,则 fs.createReadStream() 从当前的文件位置开始顺序地读取。 encoding 可以是 Buffer 接受的任何一种字符编码。
特别鸣谢:koa-video