首頁 >web前端 >js教程 >使用Node.js實作HTTP 206內容分片的教學_node.js

使用Node.js實作HTTP 206內容分片的教學_node.js

WBOY
WBOY原創
2016-05-16 15:53:351347瀏覽

 介紹

在本文中,我將闡述HTTP狀態206 分部分內容的基礎概念,並使用Node.js一步步地實現它. 我們還將用一個基於它用法最常見場景的示例來測試程式碼:一個能夠在任何時間點開始播放影片檔案的HTML5頁面.
Partial Content 的簡介

HTTP 的206 Partial Content 狀態碼和其相關的消息頭提供了讓瀏覽器以及其他用戶代理從伺服器接收部分內容而不是全部內容,這樣一種機制. 這一機制被廣泛使用在一個被大多數瀏覽器和諸如Windows Media Player和VLC Player這樣的播放器所支援視訊檔案的傳輸上.
 

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

  •     瀏覽器請求內容.
  •     伺服器告訴瀏覽器,內容可以使用 Accept-Ranges 訊息標頭進行分段請求.
  •     瀏覽器重新傳送請求,並用 Range 訊息標頭告訴伺服器所需的內容範圍.

    伺服器會分以下兩種情況回應瀏覽器的請求:

  •         如果範圍是合理的,伺服器會回傳所要求的部分內容,並帶上 206 Partial Content 狀態碼. 目前內容的範圍會在 Content-Range 訊息標頭中申明.
  •         如果範圍是無法使用的(例如,比內容的總位元組數大), 伺服器會傳回416 請求範圍不合理Requested Range Not Satisfiable 狀態碼.可用的範圍也會在Content-Range 訊息標頭中聲明.

讓我們來看看這幾個步驟中的每一個關鍵消息頭.

Accept-Ranges: 位元組(bytes)

這是會有伺服器發送的字節頭,展示可以被分部分發送給瀏覽器的內容. 這個值聲明了可被接受的每一個範圍請求, 大多數情況下是字節數bytes.


Range: 位元組數(bytes)=(開始)-(結束)

這是瀏覽器告知伺服器所需分部分內容範圍的訊息頭. 注意開始和結束位置是都包括在內的,而且是從0開始的. 這個訊息頭也可以不發送兩個位置,其意義如下:

  •     如果結束位置被去掉了,伺服器會傳回從宣告的開始位置到整個內容的結束位置內容的最後一個可用位元組.
  •     如果開始位置被去掉了,結束位置參數可以被描述成從最後一個可用的位元組算起可以被伺服器傳回的位元組數.

Content-Range:位元組數(bytes)=(開始)-(結束)/(總數)

這個訊息頭將會跟隨HTTP 狀態碼206 一起出現. 開始和結束的值展示了當前內容的範圍. 跟Range 訊息頭一樣, 兩個值都是包含在內的,並且也是從頭開始的. 總數這個值聲明了可用位元組的總數.
 
Content-Range: */(總數)

這個頭訊息和上面一個是一樣的,不過是用另一種格式,並且僅在返回HTTP狀態碼416時被發送。其中總數代表了正文總共可用的位元組數。

這裡有一對有2048個位元組檔案的範例。注意省略起點和重點的差異。

請求開始的1024個位元組

瀏覽器發送:
 

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

伺服器回傳:
 

HTTP/1.1 206 Partial Content
Date: Mon, 15 Sep 2014 22:19:34 GMT
Content-Type: video/mp4
Content-Range: bytes 0-1023/2048
Content-Length: 1024
 
(Content...)

沒有終點位置的請求

瀏覽器發送:
 

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

伺服器回傳:
 

HTTP/1.1 206 Partial Content
Date: Mon, 15 Sep 2014 22:19:34 GMT
Content-Type: video/mp4
Content-Range: bytes 1024-2047/2048
Content-Length: 1024
 
(Content...)


注意:伺服器並不需要在單一回應中傳回所有剩餘的位元組,特別是當正文太長或有其他效能的考慮。所以下面的兩個例子在這種情況下也是可接受的:
 

Content-Range: bytes 1024-1535/2048
Content-Length: 512

伺服器僅回傳剩餘正文的一半。下一次請求的範圍將從第1536個位元組開始。

 

Content-Range: bytes 1024-1279/2048
Content-Length: 256

伺服器僅傳回剩餘正文的256個位元組。下一次請求的範圍將從第1280個位元組開始。


請求最後512個位元組

瀏覽器發送:
 

GET /dota2/techies.mp4 HTTP/1.1
Host: localhost:8000
Range: bytes=-512

伺服器回傳:
 

HTTP/1.1 206 Partial Content
Date: Mon, 15 Sep 2014 22:19:34 GMT
Content-Type: video/mp4
Content-Range: bytes 1536-2047/2048
Content-Length: 512
 
(Content...)

請求不可用的範圍:

瀏覽器發送:
 

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

伺服器回傳:
 

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

理解了工作流和头部信息后,现在我们可以用Node.js去实现这个机制。

开始用Node.js实现

第一步:创建一个简单的HTTP服务器

我们将像下面的例子那样,从一个基本的HTTP服务器开始。这已经可以基本足够处理大多数的浏览器请求了。首先,我们初始化我们需要用到的对象,并且用initFolder来代表文件的位置。为了生成Content-Type头部,我们列出文件扩展名和它们相对应的MIME名称来构成一个字典。在回调函数httpListener()中,我们将仅允许GET可用。如果出现其他方法,服务器将返回405 Method Not Allowed,在文件不存在于initFolder,服务器将返回404 Not Found。
 

// 初始化需要的对象
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require("url");
 
// 初始的目录,随时可以改成你希望的目录
var initFolder = "C:\\Users\\User\\Videos";
 
// 将我们需要的文件扩展名和MIME名称列出一个字典
var mimeNames = {
  ".css": "text/css",
  ".html": "text/html",
  ".js": "application/javascript",
  ".mp3": "audio/mpeg",
  ".mp4": "video/mp4",
  ".ogg": "application/ogg", 
  ".ogv": "video/ogg", 
  ".oga": "audio/ogg",
  ".txt": "text/plain",
  ".wav": "audio/x-wav",
  ".webm": "video/webm";
};
 
http.createServer(httpListener).listen(8000);
 
function httpListener (request, response) {
  // 我们将只接受GET请求,否则返回405 'Method Not Allowed'
  if (request.method != "GET") { 
    sendResponse(response, 405, {"Allow" : "GET"}, null);
    return null;
  }
 
  var filename = 
    initFolder + url.parse(request.url, true, true).pathname.split('/').join(path.sep);
 
  var responseHeaders = {};
  var stat = fs.statSync(filename);
  // 检查文件是否存在,不存在就返回404 Not Found
  if (!fs.existsSync(filename)) {
    sendResponse(response, 404, null, null);
    return null;
  }
  responseHeaders["Content-Type"] = getMimeNameFromExt(path.extname(filename));
  responseHeaders["Content-Length"] = stat.size; // 文件大小
     
  sendResponse(response, 200, responseHeaders, fs.createReadStream(filename));
}
 
function sendResponse(response, responseStatus, responseHeaders, readable) {
  response.writeHead(responseStatus, responseHeaders);
 
  if (readable == null)
    response.end();
  else
    readable.on("open", function () {
      readable.pipe(response);
    });
 
  return null;
}
 
function getMimeNameFromExt(ext) {
  var result = mimeNames[ext.toLowerCase()];
   
  // 最好给一个默认值
  if (result == null)
    result = "application/octet-stream";
   
  return result;
<strong>}
</strong>

步骤 2 - 使用正则表达式捕获Range消息头

有了这个HTTP服务器做基础,我们现在就可以用如下代码处理Range消息头了. 我们使用正则表达式将消息头分割,以获取开始和结束字符串。然后使用 parseInt() 方法将它们转换成整形数. 如果返回值是 NaN (非数字not a number), 那么这个字符串就是没有在这个消息头中的. 参数totalLength展示了当前文件的总字节数. 我们将使用它计算开始和结束位置.

 

