Introduction
Dans cet article, je vais expliquer les concepts de base des sous-sections du statut HTTP 206 et les implémenter étape par étape à l'aide de Node.js. Nous testerons également le code avec un exemple basé sur le scénario le plus courant de son utilisation : a Une page HTML5 qui démarre la lecture du fichier vidéo à tout moment
.
Une brève introduction au contenu partiel
Le code d'état de contenu partiel 206 de HTTP et ses en-têtes de message associés fournissent un mécanisme qui permet aux navigateurs et autres agents utilisateurs de recevoir une partie du contenu du serveur au lieu de la totalité du contenu. Ce mécanisme est largement utilisé dans un grand transfert de. Les fichiers vidéo sont pris en charge par la plupart des navigateurs et lecteurs tels que Windows Media Player et VLC Player.
Le processus de base peut être décrit dans les étapes suivantes :
Le serveur répondra à la requête du navigateur dans les deux situations suivantes :
Jetons un coup d'œil à chacun des en-têtes clés de ces étapes.
Accepter-Plages : octets
Il s'agit de l'en-tête d'octet qui sera envoyé par le serveur, affichant le contenu qui peut être envoyé au navigateur en plusieurs parties. Cette valeur déclare la plage acceptée pour chaque requête, dans la plupart des cas le nombre d'octets.
Plage : Nombre d'octets (octets) = (début)-(fin)
Il s'agit de l'en-tête du message que le navigateur informe le serveur de la plage de contenu partielle requise. Notez que les positions de début et de fin sont incluses et commencent à 0. Cet en-tête de message n'a pas besoin d'envoyer les deux positions. comme suit :
Plage de contenu : Nombre d'octets (octets) = (début)-(fin)/(total)
Cet en-tête apparaîtra avec le code d'état HTTP 206. Les valeurs de début et de fin affichent la plage du contenu actuel, comme l'en-tête Range, les deux valeurs sont inclusives et commencent à zéro. nombre total d'octets disponibles.
Plage de contenu : */(nombre total)
Cet en-tête est le même que le précédent, mais dans un format différent, et n'est envoyé que lorsque le code d'état HTTP 416 est renvoyé. Le nombre total représente le nombre total d'octets disponibles pour le texte.
Voici quelques exemples avec des fichiers de 2048 octets. Faites attention à la différence entre omettre le point de départ et le point clé.
Les 1024 premiers octets de la requête
Le navigateur envoie :
GET /dota2/techies.mp4 HTTP/1.1 Host: localhost:8000 Range: bytes=0-1023
Retours du serveur :
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...)
Aucune demande de position finale
Le navigateur envoie :
GET /dota2/techies.mp4 HTTP/1.1 Host: localhost:8000 Range: bytes=1024-
Retours du serveur :
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...)
Remarque : Le serveur n'est pas obligé de renvoyer tous les octets restants dans une seule réponse, surtout si le corps est trop long ou s'il existe d'autres considérations de performances. Ainsi, les deux exemples suivants sont également acceptables dans ce cas :
Content-Range: bytes 1024-1535/2048 Content-Length: 512
Le serveur ne renvoie que la moitié du corps restant. La prochaine plage demandée commencera à l’octet 1536.
Content-Range: bytes 1024-1279/2048 Content-Length: 256
Le serveur ne renvoie que les 256 octets du corps restant. La prochaine plage demandée commencera à l'octet 1280.
Demander les 512 derniers octets
Le navigateur envoie :
GET /dota2/techies.mp4 HTTP/1.1 Host: localhost:8000 Range: bytes=-512
Retours du serveur :
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...)
Demande d'une plage indisponible :
Le navigateur envoie :
GET /dota2/techies.mp4 HTTP/1.1 Host: localhost:8000 Range: bytes=1024-4096
Retours du serveur :
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 '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并包含一个标签的。函数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-
C'est drôle, non ? Lorsque la fonction onLoad() modifie la propriété currentTime, le navigateur calcule la position de l'octet à 60 secondes du début de la vidéo. Étant donné que mainPlayer a été préchargé avec des métadonnées, notamment le format, le débit binaire et d'autres informations de base, cette position de départ est obtenue immédiatement. Le navigateur peut alors télécharger et lire la vidéo sans demander les 60 premières secondes. Succès!
Conclusion
Nous avons utilisé Node.js pour implémenter un serveur HTTP prenant en charge le texte partiel. Nous avons également testé avec des pages HTML5. Mais ce n'est que le début. Si vous avez une compréhension approfondie des informations d'en-tête et du flux de travail, vous pouvez essayer de les implémenter à l'aide d'autres frameworks tels que les services ASP.NET MVC ou WCF. Mais n'oubliez pas de lancer le Gestionnaire des tâches pour afficher l'utilisation du processeur et de la mémoire. Comme nous l'avons vu précédemment, le serveur ne renvoie pas les octets restants utilisés dans une seule réponse. Trouver l’équilibre des performances sera une tâche importante.