亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

使用Node.js實現一個簡單的FastCGI服務器實例

2019-11-20 14:42:55
字體:
來源:轉載
供稿:網友

本文是我最近對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已經在準備直接支持了。
玩得很淺,如有錯誤歡迎指正交流。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
国产精品视频中文字幕91| 亚洲自拍偷拍区| 成人午夜小视频| 国产精品一区二区久久久| 91tv亚洲精品香蕉国产一区7ujn| 亚洲女同精品视频| 久久99久久99精品中文字幕| 欧美激情第一页xxx| 日日骚久久av| 欧美日韩免费看| 日韩最新在线视频| 国产视频在线观看一区二区| 久热在线中文字幕色999舞| 亚洲电影免费观看高清完整版在线观看| 欧美最顶级的aⅴ艳星| 不卡中文字幕av| 欧美重口另类videos人妖| 777午夜精品福利在线观看| 日韩在线欧美在线| 国产精品va在线播放我和闺蜜| 日韩性xxxx爱| 中文字幕亚洲精品| 国产91在线视频| 成人网址在线观看| 午夜精品久久久久久久99黑人| 国产精品视频精品视频| 日韩va亚洲va欧洲va国产| 欧美性xxxxxx| 疯狂做受xxxx欧美肥白少妇| 欧美日韩性视频在线| 在线日韩欧美视频| 日韩成人小视频| 久久精品一本久久99精品| 91精品国产综合久久香蕉最新版| 国产日韩在线看片| 欧美日韩日本国产| 不用播放器成人网| 亚洲欧美激情一区| 欧美日韩亚洲91| 欧美午夜女人视频在线| 日韩中文字幕在线看| 亚洲色图日韩av| 大胆人体色综合| 91精品国产综合久久香蕉922| 亚洲欧洲激情在线| 中文字幕日韩欧美在线视频| 福利一区福利二区微拍刺激| 国产精品情侣自拍| 亚洲最大福利视频网| 日韩在线欧美在线国产在线| 国产aⅴ夜夜欢一区二区三区| 国产成人精品一区二区| 日韩在线一区二区三区免费视频| 日本在线精品视频| 国产在线不卡精品| 欧美亚洲伦理www| 欧美日韩免费观看中文| 国产69久久精品成人看| 国产精欧美一区二区三区| 欧美中文字幕视频在线观看| 亚洲精品久久久久久久久久久久| 久热精品视频在线观看一区| 国产成人精品一区二区三区| 欧美成人精品h版在线观看| 亚洲天堂免费在线| 蜜臀久久99精品久久久无需会员| 欧美日韩视频免费播放| 九色精品免费永久在线| 国产精品亚洲视频在线观看| 成人做爽爽免费视频| 日本久久久久久久久| 欧美日韩精品在线观看| 777国产偷窥盗摄精品视频| 亚洲欧洲视频在线| 久久国产精品99国产精| 欧美第一淫aaasss性| 亚洲影视中文字幕| 欧美激情在线播放| 日韩精品中文在线观看| 日韩黄色在线免费观看| 国产精品久久二区| 91香蕉嫩草神马影院在线观看| 久久人人爽人人爽人人片av高清| 午夜精品一区二区三区在线视频| 91av在线国产| 久久久精品欧美| 国产精品黄色影片导航在线观看| 日韩精品小视频| 91九色单男在线观看| 久久国产精品视频| 视频直播国产精品| 久久久久久欧美| 国产精品网红福利| 美日韩精品视频免费看| 欧美重口另类videos人妖| 欧美激情国内偷拍| 久久久久久国产精品久久| 欧美成人免费在线视频| 国产一区二区三区日韩欧美| 精品欧美一区二区三区| 26uuu另类亚洲欧美日本一| 成人观看高清在线观看免费| 国产精品精品国产| 欧美大片大片在线播放| 欧美日韩午夜视频在线观看| 一区二区三区四区精品| 国产在线一区二区三区| 九九综合九九综合| 国产色视频一区| 国产在线视频一区| 91久久精品国产91久久性色| 久久精品影视伊人网| 91tv亚洲精品香蕉国产一区7ujn| 国产九九精品视频| 2019中文字幕在线| 国产精品高清在线观看| 日韩大陆欧美高清视频区| 亚洲欧洲免费视频| 国产丝袜一区二区三区免费视频| 亚洲美女精品久久| 亚洲国产精品国自产拍av秋霞| 欧美一级电影在线| 日韩av在线免费观看| 97香蕉久久超级碰碰高清版| 欧洲亚洲女同hd| 国产成人免费91av在线| 久久伊人精品一区二区三区| 国产精品日韩在线| 国产精品免费观看在线| 成人性生交大片免费看小说| 成人久久18免费网站图片| 国产精品大片wwwwww| 日韩一区在线视频| 在线观看欧美www| 欧美成在线观看| 日韩精品视频在线观看网址| 亚洲国产成人精品久久| 91美女高潮出水| 黑人狂躁日本妞一区二区三区| 欧美日韩亚洲高清| 久久亚洲精品一区二区| 国产在线一区二区三区| 中文字幕不卡av| 91亚洲va在线va天堂va国| 久久久成人av| 久久亚洲精品一区二区| 日韩中文字幕在线视频| 亚洲天堂一区二区三区| 日韩经典中文字幕在线观看| 日韩电影中文字幕av| 日韩av在线不卡| 亚洲精品国产精品国自产观看浪潮| 亚洲成人激情小说| 日韩av一区二区在线观看| 按摩亚洲人久久| 国内免费精品永久在线视频| 国产日产久久高清欧美一区| 日韩中文视频免费在线观看| 亚洲iv一区二区三区| 亚洲a在线观看| 国产精品吴梦梦| 日本高清久久天堂| 欧美亚洲另类制服自拍| 日韩中文字幕国产|