function readRangeHeader(range, totalLength) {
    /*
     * Example of the method &apos;split&apos; with regular expression.
     * 
     * Input: bytes=100-200
     * Output: [null, 100, 200, null]
     * 
     * Input: bytes=-200
     * Output: [null, null, 200, null]
     */
 
  if (range == null || range.length == 0)
    return null;
 
  var array = range.split(/bytes=([0-9]*)-([0-9]*)/);
  var start = parseInt(array[1]);
  var end = parseInt(array[2]);
  var result = {
    Start: isNaN(start) &#63; 0 : start,
    End: isNaN(end) &#63; (totalLength - 1) : end
  };
   
  if (!isNaN(start) && isNaN(end)) {
    result.Start = start;
    result.End = totalLength - 1;
  }
 
  if (isNaN(start) && !isNaN(end)) {
    result.Start = totalLength - end;
    result.End = totalLength - 1;
  }
 
  return result;
}

步骤 3 - 检查数据范围是否合理

回到函数 httpListener(), 在HTTP方法通过之后,现在我们来检查请求的数据范围是否可用. 如果浏览器没有发送 Range 消息头过来, 请求就会直接被当做一般的请求对待. 服务器会返回整个文件,HTTP状态将会是 200 OK. 另外我们还会看看开始和结束位置是否比文件长度更大或者相等. 只要有一个是这种情况,请求的数据范围就是不能被满足的. 返回的状态就将会是 416 Requested Range Not Satisfiable 而 Content-Range 也会被发送. 
 

var responseHeaders = {};
  var stat = fs.statSync(filename);
  var rangeRequest = readRangeHeader(request.headers[&apos;range&apos;], stat.size);
  
  // If &apos;Range&apos; header exists, we will parse it with Regular Expression.
  if (rangeRequest == null) {
    responseHeaders[&apos;Content-Type&apos;] = getMimeNameFromExt(path.extname(filename));
    responseHeaders[&apos;Content-Length&apos;] = stat.size; // File size.
    responseHeaders[&apos;Accept-Ranges&apos;] = &apos;bytes&apos;;
     
    // If not, will return file directly.
    sendResponse(response, 200, responseHeaders, fs.createReadStream(filename));
    return null;
  }
 
  var start = rangeRequest.Start;
  var end = rangeRequest.End;
 
  // If the range can&apos;t be fulfilled. 
  if (start >= stat.size || end >= stat.size) {
    // Indicate the acceptable range.
    responseHeaders[&apos;Content-Range&apos;] = &apos;bytes */&apos; + stat.size; // File size.
 
    // Return the 416 &apos;Requested Range Not Satisfiable&apos;.
    sendResponse(response, 416, responseHeaders, null);
    return null;
  }


步骤 4 - 满足请求

最后使人迷惑的一块来了。对于状态 216 Partial Content, 我们有另外一种格式的 Content-Range 消息头,包括开始,结束位置以及当前文件的总字节数. 我们也还有 Content-Length 消息头,其值就等于开始和结束位置之间的差。在最后一句代码中,我们调用了 createReadStream() 并将开始和结束位置的值给了第二个参数选项的对象, 这意味着返回的流将只包含从开始到结束位置的只读数据.
 

// Indicate the current range. 
  responseHeaders['Content-Range'] = 'bytes ' + start + '-' + end + '/' + stat.size;
  responseHeaders['Content-Length'] = start == end &#63; 0 : (end - start + 1);
  responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));
  responseHeaders['Accept-Ranges'] = 'bytes';
  responseHeaders['Cache-Control'] = 'no-cache';
 
  // Return the 206 'Partial Content'.
  sendResponse(response, 206, 
    responseHeaders, fs.createReadStream(filename, { start: start, end: end }));

下面是完整的 httpListener() 回调函数.

 

