Home  >  Article  >  Web Front-end  >  Implement a simple FastCGI server instance using Node.js_node.js

Implement a simple FastCGI server instance using Node.js_node.js

WBOY
WBOYOriginal
2016-05-16 16:45:181656browse

This article is an idea that I came up with during my recent study of Node.js, and I would like to discuss it with everyone.

HTTP server for Node.js

It is very easy to implement an http service using Node.js. The simplest example is as shown on the official website:

Copy code The code is as follows:

var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content -Type': 'text/plain'});
res.end('Hello Worldn');
}).listen(1337, '127.0.0.1');

This way Quickly set up a web service that listens to all http requests on port 1337.
However, in a real production environment, we generally rarely use Node.js directly as the front-end web server for users. The main reasons are as follows:

1. Due to the single-threaded nature of Node.js, its robustness guarantee has relatively high requirements for developers.
2. There may be other http services on the server that have occupied port 80, and web services other than port 80 are obviously not user-friendly enough.
3.Node.js does not have much advantage in file IO processing. For example, as a regular website, it may need to respond to image and other file resources at the same time.
4. Distributed load scenario is also a challenge.

Therefore, using Node.js as a web service is more likely to be used as a game server interface and other similar scenarios. Most of them deal with services that do not require direct access by users and are only used for data exchange.

Node.js web service based on Nginx as front-end machine

Based on the above reasons, if it is a website-shaped product built using Node.js, the conventional usage is to place another mature http server on the front end of the Node.js web service, such as Nginx, which is most commonly used.
Then use Nginx as a reverse proxy to access the Node.js based web service. Such as:

Copy code The code is as follows:

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;
}
}

This will better solve the problems raised above.

Communicate using FastCGI protocol

However, the above-mentioned proxy method also has some disadvantages.
One possible scenario is that you need to control direct http access to the underlying Node.js web service. However, if you want to solve it, you can also use your own service or rely on firewall blocking.
The other reason is that the proxy method is a solution on the network application layer after all, and it is not very convenient to directly obtain and process the data that interacts with the client http, such as the processing of keep-alive, trunk and even cookies. Of course, this is also related to the capabilities and functional perfection of the proxy server itself.
So, I was thinking of trying another processing method. The first thing that came to mind was the FastCGI method that is now commonly used in php web applications.

What is FastCGI

Fast Common Gateway Interface/FastCGI is a protocol that allows interactive programs to communicate with web servers.

The background of FastCGI is to serve as an alternative to cgi web applications. One of the most obvious features is that a FastCGI service process can be used to handle a series of requests. The web server will pass the environment variables and the page request through a socket, such as The FastCGI process connects to the web server via a Unix Domain Socket or a TCP/IP connection. For more background knowledge, please refer to the Wikipedia entry.

FastCGI implementation for Node.js

Theoretically, we only need to use Node.js to create a FastCGI process, and then specify Nginx’s monitoring request to be sent to this process. Since both Nginx and Node.js are based on event-driven service models, "theoretically" they should be a natural solution. Let’s implement it ourselves below.
The net module in Node.js can be used to create a socket service. For convenience, we choose the unix socket method.
Slightly modify the configuration on the Nginx side:

Copy the code The code is as follows:

. ..
location / {
fastcgi_pass unix:/tmp/node_fcgi.sock;
}
...

Create a new file node_fcgi.js with the following content:
Copy the code The code is as follows:

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);
});
});


Then run (due to permission reasons, please ensure that the Nginx and node scripts are run by the same user or accounts with mutual permissions, otherwise you will encounter permission problems when reading and writing sock files):

node node_fcgi.js

When accessed in the browser, we see that the terminal running the node script receives the data content normally, such as this:

Copy code The code is as follows:

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...>

This proves that our theoretical foundation has achieved the first step. Next, we only need to figure out how to parse the content of this buffer.


FastCGI Protocol Basics

FastCGI records consist of a fixed-length prefix followed by a variable amount of content and padding bytes. The record structure is as follows:

