OpenResty هو NGINX المحسّن مع الطلبات والاستجابات الديناميكية
API7.ai
October 23, 2022
بعد المقدمة السابقة، يجب أن تكون قد فهمت مفهوم OpenResty وكيفية تعلمه. ستوجهنا هذه المقالة حول كيفية تعامل OpenResty مع طلبات العملاء والاستجابات.
على الرغم من أن OpenResty هو خادم ويب يعتمد على NGINX، إلا أنه يختلف جذريًا عن NGINX: NGINX يتم تشغيله بواسطة ملفات تكوين ثابتة، بينما يتم تشغيل OpenResty بواسطة واجهة برمجة تطبيقات Lua، مما يوفر مرونة وقابلية برمجة أكبر.
دعني أريك فوائد واجهة برمجة تطبيقات Lua.
فئات واجهات برمجة التطبيقات
أولاً، نحتاج إلى معرفة أن واجهة برمجة تطبيقات OpenResty مقسمة إلى الفئات التالية.
- معالجة الطلبات والاستجابات.
- المتعلقة بـ SSL.
- القاموس المشترك.
- Cosocket.
- التعامل مع حركة المرور على أربع طبقات.
- العملية والعامل.
- الوصول إلى متغيرات وتكوين NGINX.
- الوظائف العامة مثل النصوص، الوقت، الترميز، وغيرها.
هنا، أقترح عليك أيضًا فتح وثائق واجهة برمجة تطبيقات Lua الخاصة بـ OpenResty والتحقق منها مقابل قائمة واجهات برمجة التطبيقات لترى إذا كنت تستطيع الربط مع هذه الفئة.
توجد واجهات برمجة تطبيقات OpenResty ليس فقط في مشروع lua-nginx-module، ولكن أيضًا في مشروع lua-resty-core، مثل ngx.ssl
، ngx.base64
، ngx.errlog
، ngx.process
، ngx.re.split
، ngx.resp.add_header
، ngx.balancer
، ngx.semaphore
، ngx.ocsp
، وغيرها من واجهات برمجة التطبيقات.
بالنسبة لواجهات برمجة التطبيقات التي ليست في مشروع lua-nginx-module، تحتاج إلى استدعائها بشكل منفصل لاستخدامها. على سبيل المثال، إذا كنت تريد استخدام وظيفة التقسيم، تحتاج إلى استدعائها كما يلي.
$ resty -e 'local ngx_re = require "ngx.re"
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
print(res)
'
بالطبع، قد يربكك: في مشروع lua-nginx-module
، هناك عدة واجهات برمجة تطبيقات تبدأ بـ ngx.re.sub
، ngx.re.find
، إلخ. لماذا تحتاج واجهة برمجة التطبيقات ngx.re.split
إلى الاستدعاء أولاً قبل استخدامها؟
كما ذكرنا في فصل lua-resty-core
السابق، يتم تنفيذ واجهات برمجة تطبيقات OpenResty الجديدة في مستودع lua-rety-core
بطريقة FFI، لذلك هناك شعور حتمي بالتجزئة. أنا أتطلع إلى حل هذه المشكلة في المستقبل من خلال دمج مشروعي lua-nginx-module
و lua-resty-core
.
الطلب
بعد ذلك، دعنا ننظر إلى كيفية تعامل OpenResty مع طلبات العملاء والاستجابات. أولاً، دعنا ننظر إلى واجهة برمجة التطبيقات الخاصة بمعالجة الطلبات، ولكن هناك أكثر من 20 واجهة برمجة تطبيقات تبدأ بـ ngx.req
، فكيف نبدأ؟
نحن نعلم أن رسائل طلب HTTP تتكون من ثلاثة أجزاء: سطر الطلب، رأس الطلب، وجسم الطلب، لذلك سأقدم واجهة برمجة التطبيقات في هذه الأجزاء الثلاثة.
سطر الطلب
أولاً، سطر الطلب، الذي يحتوي على طريقة الطلب، URI، وإصدار بروتوكول HTTP لـ HTTP. في NGINX، يمكنك الحصول على هذه القيمة باستخدام متغير مدمج، بينما في OpenResty، يتوافق مع واجهة برمجة التطبيقات ngx.var.*
. دعنا ننظر إلى مثالين.
- المتغير المدمج
$scheme
، الذي يمثل اسم البروتوكول في NGINX، إماhttp
أوhttps
؛ في OpenResty، يمكنك استخدامngx.var.scheme
للعودة إلى نفس القيمة. $request_method
يمثل طريقة الطلب مثلGET
،POST
، إلخ؛ في OpenResty، يمكنك العودة إلى نفس القيمة عبرngx.var.request_method
.
يمكنك زيارة وثائق NGINX الرسمية للحصول على قائمة كاملة بالمتغيرات المدمجة في NGINX: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables.
إذن السؤال الذي يطرح نفسه: لماذا توفر OpenResty واجهة برمجة تطبيقات منفصلة لسطر الطلب عندما يمكنك الحصول على البيانات في سطر الطلب عن طريق إرجاع قيمة متغير مثل ngx.var.*
؟
النتيجة تحتوي على العديد من العوامل:
- أولاً، لا يُنصح بقراءة
ngx.var
بشكل متكرر بسبب أدائها غير الفعال. - ثانيًا، من ناحية البرمجة الصديقة،
ngx.var
تُرجع سلسلة نصية، وليس كائن Lua. يصعب التعامل معها عند الحصول علىargs
، والتي قد تُرجع قيم متعددة. - ثالثًا، من ناحية المرونة، معظم
ngx.var
للقراءة فقط، وفقط عدد قليل من المتغيرات قابلة للكتابة، مثل$args
وlimit_rate
. ومع ذلك، نحتاج غالبًا إلى تعديل الطريقة، URI، و args.
لذلك، توفر OpenResty عدة واجهات برمجة تطبيقات مخصصة لمعالجة سطر الطلب، والتي يمكنها إعادة كتابة سطر الطلب للعمليات اللاحقة مثل إعادة التوجيه.
دعنا ننظر إلى كيفية الحصول على رقم إصدار بروتوكول HTTP عبر واجهة برمجة التطبيقات. واجهة برمجة تطبيقات OpenResty ngx.req.http_version
تفعل نفس الشيء الذي يفعله متغير NGINX $server_protocol
: إرجاع رقم إصدار بروتوكول HTTP. ومع ذلك، فإن القيمة المرجعة من هذه الواجهة ليست سلسلة نصية ولكن بتنسيق رقمي، القيم الممكنة هي 2.0
، 1.0
، 1.1
، و 0.9
. يتم إرجاع Nil
إذا كانت النتيجة خارج نطاق هذه القيم.
دعنا ننظر إلى طريقة الحصول على الطلب في سطر الطلب. كما ذكرنا، فإن دور وقيمة الإرجاع لـ ngx.req.get_method
ومتغيرات NGINX $request_method
هي نفسها: بتنسيق سلسلة نصية.
ومع ذلك، فإن تنسيق المعلمة لطريقة طلب HTTP الحالية ngx.req.set_method
ليس سلسلة نصية ولكن ثوابت رقمية مدمجة. على سبيل المثال، الكود التالي يعيد كتابة طريقة الطلب إلى POST.
ngx.req.set_method(ngx.HTTP_POST)
للتحقق من أن الثابت المدمج، ngx.HTTP_POST
هو بالفعل رقم وليس سلسلة نصية، يمكنك طباعة قيمته ومعرفة إذا كانت النتيجة 8.
resty -e 'print(ngx.HTTP_POST)'
بهذه الطريقة، تكون القيمة المرجعة لطريقة get
هي سلسلة نصية، بينما تكون قيمة الإدخال لطريقة set
هي رقم. لا بأس عندما تمرر طريقة set
قيمة مربكة لأن الواجهة يمكن أن تتعطل وتُبلغ بخطأ 500
. ومع ذلك، في منطق التحكم التالي:
if (ngx.req.get_method() == ngx.HTTP_POST) then
-- do something
end
هذا النوع من الكود يعمل بشكل جيد، ولا يُبلغ عن أخطاء، ومن الصعب اكتشافه حتى أثناء مراجعة الكود. لقد ارتكبت خطأً مشابهًا من قبل ولا أزال أتذكره: لقد مررت بجولتين من مراجعة الكود وحالات اختبار غير مكتملة لمحاولة تغطيته. في النهاية، تم تتبع مشكلة في بيئة الإنتاج إلى المشكلة.
لا توجد طريقة عملية لحل مثل هذه المشكلة إلا أن تكون أكثر حذرًا أو تضيف طبقة أخرى من التغليف. عند تصميم واجهة برمجة التطبيقات الخاصة بك، يمكنك أيضًا التفكير في الحفاظ على تنسيق المعلمات المتسق لطرق get
و set
، حتى لو كان ذلك على حساب بعض الأداء.
بالإضافة إلى ذلك، من بين الطرق لإعادة كتابة سطر الطلب، هناك واجهتان برمجيتان، ngx.req.set_uri
و ngx.req.set_uri_args
، والتي يمكن استخدامها لإعادة كتابة URI و args. دعنا ننظر إلى تكوين NGINX التالي.
rewrite ^ /foo?a=3? break;
إذن، كيف يمكننا حلها باستخدام واجهة برمجة تطبيقات Lua المكافئة؟ الجواب هو السطرين التاليين من الكود.
ngx.req.set_uri_args("a=3")
ngx.req.set_uri("/foo")
إذا كنت قد قرأت الوثائق الرسمية، ستجد أن ngx.req.set_uri
لديها معلمة ثانية: jump
، والتي تكون "false" بشكل افتراضي. إذا قمت بتعيينها كـ "true"، فإنها تساوي تعيين علامة أمر rewrite
إلى last
بدلاً من break
في المثال أعلاه.
ومع ذلك، أنا لست من محبي تكوين العلامات لأمر rewrite
لأنها غير قابلة للقراءة وغير قابلة للتمييز وأقل بديهية وقابلية للصيانة من الكود.
رأس الطلب
كما نعلم، رؤوس طلبات HTTP تكون بتنسيق key : value
، على سبيل المثال:
Accept: text/css,*/*;q=0.1
Accept-Encoding: gzip, deflate, br
في OpenResty، يمكنك استخدام ngx.req.get_headers
لتحليل والحصول على رؤوس الطلبات، ونوع القيمة المرجعة هو table.
local h, err = ngx.req.get_headers()
if err == "truncated" then
-- يمكن اختيار تجاهل أو رفض الطلب الحالي هنا
end
for k, v in pairs(h) do
...
end
يتم افتراضيًا إرجاع أول 100 رأس. إذا تجاوز العدد 100، سيتم الإبلاغ عن خطأ truncated
، ويترك للمطور كيفية التعامل معه. قد تتساءل لماذا يتم التعامل معه بهذه الطريقة، وهو ما سأذكره لاحقًا في قسم الثغرات الأمنية.
ومع ذلك، يجب أن نلاحظ أن OpenResty لا توفر واجهة برمجة تطبيقات محددة للحصول على رأس طلب محدد، مما يعني عدم وجود شكل مثل ngx.req.header['host']
. إذا كان لديك مثل هذه الحاجة، يجب أن تعتمد على متغير NGINX $http_xxx
لتحقيق ذلك. ثم في OpenResty، يمكنك الحصول عليه عن طريق ngx.var.http_xxx
.
الآن دعنا ننظر إلى كيفية إعادة كتابة وحذف رأس الطلب. واجهات برمجة التطبيقات لكلتا العمليتين بديهية جدًا:
ngx.req.set_header("Content-Type", "text/css")
ngx.req.clear_header("Content-Type")
بالطبع، تذكر الوثائق الرسمية أيضًا طرقًا أخرى لإزالة رأس الطلب، مثل تعيين قيمة العنوان إلى nil
، إلخ. ومع ذلك، ما زلت أوصي باستخدام clear_header
للقيام بذلك بشكل موحد من أجل وضوح الكود.
جسم الطلب
أخيرًا، دعنا ننظر إلى جسم الطلب. لأسباب تتعلق بالأداء، لا يقوم OpenResty بقراءة جسم الطلب بشكل نشط إلا إذا قمت بتمكين توجيه lua_need_request_body
في nginx.conf
. بالإضافة إلى ذلك، بالنسبة لأجسام الطلبات الكبيرة، يحفظ OpenResty المحتويات في ملف مؤقت على القرص، لذلك تبدو عملية قراءة جسم الطلب بالكامل كما يلي.
ngx.req.read_body()
local data = ngx.req.get_body_data()
if not data then
local tmp_file = ngx.req.get_body_file()
-- io.open(tmp_file)
-- ...
end
يحتوي هذا الكود على عملية IO-blocking لقراءة ملف القرص. يجب عليك تعديل تكوين client_body_buffer_size
(16 KB بشكل افتراضي على أنظمة 64 بت) لتقليل عمليات الحظر؛ يمكنك أيضًا تكوين client_body_buffer_size
و client_max_body_size
ليكونوا نفس الشيء والتعامل معهم بالكامل في الذاكرة، اعتمادًا على حجم الذاكرة لديك وعدد الطلبات المتزامنة التي تتعامل معها.
بالإضافة إلى ذلك، يمكن إعادة كتابة جسم الطلب. واجهتا برمجة التطبيقات ngx.req.set_body_data
و ngx.req.set_body_file
تقبلان سلسلة نصية وملف قرص محلي كمعلمات إدخال لإعادة كتابة جسم الطلب. ومع ذلك، هذا النوع من العمليات غير شائع، ويمكنك التحقق من الوثائق للحصول على مزيد من التفاصيل.
الاستجابة
بعد معالجة الطلب، نحتاج إلى إرسال استجابة إلى العميل. مثل رسالة الطلب، تتكون رسالة الاستجابة أيضًا من عدة أجزاء: سطر الحالة، رأس الاستجابة، وجسم الاستجابة. سأقدم واجهات برمجة التطبيقات المقابلة وفقًا لهذه الأجزاء الثلاثة.
سطر الحالة
الشيء الرئيسي الذي نهتم به في سطر الحالة هو رمز الحالة. بشكل افتراضي، رمز حالة HTTP المرسل هو 200، وهو الثابت ngx.HTTP_OK
المدمج في OpenResty. ولكن في عالم الكود، دائمًا ما يكون الكود الذي يتعامل مع معظم الحالات الاستثنائية.
إذا قمت باكتشاف رسالة الطلب ووجدت أنها طلب خبيث، فأنت بحاجة إلى إنهاء الطلب:
ngx.exit(ngx.HTTP_BAD_REQUEST)
ومع ذلك، هناك ثابت خاص في رموز حالة HTTP الخاصة بـ OpenResty: ngx.OK
. في حالة ngx.exit(ngx.OK)
، يخرج الطلب من مرحلة المعالجة الحالية وينتقل إلى المرحلة التالية بدلاً من العودة مباشرة إلى العميل.
بالطبع، يمكنك أيضًا اختيار عدم الخروج ومجرد إعادة كتابة رمز الحالة باستخدام ngx.status
، كما هو مكتوب في الطريقة التالية.
ngx.status = ngx.HTTP_FORBIDDEN
يمكنك البحث عنها في الوثائق إذا كنت تريد معرفة المزيد عن ثوابت رموز الحالة.
رأس الاستجابة
بالنسبة لرأس الاستجابة، هناك طريقتان يمكنك من خلالهما تعيينه. الأولى هي الأبسط.
ngx.header.content_type = 'text/plain'
ngx.header["X-My-Header"] = 'blah blah'
ngx.header["X-My-Header"] = nil -- حذف
هنا يحمل ngx.header
معلومات رأس الاستجابة، والتي يمكن قراءتها، تعديلها، وحذفها.
الطريقة الثانية لتعيين رأس الاستجابة هي ngx_resp.add_header
، من مستودع lua-resty-core
، والتي تضيف رسالة رأس، يتم استدعاؤها بـ:
local ngx_resp = require "ngx.resp"
ngx_resp.add_header("Foo", "bar")
الفرق مع الطريقة الأولى هو أن add_header
لا تستبدل حقلًا موجودًا بنفس الاسم.
جسم الاستجابة
أخيرًا، انظر إلى جسم الاستجابة. في OpenResty، يمكنك استخدام ngx.say
و ngx.print
لإخراج جسم الاستجابة.
ngx.say('hello, world')
وظيفة واجهتي برمجة التطبيقات متطابقة، الفرق الوحيد هو أن ngx.say
لديها سطر جديد في النهاية.
لتجنب عدم كفاءة لصق السلاسل النصية، يدعم ngx.say / ngx.print
السلاسل النصية وتنسيقات المصفوفات كمعلمات.
$ resty -e 'ngx.say({"hello", ", ", "world"})'
hello, world
تتخطى هذه الطريقة لصق السلاسل النصية على مستوى Lua وتتركها لوظائف C للتعامل معها.
الخلاصة
دعنا نراجع محتوى اليوم. قدمنا واجهات برمجة تطبيقات OpenResty المرتبطة برسائل الطلب والاستجابة. كما ترى، فإن واجهة برمجة تطبيقات OpenResty أكثر مرونة وقوة من توجيهات NGINX.
وبالتالي، هل واجهة برمجة تطبيقات Lua التي توفرها OpenResty كافية لتلبية احتياجاتك عند التعامل مع طلبات HTTP؟ يرجى ترك تعليقاتك ومشاركة هذه المقالة مع زملائك وأصدقائك حتى نتمكن من التواصل والتحسين معًا.