Jalankan skrip PHP melalui php-fpm tanpa server web. Atau klien FastCGI Anda (di bawah tenda)

Saya menyambut semua pembaca "Habr".


Penafian


Artikel itu ternyata cukup panjang dan bagi mereka yang tidak ingin membaca latar belakang, tetapi ingin langsung ke intinya, saya meminta Anda langsung ke bab "Solusi".


Entri


Dalam artikel ini, saya ingin berbicara tentang menyelesaikan masalah yang agak tidak standar yang harus saya hadapi selama proses kerja. Yaitu, kami perlu menjalankan banyak skrip php dalam satu lingkaran. Saya tidak akan membahas alasan dan kontroversi solusi arsitektural dalam artikel ini, karena pada kenyataannya, itu sama sekali bukan tentang itu, itu hanya tugas, itu harus diselesaikan dan solusinya tampak cukup menarik untuk saya bagikan kepada Anda, terutama karena saya sama sekali tidak menemukan mana di Internet (ya, tentu saja, kecuali untuk spesifikasi resmi). Speck, tentu saja, baik, dan tentu saja semuanya ada di dalamnya, tetapi saya pikir Anda akan setuju bahwa jika Anda tidak terlalu akrab dengan topik, dan bahkan terbatas dalam waktu, maka memahami mereka masih menyenangkan.


Untuk siapa artikel ini?


Untuk semua orang yang bekerja dengan web dan protokol FastCgi hanya tahu bahwa ini adalah protokol yang digunakan server web untuk menjalankan skrip php, tetapi ingin mempelajarinya secara lebih rinci dan mencari di bawah tenda.


Pembenaran (mengapa artikel ini)


Secara umum, seperti yang saya tulis di atas ketika kita dihadapkan dengan kebutuhan untuk menjalankan banyak skrip php tanpa partisipasi server web (kira-kira berbicara dari skrip php lain), hal pertama yang terlintas dalam pikiran adalah ...


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

Tetapi pada awal setiap skrip, sebuah lingkungan akan dibuat, proses terpisah akan diluncurkan, secara umum, tampaknya entah bagaimana mahal untuk sumber daya. Implementasi ini ditolak. Hal kedua yang terlintas dalam pikiran adalah tentu saja php-fpm , sangat keren, hanya memulai lingkungan sekali, memonitor memori, mencatat semuanya dengan benar, memulai dan menghentikan skrip, secara umum semuanya tidak keren, dan tentu saja kami menyukai cara ini lebih lanjut.


Tapi itu nasib buruk, secara teori kami tahu cara kerjanya, secara umum (ternyata sangat umum), tetapi ternyata cukup sulit untuk menerapkan protokol ini dalam praktik tanpa partisipasi server web. Membaca spesifikasi dan beberapa jam upaya yang gagal menunjukkan bahwa akan membutuhkan waktu untuk mengimplementasikan, yang tidak kami miliki saat itu. Tidak ada mana untuk pelaksanaan usaha ini di mana interaksi ini dapat secara sederhana dan jelas dijelaskan, kami juga tidak dapat mengambil spesifikasi, dari solusi siap pakai kami menemukan skrip Python dan lib Pykhov di github, yang pada akhirnya tidak ingin diseret ke proyek saya (mungkin itu tidak benar, tetapi tidak benar-benar, kami menyukai semua jenis perpustakaan pihak ketiga dan bahkan yang tidak terlalu populer, dan karenanya tidak diuji). Secara umum, sebagai hasil dari ide ini, kami menolak dan mengimplementasikan semua ini melalui rabbitmq tua yang baik.


Meskipun masalah akhirnya diselesaikan, saya masih memutuskan untuk memahami FastCgi secara rinci, dan di samping itu saya memutuskan untuk menulis artikel tentang itu, yang akan secara sederhana dan rinci menjelaskan cara mendapatkan php-fpm untuk menjalankan skrip php tanpa server web, atau lebih tepatnya, sebagai server web akan memiliki skrip yang berbeda, maka saya akan menyebutnya klien Fcgi. Secara umum, saya berharap artikel ini akan membantu mereka yang dihadapkan dengan tugas yang sama seperti yang kita lakukan dan setelah membacanya akan dapat dengan cepat menulis semua yang dia butuhkan.


