ホームページ > 記事 > ウェブフロントエンド > Node.js_node.js を使用して HTTP 206 コンテンツの断片化を実装するチュートリアル
はじめに
この記事では、HTTP ステータス 206 サブセクションの基本概念を説明し、Node.js を使用して段階的に実装します。また、最も一般的な使用シナリオに基づいた例でコードをテストします。 a いつでもビデオ ファイルの再生を開始する HTML5 ページ
。
部分コンテンツの簡単な紹介
HTTP の 206 Partial Content ステータス コードとそれに関連するメッセージ ヘッダーは、ブラウザや他のユーザー エージェントがコンテンツ全体ではなくコンテンツの一部をサーバーから受信できるようにするメカニズムを提供し、大規模な転送で広く使用されています。ビデオ ファイルは、Windows Media Player や VLC Player などのほとんどのブラウザとプレーヤーでサポートされています。
基本的なプロセスは次の手順で説明できます:
サーバーは、次の 2 つの状況でブラウザのリクエストに応答します。
受け入れ範囲: バイト
これはサーバーによって送信されるバイト ヘッダーであり、ブラウザーに送信できるコンテンツを部分的に示します。この値は、各リクエストで受け入れられる範囲 (ほとんどの場合はバイト数) を宣言します。
範囲: バイト数 (バイト) = (開始)-(終了)
このヘッダーは HTTP ステータス コード 206 で表示されます。開始値と終了値は、Range ヘッダーと同様に、両方の値を含み、合計値は 0 から始まります。使用可能なバイトの合計数。
Content-Range: */(合計数)
このヘッダーは前のヘッダーと同じですが、形式が異なり、HTTP ステータス コード 416 が返された場合にのみ送信されます。合計数は、テキストに使用できる合計バイト数を表します。
リクエストの最初の 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 つの例も受け入れられます:
Content-Range: bytes 1024-1535/2048 Content-Length: 512
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 'split' 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) ? 0 : start, End: isNaN(end) ? (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['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; }
步骤 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 ? 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 ? 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中看起来像这样:
因为在URL中没有任何参数,文件将从最开始出播放。
接下来就是有趣的部分了。让我们试着打开这个然后看看发生了什么:http://localhost:8000/player.html?60
如果你按F12来打开Chrome的开发者工具,切换到网络标签页,然后点击查看最近一次日志的详细信息。你会发现范围的头信息(Range)被你的浏览器发送了:
Range:bytes=225084502-
面白いですよね?関数 onLoad() が currentTime プロパティを変更すると、ブラウザはビデオの 60 秒時点のバイト位置を計算します。 mainPlayer にはフォーマット、ビットレート、その他の基本情報を含むメタデータがプリロードされているため、この開始位置はすぐに取得されます。これにより、ブラウザーは最初の 60 秒をリクエストせずにビデオをダウンロードして再生できるようになります。成功!
結論
Node.js を使用して、部分テキストをサポートする HTTP サーバーを実装しました。 HTML5 ページでもテストしました。しかし、これはほんの始まりにすぎません。ヘッダー情報とワークフローを十分に理解している場合は、ASP.NET MVC や WCF サービスなどの他のフレームワークを使用して実装を試みることができます。ただし、タスク マネージャーを起動して CPU とメモリの使用状況を確認することを忘れないでください。前に説明したように、サーバーは 1 つの応答で使用された残りのバイトを返しません。パフォーマンスのバランスを見つけることは重要な作業になります。