Maison  >  Article  >  interface Web  >  Implémentez une instance de serveur FastCGI simple à l'aide de Node.js_node.js

Implémentez une instance de serveur FastCGI simple à l'aide de Node.js_node.js

WBOY
WBOYoriginal
2016-05-16 16:45:181656parcourir

Cet article est une idée qui m'est venue lors de ma récente étude de Node.js, et j'aimerais en discuter avec tout le monde.

Serveur HTTP pour Node.js

Il est très simple d'implémenter un service http en utilisant Node.js. L'exemple le plus simple est celui présenté sur le site officiel :

Copier le code Le code est le suivant :
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');
De cette façon, configurez rapidement un service Web qui écoute toutes les requêtes http sur le port 1337.
Cependant, dans un environnement de production réel, nous utilisons généralement rarement Node.js directement comme serveur Web frontal pour les utilisateurs. Les principales raisons sont les suivantes :
1. Basé sur la nature monothread de Node.js, la garantie de sa robustesse nécessite que les développeurs soient relativement élevés.

2. Il peut y avoir d'autres services http sur le serveur qui ont occupé le port 80, et les services Web autres que le port 80 ne sont évidemment pas assez conviviaux.
3.Node.js n'a pas beaucoup d'avantages dans le traitement des E/S de fichiers. Par exemple, en tant que site Web classique, il peut avoir besoin de répondre aux images et à d'autres ressources de fichiers en même temps.
4. Le scénario de charge distribuée est également un défi.

Par conséquent, l'utilisation de Node.js en tant que service Web est plus susceptible d'être utilisée comme interface de serveur de jeu et dans d'autres scénarios similaires. La plupart d'entre eux traitent de services qui ne nécessitent pas d'accès direct par les utilisateurs et ne sont utilisés que pour l'échange de données. .

Service Web Node.js basé sur Nginx comme machine frontale

Pour les raisons ci-dessus, s'il s'agit d'un produit en forme de site Web construit à l'aide de Node.js, l'utilisation conventionnelle consiste à placer un autre serveur http mature sur le front-end du service Web Node.js, tel que Nginx, qui est le plus couramment utilisé.

Utilisez ensuite Nginx comme proxy inverse pour accéder au service Web basé sur Node.js. Tel que :

Copier le code Le code est le suivant :
serveur{
écouter 80;
nom_serveur yekai.me;
root /home/andy/wwwroot/yekai;
emplacement / {

proxy_pass http://127.0.0.1:1337;
}

emplacement ~ .(gif|jpg|png|swf|ico|css|js)$ {

root /home/andy/wwwroot/yekai/static;
}
}

Cela résoudra mieux les problèmes soulevés ci-dessus.

Communication via le protocole FastCGI

Cependant, la méthode proxy mentionnée ci-dessus présente également certains inconvénients.

Un scénario possible est que vous deviez contrôler l'accès http direct au service Web Node.js sous-jacent. Cependant, si vous souhaitez le résoudre, vous pouvez également utiliser votre propre service ou compter sur le blocage du pare-feu.
L'autre raison est que la méthode proxy est après tout une solution sur la couche application réseau, et il n'est pas très pratique d'obtenir et de traiter directement les données qui interagissent avec le client http, comme le traitement du keep-alive , le coffre et même les cookies. Bien entendu, cela est également lié aux capacités et à la perfection fonctionnelle du serveur proxy lui-même.
Je pensais donc essayer une autre méthode de traitement. La première chose qui m'est venue à l'esprit était la méthode FastCGI qui est maintenant couramment utilisée dans les applications Web php.

Qu'est-ce que FastCGI

Fast Common Gateway Interface/FastCGI est un protocole qui permet aux programmes interactifs de communiquer avec des serveurs Web.

L'arrière-plan de FastCGI est de servir d'alternative aux applications Web cgi. L'une des fonctionnalités les plus évidentes est qu'un processus de service FastCGI peut être utilisé pour gérer une série de requêtes. Le serveur Web transmettra les variables d'environnement et les données. demande de page via un socket, tel que Le processus FastCGI se connecte au serveur Web via un socket de domaine Unix ou une connexion TCP/IP. Pour plus de connaissances générales, veuillez vous référer à l'entrée Wikipédia.

Implémentation FastCGI pour Node.js

Théoriquement, il suffit d'utiliser Node.js pour créer un processus FastCGI, puis de spécifier la requête de surveillance Nginx à envoyer à ce processus. Étant donné que Nginx et Node.js sont tous deux basés sur des modèles de services événementiels, « en théorie », ils devraient constituer une solution naturelle. Implémentons-le nous-mêmes ci-dessous.

Le module net dans Node.js peut être utilisé pour créer un service socket Pour plus de commodité, nous choisissons la méthode socket unix.
Modifiez légèrement la configuration côté Nginx :

Copiez le code Le code est le suivant :
..
emplacement / {
fastcgi_pass unix:/tmp/node_fcgi.sock;
}
...

Créez un nouveau fichier node_fcgi.js avec le contenu suivant :
Copiez le code Le code est le suivant :

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


Ensuite, exécutez (pour des raisons d'autorisation, veuillez vous assurer que les scripts Nginx et node sont exécutés par le même utilisateur ou par des comptes avec des autorisations mutuelles, sinon vous rencontrerez des problèmes d'autorisation lors de la lecture et de l'écriture de fichiers sock) :

node node_fcgi.js

Lors de l'accès dans le navigateur, nous voyons que le terminal exécutant le script de nœud reçoit normalement le contenu des données, comme ceci :

Copier le code Le code est le suivant :

connexion
< 00 00 01 04 00 01 01 87 01...>

Cela prouve que notre fondement théorique a franchi la première étape. Il ne nous reste plus qu'à comprendre comment analyser le contenu de ce tampon.


Bases du protocole FastCGI

Les enregistrements FastCGI se composent d'un préfixe de longueur fixe suivi d'une quantité variable de contenu et d'octets de remplissage. La structure de l'enregistrement est la suivante :

Copier le code Le code est le suivant :

typedef struct {
version de caractère non signé;
type de caractère non signé;
caractère non signé requestIdB1;
caractère non signé requestIdB0;
caractère non signé contentLengthB1;
caractère non signé contentLengthB0;
caractère non signé paddingLength;
caractère non signé réservé ;
caractère non signé contentData[contentLength];
caractère non signé paddingData[paddingLength];
} FCGI_Record;

version : version du protocole FastCGI, maintenant la valeur par défaut est 1. Dans le cas du multiplexage et de la concurrence, utilisez simplement 1 ici
contentLength : longueur du contenu, la longueur maximale ici est 65535
paddingLength : longueur de remplissage, la fonction consiste à remplir des données longues jusqu'à un multiple entier de 8 octets, principalement utilisé pour traiter les données qui restent alignées plus efficacement, principalement en raison de considérations de performances
réservé : octets réservés, pour une expansion ultérieure
contentData : données de contenu réel, qui sera discuté en détail plus tard
paddingData : remplissage des données, de toute façon. Ils sont tous à 0, ignorez-les simplement.

Pour une structure et une description spécifiques, veuillez vous référer au document du site officiel (http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3.3).

Section Demande
Cela semble très simple, il suffit de l'analyser une fois et d'obtenir les données. Cependant, il y a un piège ici, c'est-à-dire que ce qui est défini ici est la structure de l'unité de données (enregistrement), et non la structure de l'ensemble du tampon. L'ensemble du tampon est composé d'un enregistrement et d'un enregistrement. Cela n'est peut-être pas facile à comprendre au début pour ceux d'entre nous qui sont habitués au développement front-end, mais c'est la base pour comprendre le protocole FastCGI, et nous verrons plus d'exemples plus tard.

Nous devons donc analyser chaque enregistrement séparément et distinguer les enregistrements en fonction du type que nous avons obtenu plus tôt. Voici une fonction simple pour obtenir tous les enregistrements :


Copier le code Le code est le suivant :

fonction 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,
            en-tête = données .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();
    antecedents ){
    })();
}



注意这里只是简单处理,如果有上传文件等复杂情况这个函数不适应,为了最简演示就先简Il s'agit d'une demande d'identification de demande. ,并且处理会需要复杂得多。
接下来就可以根据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 FC GI_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记录的解析有"编码名-值”类型数据,标准格式为:以名字长度,后跟值的长度,后跟名字,后跟值的形式传送,其中127字节或更少的长度能在一字节中编码,而更长的长度总是在四字节中编码。长度的第一字节的高位指示长度的编码方式。高位为0意味着一个字节的编码方式,1意味着四字节的编码方式。看个综合的例子,比如长名短值的情况:


复制代码

代码如下 :

typedef struct {
    char non signé nameLengthB3 ;  /* nomLongueurB3  >> 7 == 1 */
    nom de caractère non signéLongueurB2;
    nom de caractère non signéLongueurB1;
    nom de caractère non signéLongueurB0;
    valeur de caractère non signéLongueurB0; /* valeurLongueurB0 >> 7 == 0 */
    char non signé nameData[nameLength
            ((B3 & 0x7f) << 24) (B2 << 16) (B1 << 8) B0];
    char non signé valueData[valueLength];
} FCGI_NameValuePair41;

对应的实现js方法示例:

复制代码 代码如下 :

function parseParams(body){
    var j = 0,
        params = {},
        length = body.length;
    while(j < length){
        var name,
             value,
            nameLength,
             valeurLongueur ;
        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] = valeur ;

        j = (nameLength valueLength);
    }
    return params;
}

这样就实现了一个简单可获取各种参数和环境变量的方法。完善前面的代码,演示我们如何获取客户端ip:

复制代码 代码如下 :

sock.on('data', function(data){
    getRcds(data, function(rcds){
        pour (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 (corps);
                    console.log(params.REMOTE_ADDR);
                }
        }
    })();
}

Maintenant, nous avons compris les bases de la partie requête FastCGI. Ensuite, nous allons implémenter la partie réponse et enfin compléter un service de réponse d'écho simple.

Section Réponse

La partie réponse est relativement simple. Dans le cas le plus simple, vous n'avez besoin d'envoyer que deux enregistrements, qui sont FCGI_STDOUT et FCGI_END_REQUEST.
Je n'entrerai pas dans les détails sur le contenu spécifique de l'entité enregistrée, il suffit de regarder le code :

Copier le code Le code est le suivant :

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 - contestLength % 8;

rcdStdoutHd[0] = 1;
rcdStdoutHd[1] = TYPES.FCGI_STDOUT;
rcdStdoutHd[2] = 0;
rcdStdoutHd[3] = 1;
rcdStdoutHd[4] = contestLength >> 8;
rcdStdoutHd[5] = contestLength;
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: closenrn"));
}

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

})();




Dans le cas le plus simple, cela permet d'envoyer une réponse complète. Modifiez notre code final :


Copiez le code

Le code est le suivant : var visiteurs = 0 ;server.on('connection', function(sock){
visiteurs ;
sock.on('data', function(data){
...
var requêtes = querystring.parse(params.QUERY_STRING);
var ret = res('Bienvenue,' (querys.name || 'Cher ami') '! Vous êtes l'utilisateur 'visiteur' de ce site~') ;
sock.write(ret);
ret = null;
sock.end();
...
});

Ouvrez le navigateur et visitez : http://domain/?name=yekai, vous pouvez voir quelque chose comme "Bienvenue, yekai ! Vous êtes le 7ème utilisateur de ce site~".
À ce stade, nous avons implémenté avec succès le service FastCGI le plus simple en utilisant Node.js. S'il doit être utilisé comme un véritable service, il nous suffit alors d'améliorer notre logique en fonction des spécifications du protocole.


Test de comparaison

Enfin, la question que nous devons nous poser est de savoir si cette solution est réalisable ? Certains étudiants ont peut-être remarqué le problème, je publierai donc d'abord les résultats simples du test d'effort :

Copiez le code Le code est le suivant suit :

//Mode FastCGI :
500 clients, exécutant 10 secondes.
Vitesse=27678 pages/min, 63277 octets/sec.
Requêtes : 3295 réussites, 1318 échoué.

500 clients, exécutant 20 secondes.
Vitesse=22 131 pages/min, 63 359 octets/s.
Requêtes : 6 523 réussites, 854 échecs.

//Mode proxy :
500 clients, exécutant 10 secondes.
Vitesse=28 752 pages/min, 73 191 octets/sec.
Requêtes : 3 724 réussites, 1 068 échecs.

500 clients, exécutant 20 secondes.
Vitesse=26 508 pages/min, 66 267 octets/s.
Requêtes : 6 716 ont réussi, 2 120 ont échoué.

//Accès direct à la méthode de service Node.js :
500 clients, exécutant 10 secondes.
Vitesse=101154 pages/min, 264247 octets/sec.
Requêtes : 15729 réussies, 1130 échouées.

500 clients, exécutant 20 secondes.
Vitesse=43791 pages/min, 115962 octets/sec.
Requêtes : 13898 ont réussi, 699 ont échoué.


Pourquoi la méthode proxy fonctionne-t-il à la place ? Est-ce mieux que FastCGI ? En effet, dans la solution proxy, le service back-end est directement exécuté par le module natif Node.js, tandis que la solution FastCGI est implémentée par nous-mêmes à l'aide de JavaScript. Cependant, on peut également constater qu'il n'y a pas un grand écart dans l'efficacité des deux solutions (bien sûr, la comparaison ici n'est qu'une situation simple, si dans un scénario commercial réel, l'écart devrait être plus grand), et si Node.js prend nativement en charge le service FastCGI, l'efficacité devrait alors être meilleure.

Post-scriptum

Si vous souhaitez continuer à jouer, vous pouvez consulter le code source de l'exemple que j'ai implémenté dans cet article. J'ai étudié les spécifications du protocole au cours des deux derniers jours, et ce n'est pas difficile.
En même temps, je vais y retourner et me préparer à jouer à nouveau avec uWSGI, mais le responsable a dit que la v8 se prépare déjà à la prendre en charge directement.
Le jeu est très simple. S'il y a des erreurs, veuillez me corriger et communiquer.

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn