قم بتشغيل البرامج النصية لـ PHP عبر php-fpm بدون خادم ويب. أو عميل FastCGI (تحت الغطاء)

أرحب بجميع قراء "هبر".


تنصل


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


دخول


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


من هو هذا المقال ل؟


لكل من يعمل مع الويب وبروتوكول FastCgi يعلم فقط أن هذا هو البروتوكول الذي يقوم بموجبه خادم الويب بتشغيل البرامج النصية php ، لكنه يريد دراستها بمزيد من التفاصيل والبحث تحت الغطاء.


التبرير (لماذا هذا المقال)


بشكل عام ، كما كتبت أعلاه عندما واجهنا الحاجة إلى تشغيل العديد من البرامج النصية لـ php دون مشاركة خادم ويب (تحدث تقريبًا من برنامج نصي php آخر) ، فإن أول ما يتبادر إلى الذهن هو ...


shell_exec('php \path\to\script.php') 

ولكن في بداية كل برنامج نصي ، سيتم إنشاء بيئة ، وسيتم إطلاق عملية منفصلة ، بشكل عام ، يبدو أنها مكلفة بالنسبة للموارد. تم رفض هذا التطبيق. الشيء الثاني الذي يتبادر إلى الذهن هو بالطبع php-fpm ، إنه رائع للغاية ، فهو يبدأ البيئة مرة واحدة فقط ، ويراقب الذاكرة ، ويسجل كل شيء هناك بشكل صحيح ، ويبدأ ويتوقف عن البرامج النصية ، بشكل عام كل شيء رائع ، وبالطبع كنا نود بهذه الطريقة أكثر من ذلك.


لكن هذا سيئ الحظ ، من الناحية النظرية ، عرفنا كيف يعمل ، بشكل عام (كما اتضح أنه عام جدًا) ، لكن تبين أنه من الصعب جدًا تنفيذ هذا البروتوكول في الممارسة العملية دون مشاركة خادم ويب. أظهرت قراءة المواصفات وساعتين من المحاولات غير الناجحة أن الأمر سيستغرق بعض الوقت للتنفيذ ، وهو ما لم يكن لدينا في ذلك الوقت. لا يوجد مانع لتنفيذ هذا المشروع الذي يمكن فيه وصف هذا التفاعل بكل بساطة ووضوح ، لم نتمكن من أخذ أي من المواصفات أيضًا ، من الحلول الجاهزة التي وجدناها في Python script و Pykhov lib على github ، والتي في النهاية لم ترغب في جرها إلى مشروعي (ربما هذا ليس صحيحًا ، ولكن ليس في الحقيقة ، نحن نحب جميع أنواع مكتبات الجهات الخارجية ، وحتى تلك التي لا تحظى بشعبية كبيرة ، وبالتالي لم يتم اختبارها). بشكل عام ، ونتيجة لهذه الفكرة ، رفضنا وننفذ كل هذا من خلال rabbitmq القديم الجيد.


على الرغم من أن المشكلة قد تم حلها أخيرًا ، إلا أنني قررت أن أفهم FastCgi بالتفصيل ، وبالإضافة إلى ذلك قررت أن أكتب مقالًا عنها ، والذي سيصف ببساطة وبالتفصيل كيفية الحصول على php-fpm لتشغيل برنامج نصي php بدون خادم ويب ، أو بالأحرى ، كما سيكون لخادم الويب نص برمجي مختلف ، وسأطلق عليه اسم عميل Fcgi. بشكل عام ، آمل أن يساعد هذا المقال أولئك الذين يواجهون نفس المهمة التي نقوم بها وبعد القراءة ، سيكون بإمكانهم كتابة كل ما يحتاج إليه بسرعة.


البحث الإبداعي (مسار خاطئ)


