自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Node.js中實(shí)現(xiàn)HTTP 206內(nèi)容分片

開發(fā) 前端
在本文中,我會(huì)闡述HTTP狀態(tài)206 分部分內(nèi)容 的基礎(chǔ)概念,并使用Node.js一步步地實(shí)現(xiàn)它. 我們還將用一個(gè)基于它用法最常見場景的示例來測試代碼:一個(gè)能夠在任何時(shí)間點(diǎn)開始播放視頻文件的HTML5頁面.

介紹

在本文中,我會(huì)闡述HTTP狀態(tài)206 分部分內(nèi)容 的基礎(chǔ)概念,并使用Node.js一步步地實(shí)現(xiàn)它. 我們還將用一個(gè)基于它用法最常見場景的示例來測試代碼:一個(gè)能夠在任何時(shí)間點(diǎn)開始播放視頻文件的HTML5頁面. 

Partial Content 的簡要介紹

HTTP 的 206 Partial Content 狀態(tài)碼和其相關(guān)的消息頭提供了讓瀏覽器以及其他用戶代理從服務(wù)器接收部分內(nèi)容而不是全部內(nèi)容,這樣一種機(jī)制. 這一機(jī)制被廣泛使用在一個(gè)被大多數(shù)瀏覽器和諸如Windows Media Player和VLC Player這樣的播放器所支持視頻文件的傳輸上.

基礎(chǔ)的流程可以用下面這幾步描述:

  1. 瀏覽器請求內(nèi)容.

  2. 服務(wù)器告訴瀏覽器,該內(nèi)容可以使用 Accept-Ranges 消息頭進(jìn)行分部分請求.

  3. 瀏覽器重新發(fā)送請求,用 Range 消息頭告訴服務(wù)器需要的內(nèi)容范圍.

  4. 服務(wù)器會(huì)分如下兩種情況響應(yīng)瀏覽器的請求: 

    • 如果范圍是合理的,服務(wù)器會(huì)返回所請求的部分內(nèi)容,并帶上 206 Partial Content 狀態(tài)碼. 當(dāng)前內(nèi)容的范圍會(huì)在 Content-Range 消息頭中申明.

    • 如果范圍是不可用的(例如,比內(nèi)容的總字節(jié)數(shù)大), 服務(wù)器會(huì)返回 416 請求范圍不合理 Requested Range Not Satisfiable 狀態(tài)碼. 可用的范圍也會(huì)在 Content-Range 消息頭中聲明.

讓我們來看看這幾個(gè)步驟中的每一個(gè)關(guān)鍵消息頭.

Accept-Ranges: 字節(jié)(bytes)

這是會(huì)有服務(wù)器發(fā)送的字節(jié)頭,展示可以被分部分發(fā)送給瀏覽器的內(nèi)容. 這個(gè)值聲明了可被接受的每一個(gè)范圍請求, 大多數(shù)情況下是字節(jié)數(shù) bytes

Range: 字節(jié)數(shù)(bytes)=(開始)-(結(jié)束)

這是瀏覽器告知服務(wù)器所需分部分內(nèi)容范圍的消息頭. 注意開始和結(jié)束位置是都包括在內(nèi)的,而且是從0開始的. 這個(gè)消息頭也可以不發(fā)送兩個(gè)位置,其含義如下: 

  • 如果結(jié)束位置被去掉了,服務(wù)器會(huì)返回從聲明的開始位置到整個(gè)內(nèi)容的結(jié)束位置內(nèi)容的最后一個(gè)可用字節(jié).

  • 如果開始位置被去掉了,結(jié)束位置參數(shù)可以被描述成從最后一個(gè)可用的字節(jié)算起可以被服務(wù)器返回的字節(jié)數(shù).

Content-Range:字節(jié)數(shù)(bytes)=(開始)-(結(jié)束)/(總數(shù))

這個(gè)消息頭將會(huì)跟隨 HTTP 狀態(tài)碼 206 一起出現(xiàn). 開始和結(jié)束的值展示了當(dāng)前內(nèi)容的范圍. 跟 Range 消息頭一樣, 兩個(gè)值都是包含在內(nèi)的,并且也是從零開始的. 總數(shù)這個(gè)值聲明了可用字節(jié)的總數(shù).

Content-Range: */(總數(shù))

這個(gè)頭信息和上面一個(gè)是一樣的,不過是用另一種格式,并且僅在返回HTTP狀態(tài)碼416時(shí)被發(fā)送。其中總數(shù)代表了正文總共可用的字節(jié)數(shù)。

這里有一對有2048個(gè)字節(jié)文件的例子。注意省略起點(diǎn)和重點(diǎn)的區(qū)別。

請求開始的1024個(gè)字節(jié)

瀏覽器發(fā)送:

  1. GET /dota2/techies.mp4 HTTP/1.1  
  2. Host: localhost:8000  
  3. Range: bytes=0-1023 

服務(wù)器返回:

  1. HTTP/1.1 216 Partial Content  
  2. Date: Mon, 15 Sep 2014 22:19:34 GMT  
  3. Content-Type: video/mp4  
  4. Content-Range: bytes 0-1023/2048  
  5. Content-Length: 1024  
  6.    
  7. (Content...) 

沒有終點(diǎn)位置的請求

瀏覽器發(fā)送:

  1. GET /dota2/techies.mp4 HTTP/1.1  
  2. Host: localhost:8000  
  3. Range: bytes=1024

服務(wù)器返回:

  1. HTTP/1.1 216 Partial Content  
  2. Date: Mon, 15 Sep 2014 22:19:34 GMT  
  3. Content-Type: video/mp4  
  4. Content-Range: bytes 1024-2047/2048  
  5. Content-Length: 1024  
  6.    
  7. (Content...) 

注意:服務(wù)器并不需要在單個(gè)響應(yīng)中返回所有剩下的字節(jié),特別是當(dāng)正文太長或者有其他性能的考慮。所以下面的兩個(gè)例子在這種情況下也是可接受的:

  1. Content-Range: bytes 1024-1535/2048  
  2. Content-Length: 512 

服務(wù)器僅返回剩余正文的一半。下一次請求的范圍將從第1536個(gè)字節(jié)開始。

  1. Content-Range: bytes 1024-1279/2048  
  2. Content-Length: 256 

服務(wù)器僅返回剩余正文的256個(gè)字節(jié)。下一次請求的范圍將從第1280個(gè)字節(jié)開始。

 

服務(wù)器返回:

  1. HTTP/1.1 216 Partial Content  
  2. Date: Mon, 15 Sep 2014 22:19:34 GMT  
  3. Content-Type: video/mp4  
  4. Content-Range: bytes 1536-2047/2048  
  5. Content-Length: 512  
  6.    
  7. (Content...) 

請求不可用的范圍:

瀏覽器發(fā)送:

  1. GET /dota2/techies.mp4 HTTP/1.1  
  2. Host: localhost:8000  
  3. Range: bytes=1024-4096 

服務(wù)器返回:

  1. HTTP/1.1 416 Requested Range Not Satisfiable  
  2. Date: Mon, 15 Sep 2014 22:19:34 GMT  
  3. Content-Range: bytes */2048 

理解了工作流和頭部信息后,現(xiàn)在我們可以用Node.js去實(shí)現(xiàn)這個(gè)機(jī)制。

#p#

第一步:創(chuàng)建一個(gè)簡單的HTTP服務(wù)器

我們將像下面的例子那樣,從一個(gè)基本的HTTP服務(wù)器開始。這已經(jīng)可以基本足夠處理大多數(shù)的瀏覽器請求了。首先,我們初始化我們需要用到的對象,并且用initFolder來代表文件的位置。為了生成Content-Type頭部,我們列出文件擴(kuò)展名和它們相對應(yīng)的MIME名稱來構(gòu)成一個(gè)字典。在回調(diào)函數(shù)httpListener()中,我們將僅允許GET可用。如果出現(xiàn)其他方法,服務(wù)器將返回405 Method Not Allowed,在文件不存在于initFolder,服務(wù)器將返回404 Not Found。

  1. // 初始化需要的對象  
  2. var http = require("http");  
  3. var fs = require("fs");  
  4. var path = require("path");  
  5. var url = require("url");  
  6.    
  7. // 初始的目錄,隨時(shí)可以改成你希望的目錄  
  8. var initFolder = "C:\\Users\\User\\Videos";  
  9.    
  10. // 將我們需要的文件擴(kuò)展名和MIME名稱列出一個(gè)字典  
  11. var mimeNames = {  
  12.     ".css""text/css",  
  13.     ".html""text/html",  
  14.     ".js""application/javascript",  
  15.     ".mp3""audio/mpeg",  
  16.     ".mp4""video/mp4",  
  17.     ".ogg""application/ogg",   
  18.     ".ogv""video/ogg",   
  19.     ".oga""audio/ogg",  
  20.     ".txt""text/plain",  
  21.     ".wav""audio/x-wav",  
  22.     ".webm""video/webm";  
  23. };  
  24.    
  25. http.createServer(httpListener).listen(8000);  
  26.    
  27. function httpListener (request, response) {  
  28.     // 我們將只接受GET請求,否則返回405 'Method Not Allowed'  
  29.     if (request.method != "GET") {   
  30.         sendResponse(response, 405, {"Allow" : "GET"}, null);  
  31.         return null;  
  32.     }  
  33.    
  34.     var filename =   
  35.         initFolder + url.parse(request.url, truetrue).pathname.split('/').join(path.sep);  
  36.    
  37.     var responseHeaders = {};  
  38.     var stat = fs.statSync(filename);  
  39.     // 檢查文件是否存在,不存在就返回404 Not Found  
  40.     if (!fs.existsSync(filename)) {  
  41.         sendResponse(response, 404, nullnull);  
  42.         return null;  
  43.     }  
  44.     responseHeaders["Content-Type"] = getMimeNameFromExt(path.extname(filename));  
  45.     responseHeaders["Content-Length"] = stat.size; // 文件大小  
  46.            
  47.     sendResponse(response, 200, responseHeaders, fs.createReadStream(filename));  
  48. }  
  49.    
  50. function sendResponse(response, responseStatus, responseHeaders, readable) {  
  51.     response.writeHead(responseStatus, responseHeaders);  
  52.    
  53.     if (readable == null)  
  54.         response.end();  
  55.     else 
  56.         readable.on("open"function () {  
  57.             readable.pipe(response);  
  58.         });  
  59.    
  60.     return null;  
  61. }  
  62.    
  63. function getMimeNameFromExt(ext) {  
  64.     var result = mimeNames[ext.toLowerCase()];  
  65.        
  66.     // 最好給一個(gè)默認(rèn)值  
  67.     if (result == null)  
  68.         result = "application/octet-stream";  
  69.        
  70.     return result;  

 

步驟 2 - 使用正則表達(dá)式捕獲Range消息頭

有了這個(gè)HTTP服務(wù)器做基礎(chǔ),我們現(xiàn)在就可以用如下代碼處理Range消息頭了. 我們使用正則表達(dá)式將消息頭分割,以獲取開始和結(jié)束字符串。然后使用 parseInt() 方法將它們轉(zhuǎn)換成整形數(shù). 如果返回值是 NaN (非數(shù)字not a number), 那么這個(gè)字符串就是沒有在這個(gè)消息頭中的. 參數(shù)totalLength展示了當(dāng)前文件的總字節(jié)數(shù). 我們將使用它計(jì)算開始和結(jié)束位置. 

  1. function readRangeHeader(range, totalLength) {  
  2.         /*  
  3.          * Example of the method 'split' with regular expression.  
  4.          *   
  5.          * Input: bytes=100-200  
  6.          * Output: [null, 100, 200, null]  
  7.          *   
  8.          * Input: bytes=-200  
  9.          * Output: [null, null, 200, null]  
  10.          */ 
  11.    
  12.     if (range == null || range.length == 0)  
  13.         return null;  
  14.    
  15.     var array = range.split(/bytes=([0-9]*)-([0-9]*)/);  
  16.     var start = parseInt(array[1]);  
  17.     var end = parseInt(array[2]);  
  18.     var result = {  
  19.         Start: isNaN(start) ? 0 : start,  
  20.         End: isNaN(end) ? (totalLength - 1) : end  
  21.     };  
  22.        
  23.     if (!isNaN(start) && isNaN(end)) {  
  24.         result.Start = start;  
  25.         result.End = totalLength - 1;  
  26.     }  
  27.    
  28.     if (isNaN(start) && !isNaN(end)) {  
  29.         result.Start = totalLength - end;  
  30.         result.End = totalLength - 1;  
  31.     }  
  32.    
  33.     return result;  

步驟 3 - 檢查數(shù)據(jù)范圍是否合理

回到函數(shù) httpListener(), 在HTTP方法通過之后,現(xiàn)在我們來檢查請求的數(shù)據(jù)范圍是否可用. 如果瀏覽器沒有發(fā)送 Range 消息頭過來, 請求就會(huì)直接被當(dāng)做一般的請求對待. 服務(wù)器會(huì)返回整個(gè)文件,HTTP狀態(tài)將會(huì)是 200 OK. 另外我們還會(huì)看看開始和結(jié)束位置是否比文件長度更大或者相等. 只要有一個(gè)是這種情況,請求的數(shù)據(jù)范圍就是不能被滿足的. 返回的狀態(tài)就將會(huì)是 416 Requested Range Not Satisfiable 而 Content-Range 也會(huì)被發(fā)送. 

  1. var responseHeaders = {};  
  2.     var stat = fs.statSync(filename);  
  3.     var rangeRequest = readRangeHeader(request.headers['range'], stat.size);  
  4.       
  5.     // If 'Range' header exists, we will parse it with Regular Expression.  
  6.     if (rangeRequest == null) {  
  7.         responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));  
  8.         responseHeaders['Content-Length'] = stat.size;  // File size.  
  9.         responseHeaders['Accept-Ranges'] = 'bytes';  
  10.            
  11.         //  If not, will return file directly.  
  12.         sendResponse(response, 200, responseHeaders, fs.createReadStream(filename));  
  13.         return null;  
  14.     }  
  15.    
  16.     var start = rangeRequest.Start;  
  17.     var end = rangeRequest.End;  
  18.    
  19.     // If the range can't be fulfilled.   
  20.     if (start >= stat.size || end >= stat.size) {  
  21.         // Indicate the acceptable range.  
  22.         responseHeaders['Content-Range'] = 'bytes */' + stat.size; // File size.  
  23.    
  24.         // Return the 416 'Requested Range Not Satisfiable'.  
  25.         sendResponse(response, 416, responseHeaders, null);  
  26.         return null;  
  27.     } 