Pencarian kreatif (jalur salah)


Jadi masalahnya ditunjukkan, kita harus melanjutkan ke solusi. Secara alami, seperti halnya programmer "normal", untuk memecahkan masalah yang tidak ditulis di mana pun apa yang harus dilakukan dan apa yang harus dimasukkan ke dalam konsol, saya tidak membaca dan menerjemahkan spesifikasinya, tetapi segera muncul dengan solusi "brilian" saya sendiri. Esensinya adalah sebagai berikut, saya tahu bahwa nginx (kami menggunakan nginx dan agar tidak menulis hal-hal bodoh lebih lanjut - server web, saya akan menulis nginx, karena lebih simpatik) mentransfer sesuatu ke php-fpm , ia juga memproses php-fpm untuk itu menjalankan skrip berdasarkan itu, well, semuanya tampak sederhana, saya akan mengambilnya dan berjanji itu yang mentransmisikan nginx dan saya akan melewati hal yang sama.


Netcat hebat akan membantu di sini (utilitas UNIX untuk bekerja dengan lalu lintas jaringan, yang menurut saya dapat melakukan hampir semua hal). Jadi kami mengatur netcat untuk mendengarkan pada port lokal, dan mengkonfigurasi nginx agar berfungsi dengan file php melalui socket (tentu saja, socket pada port yang sama dengan yang netcat dengarkan)


mendengarkan 9000 port


 nc -l 9000 

Anda dapat memeriksa bahwa semuanya OK, Anda dapat menghubungi alamat 127.0.0.1:9000 melalui browser dan gambar berikut harus



konfigurasikan nginx sehingga memproses skrip php melalui soket pada port 9000 (dalam pengaturan '/ etc / nginx / sites-available / default', tentu saja, mereka mungkin berbeda)


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

Setelah manipulasi ini, kami akan memeriksa apa yang terjadi dengan menghubungi skrip php melalui browser



Dapat dilihat bahwa nginx mengirimkan variabel lingkungan, serta karakter yang tidak dapat dicetak, yaitu, data ditransmisikan dalam pengkodean biner, yang berarti tidak dapat dengan mudah disalin dan dikirim ke soket php-fpm . Jika Anda menyimpannya ke file, misalnya, maka mereka disimpan dalam pengkodean heksadesimal, itu akan terlihat seperti ini berlaku



Tapi ini juga tidak memberi kita banyak, mungkin murni secara teoritis mereka dapat dikonversi ke pengkodean biner, entah bagaimana (saya bahkan tidak bisa membayangkan bagaimana) untuk mengirim mereka ke soket fpm, dan bahkan ada kemungkinan bahwa seluruh motor ini entah bagaimana akan bekerja, dan bahkan memulai semacam sebuah skrip, tapi entah bagaimana semuanya jelek dan canggung.


Menjadi jelas bahwa jalan ini benar-benar salah, Anda dapat melihat sendiri betapa menyedihkannya semua ini terlihat, dan terlebih lagi, semua tindakan ini tidak akan memungkinkan kita untuk mengontrol koneksi, juga tidak akan membawa kita lebih dekat untuk memahami interaksi antara php-fpm dan nginx .


Semuanya hilang, spesifikasinya tidak bisa dihindari!


Solusi (di sini semua garam dari artikel ini sebenarnya dimulai)


Pelatihan teori


Sekarang mari kita pertimbangkan bagaimana semua yang sama ada koneksi dan pertukaran data antara nginx dan php-fpm . Sedikit teori, semua komunikasi terjadi seperti yang sudah jelas melalui soket, kami selanjutnya akan mempertimbangkan koneksi melalui soket TCP.


Unit informasi dalam protokol FastCgi adalah catatan cgi . Server mengirimkan catatan tersebut ke aplikasi dan menerima catatan yang sama persis sebagai tanggapan.


Sedikit teori (struktur)

Selanjutnya, perhatikan struktur catatan. Untuk memahami apa yang terdiri dari catatan, Anda perlu memahami seperti apa struktur C dan memahami sebutannya. Bagi mereka yang tidak tahu lebih jauh, ini akan dijelaskan secara singkat (tetapi cukup untuk pemahaman). Saya akan mencoba menggambarkannya sesederhana mungkin, tidak ada gunanya untuk merinci, dan saya takut bahwa saya akan bingung dalam detailnya, hal utama adalah memiliki pemahaman yang sama.


Struktur hanyalah kumpulan byte, dan notasi untuknya memungkinkannya ditafsirkan. Artinya, Anda hanya memiliki urutan nol dan satu, dan beberapa data dienkripsi dalam urutan ini, tetapi sejauh ini Anda tidak memiliki anotasi untuk urutan ini, maka data ini tidak mewakili nilai apa pun bagi Anda, karena Anda tidak dapat menafsirkannya.


 //     1101111000000010010110000010011100010000 

Apa yang terlihat di sini, kita memiliki beberapa bit, jenis bit apa yang kita tidak tahu. Sebagai contoh, mari kita coba untuk membaginya menjadi byte dan mewakili dalam sistem desimal


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

Nah, kami menafsirkannya dan mendapatkan beberapa hasil, katakanlah bahwa data ini bertanggung jawab atas berapa banyak apartemen tertentu berutang untuk listrik. Ternyata di rumah 222 apartemen nomor 2 harus membayar 88 rubel. Dan apa lagi untuk dua digit, apa yang harus dilakukan dengan hanya menjatuhkannya? Tentu tidak! Faktanya adalah bahwa kita tidak memiliki notasi (format) yang akan memberi tahu kita bagaimana menafsirkan data, dan menafsirkannya dengan cara kita sendiri, dalam hal ini kita menerima tidak hanya hasil yang tidak berguna, tetapi juga berbahaya. Akibatnya, apartemen 2 dibayar sama sekali tidak seperti yang seharusnya. (Contoh-contohnya jelas dibuat-buat dan hanya berfungsi untuk menjelaskan situasi dengan lebih jelas)


Sekarang mari kita lihat bagaimana kita harus menafsirkan data ini dengan benar, memiliki notasi (format). Selanjutnya saya akan memanggil sekop sekop, yaitu notation = format (di sini format ).


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

Sekarang semuanya bertemu di rumah No. 222, apartemen 600 untuk listrik harus 1.000 rubel. Saya pikir sekarang pentingnya formatnya jelas, dan sekarang sudah jelas bagaimana kira-kira struktur yang mirip. (harap perhatikan, di sini tujuannya bukan untuk menjelaskan secara terperinci apa struktur ini, tetapi untuk memberikan pemahaman umum tentang apa itu dan bagaimana kerjanya)


Simbol dari struktur ini adalah


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

Beberapa teori lagi (entri FastCgi)

Seperti yang saya katakan di atas, unit informasi dalam protokol FastCgi adalah catatan. Server mengirim catatan ke aplikasi dan menerima catatan yang sama sebagai tanggapan. Catatan terdiri dari header dan isi dengan data.


Struktur header:


  1. versi protokol (selalu 1) dilambangkan dengan 1 byte ('C')
  2. jenis rekaman. Untuk membuka, menutup koneksi, dll. Saya tidak akan mempertimbangkan segalanya, maka saya hanya akan mempertimbangkan apa yang diperlukan untuk tugas tertentu, jika orang lain diperlukan, selamat datang spesifikasinya di sini. Itu ditunjukkan oleh 1 byte ('C').
  3. ID Permintaan, nomor arbitrer, ditunjukkan oleh 2 byte ('n')
  4. panjang badan rekaman (data), ditunjukkan oleh 2 byte ('n')
  5. panjang data penyelarasan dan data yang dicadangkan, masing-masing satu byte (tidak perlu memberikan perhatian khusus agar tidak terganggu dari yang utama dalam kasus kami akan selalu ada 0)

Berikutnya adalah badan catatan:


  1. data itu sendiri (ini dia justru variabel yang ditransfer) bisa cukup besar (hingga 65535 byte)

Berikut adalah contoh dari catatan biner FastCgi paling sederhana dengan format


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

Berlatih


Klien skrip dan soket pengirim

Untuk transfer data kami akan menggunakan ekstensi soket php standar. Dan hal pertama yang perlu dilakukan adalah mengkonfigurasi php-fpm untuk mendengarkan pada port di host lokal, misalnya 9000. Hal ini dilakukan dalam kebanyakan kasus di file '/etc/php/7.3/fpm/pool.d/www.conf', jalur tentu saja Tergantung pada pengaturan sistem Anda. Di sana Anda perlu mendaftarkan sesuatu seperti yang berikut ini (saya membawa seluruh alas kaki sehingga Anda dapat menavigasi, bagian utama adalah dengarkan di sini)


 ; 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 

Setelah mengatur fpm, langkah selanjutnya adalah menghubungkan ke soket


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

Mulai dari permintaan FCGI_BEGIN_REQUEST


Untuk membuka koneksi, kita harus mengirim entri dengan tipe FCGI_BEGIN_REQUEST = 1 Judul entri akan seperti ini (untuk mengonversi nilai numerik ke string biner dengan format yang ditentukan, paket fungsi php () akan digunakan)


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

Badan rekaman untuk membuka koneksi harus mengandung peran perekaman dan bendera yang mengendalikan koneksi


 //      //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    

Jadi, catatan untuk membuka koneksi berhasil dikirim, php-fpm akan menerimanya dan akan terus mengharapkan dari kami catatan lebih lanjut di mana kita perlu mentransfer data untuk menyebarkan lingkungan dan menjalankan skrip.


Melewati Parameter Lingkungan FCGI_PARAMS


Dalam catatan ini, kami akan meneruskan semua parameter yang diperlukan untuk menyebarkan lingkungan, serta nama skrip yang akan kami jalankan.


Pengaturan lingkungan minimum yang disyaratkan


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

Hal pertama yang perlu kita lakukan di sini adalah menyiapkan variabel-variabel yang diperlukan, yaitu pasangan nilai name => yang akan kita berikan ke aplikasi.


Struktur nilai nama pasangan akan seperti itu


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

Ada 1 byte pertama - namanya panjang, lalu 1 byte adalah nilainya


 //         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  

Dalam kasus kami, baik nama dan artinya pendek dan sesuai dengan opsi pertama, jadi kami akan mempertimbangkannya.


Encode variabel kami sesuai dengan format


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

Di sini nilai yang kurang dari 128 bit dikodekan oleh fungsi chr ($ keyLen) , lebih dari paket ('N', $ valLen) , di mana 'N' adalah singkatan dari 4 byte. Dan kemudian semua ini terjebak bersama dalam satu baris sesuai dengan format struktur. Tubuh rekaman sudah siap.


Di tajuk catatan, kami mentransfer semuanya sama seperti pada catatan sebelumnya, kecuali untuk tipe (itu akan menjadi FCGI_PARAMS = 4) dan panjang data (itu akan sama dengan panjang pasangan nama => nilai, atau panjang string $ keyValueFcgiString yang kami buat sebelumnya).


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

Dapatkan tanggapan dari FCGI_PARAMS


Sebenarnya, setelah semua yang sebelumnya telah dilakukan, dan semua yang diharapkan telah dikirim ke aplikasi, itu mulai berfungsi dan kita hanya dapat mengambil hasil pekerjaan ini dari soket.
Ingat bahwa sebagai respons kita mendapatkan catatan yang sama dan kita juga perlu menafsirkannya.


Kami mendapatkan tajuk, selalu 8 byte (kami akan menerima data dengan byte)


 $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); //      

Sekarang, sesuai dengan panjang tubuh respons yang diterima, kami akan melakukan pembacaan lain dari soket


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

Hore itu berhasil! Akhirnya itu!
Apa yang kita miliki di jawabannya, kalau misalnya di file ini


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

kami akan menulis


 <?php echo "My fcgi script"; 

maka dalam jawaban yang kita dapatkan sebagai hasilnya


gambar


Ringkasan


Saya tidak akan menulis banyak di sini, jadi artikel yang panjang ternyata. Saya harap dia membantu seseorang. Dan saya akan memberikan skrip final itu sendiri, ternyata cukup kecil. Tentu saja, dia bisa melakukan sedikit hal dalam formulir ini, dan dia tidak memiliki penanganan kesalahan dan semua ini, tetapi dia tidak membutuhkannya, dia membutuhkannya sebagai contoh untuk menunjukkan dasar-dasarnya.


Versi lengkap skrip
 <?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/id472190/


All Articles