ホームページ  >  記事  >  ウェブフロントエンド  >  Node.js_node.js を使用して HTTP 206 コンテンツの断片化を実装するチュートリアル

Node.js_node.js を使用して HTTP 206 コンテンツの断片化を実装するチュートリアル

WBOY
WBOYオリジナル
2016-05-16 15:53:351285ブラウズ

はじめに

この記事では、HTTP ステータス 206 サブセクションの基本概念を説明し、Node.js を使用して段階的に実装します。また、最も一般的な使用シナリオに基づいた例でコードをテストします。 a いつでもビデオ ファイルの再生を開始する HTML5 ページ
。 部分コンテンツの簡単な紹介

HTTP の 206 Partial Content ステータス コードとそれに関連するメッセージ ヘッダーは、ブラウザや他のユーザー エージェントがコンテンツ全体ではなくコンテンツの一部をサーバーから受信できるようにするメカニズムを提供し、大規模な転送で広く使用されています。ビデオ ファイルは、Windows Media Player や VLC Player などのほとんどのブラウザとプレーヤーでサポートされています。

基本的なプロセスは次の手順で説明できます:

  • ブラウザはコンテンツをリクエストします。
  • サーバーは、Accept-Ranges ヘッダーを使用してコンテンツを部分的にリクエストできることをブラウザーに伝えます。
  • ブラウザはリクエストを再送信し、Range ヘッダーを使用してサーバーに必要なコンテンツ範囲を伝えます。

サーバーは、次の 2 つの状況でブラウザのリクエストに応答します。

  • 範囲が妥当な場合、サーバーは、要求された部分コンテンツを 206 Partial Content ステータス コードで返します。現在のコンテンツの範囲は、Content-Range ヘッダーで宣言されます。
  • 範囲が使用できない場合 (たとえば、コンテンツの合計バイト数より大きい場合)、サーバーは 416 Requested Range Not Satisfiable ステータス コードを返します。使用可能な範囲は Content-Range ヘッダーでも宣言されます。 .
これらの手順の主要なヘッダーをそれぞれ見てみましょう。

受け入れ範囲: バイト

これはサーバーによって送信されるバイト ヘッダーであり、ブラウザーに送信できるコンテンツを部分的に示します。この値は、各リクエストで受け入れられる範囲 (ほとんどの場合はバイト数) を宣言します。


範囲: バイト数 (バイト) = (開始)-(終了)

これは、ブラウザがサーバーに必要な部分コンテンツ範囲を通知するメッセージ ヘッダーです。開始位置と終了位置が含まれており、0 から始まることに注意してください。このメッセージ ヘッダーは、2 つの位置を送信する必要はありません。以下のように:

    終了位置が削除された場合、サーバーは宣言された開始位置からコンテンツ全体の終了位置までのコンテンツの最後の利用可能なバイトを返します。
  • 開始位置が削除された場合、終了位置パラメーターは、使用可能な最後のバイトから開始してサーバーによって返されるバイト数として記述することができます。
  • Content-Range: バイト数 (bytes) = (開始)-(終了)/(合計)

このヘッダーは HTTP ステータス コード 206 で表示されます。開始値と終了値は、Range ヘッダーと同様に、両方の値を含み、合計値は 0 から始まります。使用可能なバイトの合計数。

Content-Range: */(合計数)

このヘッダーは前のヘッダーと同じですが、形式が異なり、HTTP ステータス コード 416 が返された場合にのみ送信されます。合計数は、テキストに使用できる合計バイト数を表します。

ここでは、2048 バイトのファイルを使用した 2 つの例を示します。開始点とキーポイントの省略の違いに注意してください。

リクエストの最初の 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...)
注: 特に本文が長すぎる場合、またはその他のパフォーマンス上の考慮事項がある場合、サーバーは 1 つの応答で残りのバイトをすべて返す必要はありません。したがって、この場合、次の 2 つの例も受け入れられます:




サーバーは残りの本体の半分のみを返します。次に要求される範囲はバイト 1536 から始まります。
Content-Range: bytes 1024-1535/2048
Content-Length: 512


サーバーは残りの本文の 256 バイトのみを返します。次に要求される範囲はバイト 1280 から始まります。
Content-Range: bytes 1024-1279/2048
Content-Length: 256

最後の 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



理解了工作流和头部信息后,现在我们可以用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 とメモリの使用状況を確認することを忘れないでください。前に説明したように、サーバーは 1 つの応答で使用された残りのバイトを返しません。パフォーマンスのバランスを見つけることは重要な作業になります。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。