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


Posted in Javascript onJune 09, 2014

本文是我最近对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 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00 01 04 00 01 01 87 01...>

这就证明我们的理论基础已经实现了第一步,接下来只需要搞清楚这个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] << 8) + header[3],
            contentLength = (header[4] << 8) + header[5],
            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) << 24) + (B2 << 16) + (B1 << 8) + B0];
    unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

对应的实现js方法示例:

function parseParams(body){
    var j = 0,
        params = {},
        length = body.length;
    while(j < length){
        var name,
            value,
            nameLength,
            valueLength;
        if(body[j] >> 7 == 1){
            nameLength = ((body[j++] & 0x7f) << 24) + (body[j++] << 16) + (body[j++] << 8) + body[j++];
        } else {
            nameLength = body[j++];
        }
        if(body[j] >> 7 == 1){
            valueLength = ((body[j++] & 0x7f) << 24) + (body[j++] << 16) + (body[j++] << 8) + body[j++];
        } 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 < l; 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 < l; i += MaxLength + 1){
            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已经在准备直接支持了。
玩得很浅,如有错误欢迎指正交流。

Javascript 相关文章推荐
JQuery中使用Ajax赋值给全局变量异常的解决方法
Jan 10 Javascript
jQuery中width()方法用法实例
Dec 24 Javascript
Jquery Ajax xmlhttp请求成功问题
Feb 04 Javascript
基于javascript实现表格的简单操作
May 21 Javascript
微信小程序 动画的简单实例
Oct 12 Javascript
JavaScript html5 canvas实现图片上画超链接
Oct 20 Javascript
bootstrap+jquery项目引入文件报错的解决方法
Jan 22 jQuery
node前端模板引擎Jade之标签的基本写法
May 11 Javascript
JSON的parse()方法介绍
Jan 31 Javascript
vue自定义表单生成器form-create使用详解
Jul 19 Javascript
基于leaflet.js实现修改地图主题样式的流程分析
May 15 Javascript
Element-ui Layout布局(Row和Col组件)的实现
Dec 06 Vue.js
初识SmartJS - AOP三剑客
Jun 08 #Javascript
javascript实例分享---具有立体效果的图片特效
Jun 08 #Javascript
js 中将多个逗号替换为一个逗号的代码
Jun 07 #Javascript
js 去除字符串第一位逗号的方法
Jun 07 #Javascript
javascript去除字符串中所有标点符号和提取纯文本的正则
Jun 07 #Javascript
用js替换除数字与逗号以外的所有字符的代码
Jun 07 #Javascript
javascript写的异步加载js文件函数(支持数组传参)
Jun 07 #Javascript
You might like
特详细的PHPMYADMIN简明安装教程
2008/08/01 PHP
xml在joomla表单中的应用详解分享
2012/07/19 PHP
php cli换行示例
2014/04/22 PHP
Smarty环境配置与使用入门教程
2016/05/11 PHP
PHP查询分页的实现代码
2017/06/09 PHP
jquery实现的元素的left增加N像素 鼠标移开会慢慢的移动到原来的位置
2010/03/21 Javascript
js单词形式的运算符
2014/05/06 Javascript
5个JavaScript经典面试题
2014/10/13 Javascript
JavaScript中用于生成随机数的Math.random()方法
2015/06/15 Javascript
jquery 判断selection range 是否在容器中的简单实例
2016/08/02 Javascript
基于jQuery实现滚动切换效果
2016/12/02 Javascript
详解JS中的立即执行函数
2017/02/24 Javascript
关于redux-saga中take使用方法详解
2018/02/27 Javascript
JS高级技巧(简洁版)
2018/07/29 Javascript
react 兄弟组件如何调用对方的方法示例
2018/10/23 Javascript
微信小程序学习笔记之获取位置信息操作图文详解
2019/03/29 Javascript
微信小程序的mpvue框架快速上手指南
2019/05/15 Javascript
vue.js 打包时出现空白页和路径错误问题及解决方法
2019/06/26 Javascript
浅谈layui 绑定form submit提交表单的注意事项
2019/10/25 Javascript
vue实现随机验证码功能(完整代码)
2019/12/10 Javascript
python 解析XML python模块xml.dom解析xml实例代码
2014/02/07 Python
简单的编程0基础下Python入门指引
2015/04/01 Python
对numpy的array和python中自带的list之间相互转化详解
2018/04/13 Python
基于python3监控服务器状态进行邮件报警
2019/10/19 Python
python 实现关联规则算法Apriori的示例
2020/09/30 Python
详解CSS3弹性伸缩盒
2020/09/21 HTML / CSS
Lookfantastic香港官网:英国知名美妆购物网站
2018/06/19 全球购物
满月酒主持词
2014/03/27 职场文书
学生安全责任书模板
2014/07/25 职场文书
我的中国梦演讲稿800字
2014/08/19 职场文书
小学生我的梦想演讲稿
2014/08/21 职场文书
2014党员自我评议表范文
2014/09/20 职场文书
入党积极分子自我批评思想汇报
2014/10/10 职场文书
2015年世界卫生日活动总结
2015/02/09 职场文书
退休教师欢送会致辞
2015/07/31 职场文书
python如何做代码性能分析
2021/04/26 Python