步驟 4 - 滿足請求

最后使人迷惑的一塊來了。對于狀態(tài) 216 Partial Content, 我們有另外一種格式的 Content-Range 消息頭,包括開始,結(jié)束位置以及當(dāng)前文件的總字節(jié)數(shù). 我們也還有 Content-Length 消息頭,其值就等于開始和結(jié)束位置之間的差。在最后一句代碼中,我們調(diào)用了 createReadStream() 并將開始和結(jié)束位置的值給了第二個(gè)參數(shù)選項(xiàng)的對象, 這意味著返回的流將只包含從開始到結(jié)束位置的只讀數(shù)據(jù).

  1. // Indicate the current range.   
  2.     responseHeaders['Content-Range'] = 'bytes ' + start + '-' + end + '/' + stat.size;  
  3.     responseHeaders['Content-Length'] = start == end ? 0 : (end - start + 1);  
  4.     responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));  
  5.     responseHeaders['Accept-Ranges'] = 'bytes';  
  6.     responseHeaders['Cache-Control'] = 'no-cache';  
  7.    
  8.     // Return the 206 'Partial Content'.  
  9.     sendResponse(response, 206,   
  10.         responseHeaders, fs.createReadStream(filename, { start: start, end: end })); 

下面是完整的 httpListener() 回調(diào)函數(shù).

  1. function httpListener(request, response) {  
  2.     // We will only accept 'GET' method. Otherwise will return 405 'Method Not Allowed'.  
  3.     if (request.method != 'GET') {  
  4.         sendResponse(response, 405, { 'Allow''GET' }, null);  
  5.         return null;  
  6.     }  
  7.    
  8.     var filename =  
  9.         initFolder + url.parse(request.url, truetrue).pathname.split('/').join(path.sep);  
  10.    
  11.     // Check if file exists. If not, will return the 404 'Not Found'.   
  12.     if (!fs.existsSync(filename)) {  
  13.         sendResponse(response, 404, nullnull);  
  14.         return null;  
  15.     }  
  16.    
  17.     var responseHeaders = {};  
  18.     var stat = fs.statSync(filename);  
  19.     var rangeRequest = readRangeHeader(request.headers['range'], stat.size);  
  20.    
  21.     // If 'Range' header exists, we will parse it with Regular Expression.  
  22.     if (rangeRequest == null) {  
  23.         responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));  
  24.         responseHeaders['Content-Length'] = stat.size;  // File size.  
  25.         responseHeaders['Accept-Ranges'] = 'bytes';  
  26.    
  27.         //  If not, will return file directly.  
  28.         sendResponse(response, 200, responseHeaders, fs.createReadStream(filename));  
  29.         return null;  
  30.     }  
  31.    
  32.     var start = rangeRequest.Start;  
  33.     var end = rangeRequest.End;  
  34.    
  35.     // If the range can't be fulfilled.   
  36.     if (start >= stat.size || end >= stat.size) {  
  37.         // Indicate the acceptable range.  
  38.         responseHeaders['Content-Range'] = 'bytes */' + stat.size; // File size.  
  39.    
  40.         // Return the 416 'Requested Range Not Satisfiable'.  
  41.         sendResponse(response, 416, responseHeaders, null);  
  42.         return null;  
  43.     }  
  44.    
  45.     // Indicate the current range.   
  46.     responseHeaders['Content-Range'] = 'bytes ' + start + '-' + end + '/' + stat.size;  
  47.     responseHeaders['Content-Length'] = start == end ? 0 : (end - start + 1);  
  48.     responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));  
  49.     responseHeaders['Accept-Ranges'] = 'bytes';  
  50.     responseHeaders['Cache-Control'] = 'no-cache';  
  51.    
  52.     // Return the 206 'Partial Content'.  
  53.     sendResponse(response, 206,   
  54.         responseHeaders, fs.createReadStream(filename, { start: start, end: end }));  