Copy code The code is as follows:

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 protocol version, now the default is 1 In the case of multiplexing and concurrency, just use 1 here
contentLength: content length, the maximum length here is 65535
paddingLength: padding length, the function is to fill long data to an integer multiple of 8 bytes, mainly Used to process data that remains aligned more efficiently, mainly due to performance considerations
reserved: reserved bytes, for subsequent expansion
contentData: real content data, which will be discussed in detail later
paddingData: padding data, anyway They are all 0, just ignore them.

For specific structure and description, please refer to the official website document (http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3.3).

Request section
It seems very simple, just parse it once and get the data. However, there is a pitfall here, that is, what is defined here is the structure of the data unit (record), not the structure of the entire buffer. The entire buffer is composed of one record and one record. It may not be easy to understand at first for those of us who are used to front-end development, but this is the basis for understanding the FastCGI protocol, and we will see more examples later.

So, we need to parse each record separately and distinguish the records based on the type we obtained earlier. Here is a simple function to get all records:


Copy code The code is as follows:

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);
                }
        }
    })();
}

Now we have understood the basics of the FastCGI request part. Next, we will implement the response part and finally complete a simple echo response service.

Response section

The response part is relatively simple. In the simplest case, you only need to send two records, which are FCGI_STDOUT and FCGI_END_REQUEST.
I won’t go into details about the specific content of the recorded entity, just look at the code:

Copy code The code is as follows:

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 OKrnContent-Type:text/html; charset=utf-8rnConnection: closernrn"));
}

function writeHttpBody(bodyStr){
var bodyBuffer = [],
body = new Buffer(bodyStr);
for(var i = 0, l = body.length; i < l; i = MaxLength 1){
                                                                                                                                          
function writeEnd(){
var rcdEndHd = new Buffer(8);
rcdEndHd[0] = 1;

rcdEndHd[1] = TYPES.FCGI_END_REQUEST;

rc dEndHd[2] = 0;
rcdEndHd[3] = 1;
rcdEndHd[4] = 0;
rcdEndHd[5] = 8;
rcdEndHd[6] = 0;
rcd EndHd[7] = 0;
return Buffer.concat([rcdEndHd, buffer0(8)]);
}

return function(data){
return Buffer.concat([writeHttpHead(), writeHttpBody(data), writeEnd()]);
};

})();




In the simplest case, this allows a complete response to be sent. Modify our final code:


Copy the code

The code is as follows: var visitors = 0 ;server.on('connection', function(sock){
visitors ;
sock.on('data', function(data){
...
var queries = querystring.parse(params.QUERY_STRING);
var ret = res('Welcome,' (querys.name || 'Dear friend') '! You are the 'visitor' user of this site~') ;
sock.write(ret);
ret = null;
sock.end();
...
});

Open the browser and visit: http://domain/?name=yekai, you can see something like "Welcome, yekai! You are the 7th user of this site~".
At this point, we have successfully implemented the simplest FastCGI service using Node.js. If it needs to be used as a real service, then we only need to improve our logic according to the protocol specifications.


Comparison test

Finally, the question we need to consider is whether this solution is feasible? Some students may have noticed the problem, so I will post the simple stress test results first:

Copy the code The code is as follows:

//FastCGI mode:
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 mode:
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.

//Direct access to Node.js service method:
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.


Why does the proxy method work instead? Is it better than FastCGI? That's because under the proxy solution, the back-end service is directly run by the Node.js native module, while the FastCGI solution is implemented by ourselves using JavaScript. However, it can also be seen that there is not a big gap in the efficiency of the two solutions (of course, the comparison here is only a simple situation, if in a real business scenario, the gap should be larger), and if Node.js natively supports FastCGI service, then the efficiency should be better.

Postscript

If you are interested in continuing to play, you can check the source code of the example I implemented in this article. I have studied the protocol specifications in the past two days, and it is not difficult.
At the same time, I am planning to play uWSGI again, but the official said that v8 is already preparing to directly support it.
The game is very simple, please correct me if there are any mistakes.

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn