소개
이 글에서는 HTTP 상태 206 하위 섹션의 기본 개념을 설명하고 Node.js를 사용하여 단계별로 구현해 보겠습니다. 또한 가장 일반적인 사용 시나리오를 기반으로 한 예제를 통해 코드를 테스트할 것입니다. a 언제든지 비디오 파일 재생을 시작하는 HTML5 페이지
부분 콘텐츠에 대한 간략한 소개
HTTP의 206 부분 콘텐츠 상태 코드 및 관련 메시지 헤더는 브라우저 및 기타 사용자 에이전트가 서버에서 전체 콘텐츠 대신 콘텐츠의 일부를 수신할 수 있는 메커니즘을 제공합니다. 비디오 파일은 Windows Media Player 및 VLC Player와 같은 대부분의 브라우저 및 플레이어에서 지원됩니다.
기본 프로세스는 다음 단계로 설명할 수 있습니다.
서버는 다음 두 가지 상황에서 브라우저의 요청에 응답합니다.
허용 범위: 바이트
이것은 서버에서 전송하는 바이트 헤더로, 브라우저에 부분적으로 전송될 수 있는 콘텐츠를 표시합니다. 이 값은 각 요청에 대해 허용되는 범위(대부분의 경우 바이트 수)를 선언합니다.
범위: 바이트 수(바이트) = (시작)-(끝)
이 헤더는 HTTP 상태 코드 206과 함께 표시됩니다. 시작 및 끝 값은 현재 콘텐츠의 범위를 표시합니다. Range 헤더와 마찬가지로 두 값 모두 포함되며 총 값은 0부터 시작됩니다. 사용 가능한 총 바이트 수.
내용 범위: */(총 개수)
이 헤더는 이전 헤더와 동일하지만 형식이 다르며, 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...)참고: 서버는 단일 응답으로 나머지 바이트를 모두 반환할 필요가 없습니다. 특히 본문이 너무 길거나 다른 성능 고려 사항이 있는 경우에는 더욱 그렇습니다. 따라서 이 경우에는 다음 두 가지 예도 허용됩니다.
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 및 메모리 사용량을 보려면 작업 관리자를 실행하는 것을 잊지 마십시오. 앞서 논의한 것처럼 서버는 단일 응답에 사용된 나머지 바이트를 반환하지 않습니다. 성과 균형을 찾는 것이 중요한 작업이 될 것입니다.