#p#

測試實(shí)現(xiàn)

我們怎么來測試我們的代碼呢?就像在介紹中提到的,部分正文最常用的場景是流和播放視頻。所以我們創(chuàng)建了一個(gè)ID為mainPlayer并包含一個(gè)<source/>標(biāo)簽的<video/>。函數(shù)onLoad()將在mainPlayer預(yù)讀取當(dāng)前視頻的元數(shù)據(jù)時(shí)被觸發(fā),這用于檢查在URL中是否有數(shù)字參數(shù),如果有,mainPlayer將跳到指定的時(shí)間點(diǎn)。

  1. <!DOCTYPE html> 
  2. <html> 
  3.     <head> 
  4.         <script type="text/javascript"> 
  5.    
  6.             function onLoad() {  
  7.                 var sec = parseInt(document.location.search.substr(1));  
  8.                    
  9.                 if (!isNaN(sec))  
  10.                     mainPlayer.currentTime = sec;  
  11.             }  
  12.            
  13.         </script> 
  14.         <title>Partial Content Demonstration</title> 
  15.     </head> 
  16.     <body> 
  17.         <h3>Partial Content Demonstration</h3> 
  18.         <hr /> 
  19.         <video id="mainPlayer" width="640" height="360"   
  20.             autoplay="autoplay" controls="controls" onloadedmetadata="onLoad()"> 
  21.             <source src="dota2/techies.mp4" /> 
  22.         </video> 
  23.     </body> 
  24. </html> 

現(xiàn)在我們把頁面保存為"player.html"并和"dota2/techies.mp4"一起放在initFolder目錄下。然后在瀏覽器中打開URL:http://localhost:8000/player.html

在Chrome中看起來像這樣:

因?yàn)樵赨RL中沒有任何參數(shù),文件將從最開始出播放。

接下來就是有趣的部分了。讓我們試著打開這個(gè)然后看看發(fā)生了什么:http://localhost:8000/player.html?60

如果你按F12來打開Chrome的開發(fā)者工具,切換到網(wǎng)絡(luò)標(biāo)簽頁,然后點(diǎn)擊查看最近一次日志的詳細(xì)信息。你會(huì)發(fā)現(xiàn)范圍的頭信息(Range)被你的瀏覽器發(fā)送了:

  1. Range:bytes=225084502

很有趣,對吧?當(dāng)函數(shù)onLoad()改變currentTime屬性的時(shí)候,瀏覽器計(jì)算這部視頻60秒處的字節(jié)位置。因?yàn)閙ainPlayer已經(jīng)預(yù)加載了元數(shù)據(jù),包括格式、比特率和其他基本信息,這個(gè)起始位置立刻就被得到了。之后,瀏覽器就可以下載并播放視頻而不需要請求開頭的60秒了。成功了!

我們已經(jīng)用Node.js來實(shí)現(xiàn)支持部分正文的HTTP服務(wù)器端了。我們也用HTML5頁面測試了。但這只是一個(gè)開始。如果你對頭部信息和工作流這些都已經(jīng)理解透徹了,你可以試著用其他像ASP.NET MVC或者WCF服務(wù)這類框架來實(shí)現(xiàn)它。但是不要忘記啟動(dòng)任務(wù)管理器來查看CPU和內(nèi)存的使用。像我們在之前討論到的,服務(wù)器沒有在單個(gè)響應(yīng)中返回所用剩余的字節(jié)。要找到性能的平衡點(diǎn)將是一項(xiàng)重要的任務(wù)。

英文原文:HTTP 206 Partial Content In Node.js

 

譯文出自:http://www.oschina.net/translate/http-partial-content-in-node-js

結(jié)論

開始用Node.js實(shí)現(xiàn)

請求最后512個(gè)字節(jié)

瀏覽器發(fā)送:

  1. GET /dota2/techies.mp4 HTTP/1.1  
  2. Host: localhost:8000  
  3. Range: bytes=-512 
責(zé)任編輯:林師授 來源: 開源中國社區(qū) 編譯
相關(guān)推薦

2017-04-24 08:31:26

Node.jsExpress.jsHTTP

2011-09-08 14:16:12

Node.js

2023-06-30 23:25:46

HTTP模塊內(nèi)存

2013-11-01 09:34:56

Node.js技術(shù)

2015-03-10 10:59:18

Node.js開發(fā)指南基礎(chǔ)介紹

2017-03-19 16:40:28

漏洞Node.js內(nèi)存泄漏

2017-03-20 13:43:51

Node.js內(nèi)存泄漏

2017-08-17 13:56:30

JavascriptNode.jsHttp

2021-10-03 15:02:50

HTTPNodejs

2021-07-16 04:56:03

NodejsAddon

2021-03-09 08:03:21

Node.js 線程JavaScript

2011-09-09 14:23:13

Node.js

2011-11-01 10:30:36

Node.js

2011-09-08 13:46:14

node.js

2011-09-02 14:47:48

Node

2024-01-05 08:49:15

Node.js異步編程

2016-08-11 14:02:02

NodeJS前端

2020-04-15 15:48:03

Node.jsstream前端

2021-05-21 09:36:42

開發(fā)技能代碼

2021-10-21 08:59:17

技術(shù)HTTP攻擊
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)