function httpListener(request, response) {
  // We will only accept 'GET' method. Otherwise will return 405 'Method Not Allowed'.
  if (request.method != 'GET') {
    sendResponse(response, 405, { 'Allow': 'GET' }, null);
    return null;
  }
 
  var filename =
    initFolder + url.parse(request.url, true, true).pathname.split('/').join(path.sep);
 
  // Check if file exists. If not, will return the 404 'Not Found'. 
  if (!fs.existsSync(filename)) {
    sendResponse(response, 404, null, null);
    return null;
  }
 
  var responseHeaders = {};
  var stat = fs.statSync(filename);
  var rangeRequest = readRangeHeader(request.headers['range'], stat.size);
 
  // If 'Range' header exists, we will parse it with Regular Expression.
  if (rangeRequest == null) {
    responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));
    responseHeaders['Content-Length'] = stat.size; // File size.
    responseHeaders['Accept-Ranges'] = 'bytes';
 
    // If not, will return file directly.
    sendResponse(response, 200, responseHeaders, fs.createReadStream(filename));
    return null;
  }
 
  var start = rangeRequest.Start;
  var end = rangeRequest.End;
 
  // If the range can't be fulfilled. 
  if (start >= stat.size || end >= stat.size) {
    // Indicate the acceptable range.
    responseHeaders['Content-Range'] = 'bytes */' + stat.size; // File size.
 
    // Return the 416 'Requested Range Not Satisfiable'.
    sendResponse(response, 416, responseHeaders, null);
    return null;
  }
 
  // Indicate the current range. 
  responseHeaders['Content-Range'] = 'bytes ' + start + '-' + end + '/' + stat.size;
  responseHeaders['Content-Length'] = start == end &#63; 0 : (end - start + 1);
  responseHeaders['Content-Type'] = getMimeNameFromExt(path.extname(filename));
  responseHeaders['Accept-Ranges'] = 'bytes';
  responseHeaders['Cache-Control'] = 'no-cache';
 
  // Return the 206 'Partial Content'.
  sendResponse(response, 206, 
    responseHeaders, fs.createReadStream(filename, { start: start, end: end }));
}


测试实现

我们怎么来测试我们的代码呢?就像在介绍中提到的,部分正文最常用的场景是流和播放视频。所以我们创建了一个ID为mainPlayer并包含一个1c7cc8f5fa4f0d0d4241689491c1ad99标签的70633c9b67785beb88584f07e348a383。函数onLoad()将在mainPlayer预读取当前视频的元数据时被触发,这用于检查在URL中是否有数字参数,如果有,mainPlayer将跳到指定的时间点。

 

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript">
 
      function onLoad() {
        var sec = parseInt(document.location.search.substr(1));
         
        if (!isNaN(sec))
          mainPlayer.currentTime = sec;
      }
     
    </script>
    <title>Partial Content Demonstration</title>
  </head>
  <body>
    <h3>Partial Content Demonstration</h3>
    <hr />
    <video id="mainPlayer" width="640" height="360" 
      autoplay="autoplay" controls="controls" onloadedmetadata="onLoad()">
      <source src="dota2/techies.mp4" />
    </video>
  </body>
</html>

 

现在我们把页面保存为"player.html"并和"dota2/techies.mp4"一起放在initFolder目录下。然后在浏览器中打开URL:http://localhost:8000/player.html

在Chrome中看起来像这样:

2015623105803917.png (680×535)

因为在URL中没有任何参数,文件将从最开始出播放。

接下来就是有趣的部分了。让我们试着打开这个然后看看发生了什么:http://localhost:8000/player.html?60

2015623105918021.png (680×535)

如果你按F12来打开Chrome的开发者工具,切换到网络标签页,然后点击查看最近一次日志的详细信息。你会发现范围的头信息(Range)被你的浏览器发送了:
 

Range:bytes=225084502-

很有趣,對吧?當函式onLoad()改變currentTime屬性的時候,瀏覽器計算這部影片60秒處的位元組位置。因為mainPlayer已經預先載入了元數據,包括格式、位元率和其他基本資訊,這個起始位置立刻就被得到了。之後,瀏覽器就可以下載並播放影片而不需要請求開頭的60秒了。成功了!
 

結論

我們已經用Node.js來實作支援部分正文的HTTP伺服器端了。我們也用HTML5頁面測試了。但這只是一個開始。如果你對頭部資訊和工作流程這些都已經理解透徹了,你可以試著用其他像ASP.NET MVC或WCF服務這類框架來實現它。但是不要忘記啟動任務管理器來查看CPU和記憶體的使用。像我們在之前討論到的,伺服器沒有在單一回應中傳回所用剩餘的位元組。要找到效能的平衡點將是一項重要的任務。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn