Node.js 中实现 HTTP 206 内容分片 已翻译 100%

oschina 投递于 2014/09/09 06:51 (共 13 段, 翻译完成于 09-10)
阅读 12617
收藏 159
17
加载中

Table of content

Introduction

In this article, I would like to explain the basic concept of HTTP status 206 Partial Content and a step-by-step implementation walkthrough with Node.js. Also, we will test the code with an example based on the most common scenario of its usage: an HTML5 page which is able to play video file starting at any second. 

A brief of partial content

The HTTP 206 Partial Content status code and its related headers provide a mechanism which allows browser and other user agents to receive partial content instead of entire one from server. This mechanism is widely used in streaming a video file and supported by most of browsers and players such as Windows Media Player and VLC Player.

已有 1 人翻译此段
我来翻译

The basic workflow could be explained by these following steps:

  1. Browser requests the content.

  2. Server tells browser that the content can be requested partially with Accept-Ranges header.

  3. Browser resends the request, tells server the expecting range with Range header.

  4. Server responses browser in one of following siturations: 

    • If range is available, server returns the partial content with status 206 Partial Content. Range of current content will be indicated in Content-Range header.

    • If range is unavailable (for example, greater than total bytes of content), server return status 416 Requested Range Not Satisfiable. The available range will be indicated in Content-Range header too.

Let's take a look at each key header of these steps.

Accept-Ranges: bytes

This is the header which is sent by server, represents the content that can be partially returned to browser. The value indicates the acceptable unit of each range request, usually is bytes in most of situations. 

已有 1 人翻译此段
我来翻译
Range: bytes=(start)-(end)

This is the header for browser telling server the expecting range of content. Note that start and end positions are both inclusive and zero-based. This header could be sent without one of them in following meanings: 

  • If end position is omitted, server returns the content from indicated start position to the position of last available byte.

  • If start position is omitted, the end position will be described as how many bytes shall server returns counting from the last available byte.

Content-Range: bytes (start)-(end)/(total)

This is the header which shall appear following HTTP status 206. Values start and end represent the range of current content. Like Range header, both values are inclusive and zero-based. Value total indicates the total avaliable bytes.

已有 1 人翻译此段
我来翻译
Content-Range: */(total)

This is same header but in another format and will only be sent following HTTP status 416. Value total also indicates the total avaliable bytes of content.

Here is couple examples of a file with 2048 bytes long. Note the different meaning of end when start is omitted.

Request first 1024 bytes

What browser sends:

GET /dota2/techies.mp4 HTTP/1.1
Host: localhost:8000
Range: bytes=0-1023

What server returns:

HTTP/1.1 216 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...)

Request without end position

What browser sends:

GET /dota2/techies.mp4 HTTP/1.1
Host: localhost:8000
Range: bytes=1024-

What server returns:

HTTP/1.1 216 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 人翻译此段
我来翻译

Note that server does not have to return all remaining bytes in single response especially when content is too long or there are other performance considerations. So following two examples are also acceptable in this case:

Content-Range: bytes 1024-1535/2048
Content-Length: 512

Server only returns half of remaining content. The range of next request will start at 1536th byte.

Content-Range: bytes 1024-1279/2048
Content-Length: 256

Server only returns 256 bytes of remaining content. The range of next request will start at 1280th byte.

已有 1 人翻译此段
我来翻译

Request last 512 bytes

What browser sends:

GET /dota2/techies.mp4 HTTP/1.1
Host: localhost:8000
Range: bytes=-512

What server returns:

HTTP/1.1 216 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...)

Request with unavailable range 

What browser sends:

GET /dota2/techies.mp4 HTTP/1.1
Host: localhost:8000
Range: bytes=1024-4096

What server returns:

HTTP/1.1 416 Requested Range Not Satisfiable
Date: Mon, 15 Sep 2014 22:19:34 GMT
Content-Range: bytes */2048

With understanding of workflow and headers below, now we are able to implement the mechanism in Node.js.

已有 1 人翻译此段
我来翻译

Get started to implement in Node.js

Step 1 - Create a simple HTTP server

We will start from a basic HTTP server as following example shows. This is pretty enough to handle most of requests from browsers. At first, we initialize each object we need, and indicate where are the files located at with initFolder. We also list couple of filename extensions and their corresponding MIME names to construct a dictionary, which is for generating Content-Type header. In the callback function httpListener(), we limit GET as the only allowed HTTP method. Before start to fulfill the request, server will return status 405 Method Not Allowed if other methods appear and return status 404 Not Found if file does not exist in initFolder.  

// Initialize all required objects.
var http = require("http");
var fs = require("fs");
var path = require("path");
var url = require('url');

// Give the initial folder. Change the location to whatever you want.
var initFolder = 'C:\\Users\\User\\Videos';

// List filename extensions and MIME names we need as a dictionary. 
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) {
    // 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);

    var responseHeaders = {};
    var stat = fs.statSync(filename);

    // Check if file exists. If not, will return the 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; // File 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()];
    
    // It's better to give a default value.
    if (result == null)
        result = 'application/octet-stream';
    
    return result;
}
已有 1 人翻译此段
我来翻译

Step 2 - Capture the Range header by using regular expression

With the basic HTTP server, now we can handle the Range header as following code shows. We split the header with regular expression to capture start and end strings. Then use parseInt() method to parse them to integers. If returned value is NaN (not a number), the string does not exist in header. The parameter totalLength represents total bytes of current file. We will use it to calculate start and end positions. 

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;
}

已有 1 人翻译此段
我来翻译

Step 3 - Check if range can be satisfied

Back to the function httpListener(), now we check if the range is available after the HTTP method gets approved. If browser does not send Range header, the request will be directly treated as normal request. Server returns entire file and HTTP status is 200 OK. Otherwise we will see if start or end position is greater or equal to file length. If one of them is, the range can not be fulfilled. The status will be 416 Requested Range Not Satisfiable and the Content-Range will be sent. 

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;
    }

已有 1 人翻译此段
我来翻译

Step 4 - Fulfill the request

Finally the last puzzle piece comes. For the status 216 Partial Content, we have another format of Content-Range header including start, end and total bytes of current file. We also have Content-Length header and the value is exaclty equal to the difference between start and end. In the last statement, we call createReadStream() and assign start and end values to the object of second parameter options, which means the returned stream will be only readable from/to the positions.

// 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 }));

Here is the complete httpListener() callback function.

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 }));
}

已有 1 人翻译此段
我来翻译
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(15)

Ouyang_Hibing
Ouyang_Hibing

引用来自“同城陌路人”的评论

文中又是216又是206的,我数度的少,你不要骗我~~

引用来自“LeoG0816”的评论

感谢指正,以将我的部分统一成206
http://www.oschina.net
断风格男丶
断风格男丶
先收藏 好文章
吾爱
吾爱
好顶赞
xiaolongwang
xiaolongwang
mark
LeoG0816
LeoG0816

引用来自“同城陌路人”的评论

文中又是216又是206的,我数度的少,你不要骗我~~
感谢指正,以将我的部分统一成206
dadait
dadait
收了。
dadait
dadait
好文章。
last
last
涨知识了
亚林瓜子
亚林瓜子
来个SpringMVC的文件分块分片
L3ve
L3ve
dota2 赞一个...翻译也顶下
返回顶部
顶部