Heim  >  Artikel  >  Web-Frontend  >  Implementieren Sie eine einfache FastCGI-Serverinstanz mit Node.js_node.js

Implementieren Sie eine einfache FastCGI-Serverinstanz mit Node.js_node.js

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

Dieser Artikel ist eine Idee, die mir während meines letzten Studiums von Node.js gekommen ist, und ich möchte sie gerne mit allen diskutieren.

HTTP-Server für Node.js

Es ist sehr einfach, einen HTTP-Dienst mit Node.js zu implementieren. Das einfachste Beispiel ist auf der offiziellen Website zu sehen:

Code kopieren Der Code lautet wie folgt:

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

Auf diese Weise können Sie schnell einen Webdienst einrichten, der alle HTTP-Anfragen auf Port 1337 abhört.
In einer realen Produktionsumgebung verwenden wir Node.js jedoch im Allgemeinen selten direkt als Front-End-Webserver für Benutzer. Die Hauptgründe sind folgende:

1. Aufgrund der Single-Threaded-Natur von Node.js müssen die Entwickler eine relativ hohe Garantie für seine Robustheit haben.
2. Möglicherweise gibt es andere http-Dienste auf dem Server, die Port 80 belegt haben, und andere Webdienste als Port 80 sind offensichtlich nicht benutzerfreundlich genug.
3.Node.js hat bei der Datei-E/A-Verarbeitung keine großen Vorteile. Als normale Website muss es beispielsweise möglicherweise gleichzeitig auf Bild- und andere Dateiressourcen reagieren.
4. Das verteilte Lastszenario ist ebenfalls eine Herausforderung.

Daher wird die Verwendung von Node.js als Webdienst eher als Spieleserverschnittstelle und in anderen ähnlichen Szenarien verwendet. Die meisten davon befassen sich mit Diensten, die keinen direkten Zugriff durch Benutzer erfordern und nur zum Datenaustausch verwendet werden .

Node.js-Webdienst basierend auf Nginx als Front-End-Maschine

Wenn es sich aus den oben genannten Gründen um ein Website-förmiges Produkt handelt, das mit Node.js erstellt wurde, besteht die herkömmliche Verwendung darin, einen anderen ausgereiften HTTP-Server am Front-End des Node.js-Webdienstes zu platzieren, z. B. Nginx wird am häufigsten verwendet.
Verwenden Sie dann Nginx als Reverse-Proxy, um auf den Node.js-basierten Webdienst zuzugreifen. Zum Beispiel:

Code kopieren Der Code lautet wie folgt:

Server{
listen 80;
Servername yekai.me;
root /home/andy/wwwroot/yekai;

Standort / {
Proxy_Pass http://127.0.0.1:1337;
}

Standort ~ .(gif|jpg|png|swf|ico|css|js)$ {
root /home/andy/wwwroot/yekai/static;
}
}

Dadurch werden die oben genannten Probleme besser gelöst.

Kommunikation über das FastCGI-Protokoll

Allerdings hat die oben genannte Proxy-Methode auch einige Nachteile.
Ein mögliches Szenario besteht darin, dass Sie den direkten HTTP-Zugriff auf den zugrunde liegenden Node.js-Webdienst steuern müssen. Wenn Sie das Problem jedoch lösen möchten, können Sie auch Ihren eigenen Dienst nutzen oder auf die Firewall-Blockierung setzen.
Der andere Grund ist, dass die Proxy-Methode schließlich eine Lösung auf der Netzwerkanwendungsebene ist und es nicht sehr praktisch ist, die Daten, die mit dem Client-HTTP interagieren, direkt abzurufen und zu verarbeiten, beispielsweise die Keep-Alive-Verarbeitung , Kofferraum und sogar Kekse. Dies hängt natürlich auch mit der Leistungsfähigkeit und Funktionsperfektion des Proxy-Servers selbst zusammen.
Also dachte ich darüber nach, eine andere Verarbeitungsmethode auszuprobieren. Das erste, was mir in den Sinn kam, war die FastCGI-Methode, die heute häufig in PHP-Webanwendungen verwendet wird.

Was ist FastCGI

Fast Common Gateway Interface/FastCGI ist ein Protokoll, das interaktiven Programmen die Kommunikation mit Webservern ermöglicht.

Der Hintergrund von FastCGI besteht darin, als Alternative zu CGI-Webanwendungen zu dienen. Eine der offensichtlichsten Funktionen besteht darin, dass ein FastCGI-Dienstprozess verwendet werden kann, um eine Reihe von Anforderungen zu verarbeiten. Der Webserver übergibt die Umgebungsvariablen Seitenanforderung über einen Socket, z. B. Der FastCGI-Prozess stellt über einen Unix-Domain-Socket oder eine TCP/IP-Verbindung eine Verbindung zum Webserver her. Weiteres Hintergrundwissen finden Sie im Wikipedia-Eintrag.

