首頁  >  文章  >  web前端  >  使用Node.js實作一個簡單的FastCGI伺服器實例_node.js

使用Node.js實作一個簡單的FastCGI伺服器實例_node.js

WBOY
WBOY原創
2016-05-16 16:45:181676瀏覽

本文是我最近對Node.js學習過程中產生的一個想法,提出來和大家一起探討。

Node.js的HTTP伺服器

使用Node.js可以非常容易的實作一個http服務,最簡的例子如官方網站的範例:

複製程式碼 程式碼如下:

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {
    res.writeHead(200, {Contentent -Type': 'text/plain'});
    res.end('Hello Worldn');
}).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{
 🎜>
server{

 🎜>

server{

 🎜>


server{
 🎜>

server{

 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;   root /home/andy/wwwroot}1kai/static;   root /home/andy/ > 🎜>

這樣就比較好的解決了上面提出的幾個問題。

使用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的詞條。




複製代碼

代碼如下:. ..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記錄由一個定長前綴後面跟著可變數量的內容和填充位元組組成。記錄架構如下:

程式碼如下:

    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
 0;
    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,
       ){
if(start >= length){
            cb && cb(rcds);
                 }
        var end = start 8,
           .slice(開始, 結束),
            version = header[0],
          questId = (header[2] contentLength = (header[4]            paddingLength = header[6];
  
        var body = contentLength ? data.slice(end, contentLength) : null;
        rcds.push([type, body, requestId]);

        return argument.callee();    }

}
//使用

sock.on('data', function(data){    })();
}



注意這裡只是簡單處理,如果有上傳文件等複雜情況這個功能不適應,為了最簡單的教程就先不用處理了。忽略,且處理會需要複雜的倍數。


代碼如下:

#define FCGI_BEGIN_REQUEST       1
       3
#定義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)



接下來就可以根據記錄的類型來解析獲取真正的數據,下面我只拿最常用的FCGI_PARAMS、FCGI_GET_VALUES、FCGI_GET_VALUES_RESULT來說明,好在他們的解析方式是一致的。不同的規則,可以參考規範的定義實現,我這裡不細說了。名稱,後跟值形式的傳送,其中127位元組或更少的長度能在位元組中編碼,而更長的長度總是在四位元組中編碼。方式。




複製程式碼


程式碼如下:

typedef struct {
    unsigned char nameLengthB3;  /* nameLengthB3 >>> 7 == 1 */
    unsigned char nameLengthB2;
    unsigned char nameLengthB1;
    unsigned char nameLengthB1;
    unsigned char nameLengthB1;
    unsigned char nameLengthB1;
    unsigned char nameLengthB1;>> 7 == 0 */
    unsigned char nameData[nameLength
            ((B3 & 0x7f)     unsigned char valueData[valueLength];
} FCGI_NameValuePair41;

對應的實作js方法範例:

程式碼如下:


function parseParams(body){

function parseParams(body){

function parseParams(body){
0,
        params = {},
        length = body.length;
            value,
            nameLength,
        
        if(body[j] >> 7 == 1){
            nameLength = ((body[j ] & 0x7f)         } else {

            nameLength = 身體[ 
        if(body[j] >> 7 == 1){
            valueLength = ((body[j] & 0x7f)         } else {
            valueLength = body[

        var ret = body.asciiSlice(j, j nameLength valueLength);

       ength);
        params[name] = 值;

        j = (nameLength valueLength);

    }

   回傳參數;
}


這樣就實作了一個簡單的可取得各種參數和環境變數的方法。程式碼


程式碼如下:sock.on('data', function(data){    getRcds(data, function(data){    getRcds(data, function(rc){ for (var i = 0, l = rcds.length; i             var bodyData = rcds[i],if(body &&(type === types.fcgi_params || type === types.fcgi_get_values || type ======== types.fcgi_get_values_result   ;
                }
        }
    }();

到現在我們已經了解了FastCGI請求部分的基礎,下面接著將回應部分的實現,並最終完成一個簡單的echo應答服務。

回應部分

回應部分相對比較簡單,最簡單的情況只需要發送兩個記錄就行了,那就是FCGI_STDOUT和FCGI_END_REQUEST。
具體記錄實體的內容就不冗餘了,直接看程式碼吧:

複製程式碼 程式碼如下:

var res = (function(){  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),

    th        paddingLength = 8 - contendLength % 8;

        rcdStdoutHd[0] = 1;
        rcdStdoutHd[1] = TYPES.FCGI_STDOUT;

       rcdStdoutHd[3] = 1;

        rcdStdoutHd[4] = contendLength >> 8;
        rcdStdoutHd[5] = contendLength;
        rcdStoo

        return Buffer.concat([rcdStdoutHd, data, buffer0(paddingLength)]);
    };

    function writeHttpHead(){

        return writeStdout(new Buffer("HTTP/1.1 200 OKrnContent-Type:text/html; charsetConnection=utf-8"):

    function writeHttpBody(bodyStr){
        var bodyBuffer = [],

      0, l = body.length; i            🎜>    }

    function writeEnd(){        var rcdEndHd = new Buffer(8);

      PES.FCGI_END_REQUEST;
        rcdEndHd[2] = 0;
        rcdEndHd[3] = 1;
        rcdEndHd[4] = 0;        rcdEndHd[7] = 0;
        return Buffer.concat([rcdEndHd, buffer0(8)]);
    }

    return function(data){

        return Buffer.concat([writeHttpHead(), writeHttpBody(data), writeEnd()]);
; 🎜>

在最簡單的情況下,這樣就可以發送一個完整的回應了。把我們最後的程式碼修改一下:




複製程式碼


程式碼如下:


var visitors = 0 visitors = 0 visitors = 0 visitors = 0 visitors = 0 visitors = 0 visitors = 0 visitors = 0 ;
server.on('connection', function(sock){
    visitors ;
    sock.on('data', function(data){  querystring.parse(params.QUERY_STRING);

            var ret = res('歡迎你,' (querys.name || '親愛的朋友') '!你是本站第' visitors '位用戶~哦~ ;            sock.write(ret);

                  ...
    });


開啟瀏覽器造訪:http://domain/?name=yekai,可看到類似「歡迎你,yekai!你是本站第7位使用者喔~」。
至此,我們就成功的使用Node.js實作了一個最簡單的FastCGI服務。如果需要作為真正的服務使用,接下來只需要對照協議規範來完善我們的邏輯就行了。


對比測驗

最後,我們需要考慮的問題是這個方案具體是否具有可行性?可能已經有同學看出問題,我先把簡單的壓測結果放上來:

複製程式碼 程式碼如下:

//FastCGI方式:
500 clients, running 10 sec.
Speed=27678 pages/min, 63277 bytes/sec.
Requests: 500 clients, running 20 sec.

Speed=22131 pages/min, 63359 bytes/sec.
Requests: 6523 susceed, 854 failed.
Requests: 6523 susceed, 854 failed.

//proxy方式:
500 clients, running 10 sec.
Speed=28752 pages/min, 73191 bytes/sec.
Requests: 3724 susceed, 1068 fail.

500 clients, running 20 sec.

Speed=26508 pages/min, 66267 bytes/sec.
Requests: 6716 susceed, 2120 failed.
Requests: 6716 susceed, 2120 failed.

//直接存取Node.js服務方式:
500 clients, running 10 sec.
Speed=101154 pages/min, 264247 bytes/sec.
Requests: 15729 susceed. 🎜>

500 clients, running 20 sec.

Speed=43791 pages/min, 115962 bytes/sec.
Requests: 13898 susceed, 699 failed.
Requests: 13898 susceed, 699 failed.優於FastCGI方式呢?那是因為在proxy方案下後端服務是直接由Node.js原生模組跑的,而FastCGI方案是我們自己使用JavaScrip實現的。不過,也可以看出兩者方案效率上並沒有很大的差距(當然,這裡對比的只是簡單的情況,如果在真正的業務場景下,差距應該會更大),並且如果Node.js原生支持FastCGI服務,那麼效率上應該會更優。


後記

如果有興趣繼續玩的同學可以查看我本文實現的例子源碼,這兩天研究下了協議規範,其實不難。 同時,回頭準備再玩玩uWSGI,不過官方說v8已經在準備直接支援了。

玩得很淺,如有錯誤歡迎指正交流。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn