جوهر OpenResty: cosocket

API7.ai

October 28, 2022

OpenResty (NGINX + Lua)

اليوم سنتعلم عن التقنية الأساسية في OpenResty: cosocket.

لقد ذكرناها عدة مرات في المقالات السابقة، cosocket هي أساس العديد من مكتبات lua-resty-* غير المتزامنة. بدون cosocket، لا يمكن للمطورين استخدام Lua للاتصال بخدمات الويب الخارجية بسرعة.

في الإصدارات السابقة من OpenResty، إذا كنت تريد التفاعل مع خدمات مثل Redis وmemcached، كنت بحاجة إلى استخدام وحدات C مثل redis2-nginx-module، redis-nginx-module وmemc-nginx-module. هذه الوحدات لا تزال متاحة في توزيعة OpenResty.

ومع ذلك، مع إضافة ميزة cosocket، تم استبدال وحدات C بـ lua-resty-redis وlua-resty-memcached. لم يعد أحد يستخدم وحدات C للاتصال بالخدمات الخارجية.

ما هو cosocket؟

إذن ما هو cosocket بالضبط؟ cosocket هو مصطلح خاص في OpenResty. يتكون اسم cosocket من coroutine + socket.

cosocket يتطلب دعم ميزة التزامن في Lua وآلية الأحداث الأساسية في NGINX، مما يجتمع لتمكين I/O الشبكة غير المتزامن. cosocket يدعم أيضًا TCP، UDP، وUnix Domain Socket.

التنفيذ الداخلي يبدو كما في الرسم التالي إذا قمنا باستدعاء دالة مرتبطة بـ cosocket في OpenResty.

استدعاء دالة مرتبطة بـ cosocket

لقد استخدمت هذا الرسم أيضًا في المقال السابق عن مبادئ OpenResty والمفاهيم الأساسية. كما ترى من الرسم، لكل عملية شبكة يتم تشغيلها بواسطة نص Lua الخاص بالمستخدم، سيكون هناك yield وresume للـ coroutine.

عند مواجهة I/O الشبكة، يتم تسجيل حدث الشبكة في قائمة المستمعين NGINX ونقل التحكم (yield) إلى NGINX. عندما يصل حدث NGINX إلى شرط التشغيل، يتم إيقاظ الـ coroutine لمواصلة المعالجة (resume).

العملية المذكورة أعلاه هي المخطط الذي تستخدمه OpenResty لتغليف عمليات الاتصال، الإرسال، الاستقبال، إلخ. التي تشكل واجهات برمجة تطبيقات cosocket التي نراها اليوم. سأستخدم واجهة برمجة التطبيقات الخاصة بـ TCP كمثال. واجهة التحكم في UDP وUnix Domain sockets هي نفسها الخاصة بـ TCP.

مقدمة عن واجهات برمجة تطبيقات cosocket والأوامر

يمكن تقسيم واجهات برمجة تطبيقات cosocket المتعلقة بـ TCP إلى الفئات التالية.

  • إنشاء الكائنات: ngx.socket.tcp.
  • تعيين المهلة: tcpsock:settimeout وtcpsock:settimeouts.
  • إنشاء الاتصال: tcpsock:connect.
  • إرسال البيانات: tcpsock:send.
  • استقبال البيانات: tcpsock:receive، tcpsock:receiveany، وtcpsock:receiveuntil.
  • تجميع الاتصالات: tcpsock:setkeepalive.
  • إغلاق الاتصال: tcpsock:close.

يجب أن ننتبه أيضًا إلى السياقات التي يمكن استخدام هذه الواجهات فيها.

rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_

نقطة أخرى أريد التأكيد عليها هي وجود العديد من البيئات غير المتاحة بسبب قيود مختلفة في نواة NGINX. على سبيل المثال، واجهة برمجة تطبيقات cosocket غير متاحة في set_by_lua*، log_by_lua*، header_filter_by_lua*، وbody_filter_by_lua*. وهي غير متاحة في init_by_lua* وinit_worker_by_lua* حاليًا، ولكن نواة NGINX لا تقيد هاتين المرحلتين، ويمكن إضافة الدعم لهما لاحقًا.

