Node.js中的流
Node.js的流(Stream) API 非常強大,它是處理流數據的抽象接口。流可以看成是一種數據的集合,但它並不是一下子全部讀到內存裏面,而是一塊一塊地去產生、消耗,這種方式最顯而易見的好處是可以方便地處理大文件。數據流可以是可讀流、可寫流,實際上Node.js中的流分爲4種類型 : Readable、Writable、Duplex、Transform。
- Readable Stream:可讀流是對可消費的數據源進行的抽象,比如fs.createReadStream
- Writable Stream:可寫流是對流的目的地(destination)的抽象,destination運允許數據寫入,比如fs.createWriteStream
- Duplex Stream:雙工流是同時實現了 Readable 和 Writable 接口的流,既能寫又能讀。比如TCP socket
- Transform Stream:交換流本質上是一種Duplex流,可以將其看成輸入Writable流,輸出的是Readable流,也可以稱之爲“通過流”(through streams)。比如zlib streams。
Node中有許多內置對象實現了Stream接口:
對於TCP sockets、zlib 和 crypto 流而言,他們是Duplex Stream。
.pipe()方法
對於Readable流而言,有兩種消費數據流的方式:Paused Mode 和 Flowing Mode。簡單來說,Paused Mode就像是把水缸裏面的水一瓢瓢舀出來,可以根據需要使用read()方法去消費數據流;Flowing Mode就像是給水缸接了根管子,水可以從高處流到低處,我們可以監聽data事件得到一塊數據流。
所有的Readable流默認是Paused Mode,使用resume()、pause()方法可以與 Flowing Mode 相互切換,resume()方法就像是給水缸接好管子,水自動流動;pause()方法就像移除管子,我們得手動去舀水。這種切換方式很簡單,是有時候是自動發生的。
當Readable流使用pipe()方法時,就相當於給數據流接上了管子,數據流會自動從上游流到下游。在使用pipe()時,需要注意的是上游是Readable,下游是Writable,即:
readableSrc.pipe(writableDest)
由於Duplex流 實現了Readable、和Writable,可以將Duplex或Transform 放在“中游”:
readableSrc
.pipe(transformStream1)
.pipe(transformStream2)
.pipe(finalWrtitableDest)
我們知道,Node中的Server端HTTP response是Writable流,而通過fs.createReadStream讀取視頻數據得到的是Readable流。因此,Node的Server端可以直接使用pipe()方法將視頻流發到前端:
//前端
<video src="/video"></video>
//服務端
router.get('/video', function(req, res, next) {
let head = { 'Content-Type': 'video/mp4' };
//需要設置HTTP HEAD
res.writeHead(200, head);
//使用pipe
fs.createReadStream('./assets/sintel.mp4')
.pipe(res);
});
前端結果:
可以注意到前端下載了一個html和video。
HTTP 206
HTTP/1.1 206狀態碼錶示的是:客戶端通過發送範圍請求頭Range獲取資源的部分數據。這種請求可以將服務端文件分割成多個部分傳給客戶端,可用於解決網絡問題以及大文件下載問題。對於一個很大的視頻,就可以採用這種請求將視頻流分成多個部分下載。
需要關注的HTTP Headers有:
- Range:用於請求頭中,指定第一個字節的位置和最後一個字節的位置,一般格式:Range:(unit=first byte pos)-[last byte pos] 。如 Range:bytes=0- 表示請求服務端第0及以後bytes的數據; Range:bytes=0-999 表示0到999 bytes的數據,注意這個區間的長度是1000bytes。
- Accept-Ranges:用於響應頭,表明服務器支持Range請求,以及服務器所支持的單位是字節(這也是唯一可用的單位);Accept-Ranges: none 響應頭表示服務器不支持範圍請求。
- Content-Range:用於響應頭,指定整個實體中的一部分的插入位置,一般格式: Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]。如Content-Range:bytes 1000000-1999999/3332038 表示的是服務端資源總大小3332038 bytes,此次返回的是其中第1000000到1999999 bytes 的數據。
- Content-Length:用於響應頭,表明了響應實體的大小,它應該等於Content-Range中的 last byte pos - first byte pos + 1。
將視頻流分成多個部分發給前端,只要注意控制好流的數據區間即可,服務端代碼如下:
router.get('/video', function(req, res, next) {
let path = './assets/sintel.mp4';
let stat = fs.statSync(path);
let fileSize = stat.size;
let range = req.headers.range;
// fileSize 3332038
if (range) {
//有range頭才使用206狀態碼
let parts = range.replace(/bytes=/, "").split("-");
let start = parseInt(parts[0], 10);
let end = parts[1] ? parseInt(parts[1], 10) : start + 999999;
// end 在最後取值爲 fileSize - 1
end = end > fileSize - 1 ? fileSize - 1 : end;
let chunksize = (end - start) + 1;
let file = fs.createReadStream(path, { start, end });
let head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
};
res.writeHead(206, head);
file.pipe(res);
} else {
let head = {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
};
res.writeHead(200, head);
fs.createReadStream(path).pipe(res);
}
});
前端結果:
這次的視頻就被分成了4個部分,每個video請求及結果如下:
順序 | Request | Response |
---|---|---|
1 | Range:bytes=0- | Content-Range:bytes 0-999999/3332038 Content-Length:1000000 |
2 | Range:bytes=1000000- | Content-Range:bytes 1000000-1999999/3332038 Content-Length:1000000 |
3 | Range:bytes=2000000- | Content-Range:bytes 2000000-2999999/3332038 Content-Length:1000000 |
4 | Range:bytes=3000000- | Content-Range:bytes 3000000-3332037/3332038 Content-Length:332038 |
代碼地址:https://git.oschina.net/liuyaqi/node-video-stream_demo.git
參考鏈接:
菜鳥教程: Node.js Stream(流)
Node中文文檔 Stream
Node.js Streams: Everything you need to know
RFC 2616: HTTP狀態碼定義
http斷點續傳原理:http頭 Range、Content-Range