Heim > Artikel > Web-Frontend > Tutorial zum Implementieren der HTTP 206-Inhaltsfragmentierung mit Node.js_node.js
Einleitung
In diesem Artikel werde ich die Grundkonzepte der HTTP-Status-206-Unterabschnitte erklären und sie Schritt für Schritt mit Node.js implementieren. Wir werden den Code auch anhand eines Beispiels testen, das auf dem häufigsten Szenario seiner Verwendung basiert: a Eine HTML5-Seite, die zu jedem Zeitpunkt mit der Wiedergabe der Videodatei beginnt
Eine kurze Einführung in Partial Content
Dies ist der Byte-Header, der vom Server gesendet wird und den Inhalt anzeigt, der in Teilen an den Browser gesendet werden kann. Dieser Wert gibt den Bereich an, der für jede Anfrage akzeptiert wird, in den meisten Fällen die Anzahl der Bytes.
Bereich: Anzahl der Bytes (Bytes) = (Anfang)-(Ende)
Dies ist der Nachrichtenheader, den der Browser dem Server über den erforderlichen Teilinhaltsbereich mitteilt. Beachten Sie, dass die Start- und Endpositionen enthalten sind und bei 0 beginnen. Dieser Nachrichtenheader muss die beiden Positionen nicht senden wie folgt:
Wenn die Endposition entfernt wird, gibt der Server das letzte verfügbare Byte des Inhalts von der deklarierten Startposition an die Endposition des gesamten Inhalts zurück
Dieser Header wird mit dem HTTP-Statuscode 206 angezeigt. Die Start- und Endwerte zeigen den Bereich des aktuellen Inhalts an. Beide Werte sind inklusiv und beginnen bei Null Gesamtzahl der verfügbaren Bytes.
Inhaltsbereich: */(Gesamtzahl)
Dieser Header ist derselbe wie der vorherige, jedoch in einem anderen Format, und wird nur gesendet, wenn der HTTP-Statuscode 416 zurückgegeben wird. Die Gesamtzahl stellt die Gesamtzahl der für den Text verfügbaren Bytes dar.
Die ersten 1024 Bytes der Anfrage
Browser sendet:
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...)Keine Endpositionsanforderung
Browser sendet:
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...)Hinweis: Der Server ist nicht verpflichtet, alle verbleibenden Bytes in einer einzigen Antwort zurückzugeben, insbesondere wenn der Text zu lang ist oder andere Leistungsaspekte vorliegen. Daher sind in diesem Fall auch die folgenden beiden Beispiele akzeptabel:
Content-Range: bytes 1024-1535/2048 Content-Length: 512
Content-Range: bytes 1024-1279/2048 Content-Length: 256
Letzte 512 Bytes anfordern
Browser sendet:
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...)Einen nicht verfügbaren Bereich anfordern:
Browser sendet:
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-
Lustig, oder? Wenn die Funktion onLoad() die Eigenschaft currentTime ändert, berechnet der Browser die Byteposition nach 60 Sekunden im Video. Da mainPlayer bereits mit Metadaten wie Format, Bitrate und anderen grundlegenden Informationen geladen wurde, wird diese Ausgangsposition sofort erreicht. Der Browser kann das Video dann herunterladen und abspielen, ohne die ersten 60 Sekunden anzufordern. Erfolg!
Fazit
Wir haben Node.js verwendet, um einen HTTP-Server zu implementieren, der Teiltext unterstützt. Wir haben auch mit HTML5-Seiten getestet. Aber das ist erst der Anfang. Wenn Sie über umfassende Kenntnisse der Header-Informationen und des Workflows verfügen, können Sie versuchen, diese mit anderen Frameworks wie ASP.NET MVC- oder WCF-Diensten zu implementieren. Vergessen Sie jedoch nicht, den Task-Manager zu starten, um die CPU- und Speicherauslastung anzuzeigen. Wie bereits erwähnt, gibt der Server die verbleibenden Bytes, die in einer einzelnen Antwort verwendet werden, nicht zurück. Eine wichtige Aufgabe wird es sein, die Leistungsbalance zu finden.