使用Node.js實現(xiàn)一個簡單的FastCGI服務(wù)器實例
本文是我最近對Node.js學(xué)習(xí)過程中產(chǎn)生的一個想法,提出來和大家一起探討。
Node.js的HTTP服務(wù)器
使用Node.js可以非常容易的實現(xiàn)一個http服務(wù),最簡的例子如官方網(wǎng)站的示例:
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');
這樣就快速的搭建了一個監(jiān)聽在1337端口所有http請求的web服務(wù)。
但是,在真正的生產(chǎn)環(huán)境中,我們一般很少直接使用Node.js作為面向用戶的最前端web服務(wù)器,原因主要有以下幾種:
1.基于Node.js單線程特性的原因,其健壯性的保證對開發(fā)人員要求比較高。
2.服務(wù)器上可能已有其他http服務(wù)已占用80端口,而非80端口的web服務(wù)對用戶顯然不夠友好。
3.Node.js對文件IO處理并沒太大優(yōu)勢,如作為常規(guī)網(wǎng)站可能需同時響應(yīng)圖片等文件資源。
4.分布式負(fù)載場景也是一個挑戰(zhàn)。
所以,使用Node.js作為web服務(wù)更多可能是作為游戲服務(wù)器接口等類似場景,大多是處理不需用戶直接訪問且僅作數(shù)據(jù)交換的服務(wù)。
基于Nginx作為前端機(jī)的Node.js web服務(wù)
基于上述原因,如果是使用Node.js搭建的網(wǎng)站形的產(chǎn)品,常規(guī)的使用方式是在Node.js的web服務(wù)前端放置另一個成熟的http服務(wù)器,如最常使用的是Nginx。
然后使用Nginx作為反向代理訪問基于Node.js的web服務(wù)。如:
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協(xié)議通訊
不過,上述代理的方式也有一些不是很好的地方。
一個是有可能的場景是需要控制后面的Node.js的web服務(wù)的直接http訪問。不過,要解決的話也可以使用自身的服務(wù)或者依靠防火墻阻擋。
另外一個是因為代理的方式畢竟是網(wǎng)絡(luò)應(yīng)用層上的方案,也不是很方便直接獲取和處理與客戶端http交互的數(shù)據(jù),比如對keep-alive、trunk甚至cookie等的處理。當(dāng)然這也與代理服務(wù)器自身的能力和功能完善程度相關(guān)。
所以,我在想嘗試另外一種處理方式,首先想到的就是現(xiàn)在在php web應(yīng)用上普遍使用的FastCGI的方式。
什么是FastCGI
快速通用網(wǎng)關(guān)接口(Fast Common Gateway Interface/FastCGI)是一種讓交互程序與Web服務(wù)器通信的協(xié)議。
FastCGI產(chǎn)生的背景是用來作為cgi web應(yīng)用的替代方案,一個最明顯的特點是一個FastCGI服務(wù)進(jìn)程可以用來處理一連串的請求,web服務(wù)器會把環(huán)境變量和這個頁面請求通過一個socket比如FastCGI進(jìn)程與web服務(wù)器連接起來,連接可用Unix Domain Socket或是一個TCP/IP連接。關(guān)于更多的背景知識可以參考Wikipedia的詞條。
Node.js的FastCGI實現(xiàn)
那么理論上我們只需要使用Node.js創(chuàng)建一個FastCGI進(jìn)程,再指定Nginx的監(jiān)聽請求發(fā)送到這個進(jìn)程就行了。由于Nginx和Node.js都是基于事件驅(qū)動的服務(wù)模型,“理論”上應(yīng)該是天作地合的解決方案。下面我們就親自實現(xiàn)一下。
在Node.js中net模塊剛好可用來建立一個socket服務(wù),為了方便我們就選用unix socket的方式。
在Nginx端的配置稍微修改下:
...
location / {
fastcgi_pass unix:/tmp/node_fcgi.sock;
}
...
新建一個文件node_fcgi.js,內(nèi)容如下:
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);
});
});
然后運行(因為權(quán)限的原因,請保證Nginx和node腳本使用同一用戶或有相互權(quán)限的帳號運行,不然讀寫sock文件會遇到權(quán)限問題):
node node_fcgi.js
在瀏覽器訪問,我們看到運行node腳本的終端正常的接收到了數(shù)據(jù)內(nèi)容,比如這樣:
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...>
這就證明我們的理論基礎(chǔ)已經(jīng)實現(xiàn)了第一步,接下來只需要搞清楚這個buffer的內(nèi)容如何解析就行了。
FastCGI協(xié)議基礎(chǔ)
FastCGI記錄由一個定長前綴后跟可變數(shù)量的內(nèi)容和填充字節(jié)組成。記錄結(jié)構(gòu)如下:
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協(xié)議版本,現(xiàn)在默認(rèn)就用1就好
type :記錄類型,其實可以當(dāng)做是不同狀態(tài),后面具體說
requestId :請求id,返回時需對應(yīng),如果不是多路復(fù)用并發(fā)的情況,這里直接用1就好
contentLength :內(nèi)容長度,這里最大長度是65535
paddingLength :填充長度,作用就是長數(shù)據(jù)填充為滿8字節(jié)的整數(shù)倍,主要是用來更有效地處理保持對齊的數(shù)據(jù),主要是性能考慮
reserved :保留字節(jié),為了后續(xù)擴(kuò)展
contentData :真正的內(nèi)容數(shù)據(jù),一會具體說
paddingData :填充數(shù)據(jù),反正都是0,直接忽略就好。
具體的結(jié)構(gòu)和說明請參考官網(wǎng)文檔(http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3.3)。
請求部分
似乎好像很簡單,就是這樣解析一次拿到數(shù)據(jù)就行了。不過,這里有一個坑,那就是這里定義的是數(shù)據(jù)單元(記錄)的結(jié)構(gòu),并不是整個buffer的結(jié)構(gòu),整個buffer由一個記錄一個記錄這樣的組成。一開始可能對于我們習(xí)慣了前端開發(fā)的同學(xué)不大好理解,但是這是理解FastCGI協(xié)議的基礎(chǔ),后面還會看到更多例子。
所以,我們需要將一個記錄一個記錄單獨解析出來,根據(jù)前面拿到的type來區(qū)分記錄。這里是一個簡單的獲取所有記錄的函數(shù):
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){
})();
}
注意這里只是簡單處理,如果有上傳文件等復(fù)雜情況這個函數(shù)不適應(yīng),為了最簡演示就先簡便處理了。同時,也忽略了requestId參數(shù),如果是多路復(fù)用的情況下不能忽略,并且處理會需要復(fù)雜得多。
接下來就可以根據(jù)type來對不同的記錄進(jìn)行處理了。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)
接下來就可以根據(jù)記錄的type來解析拿到真正的數(shù)據(jù),下面我只拿最常用的FCGI_PARAMS、FCGI_GET_VALUES、FCGI_GET_VALUES_RESULT來說明,好在他們的解析方式是一致的。其他type記錄的解析有自己不同的規(guī)則,可以參考規(guī)范的定義實現(xiàn),我這里就不細(xì)說了。
FCGI_PARAMS、FCGI_GET_VALUES、FCGI_GET_VALUES_RESULT都是“編碼名-值”類型數(shù)據(jù),標(biāo)準(zhǔn)格式為:以名字長度,后跟值的長度,后跟名字,后跟值的形式傳送,其中127字節(jié)或更少的長度能在一字節(jié)中編碼,而更長的長度總是在四字節(jié)中編碼。長度的第一字節(jié)的高位指示長度的編碼方式。高位為0意味著一個字節(jié)的編碼方式,1意味著四字節(jié)的編碼方式。看個綜合的例子,比如長名短值的情況:
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;
對應(yīng)的實現(xiàn)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;
}
這樣就實現(xiàn)了一個簡單可獲取各種參數(shù)和環(huán)境變量的方法。完善前面的代碼,演示我們?nèi)绾潍@取客戶端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);
}
}
})();
}
到現(xiàn)在我們已經(jīng)了解了FastCGI請求部分的基礎(chǔ),下面接著將響應(yīng)部分的實現(xiàn),并最終完成一個簡單的echo應(yīng)答服務(wù)。
響應(yīng)部分
響應(yīng)部分相對比較簡單,最簡單的情況只需要發(fā)送兩個記錄就行了,那就是FCGI_STDOUT和FCGI_END_REQUEST。
具體記錄實體的內(nèi)容就不冗述了,直接看代碼吧:
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()]);
};
})();
在最簡單的情況下,這樣就可以發(fā)送一個完整的響應(yīng)了。把我們最終的代碼修改一下:
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實現(xiàn)了一個最簡單的FastCGI服務(wù)。如果需要作為真正的服務(wù)使用,接下來只需要對照協(xié)議規(guī)范完善我們的邏輯就行了。
對比測試
最后,我們需要考慮的問題是這個方案具體是否具有可行性?可能已經(jīng)有同學(xué)看出了問題,我先把簡單的壓測結(jié)果放上來:
//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服務(wù)方式:
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方式反而會優(yōu)于FastCGI方式呢?那是因為在proxy方案下后端服務(wù)是直接由Node.js原生模塊跑的,而FastCGI方案是我們自己使用JavaScrip實現(xiàn)的。不過,也可以看出兩者方案效率上并沒有很大的差距(當(dāng)然,這里對比的只是簡單的情況,如果在真正的業(yè)務(wù)場景下,差距應(yīng)該會更大),并且如果Node.js原生支持FastCGI服務(wù),那么效率上應(yīng)該會更優(yōu)。
后記
如果有興趣繼續(xù)玩的同學(xué)可以查看我本文實現(xiàn)的例子源碼,這兩天研究下了協(xié)議規(guī)范,其實不難。
同時,回頭準(zhǔn)備再玩玩uWSGI,不過官方說v8已經(jīng)在準(zhǔn)備直接支持了。
玩得很淺,如有錯誤歡迎指正交流。
- Node.js實戰(zhàn) 建立簡單的Web服務(wù)器
- node.js+Ajax實現(xiàn)獲取HTTP服務(wù)器返回數(shù)據(jù)
- Node.js:Windows7下搭建的Node.js服務(wù)(來玩玩服務(wù)器端的javascript吧,這可不是前端js插件)
- 教你如何使用node.js制作代理服務(wù)器
- [將免費進(jìn)行到底]在Amazon的一年免費服務(wù)器上安裝Node.JS, NPM和OurJS博客
- 服務(wù)器端的JavaScript腳本 Node.js 使用入門
- 為Node.js程序配置使用Nginx服務(wù)器的簡明教程
- Node.js實現(xiàn)簡單聊天服務(wù)器
- Node.js 服務(wù)器端應(yīng)用開發(fā)框架 -- Hapi.js
- 利用node.js本地搭建HTTP服務(wù)器
相關(guān)文章
完美解決node.js中使用https請求報CERT_UNTRUSTED的問題
下面小編就為大家?guī)硪黄昝澜鉀Qnode.js中使用https請求報CERT_UNTRUSTED的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01Nodejs alpine基礎(chǔ)之docker鏡像構(gòu)建
這篇文章主要為大家介紹了Nodejs alpine基礎(chǔ)之docker鏡像構(gòu)建,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Nodejs中session的簡單使用及通過session實現(xiàn)身份驗證的方法
session的本質(zhì)使用cookie來實現(xiàn)。本文給大家介紹Nodejs中session的簡單使用及通過session實現(xiàn)身份驗證的方法,對node.js session相關(guān)知識感興趣的朋友一起學(xué)習(xí)吧2016-02-02node 利用進(jìn)程通信實現(xiàn)Cluster共享內(nèi)存
本篇文章主要介紹了node 利用進(jìn)程通信實現(xiàn)Cluster共享內(nèi)存,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10Node.JS獲取GET,POST數(shù)據(jù)之queryString模塊使用方法詳解
本文將詳細(xì)介紹nodeJS中的queryString模塊使用方法,包括Node.JS獲取GET,POST數(shù)據(jù)的方法,需要的朋友可以參考下2020-02-02