您好,欢迎访问一九零五行业门户网

使用Node.js实现一个简单的FastCGI服务器实例_node.js

本文是我最近对node.js学习过程中产生的一个想法,提出来和大家一起探讨。
node.js的http服务器
使用node.js可以非常容易的实现一个http服务,最简的例子如官方网站的示例:
复制代码 代码如下:
var http = require('http');
http.createserver(function (req, res) {
    res.writehead(200, {'content-type': 'text/plain'});
    res.end('hello world\n');
}).listen(1337, '127.0.0.1');
这样就快速的搭建了一个监听在1337端口所有http请求的web服务。
但是,在真正的生产环境中,我们一般很少直接使用node.js作为面向用户的最前端web服务器,原因主要有以下几种:1.基于node.js单线程特性的原因,其健壮性的保证对开发人员要求比较高。
2.服务器上可能已有其他http服务已占用80端口,而非80端口的web服务对用户显然不够友好。
3.node.js对文件io处理并没太大优势,如作为常规网站可能需同时响应图片等文件资源。
4.分布式负载场景也是一个挑战。
所以,使用node.js作为web服务更多可能是作为游戏服务器接口等类似场景,大多是处理不需用户直接访问且仅作数据交换的服务。
基于nginx作为前端机的node.js web服务
基于上述原因,如果是使用node.js搭建的网站形的产品,常规的使用方式是在node.js的web服务前端放置另一个成熟的http服务器,如最常使用的是nginx。
然后使用nginx作为反向代理访问基于node.js的web服务。如:
复制代码 代码如下:
server{
    listen 80;
    server_name yekai.me;
    root /home/andy/wwwroot/yekai;    location / {
        proxy_pass http://127.0.0.1:1337;
    }
    location ~ \.(gif|jpg|png|swf|ico|css|js)$ {
        root /home/andy/wwwroot/yekai/static;
    }
}
这样就比较好的解决了上面提出的几个问题。
使用fastcgi协议通讯
不过,上述代理的方式也有一些不是很好的地方。
一个是有可能的场景是需要控制后面的node.js的web服务的直接http访问。不过,要解决的话也可以使用自身的服务或者依靠防火墙阻挡。
另外一个是因为代理的方式毕竟是网络应用层上的方案,也不是很方便直接获取和处理与客户端http交互的数据,比如对keep-alive、trunk甚至cookie等的处理。当然这也与代理服务器自身的能力和功能完善程度相关。
所以,我在想尝试另外一种处理方式,首先想到的就是现在在php web应用上普遍使用的fastcgi的方式。
什么是fastcgi
快速通用网关接口(fast common gateway interface/fastcgi)是一种让交互程序与web服务器通信的协议。
fastcgi产生的背景是用来作为cgi web应用的替代方案,一个最明显的特点是一个fastcgi服务进程可以用来处理一连串的请求,web服务器会把环境变量和这个页面请求通过一个socket比如fastcgi进程与web服务器连接起来,连接可用unix domain socket或是一个tcp/ip连接。关于更多的背景知识可以参考wikipedia的词条。
node.js的fastcgi实现
那么理论上我们只需要使用node.js创建一个fastcgi进程,再指定nginx的监听请求发送到这个进程就行了。由于nginx和node.js都是基于事件驱动的服务模型,“理论”上应该是天作地合的解决方案。下面我们就亲自实现一下。
在node.js中net模块刚好可用来建立一个socket服务,为了方便我们就选用unix socket的方式。
在nginx端的配置稍微修改下:
复制代码 代码如下:
...
location / {
    fastcgi_pass   unix:/tmp/node_fcgi.sock;
}
...
新建一个文件node_fcgi.js,内容如下:
复制代码 代码如下:
var net = require('net');var server = net.createserver();
server.listen('/tmp/node_fcgi.sock');
server.on('connection', function(sock){
    console.log('connection');
    sock.on('data', function(data){
        console.log(data);
    });
});
然后运行(因为权限的原因,请保证nginx和node脚本使用同一用户或有相互权限的帐号运行,不然读写sock文件会遇到权限问题):
node node_fcgi.js
在浏览器访问,我们看到运行node脚本的终端正常的接收到了数据内容,比如这样:
复制代码 代码如下:
connection
这就证明我们的理论基础已经实现了第一步,接下来只需要搞清楚这个buffer的内容如何解析就行了。
fastcgi协议基础
fastcgi记录由一个定长前缀后跟可变数量的内容和填充字节组成。记录结构如下:
复制代码 代码如下:
typedef struct {
    unsigned char version;
    unsigned char type;
    unsigned char requestidb1;
    unsigned char requestidb0;
    unsigned char contentlengthb1;
    unsigned char contentlengthb0;
    unsigned char paddinglength;
    unsigned char reserved;
    unsigned char contentdata[contentlength];
    unsigned char paddingdata[paddinglength];
} fcgi_record;
version :fastcgi协议版本,现在默认就用1就好
type :记录类型,其实可以当做是不同状态,后面具体说
requestid :请求id,返回时需对应,如果不是多路复用并发的情况,这里直接用1就好
contentlength :内容长度,这里最大长度是65535
paddinglength :填充长度,作用就是长数据填充为满8字节的整数倍,主要是用来更有效地处理保持对齐的数据,主要是性能考虑
reserved :保留字节,为了后续扩展
contentdata :真正的内容数据,一会具体说
paddingdata :填充数据,反正都是0,直接忽略就好。
具体的结构和说明请参考官网文档(http://www.fastcgi.com/devkit/doc/fcgi-spec.html#s3.3)。
请求部分
似乎好像很简单,就是这样解析一次拿到数据就行了。不过,这里有一个坑,那就是这里定义的是数据单元(记录)的结构,并不是整个buffer的结构,整个buffer由一个记录一个记录这样的组成。一开始可能对于我们习惯了前端开发的同学不大好理解,但是这是理解fastcgi协议的基础,后面还会看到更多例子。
所以,我们需要将一个记录一个记录单独解析出来,根据前面拿到的type来区分记录。这里是一个简单的获取所有记录的函数:
复制代码 代码如下:
function getrcds(data, cb){
    var rcds = [],
        start = 0,
        length = data.length;
    return function (){
        if(start >= length){
            cb && cb(rcds);
            rcds = null;
            return;
        }
        var end = start + 8,
            header = data.slice(start, end),
            version = header[0],
            type    = header[1],
            requestid = (header[2]             contentlength = (header[4]             paddinglength = header[6];
        start = end + contentlength + paddinglength;        var body = contentlength ? data.slice(end, contentlength) : null;
        rcds.push([type, body, requestid]);
        return arguments.callee();
    }
}
//使用
sock.on('data', function(data){
    getrcds(data, function(rcds){
    })();
}
注意这里只是简单处理,如果有上传文件等复杂情况这个函数不适应,为了最简演示就先简便处理了。同时,也忽略了requestid参数,如果是多路复用的情况下不能忽略,并且处理会需要复杂得多。
接下来就可以根据type来对不同的记录进行处理了。type的定义如下:
复制代码 代码如下:
#define fcgi_begin_request       1
#define fcgi_abort_request       2
#define fcgi_end_request         3
#define fcgi_params              4
#define fcgi_stdin               5
#define fcgi_stdout              6
#define fcgi_stderr              7
#define fcgi_data                8
#define fcgi_get_values          9
#define fcgi_get_values_result  10
#define fcgi_unknown_type       11
#define fcgi_maxtype (fcgi_unknown_type)
接下来就可以根据记录的type来解析拿到真正的数据,下面我只拿最常用的fcgi_params、fcgi_get_values、fcgi_get_values_result来说明,好在他们的解析方式是一致的。其他type记录的解析有自己不同的规则,可以参考规范的定义实现,我这里就不细说了。
fcgi_params、fcgi_get_values、fcgi_get_values_result都是“编码名-值”类型数据,标准格式为:以名字长度,后跟值的长度,后跟名字,后跟值的形式传送,其中127字节或更少的长度能在一字节中编码,而更长的长度总是在四字节中编码。长度的第一字节的高位指示长度的编码方式。高位为0意味着一个字节的编码方式,1意味着四字节的编码方式。看个综合的例子,比如长名短值的情况:
复制代码 代码如下:
typedef struct {
    unsigned char namelengthb3;  /* namelengthb3  >> 7 == 1 */
    unsigned char namelengthb2;
    unsigned char namelengthb1;
    unsigned char namelengthb0;
    unsigned char valuelengthb0; /* valuelengthb0 >> 7 == 0 */
    unsigned char namedata[namelength
            ((b3 & 0x7f)     unsigned char valuedata[valuelength];
} fcgi_namevaluepair41;
对应的实现js方法示例:
复制代码 代码如下:
function parseparams(body){
    var j = 0,
        params = {},
        length = body.length;
    while(j         var name,
            value,
            namelength,
            valuelength;
        if(body[j] >> 7 == 1){
            namelength = ((body[j++] & 0x7f)         } else {
            namelength = body[j++];
        }        if(body[j] >> 7 == 1){
            valuelength = ((body[j++] & 0x7f)         } else {
            valuelength = body[j++];
        }
        var ret = body.asciislice(j, j + namelength + valuelength);
        name = ret.substring(0, namelength);
        value = ret.substring(namelength);
        params[name] = value;
        j += (namelength + valuelength);
    }
    return params;
}
这样就实现了一个简单可获取各种参数和环境变量的方法。完善前面的代码,演示我们如何获取客户端ip:
复制代码 代码如下:
sock.on('data', function(data){
    getrcds(data, function(rcds){
        for(var i = 0, l = rcds.length; i             var bodydata = rcds[i],
                type = bodydata[0],
                body = bodydata[1];
            if(body && (type === types.fcgi_params || type === types.fcgi_get_values || type === types.fcgi_get_values_result)){
                    var params = parseparams(body);
                    console.log(params.remote_addr);
                }
        }
    })();
}
到现在我们已经了解了fastcgi请求部分的基础,下面接着将响应部分的实现,并最终完成一个简单的echo应答服务。
响应部分
响应部分相对比较简单,最简单的情况只需要发送两个记录就行了,那就是fcgi_stdout和fcgi_end_request。
具体记录实体的内容就不冗述了,直接看代码吧:
复制代码 代码如下:
var res = (function(){
    var maxlength = math.pow(2, 16);    function buffer0(len){
        return new buffer((new array(len + 1)).join('\u0000'));
    };
    function writestdout(data){
        var rcdstdouthd = new buffer(8),
            contendlength = data.length,
            paddinglength = 8 - contendlength % 8;
        rcdstdouthd[0] = 1;
        rcdstdouthd[1] = types.fcgi_stdout;
        rcdstdouthd[2] = 0;
        rcdstdouthd[3] = 1;
        rcdstdouthd[4] = contendlength >> 8;
        rcdstdouthd[5] = contendlength;
        rcdstdouthd[6] = paddinglength;
        rcdstdouthd[7] = 0;
        return buffer.concat([rcdstdouthd, data, buffer0(paddinglength)]);
    };
    function writehttphead(){
        return writestdout(new buffer(http/1.1 200 ok\r\ncontent-type:text/html; charset=utf-8\r\nconnection: close\r\n\r\n));
    }
    function writehttpbody(bodystr){
        var bodybuffer = [],
            body = new buffer(bodystr);
        for(var i = 0, l = body.length; i             bodybuffer.push(writestdout(body.slice(i, i + maxlength)));
        }
        return buffer.concat(bodybuffer);
    }
    function writeend(){
        var rcdendhd = new buffer(8);
        rcdendhd[0] = 1;
        rcdendhd[1] = types.fcgi_end_request;
        rcdendhd[2] = 0;
        rcdendhd[3] = 1;
        rcdendhd[4] = 0;
        rcdendhd[5] = 8;
        rcdendhd[6] = 0;
        rcdendhd[7] = 0;
        return buffer.concat([rcdendhd, buffer0(8)]);
    }
    return function(data){
        return buffer.concat([writehttphead(), writehttpbody(data), writeend()]);
    };
})();
在最简单的情况下,这样就可以发送一个完整的响应了。把我们最终的代码修改一下:
复制代码 代码如下:
var visitors = 0;
server.on('connection', function(sock){
    visitors++;
    sock.on('data', function(data){
        ...
        var querys = querystring.parse(params.query_string);
            var ret = res('欢迎你,' + (querys.name || '亲爱的朋友') + '!你是本站第' + visitors + '位用户哦~');
            sock.write(ret);
            ret = null;
            sock.end();
        ...
    });
打开浏览器访问:http://domain/?name=yekai,可看到类似“欢迎你,yekai!你是本站第7位用户哦~”。
至此,我们就成功的使用node.js实现了一个最简单的fastcgi服务。如果需要作为真正的服务使用,接下来只需要对照协议规范完善我们的逻辑就行了。
对比测试
最后,我们需要考虑的问题是这个方案具体是否具有可行性?可能已经有同学看出了问题,我先把简单的压测结果放上来:
复制代码 代码如下:
//fastcgi方式:
500 clients, running 10 sec.
speed=27678 pages/min, 63277 bytes/sec.
requests: 3295 susceed, 1318 failed.500 clients, running 20 sec.
speed=22131 pages/min, 63359 bytes/sec.
requests: 6523 susceed, 854 failed.
//proxy方式:
500 clients, running 10 sec.
speed=28752 pages/min, 73191 bytes/sec.
requests: 3724 susceed, 1068 failed.
500 clients, running 20 sec.
speed=26508 pages/min, 66267 bytes/sec.
requests: 6716 susceed, 2120 failed.
//直接访问node.js服务方式:
500 clients, running 10 sec.
speed=101154 pages/min, 264247 bytes/sec.
requests: 15729 susceed, 1130 failed.
500 clients, running 20 sec.
speed=43791 pages/min, 115962 bytes/sec.
requests: 13898 susceed, 699 failed.
为什么proxy方式反而会优于fastcgi方式呢?那是因为在proxy方案下后端服务是直接由node.js原生模块跑的,而fastcgi方案是我们自己使用javascrip实现的。不过,也可以看出两者方案效率上并没有很大的差距(当然,这里对比的只是简单的情况,如果在真正的业务场景下,差距应该会更大),并且如果node.js原生支持fastcgi服务,那么效率上应该会更优。后记
如果有兴趣继续玩的同学可以查看我本文实现的例子源码,这两天研究下了协议规范,其实不难。
同时,回头准备再玩玩uwsgi,不过官方说v8已经在准备直接支持了。
玩得很浅,如有错误欢迎指正交流。
其它类似信息

推荐信息