لذلك تتم الإشارة إلى المشكلة ، يجب أن ننتقل إلى الحل. بطبيعة الحال ، مثل أي مبرمج "عادي" ، لحل مشكلة لا تتم كتابتها في أي مكان ما يجب القيام به وما يجب إدخاله في وحدة التحكم ، لم أقرأ المواصفات وقم بترجمتها ، لكنني توصلت على الفور إلى حل "متألق" خاص بي. جوهرها هو على النحو التالي ، وأنا أعلم أن nginx (نستخدم nginx وحتى لا نكتب أشياء حمقاء أخرى - خادم ويب ، سأكتب nginx ، لأنه أكثر تعاطفًا) ينقل شيئًا إلى php-fpm ، كما أنه يعالج php-fpm إلى إنه يقوم بتشغيل البرنامج النصي على أساسه ، حسنًا ، يبدو أن كل شيء بسيط ، وسوف أعتبره وأتعهد له بنقل nginx وسأمر بالشيء نفسه.


سوف يساعد netcat العظيم هنا (أداة UNIX للعمل مع حركة مرور الشبكة ، والتي في رأيي يمكنها فعل أي شيء تقريبًا) لذلك قمنا بتعيين netcat للاستماع على المنفذ المحلي ، وتكوين nginx للعمل مع ملفات php من خلال المقبس (بالطبع ، المقبس على نفس المنفذ الذي يستمع إليه netcat )


الاستماع إلى 9000 منفذ


 nc -l 9000 

يمكنك التحقق من أن كل شيء على ما يرام ، يمكنك الاتصال بالعنوان 127.0.0.1:9000 من خلال متصفح ويجب أن تكون الصورة التالية



قم بتكوين nginx بحيث يتعامل مع البرامج النصية php من خلال مأخذ توصيل على المنفذ 9000 (في الإعدادات / etc / nginx / sites-available / default "، بالطبع ، قد تختلف)


 location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass 127.0.0.1:9000; } 

بعد هذه التلاعب ، سوف نتحقق مما حدث من خلال الاتصال بنص php من خلال المتصفح



يمكن ملاحظة أن متغيرات البيئة المرسلة nginx المرسلة ، وكذلك الأحرف غير القابلة للطباعة ، أي تم نقل البيانات بترميز ثنائي ، مما يعني أنه لا يمكن نسخها وإرسالها بسهولة إلى مأخذ توصيل php-fpm . إذا قمت بحفظها في ملف ، على سبيل المثال ، يتم حفظها في تشفير سداسي عشري ، سيبدو هذا ينطبق



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


أصبح من الواضح أن هذا المسار كان خاطئًا تمامًا ، يمكنك أن ترى بنفسك مدى بؤس كل هذا ، وحتى أكثر من ذلك ، كل هذه الإجراءات لن تسمح لنا بالتحكم في الاتصال ، ولن تقربنا من فهم التفاعل بين php-fpm و nginx .


ذهب كل شيء ، لا يمكن تجنب المواصفات!


الحل (هنا يبدأ كل ملح هذه المقالة بالفعل)


التدريب النظري


دعونا الآن نفكر في كيفية وجود اتصال وتبادل للبيانات بين nginx و php-fpm . هناك القليل من النظرية ، حيث يتم إجراء جميع الاتصالات كما هو واضح بالفعل من خلال مآخذ التوصيل ، وسوف ننظر بشكل خاص في اتصال عبر مأخذ توصيل TCP.


وحدة المعلومات في بروتوكول FastCgi هي سجل cgi . يرسل الخادم مثل هذه السجلات إلى التطبيق ويتلقى نفس السجلات بالضبط استجابة لذلك.


القليل من النظرية (البنية)

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


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


 //     1101111000000010010110000010011100010000 

ما هو مرئي هنا ، لدينا بعض البتات ، أي نوع من البتات ليس لدينا أي فكرة. حسنًا ، دعنا نحاول على سبيل المثال تقسيمها إلى بايت وتمثيلها في نظام عشري


 //   5  11011110 00000010 01011000 00100111 00010000 //    222 2 88 39 16 

حسنًا ، لقد فسرناها وحصلنا على بعض النتائج ، دعنا نقول أن هذه البيانات هي المسؤولة عن مقدار ما تدين به شقة معينة للكهرباء. اتضح أنه في المنزل 222 شقة رقم 2 يجب أن تدفع 88 روبل. وماذا عن رقمين ، ماذا تفعل معهم فقط لإسقاط؟ بالطبع لا! الحقيقة هي أنه لم يكن لدينا تدوين (تنسيق) من شأنه أن يخبرنا بكيفية ترجمة البيانات ، وتفسيرها بطريقتنا الخاصة ، في هذا الصدد ، لم نتلق نتائج غير مجدية فحسب ، ولكن أيضًا نتائج ضارة. نتيجة لذلك ، دفعت شقة 2 على الاطلاق وليس ما ينبغي أن يكون. (من المؤكد أن الأمثلة بعيدة المنال ولا تخدم إلا في شرح الموقف بشكل أوضح)


