當你使用手機暢快的看着視頻、聽着音樂,這時,你有想過這些東西是怎麼傳輸到你的手機上的麼?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