هذه المقالة هي فكرة حديثة قمت بتطويرها خلال عملية التعلم Node.js ، وسأناقشها معك.
node.js خادم http
يمكن استخدام Node.js لتنفيذ خدمة HTTP بسهولة بالغة. أبسط مثال يشبه مثال موقع الويب الرسمي:
نسخة الكود كما يلي:
var http = require ('http') ؛
http.createserver (وظيفة (req ، الدقة) {
Res.Writehead (200 ، {'content-type': 'text/plain'}) ؛
res.end ('Hello World/n') ؛
}). الاستماع (1337 ، '127.0.0.1') ؛
هذا يبني بسرعة خدمة ويب تستمع إلى جميع طلبات HTTP على المنفذ 1337.
ومع ذلك ، في بيئة إنتاج حقيقية ، نادراً ما نستخدم Node.js مباشرة كخادم الويب الأمامي للمستخدمين. الأسباب الرئيسية هي كما يلي:
1. استنادًا إلى الميزة المفردة من Node.js ، ضمان المتانة مرتفع نسبيًا للمطورين.
2. قد تشغل خدمات HTTP الأخرى على الخادم المنفذ 80 بالفعل ، ومن الواضح أن خدمات الويب غير المنفذ 80 ليست سهلة الاستخدام بما يكفي للمستخدمين.
3.node.js ليس لديه ميزة كبيرة في معالجة ملف IO. على سبيل المثال ، كموقع ويب عادي ، قد يتطلب منك الاستجابة لموارد الملف مثل الصور في نفس الوقت.
4. سيناريوهات الحمل الموزعة هي أيضًا تحد.
لذلك ، قد يكون استخدام Node.js كخدمة ويب أكثر عرضة لواجهة خادم اللعبة وغيرها من السيناريوهات المماثلة ، معظمها للتعامل مع الخدمات التي لا تتطلب وصولًا مباشرًا للمستخدم وأداء تبادل البيانات فقط.
خدمة الويب Node.js استنادًا إلى Nginx كآلة أمامية
استنادًا إلى الأسباب المذكورة أعلاه ، إذا كان منتجًا على شكل موقع ويب مصمم باستخدام Node.js ، فإن الطريقة التقليدية لاستخدامه هي وضع خادم HTTP ناضج آخر على الطرف الأمامي من خدمة الويب Node.js ، مثل Nginx هو الأكثر استخدامًا.
ثم استخدم NGINX كوكيل عكسي للوصول إلى خدمة الويب المستندة إلى Node.js. يحب:
نسخة الكود كما يلي:
الخادم {
استمع 80 ؛
server_name yekai.me ؛
الجذر/الصفحة الرئيسية/Andy/wwwroot/yekai ؛
موقع / {
proxy_pass http://127.0.0.1:1337 ؛
}
الموقع ~ /.
الجذر/الصفحة الرئيسية/Andy/wwwroot/yekai/static ؛
}
}
سيؤدي هذا إلى حل المشكلات العديدة التي أثيرت أعلاه.
الاتصال باستخدام بروتوكول FastCGI
ومع ذلك ، هناك بعض الأشياء التي ليست جيدة جدًا في طريقة الوكيل أعلاه.
واحد هو السيناريوهات المحتملة التي تتطلب الوصول المباشر HTTP إلى خدمة الويب Node.js التي تحتاج إلى التحكم في وقت لاحق. ومع ذلك ، إذا كنت ترغب في حل المشكلة ، فيمكنك أيضًا استخدام خدماتك الخاصة أو الاعتماد على جدار الحماية لمنعها.
سبب آخر هو أن طريقة الوكيل هي الحل في طبقة تطبيق الشبكة بعد كل شيء ، وليس من المناسب الحصول مباشرة على البيانات ومعالجة البيانات التي تتفاعل مع HTTP العميل ، مثل معالجة الحفاظ على الجذع وحتى ملفات تعريف الارتباط. بالطبع ، يرتبط هذا أيضًا بالقدرات والكمال الوظيفي لخادم الوكيل نفسه.
لذلك ، كنت أفكر في تجربة طريقة أخرى للتعامل معها. أول شيء فكرت فيه هو طريقة FastCgi التي يتم استخدامها عادة في تطبيقات الويب PHP الآن.
ما هو fastcgi
Interface Common Gateway Interface (FastCGI) هي بروتوكول يسمح للبرامج التفاعلية بالتواصل مع خوادم الويب.
يتم استخدام الخلفية التي تم إنشاؤها بواسطة FASTCGI كبديل لتطبيقات الويب CGI. واحدة من أكثر الميزات وضوحا هي أنه يمكن استخدام عملية خدمة fastcgi للتعامل مع سلسلة من الطلبات. سيقوم خادم الويب بتوصيل متغيرات البيئة وطلب هذه الصفحة إلى خادم الويب من خلال مقبس مثل عملية FastCGI. يمكن توصيل الاتصال بخادم الويب بواسطة مقبس DOCK UNIX أو اتصال TCP/IP. لمزيد من المعرفة الأساسية ، يرجى الرجوع إلى دخول ويكيبيديا.
تطبيق FastCgi لـ Node.js
من الناحية النظرية ، نحتاج فقط إلى استخدام node.js لإنشاء عملية fastcgi ، ثم حدد أن طلب الاستماع الخاص بـ Nginx يتم إرساله إلى هذه العملية. نظرًا لأن نماذج Nginx و Node.js كلاهما نماذج خدمة تعتمد على الأحداث ، فيجب أن تكون حلولًا "نظرية" لتتناسب مع العالم. دعونا نفعل ذلك بنفسك.
في Node.js ، يمكن استخدام الوحدة النمطية الصافية لإنشاء خدمة المقبس. من أجل الراحة ، نختار طريقة Socket Unix.
مع تعديل طفيف لتكوين nginx:
نسخة الكود كما يلي:
...
موقع / {
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 (البيانات) ؛
}) ؛
}) ؛
ثم قم بتشغيله (بسبب الأذونات ، يرجى التأكد من أن البرامج النصية Nginx و Node تعمل مع نفس المستخدم أو الحساب مع الأذونات المتبادلة ، وإلا فإنك ستواجه مشاكل الإذن عند قراءة ملفات الجورب وكتابة):
العقدة node_fcgi.js
عند الوصول إلى المتصفح ، نرى أن المحطة التي تقوم بتشغيل البرنامج النصي للعقدة تتلقى عادة محتوى البيانات ، مثل هذا:
نسخة الكود كما يلي:
اتصال
<Buffer 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00 01 04 00 01 01 87 01 ...>
هذا يثبت أن أساسنا النظري قد حقق الخطوة الأولى. بعد ذلك ، نحتاج فقط إلى معرفة كيفية تحليل محتوى هذا المخزن المؤقت.
Fastcgi بروتوكول مؤسسة
يتكون سجل FastCGI من بادئة طول ثابت متبوعًا بعدد متغير من المحتوى والبايت المبطنة. هيكل السجل كما يلي:
نسخة الكود كما يلي:
Typedef struct {
نسخة شار غير موقعة ؛
نوع شار غير موقّع ؛
char armerb1 غير موقعة ؛
char armerb0 غير موقعة ؛
غير موقعة char contentLengthB1 ؛
غير موقعة char contentLengthB0 ؛
char char paddinglength غير موقعة.
Char غير موقعة محفوظة.
غير موقعة char contentData [contentLength] ؛
char paddingdata غير موقعة [PaddingLength] ؛
} fcgi_record ؛
الإصدار: إصدار بروتوكول fastcgi ، استخدم الآن 1 افتراضيًا
النوع: يمكن اعتبار نوع السجل في الواقع حالة مختلفة ، وسيتم مناقشته بالتفصيل لاحقًا
طلب: معرف الطلب ، يجب أن يتوافق عند العودة. إذا لم تكن حالة توافق مضاعفة ، فما عليك سوى استخدام 1 هنا
ContentLength: طول المحتوى ، الحد الأقصى للطول هنا هو 65535
PaddingLength: يتم استخدام طول الحشو لملء البيانات الطويلة في مضاعف عدد صحيح من البايتات الكاملة 8. يتم استخدامه بشكل أساسي لمعالجة البيانات التي يتم محاذاة أكثر فعالية ، وخاصة لاعتبارات الأداء
محجوز: بايت محجوز للتوسع اللاحق
ContentData: بيانات المحتوى الحقيقي ، دعنا نتحدث عن ذلك بالتفصيل لاحقًا
PaddingData: املأ البيانات ، فهي 0 على أي حال ، فقط تجاهلها مباشرة.
للحصول على بنية ووصف محددين ، يرجى الرجوع إلى وثيقة الموقع الرسمي (http://www.fastcgi.com/devkit/doc/fcgi-spec.html#s3.3).
طلب جزء
يبدو الأمر بسيطًا للغاية ، فقط تحليل والحصول على البيانات دفعة واحدة. ومع ذلك ، هناك حفرة هنا ، أي ما هو محدد هنا هو بنية وحدة البيانات (السجل) ، وليس بنية المخزن المؤقت بالكامل. يتكون المخزن المؤقت بأكمله من سجل واحد وسجل واحد. في البداية ، قد لا يكون من السهل على الطلاب الذين اعتادوا على التطوير الأمامي ، ولكن هذا هو الأساس لفهم بروتوكول FastCGI ، وسنرى المزيد من الأمثلة لاحقًا.
لذلك ، نحتاج إلى تحليل سجل وتمييز السجلات بناءً على النوع الذي حصلنا عليه من قبل. فيما يلي وظيفة بسيطة للحصول على جميع السجلات:
نسخة الكود كما يلي:
وظيفة getRcds (البيانات ، CB) {
var rcds = [] ،
ابدأ = 0 ،
طول = data.length ؛
وظيفة الإرجاع () {
إذا (ابدأ> = طول) {
CB && cb (rcds) ؛
rcds = فارغة ؛
يعود؛
}
var end = start + 8 ،
header = data.slice (ابدأ ، نهاية) ،
الإصدار = رأس [0] ،
اكتب = رأس [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]) ؛
إرجاع الحجج. callee () ؛
}
}
//يستخدم
Sock.on ('Data' ، function (data) {
getRcds (البيانات ، الدالة (rcds) {
}) () ؛
}
لاحظ أن هذه مجرد عملية بسيطة. إذا كانت هناك مواقف معقدة مثل تحميل الملفات ، فإن هذه الوظيفة ليست مناسبة لأبسط مظاهرة. في الوقت نفسه ، يتم تجاهل المعلمة requestID أيضًا. إذا كانت مضاعفة ، لا يمكن تجاهلها ، وسوف تحتاج المعالجة إلى أن تكون أكثر تعقيدًا.
بعد ذلك ، يمكن معالجة سجلات مختلفة وفقًا للنوع. تعريف النوع كما يلي:
نسخة الكود كما يلي:
#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)
بعد ذلك ، يمكنك تحليل البيانات الحقيقية وفقًا للنوع المسجل. سأستخدم فقط FCGI_Params الأكثر استخدامًا ، FCGI_GET_VALUES ، و FCGI_GET_VALUES_RESULT للتوضيح. لحسن الحظ ، أساليب تحليلهم متسقة. إن تحليل سجلات الأنواع الأخرى له قواعد مختلفة خاصة به ، ويمكنك الرجوع إلى تعريف المواصفات لتنفيذها. لن أخوض في التفاصيل هنا.
FCGI_PARAMS ، FCGI_GET_VALUES ، FCGI_GET_VALUES_RESULT كلها بيانات نوع "اسم مشفر". التنسيق القياسي هو: يتم نقله في شكل طول اسم ، يليه طول القيمة ، يليه الاسم ، يليه القيمة ، حيث يمكن تشفير 127 بايت أو أقل في بايت واحد ، بينما يتم تشفير أطوال أطول دائمًا في أربعة بايت. يشير البايت العالي من البايت الأول للطول إلى كيفية ترميز الطول. يعني جزء مرتفع من 0 طريقة ترميز بايت ، و 1 يعني طريقة ترميز بأربعة بايت. دعونا نلقي نظرة على مثال شامل ، مثل حالة الأسماء الطويلة والقيم القصيرة:
نسخة الكود كما يلي:
Typedef struct {
غير موقعة char namelengthb3 ؛ / * nameLengthB3 >> 7 == 1 */
غير موقعة char namelengthb2 ؛
char namelengthb1 غير موقعة ؛
char namelengthb0 char غير موقعة ؛
char valuelengthb0 غير موقعة ؛ / * valuelengthb0 >> 7 == 0 */
غير موقعة تشاراتا [namelength
((B3 & 0x7F) << 24) + (B2 << 16) + (B1 << 8) + B0] ؛
char daluedata غير موقعة [Valuelength] ؛
} fcgi_namevaluepair41 ؛
التنفيذ المقابل مثال على طريقة JS:
نسخة الكود كما يلي:
وظيفة parseparams (الجسم) {
var J = 0 ،
params = {} ،
الطول = body.length ؛
بينما (j <length) {
اسم var ،
قيمة،
طول الأسماء ،
فالويل
if (body [j] >> 7 == 1) {
namelength = ((body [j ++] & 0x7f) << 24)+(body [j ++] << 16)+(body [j ++] << 8)+body [j ++] ؛
} آخر {
namelength = body [j ++] ؛
}
if (body [j] >> 7 == 1) {
valuelength = ((body [j ++] & 0x7f) << 24)+(body [j ++] << 16)+(body [j ++] << 8)+body [j ++] ؛
} آخر {
valuelength = body [j ++] ؛
}
var ret = body.asciislice (j ، j + namelength + valuelength) ؛
name = ret.substring (0 ، namelength) ؛
القيمة = ret.substring (namelength) ؛
params [name] = value ؛
J + = (namelength + valuelength) ؛
}
إرجاع المعلمة ؛
}
هذا ينفذ طريقة بسيطة للحصول على معلمات مختلفة ومتغيرات البيئة. تحسين الكود السابق وتوضح كيف يمكننا الحصول على IP العميل:
نسخة الكود كما يلي:
Sock.on ('Data' ، function (data) {
getRcds (البيانات ، الدالة (rcds) {
لـ (var i = 0 ، l = rcds.length ؛ i <l ؛ i ++) {
var bodydata = rcds [i] ،
اكتب = bodydata [0] ،
الجسم = bodydata [1] ؛
if (body && (type === types.fcgi_params || type === types.fcgi_get_values || type === types.fcgi_get_values_result)) {
var params = parseparams (الجسم) ؛
console.log (params.remote_addr) ؛
}
}
}) () ؛
}
لقد فهمنا حتى الآن أساسيات جزء طلب fastcgi ، وبعد ذلك سنقوم بتنفيذ جزء الاستجابة وأخيراً نكمل خدمة رد صدى بسيطة.
جزء الاستجابة
جزء الاستجابة بسيط نسبيا. في أبسط الحالات ، تحتاج فقط إلى إرسال سجلين ، أي FCGI_STDOUT و FCGI_END_REQUEST.
لن أصف المحتوى المحدد للكيان ، فقط انظر إلى الكود:
نسخة الكود كما يلي:
var res = (function () {
var maxlength = math.pow (2 ، 16) ؛
وظيفة buffer0 (len) {
إرجاع عازلة جديدة ((صفيف جديد (Len + 1)). انضم ('/u0000')) ؛
} ؛
وظيفة WritestDout (البيانات) {
var rcdstdouthd = عازلة جديدة (8) ،
contentLength = data.length ،
PaddingLength = 8 - ContentLength ٪ 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 ؛
إرجاع buffer.concat ([rcdstdouthd ، البيانات ، buffer0 (paddingLength)]) ؛
} ؛
وظيفة الكتابة
Return WritestDout (عازلة جديدة ("http/1.1 200 OK/r/ncontent-type: text/html ؛ charset = utf-8/r/nconnection: close/n/r/n")) ؛
}
وظيفة trusttpbody (bodystr) {
var bodybuffer = [] ،
الجسم = عازلة جديدة (bodystr) ؛
لـ (var i = 0 ، l = body.length ؛ i <l ؛ i + = maxLength + 1) {
bodybuffer.push (WritestDout (body.slice (i ، i + maxlength))) ؛
}
إرجاع buffer.concat (Bodybuffer) ؛
}
وظيفة الكتابة () {
var rcdendhd = عازلة جديدة (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 ؛
إرجاع buffer.concat ([rcdendhd ، buffer0 (8)]) ؛
}
وظيفة الإرجاع (البيانات) {
إرجاع buffer.concat ([trustttphead () ، writehttpbody (data) ، writeend ()]) ؛
} ؛
}) () ؛
في أبسط الحالات ، سيتيح لك ذلك إرسال استجابة كاملة. تغيير رمزنا النهائي:
نسخة الكود كما يلي:
var زوار = 0 ؛
server.on ('connection' ، function (sock) {
الزوار ++ ؛
Sock.on ('Data' ، function (data) {
...
var querys = querystring.parse (params.query_string) ؛
var ret = res ('مرحبًا ،' + (querys.name || 'عزيزي صديق') + '! أنت الرقم " + زوار +' وثيقة ~ ') ؛
Sock.write (ret) ؛
RET = NULL ؛
Sock.end () ؛
...
}) ؛
افتح المتصفح وزيارة: http: // domain/؟ name = yekai ، ويمكنك رؤية شيء مثل "مرحبًا ، yekai! أنت المستخدم السابع لهذا الموقع ~".
في هذه المرحلة ، قمنا بنجاح بتنفيذ أبسط خدمة fastcgi باستخدام Node.js. إذا كانت هناك حاجة إلى استخدامها كخدمة حقيقية ، فنحن بحاجة فقط إلى مقارنة مواصفات البروتوكول لتحسين منطقنا.
اختبار مقارن
أخيرًا ، فإن السؤال الذي نحتاج إلى مراعاته هو ما إذا كان هذا الحل ممكنًا على وجه التحديد؟ ربما يكون بعض الطلاب قد رأوا المشكلة ، لذلك سأضع نتائج اختبار الضغط البسيطة أولاً:
نسخة الكود كما يلي:
// طريقة fastcgi:
500 عميل ، تشغيل 10 ثانية.
السرعة = 27678 صفحة/دقيقة ، 63277 بايت/ثانية.
الطلبات: 3295 SESCEED ، فشل 1318.
500 عميل ، تشغيل 20 ثانية.
السرعة = 22131 صفحة/دقيقة ، 63359 بايت/ثانية.
الطلبات: 6523 SESCEED ، فشلت 854.
// طريقة الوكيل:
500 عميل ، تشغيل 10 ثانية.
السرعة = 28752 صفحة/دقيقة ، 73191 بايت/ثانية.
الطلبات: 3724 SESCEED ، فشل 1068.
500 عميل ، تشغيل 20 ثانية.
السرعة = 26508 صفحة/دقيقة ، 66267 بايت/ثانية.
الطلبات: 6716 SUPEST ، فشل 2120.
// الوصول مباشرة إلى Node.js طريقة الخدمة:
500 عميل ، تشغيل 10 ثانية.
السرعة = 101154 صفحات/دقيقة ، 264247 بايت/ثانية.
الطلبات: 15729 SESCEED ، فشل 1130.
500 عميل ، تشغيل 20 ثانية.
السرعة = 43791 صفحة/دقيقة ، 115962 بايت/ثانية.
الطلبات: 13898 SESCEED ، فشل 699.
لماذا طريقة الوكيل أفضل من طريقة fastcgi؟ ذلك لأنه بموجب مخطط الوكيل ، يتم تشغيل خدمة الخلفية مباشرة بواسطة الوحدة الأصلية Node.js ، ويتم تنفيذ مخطط FASTCGI من قبل أنفسنا باستخدام JavaScript. ومع ذلك ، يمكن أيضًا ملاحظة أنه لا توجد فجوة كبيرة في الكفاءة بين الحلين (بالطبع ، المقارنة هنا هي مجرد موقف بسيط. إذا كانت الفجوة أكبر في سيناريوهات الأعمال الحقيقية) ، وإذا كانت Node.js تدعم خدمات FastCGI أصلاً ، فيجب أن تكون الكفاءة أفضل.
PostScript
إذا كنت مهتمًا بالاستمرار في اللعب ، فيمكنك التحقق من الكود المصدري للأمثلة التي قمت بتطبيقها في هذه المقالة. لقد درست مواصفات البروتوكول في اليومين الماضيين ، لكن هذا ليس صعبًا.
في الوقت نفسه ، سأعود وألعب مع UWSGI ، لكن المسؤول قال إن V8 جاهز بالفعل لدعمه مباشرة.
لدي لعبة ضحلة جدا. إذا كان هناك أي خطأ ، يرجى تصحيح لي والتواصل.