هناك ثمانية أوامر NGINX تبدأ بـ lua_socket_ مرتبطة بهذه الواجهات. دعونا نلقي نظرة سريعة.

  • lua_socket_connect_timeout: مهلة الاتصال، الافتراضي 60 ثانية.
  • lua_socket_send_timeout: مهلة الإرسال، الافتراضي 60 ثانية.
  • lua_socket_send_lowat: عتبة الإرسال (منخفضة)، الافتراضي 0.
  • lua_socket_read_timeout: مهلة القراءة، الافتراضي 60 ثانية.
  • lua_socket_buffer_size: حجم المخزن المؤقت لقراءة البيانات، الافتراضي 4k/8k.
  • lua_socket_pool_size: حجم تجميع الاتصالات، الافتراضي 30.
  • lua_socket_keepalive_timeout: وقت الخمول لكائن cosocket في تجميع الاتصالات، الافتراضي 60 ثانية.
  • lua_socket_log_errors: ما إذا كان يجب تسجيل أخطاء cosocket عند حدوثها، الافتراضي on.

هنا يمكنك أيضًا أن ترى أن بعض الأوامر لها نفس وظيفة الواجهة، مثل تعيين المهلة وحجم تجميع الاتصالات. ومع ذلك، إذا كان هناك تعارض بين الاثنين، فإن الواجهة لها أولوية أعلى من الأوامر وستتجاوز القيمة التي تم تعيينها بواسطة الأمر. لذلك، بشكل عام، نوصي باستخدام الواجهات لإجراء الإعدادات، وهو أيضًا أكثر مرونة.

بعد ذلك، دعونا نلقي نظرة على مثال ملموس لفهم كيفية استخدام واجهات برمجة تطبيقات cosocket هذه. وظيفة الكود التالي بسيطة، وهي إرسال طلب TCP إلى موقع ويب وطباعة المحتوى الذي تم إرجاعه:

$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- مهلة ثانية واحدة
local ok, err = sock:connect("api7.ai", 80)
if not ok then
    ngx.say("فشل الاتصال: ", err)
    return
end
local req_data = "GET / HTTP/1.1\r\nHost: api7.ai\r\n\r\n"
local bytes, err = sock:send(req_data)
if err then
    ngx.say("فشل الإرسال: ", err)
    return
end
local data, err, partial = sock:receive()
if err then
    ngx.say("فشل الاستقبال: ", err)
    return
end
sock:close()
ngx.say("الاستجابة هي: ", data)'

دعونا نحلل هذا الكود بالتفصيل.

  • أولاً، قم بإنشاء كائن TCP cosocket باسم sock باستخدام ngx.socket.tcp().
  • ثم، استخدم settimeout() لتعيين المهلة إلى ثانية واحدة. لاحظ أن المهلة هنا لا تفرق بين الاتصال والاستقبال؛ إنها إعداد موحد.
  • بعد ذلك، استخدم واجهة برمجة التطبيقات connect() للاتصال بالمنفذ 80 للموقع المحدد والخروج إذا فشل.
  • إذا نجح الاتصال، استخدم send() لإرسال البيانات المبنية والخروج إذا فشل.
  • إذا تم إرسال البيانات بنجاح، استخدم receive() لاستقبال البيانات من الموقع. هنا، المعلمة الافتراضية لـ receive() هي *l، مما يعني أن يتم إرجاع السطر الأول من البيانات فقط. إذا تم تعيين المعلمة إلى *a، فإنه يستقبل البيانات حتى يتم إغلاق الاتصال.
  • أخيرًا، استدعِ close() لإغلاق اتصال المقبس بشكل نشط.

كما ترى، استخدام واجهات برمجة تطبيقات cosocket للقيام باتصالات الشبكة بسيط في بضع خطوات فقط. دعونا نجري بعض التعديلات لاستكشاف المثال بشكل أعمق.

1. تعيين وقت المهلة لكل من الإجراءات الثلاثة: اتصال المقبس، الإرسال، والقراءة.

استخدمنا settimeout() لتعيين وقت المهلة إلى قيمة واحدة. لتعيين وقت المهلة بشكل منفصل، تحتاج إلى استخدام دالة settimeouts()، مثل التالي.

sock:settimeouts(1000, 2000, 3000)

معلمات settimeouts هي بالمللي ثانية. يشير هذا السطر من الكود إلى مهلة اتصال بـ 1 ثانية، مهلة إرسال بـ 2 ثانية، ومهلة قراءة بـ 3 ثوانٍ.

في OpenResty ومكتبات lua-resty، معظم معلمات واجهات برمجة التطبيقات المتعلقة بالوقت هي بالمللي ثانية. ولكن هناك استثناءات تحتاج إلى الانتباه إليها عند استدعائها.

2. استقبال محتويات الحجم المحدد.

كما قلت للتو، يمكن لواجهة برمجة التطبيقات receive() استقبال سطر واحد من البيانات أو استقبال البيانات بشكل مستمر. ومع ذلك، إذا كنت تريد فقط استقبال بيانات بحجم 10K، كيف يجب عليك إعدادها؟

هنا يأتي دور receiveany(). تم تصميمها لتلبية هذه الحاجة، لذا انظر إلى السطر التالي من الكود.

local data, err, partial = sock:receiveany(10240)

هذا الكود يعني أنه سيتم استقبال ما يصل إلى 10K من البيانات فقط.

بالطبع، هناك متطلب عام آخر لـ receive() وهو الاستمرار في جلب البيانات حتى يتم مواجهة سلسلة محددة.

تم تصميم receiveuntil() لحل هذا النوع من المشاكل. بدلاً من إرجاع سلسلة مثل receive() وreceiveany()، فإنه سيعيد مكررًا. بهذه الطريقة، يمكنك استدعاؤه في حلقة لقراءة البيانات المطابقة في أجزاء وإرجاع nil عند الانتهاء من القراءة. هنا مثال.

local reader = sock:receiveuntil("\r\n")
     while true do
         local data, err, partial = reader(4)
         if not data then
             if err then
                 ngx.say("فشل قراءة تدفق البيانات: ", err)
                 break
             end
             ngx.say("تمت القراءة")
             break
         end
         ngx.say("قراءة جزء: [", data, "]")
     end

يعيد receiveuntil البيانات قبل \r\n ويقرأ أربعة بايتات منها في كل مرة من خلال المكرر.

3. بدلاً من إغلاق المقبس مباشرة، ضعه في تجميع الاتصالات.

كما نعلم، بدون تجميع الاتصالات، يجب إنشاء اتصال جديد، مما يتسبب في إنشاء كائنات cosocket في كل مرة يأتي فيها طلب وتدميرها بشكل متكرر، مما يؤدي إلى فقدان الأداء غير الضروري.

لتجنب هذه المشكلة، بعد الانتهاء من استخدام cosocket، يمكنك استدعاء setkeepalive() لوضعه في تجميع الاتصالات، مثل التالي.

local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
    ngx.say("فشل تعيين القابلية لإعادة الاستخدام: ", err)
end

يحدد هذا الكود وقت الخمول للاتصال بـ 2 ثانية وحجم تجميع الاتصالات بـ 100، بحيث عند استدعاء دالة connect()، سيتم جلب كائن cosocket من تجميع الاتصالات أولاً.

ومع ذلك، هناك شيئان نحتاج إلى الانتباه إليهما عند استخدام تجميع الاتصالات.

  • أولاً، لا يمكنك وضع اتصال خاطئ في تجميع الاتصالات. وإلا، في المرة القادمة التي تستخدمه فيها، ستفشل في إرسال واستقبال البيانات. إنه أحد الأسباب التي تجعلنا بحاجة إلى تحديد ما إذا كان كل استدعاء لواجهة برمجة التطبيقات ناجحًا أم لا.
  • ثانيًا، نحتاج إلى معرفة عدد الاتصالات. تجميع الاتصالات هو على مستوى Worker، وكل Worker لديه تجميع اتصالات خاص به. إذا كان لديك 10 Workers وحجم تجميع الاتصالات مضبوط على 30، فهذا يعني 300 اتصال لخدمة الخلفية.

الخلاصة

للتلخيص، تعلمنا المفاهيم الأساسية، الأوامر ذات الصلة، وواجهات برمجة تطبيقات cosocket. مثال عملي جعلنا نتعرف على كيفية استخدام واجهات برمجة التطبيقات المتعلقة بـ TCP. استخدام UDP وUnix Domain Socket مشابه لاستخدام TCP. يمكنك التعامل بسهولة مع كل هذه الأسئلة بعد فهم ما تعلمناه اليوم.

نعلم أن cosocket سهل الاستخدام نسبيًا، ويمكننا الاتصال بالعديد من الخدمات الخارجية باستخدامه بشكل جيد.

أخيرًا، يمكننا التفكير في سؤالين.

السؤال الأول، في مثال اليوم، tcpsock:send يرسل سلسلة؛ ماذا لو كنا بحاجة إلى إرسال جدول مكون من سلاسل؟

السؤال الثاني، كما ترى، لا يمكن استخدام cosocket في العديد من المراحل، فهل يمكنك التفكير في بعض الطرق لتجاوز ذلك؟

لا تتردد في ترك تعليق ومشاركته معي. مرحبًا بمشاركة هذه المقالة مع زملائك وأصدقائك حتى نتمكن من التواصل والتقدم معًا.