الآن ، لنرى كيف يجب علينا تفسير هذه البيانات بشكل صحيح ، مع وجود ترميز (تنسيق). علاوة على ذلك ، سأتصل بأسمائها بأسمائها الحقيقية ، أي التدوين = التنسيق ( التنسيقات هنا ).


 //  "Cnn" //  //C -   (char) (8 ) //n -  short (16 ) //      11011110 0000001001011000 0010011100010000 //    222 600 10000 

الآن ، كل شيء يتقارب في المنزل رقم 222 ، يجب أن تكون الشقة 600 للكهرباء 1000 روبل ، وأعتقد الآن أن أهمية التنسيق واضحة ، والآن أصبح من الواضح كيف تبدو بنية مشابهة تقريبًا. (يرجى الانتباه ، والهدف هنا هو عدم توضيح ما هي هذه الهياكل بالتفصيل ، ولكن لإعطاء فهم عام لما هو وكيف تعمل)


سيكون رمز هذا الهيكل


 struct { unsigned char houseNumber; unsigned char flatNumperA1; unsigned char flatNumperA2; unsigned char summB1; unsigned char summB2; }; // ,           // houseNumber -  // flatNumperA1 && flatNumperA2 -  // summB1 && summB2 -   

بعض النظرية (إدخالات FastCgi)

كما قلت أعلاه ، فإن وحدة المعلومات في بروتوكول FastCgi هي سجلات. يرسل الخادم السجلات إلى التطبيق ويتلقى نفس السجلات استجابة لذلك. يتكون السجل من رأس وجسم به بيانات.


هيكل الرأس:


  1. يتم تمييز إصدار البروتوكول (دائمًا 1) بمقدار 1 بايت ('C')
  2. نوع السجل. لفتح ، إغلاق الاتصال ، وما إلى ذلك. لن أفكر في كل شيء ، ثم سأدرس فقط ما هو مطلوب لمهمة محددة ، إذا كانت هناك حاجة إلى الآخرين ، أرحب بالمواصفات هنا. يشار إليه بواسطة 1 بايت ('C').
  3. يتم تعريف معرف الطلب ، وهو رقم عشوائي ، بمقدار 2 بايت ('n')
  4. طول نص السجل (البيانات) ، المشار إليه بواسطة 2 بايت ('n')
  5. طول بيانات المحاذاة والبيانات المحجوزة ، بايت واحد لكل منها (ليست هناك حاجة إلى إيلاء اهتمام خاص حتى لا يصرف الانتباه عن الشيء الرئيسي في حالتنا ، سيكون هناك دائمًا 0)

التالي هو نص السجل:


  1. يمكن أن تكون البيانات نفسها (هنا بالتحديد المتغيرات التي يتم نقلها) كبيرة جدًا (تصل إلى 65535 بايت)

فيما يلي مثال على أبسط سجل FastCgi ثنائي بالتنسيق


 struct { // unsigned char version; unsigned char type; unsigned char idA1; unsigned char idA2; unsigned char bodyLengthB1; unsigned char bodyLengthB2; unsigned char paddingLength; unsigned char reserved; //  unsigned char contentData; // 65535  unsigned char paddingData; }; 

ممارسة


النصي العميل ونقل المقبس

لنقل البيانات سوف نستخدم امتداد مأخذ توصيل php القياسي. وأول ما يجب القيام به هو تكوين php-fpm للاستماع على المنفذ على المضيف المحلي ، على سبيل المثال 9000. ويتم ذلك في معظم الحالات في الملف '/etc/php/7.3/fpm/pool.d/www.conf' ، مسار الدورة التدريبية يعتمد على إعدادات النظام الخاص بك. هناك تحتاج إلى تسجيل شيء مثل التالي (أحمل مجموعة القدم بالكامل بحيث يمكنك التنقل ، القسم الرئيسي هو الاستماع هنا)


 ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on ; a specific port; ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on ; a specific port; ; 'port' - to listen on a TCP socket to all addresses ; (IPv6 and IPv4-mapped) on a specific port; ; '/path/to/unix/socket' - to listen on a unix socket. ; Note: This value is mandatory. ;listen = /run/php/php7.3-fpm.sock listen = 127.0.0.1:9002 

بعد إعداد fpm ، تكون الخطوة التالية هي الاتصال بالمقبس


 $service_port = 9000; $address = '127.0.0.1'; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); $result = socket_connect($socket, $address, $service_port); 

بداية طلب FCGI_BEGIN_REQUEST


لفتح اتصال ، يجب أن نرسل إدخالًا بنوع FCGI_BEGIN_REQUEST = 1 سيكون عنوان الإدخال مثل هذا (لتحويل القيم الرقمية إلى سلسلة ثنائية بالتنسيق المحدد ، سيتم استخدام حزمة وظائف php ())


 socket_write($socket, pack('CCnnCx', 1, 1, 1, 8, 0)); //  - 1 //  - 1 - FCGI_BEGIN_REQUEST //id - 1 //   - 8  // - 0 

يجب أن يحتوي نص التسجيل لفتح الاتصال على دور تسجيل وعلامة تتحكم في الاتصال


 //      //struct { // unsigned char roleB1; // unsigned char roleB0; // unsigned char flags; // unsigned char reserved[5]; //}; //php  socket_write($socket, pack('nCxxxxx', 1, 0)); // - 1 -  // - 1 -    1    

لذلك ، تم إرسال سجل فتح الاتصال بنجاح ، وسيقبل php-fpm ذلك وسيظل يتوقع منا سجلًا آخر نحتاج فيه إلى نقل البيانات لنشر البيئة وتشغيل البرنامج النصي.


تمرير معلمات البيئة FCGI_PARAMS


في هذا السجل ، سنمرر جميع المعلمات اللازمة لنشر البيئة ، وكذلك اسم البرنامج النصي الذي سنحتاج إلى تشغيله.


الحد الأدنى من إعدادات البيئة المطلوبة


 $url = '/path/to/script.php' $env = [ 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $url, ]; 

أول شيء نحتاج إلى القيام به هنا هو إعداد المتغيرات الضرورية ، أي الاسم => أزواج القيمة التي سننقلها إلى التطبيق.


سيكون هيكل قيمة اسم أزواج مثل هذا


 //          128  typedef struct { unsigned char nameLength; unsigned char valueLength; unsigned char nameData unsigned char valueData; }; //    1  

هناك 1 بايت أولاً - الاسم طويل ، ثم 1 بايت هي القيمة


 //         128  typedef struct { unsigned char nameLengthA1; unsigned char nameLengthA2; unsigned char nameLengthA3; unsigned char nameLengthA4; unsigned char valueLengthB1; unsigned char valueLengthB2; unsigned char valueLengthB3; unsigned char valueLengthB4; unsigned char nameData unsigned char valueData; }; //    4  

في حالتنا ، يكون كل من الاسم والمعاني قصيرًا ويناسب الخيار الأول ، لذلك سننظر فيه.


تشفير المتغيرات لدينا وفقا للتنسيق


 $keyValueFcgiString = ''; foreach ($env as $key => $value) { //        //  128         $keyLen = strlen($key); $lenKeyChar = $keyLen < 128 ? chr($keyLen) : pack('N', $keyLen); $valLen = strlen($value); $valLenChar = $valLen < 128 ? chr($valLen) : pack('N', $valLen); $keyValueFcgiString .= $lenKeyChar . $valLenChar . $key . $value; } 

هنا يتم ترميز القيم الأقل من 128 بت بواسطة الدالة chr ($ keyLen) ، أكثر من pack ('N' ، $ valLen) ، حيث تشير N إلى 4 بايت. ثم يتم تعليق كل هذا معًا في سطر واحد وفقًا لتنسيق الهيكل. جسم التسجيل جاهز.


في رأس السجل ، ننقل كل شيء كما في السجل السابق ، باستثناء النوع (سيكون FCGI_PARAMS = 4) وطول البيانات (سيكون مساويًا لطول أزواج قيمة name => أو طول السلسلة $ keyValueFcgiString التي قمنا بتكوينها مسبقًا).


 //  socket_write($socket, pack('CCnnCx', 1, 4, 1, strlen($keyValueFcgiString), 0)); // body socket_write($socket, $keyValueFcgiString); //             //  body socket_write($socket, pack('CCnnCx', 1, 4, 1, 0, 0)); 

الحصول على استجابة من FCGI_PARAMS


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


نحصل على العنوان ، وهو دائمًا 8 بايت (سوف نستلم البيانات بالبايت)


 $buf = ''; $arrData = []; $len = 8; while ($len) { socket_recv($socket, $buf, 1, MSG_WAITALL); //   1       $arrData[] = $buf; $len--; } //      'CCnnCx' $protocol = unpack('C', $arrData[0]); $type = unpack('C', $arrData[1]); $id = unpack('n', $arrData[2] . $arrData[3]); $dataLen = unpack('n', $arrData[4] . $arrData[5])[1]; //   ,        (unpack  ,    ) $foo = unpack('C', $arrData[6]); var_dump($dataLen); //      

الآن ، وفقًا لطول جسم الاستجابة المستلمة ، سنفعل قراءة أخرى من المقبس


 $buf2 = ''; $result = []; while ($dataLen) { socket_recv($socket, $buf2, 1, MSG_WAITALL); $result[] = $buf2; $dataLen--; } var_dump(implode('', $result)); //       socket_close($socket); 

الصيحة انها عملت! وأخيرا هذا!
ماذا لدينا في الجواب ، على سبيل المثال في هذا الملف


 $url = '/path/to/script.php' //     

سوف نكتب


 <?php echo "My fcgi script"; 

ثم في الجواب نحصل عليه نتيجة لذلك


صورة


النتائج


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


النسخة الكاملة من البرنامج النصي
 <?php $url = '/path/to/script.php'; $env = [ 'REQUEST_METHOD' => 'GET', 'SCRIPT_FILENAME' => $url, ]; $service_port = 9000; $address = '127.0.0.1'; $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); $result = socket_connect($socket, $address, $service_port); //  //     php-fpm //    ,   (    ), id ,   ,     socket_write($socket, pack('CCnnCx', 1, 1, 1, 8, 0)); //     // ,     socket_write($socket, pack('nCxxxxx', 1, 0)); $keyValueFcgiString = ''; foreach ($env as $key => $value) { //        //  128         $keyLen = strlen($key); $lenKeyChar = $keyLen < 128 ? chr($keyLen) : pack('N', $keyLen); $valLen = strlen($value); $valLenChar = $valLen < 128 ? chr($valLen) : pack('N', $valLen); $keyValueFcgiString .= $lenKeyChar . $valLenChar . $key . $value; } // ,      php-fpm           //      //1- ( ), 4-  (,    - FCGI_PARAMS), id  ( ),    (   -),     socket_write($socket, pack('CCnnCx', 1, 4, 1, strlen($keyValueFcgiString), 0)); //      socket_write($socket, $keyValueFcgiString); //  socket_write($socket, pack('CCnnCx', 1, 4, 1, 0, 0)); $buf = ''; $arrData = []; $len = 8; while ($len) { socket_recv($socket, $buf, 1, MSG_WAITALL); //   1       $arrData[] = $buf; $len--; } //      'CCnnCx' $protocol = unpack('C', $arrData[0]); $type = unpack('C', $arrData[1]); $id = unpack('n', $arrData[2] . $arrData[3]); $dataLen = unpack('n', $arrData[4] . $arrData[5])[1]; //   ,        (unpack  ,    ) $foo = unpack('C', $arrData[6]); $buf2 = ''; $result = []; while ($dataLen) { socket_recv($socket, $buf2, 1, MSG_WAITALL); $result[] = $buf2; $dataLen--; } var_dump(implode('', $result)); //       socket_close($socket); 

Source: https://habr.com/ru/post/ar472190/


All Articles