FastCGI-Implementierung für Node.js

Theoretisch müssen wir nur Node.js verwenden, um einen FastCGI-Prozess zu erstellen, und dann die Nginx-Überwachungsanforderung angeben, die an diesen Prozess gesendet werden soll. Da Nginx und Node.js beide auf ereignisgesteuerten Servicemodellen basieren, sollten sie „theoretisch“ eine natürliche Lösung sein. Lassen Sie es uns unten selbst umsetzen.
Das Netzmodul in Node.js kann zum Erstellen eines Socket-Dienstes verwendet werden. Der Einfachheit halber wählen wir die Unix-Socket-Methode.
Ändern Sie die Konfiguration auf der Nginx-Seite leicht:

Kopieren Sie den Code Der Code lautet wie folgt:

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

Erstellen Sie eine neue Datei node_fcgi.js mit folgendem Inhalt:
Kopieren Sie den Code Der Code lautet wie folgt:

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


Dann ausführen (aus Berechtigungsgründen stellen Sie bitte sicher, dass die Nginx- und Node-Skripte von demselben Benutzer oder Konto mit gegenseitigen Berechtigungen ausgeführt werden, da sonst beim Lesen und Schreiben von Sock-Dateien Berechtigungsprobleme auftreten):

node node_fcgi.js

Beim Zugriff im Browser sehen wir, dass das Terminal, auf dem das Node-Skript ausgeführt wird, den Dateninhalt normal empfängt, wie zum Beispiel diesen:

Code kopierenDer Code lautet wie folgt:

Verbindung
< Puffer 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00 01 04 00 01 01 87 01...>

Dies beweist, dass unsere theoretische Grundlage den ersten Schritt erreicht hat. Als nächstes müssen wir nur noch herausfinden, wie der Inhalt dieses Puffers analysiert wird.


Grundlagen des FastCGI-Protokolls

FastCGI-Datensätze bestehen aus einem Präfix fester Länge, gefolgt von einer variablen Menge an Inhalten und Füllbytes. Die Datensatzstruktur ist wie folgt:

Code kopieren Der Code lautet wie folgt:

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 reserviert ;
unsigned char contentData[contentLength];
unsigned char paddingData[paddingLength];
} FCGI_Record;

Version: FastCGI-Protokollversion, jetzt ist der Standardwert 1. Im Fall von Multiplexing und Parallelität verwenden Sie hier einfach 1
contentLength: Inhaltslänge, die maximale Länge beträgt hier 65535
paddingLength: Auffülllänge, die Funktion besteht darin, lange Daten auf ein ganzzahliges Vielfaches von 8 Bytes zu füllen. Wird hauptsächlich zur effizienteren Verarbeitung von Daten verwendet, die ausgerichtet bleiben, hauptsächlich aus Leistungsgründen.
reserviert: reservierte Bytes für die spätere Erweiterung
contentData: echte Inhaltsdaten, die wird später ausführlich besprochen
paddingData: Fülldaten auf jeden Fall Sie sind alle 0, ignorieren Sie sie einfach.

Eine genaue Struktur und Beschreibung finden Sie im offiziellen Website-Dokument (http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3.3).

Anfragebereich
Es scheint sehr einfach zu sein, analysieren Sie es einfach einmal und erhalten Sie die Daten. Hier gibt es jedoch eine Gefahr: Hier wird die Struktur der Dateneinheit (Datensatz) definiert, nicht die Struktur des gesamten Puffers. Der gesamte Puffer besteht aus einem Datensatz und einem Datensatz. Für diejenigen von uns, die an Front-End-Entwicklung gewöhnt sind, ist es zunächst vielleicht nicht leicht zu verstehen, aber dies ist die Grundlage für das Verständnis des FastCGI-Protokolls, und wir werden später weitere Beispiele sehen.

Wir müssen also jeden Datensatz separat analysieren und die Datensätze anhand des zuvor erhaltenen Typs unterscheiden. Hier ist eine einfache Funktion, um alle Datensätze abzurufen:


Code kopieren Der Code lautet wie folgt:

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 = Daten .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 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记录的解析有自己不同的规则, 可以参考规范的定义实现, 我这里就不细说了.
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] = Wert;

        j = (nameLength valueLength);
    }
    Rückgabeparameter;
}

这样就实现了一个简单可获取各种参数和环境变量的方法.完善前面的代码,演示我们如何获取客户端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);
                }
        }
    })();
}

Jetzt haben wir die Grundlagen des FastCGI-Anfrageteils verstanden. Als nächstes implementieren wir den Antwortteil und vervollständigen schließlich einen einfachen Echo-Antwortdienst.

Antwortabschnitt

Der Antwortteil ist relativ einfach. Im einfachsten Fall müssen Sie nur zwei Datensätze senden, nämlich FCGI_STDOUT und FCGI_END_REQUEST.
Ich werde nicht im Detail auf den spezifischen Inhalt der aufgezeichneten Entität eingehen, schauen Sie sich einfach den Code an:

Code kopieren Der Code lautet wie folgt:

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

})();




Im einfachsten Fall kann so eine vollständige Antwort versendet werden. Ändern Sie unseren endgültigen Code:


Kopieren Sie den Code

Der Code lautet wie folgt: var Visitors = 0 ;server.on('connection', function(sock){
Visitors ;
sock.on('data', function(data){
...
var querys = querystring.parse(params.QUERY_STRING);
var ret = res('Willkommen,' (querys.name || 'Lieber Freund') '! Du bist der 'Besucher'-Benutzer dieser Site~') ;
sock.write(ret);
ret = null;
sock.end();
...
});

Öffnen Sie den Browser und besuchen Sie: http://domain/?name=yekai. Sie sehen etwas wie „Willkommen, yekai! Sie sind der 7. Benutzer dieser Website~“.
Zu diesem Zeitpunkt haben wir den einfachsten FastCGI-Dienst mit Node.js erfolgreich implementiert. Wenn es als echter Dienst verwendet werden muss, müssen wir nur unsere Logik gemäß den Protokollspezifikationen verbessern.


Vergleichstest

Abschließend müssen wir uns noch die Frage stellen, ob diese Lösung machbar ist. Einige Schüler haben das Problem möglicherweise bemerkt, daher werde ich zuerst die einfachen Stresstestergebnisse veröffentlichen:

Kopieren Sie den Code Der Code lautet wie folgt folgt:

//FastCGI-Modus:
500 Clients, 10 Sek. ausgeführt.
Geschwindigkeit=27678 Seiten/Min., 63277 Bytes/Sek.
Anfragen: 3295 erfolgreich, 1318 fehlgeschlagen.

500 Clients, 20 Sek. ausgeführt.
Geschwindigkeit = 22131 Seiten/Min., 63359 Bytes/Sek.
Anfragen: 6523 erfolgreich, 854 fehlgeschlagen.

//Proxy-Modus:
500 Clients, 10 Sek. ausgeführt.
Geschwindigkeit = 28752 Seiten/Min., 73191 Bytes/Sek.
Anfragen: 3724 erfolgreich, 1068 fehlgeschlagen.

500 Clients, 20 Sek. ausgeführt.
Geschwindigkeit = 26508 Seiten/Min., 66267 Bytes/Sek.
Anfragen: 6716 erfolgreich, 2120 fehlgeschlagen.

//Direkter Zugriff auf die Dienstmethode von Node.js:
500 Clients, 10 Sek. ausgeführt.
Geschwindigkeit = 101154 Seiten/Min., 264247 Bytes/Sek.
Anfragen: 15729 erfolgreich, 1130 fehlgeschlagen.

500 Clients, 20 Sek. ausgeführt.
Geschwindigkeit = 43791 Seiten/Min., 115962 Bytes/Sek.
Anfragen: 13898 erfolgreich, 699 fehlgeschlagen.


Warum funktioniert die Proxy-Methode? Funktioniert es stattdessen? Ist es besser als FastCGI? Das liegt daran, dass bei der Proxy-Lösung der Back-End-Dienst direkt vom nativen Node.js-Modul ausgeführt wird, während die FastCGI-Lösung von uns selbst mithilfe von JavaScript implementiert wird. Es ist jedoch auch ersichtlich, dass es keine große Lücke in der Effizienz der beiden Lösungen gibt (natürlich ist der Vergleich hier nur eine einfache Situation, wenn in einem realen Geschäftsszenario die Lücke größer sein sollte) und wenn Node.js unterstützt den FastCGI-Dienst nativ, dann sollte die Effizienz besser sein.

Postskriptum

Wenn Sie daran interessiert sind, weiterzuspielen, können Sie den Quellcode des Beispiels überprüfen, das ich in diesem Artikel implementiert habe. Ich habe die Protokollspezifikationen in den letzten zwei Tagen studiert, und es ist nicht schwierig.
Gleichzeitig werde ich zurückgehen und mich darauf vorbereiten, wieder mit uWSGI zu spielen, aber der Beamte sagte, dass v8 sich bereits darauf vorbereitet, es direkt zu unterstützen.
Das Spiel ist sehr einfach. Wenn es Fehler gibt, korrigieren Sie mich bitte und teilen Sie mir mit.

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn