
Pengembang backend aplikasi seluler Skyeng, Sergey Zhuk, terus menulis buku-buku bagus. Kali ini ia merilis buku teks dalam bahasa Rusia untuk pemirsa yang menguasai PHP. Saya meminta Sergey untuk membagikan bab swadaya yang berguna dari bukunya, dan untuk memberi pembaca Habra kode diskon. Di bawah ini keduanya.
Pertama, mari kita beri tahu Anda apa yang kami berhenti di bab-bab sebelumnya.Kami telah menulis server HTTP sederhana kami dalam PHP. Kami memiliki file index.php
utama - skrip yang memulai server. Berikut adalah kode level tertinggi: kami membuat loop acara, mengkonfigurasi perilaku server HTTP dan memulai loop:
use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; $loop = React\EventLoop\Factory::create(); $router = new Router(); $router->load('routes.php'); $server = new Server( function (ServerRequestInterface $request) use ($router) { return $router($request); } ); $socket = new React\Socket\Server(8080, $loop); $server->listen($socket); $loop->run();
Untuk merutekan permintaan, server menggunakan router:
Rute dari file routes.php
dimuat ke routes.php
. Sekarang hanya dua rute yang telah diumumkan di sini:
use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Main page' ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ];
Sejauh ini, semuanya sederhana, dan aplikasi asinkron kami cocok dengan beberapa file.
Kita beralih ke hal-hal yang lebih "berguna". Jawaban dari beberapa kata dari teks biasa yang kami pelajari berasal dari bab-bab sebelumnya tidak terlihat sangat menarik. Kita perlu mengembalikan sesuatu yang nyata, seperti halaman HTML.
Jadi, di mana kita meletakkan HTML ini? Tentu saja, Anda dapat meng-hardcode konten halaman web tepat di dalam file rute:
Tapi jangan lakukan itu! Anda tidak dapat mencampur logika bisnis (perutean) dengan presentasi (halaman HTML). Mengapa Bayangkan Anda perlu mengubah sesuatu dalam kode HTML, misalnya, warna tombol. Dan file mana yang perlu diubah? File dengan rute router.php
? Kedengarannya aneh, kan? Buat perubahan pada perutean untuk mengubah warna tombol ...
Oleh karena itu, kami akan meninggalkan rute sendirian, dan untuk halaman HTML kami akan membuat direktori terpisah. Di root proyek, tambahkan direktori baru yang disebut halaman. Kemudian di dalamnya kita buat file index.html
. Ini akan menjadi halaman utama kami. Berikut isinya:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ReactPHP App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" > </head> <body> <div class="container"> <div class="row"> <form action="/upload" method="POST" class="justify-content-center"> <div class="form-group"> <label for="text">Text</label> <textarea name="text" id="text" class="form-control"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </body> </html>
Halaman ini cukup sederhana, hanya berisi satu elemen - formulir. Formulir di dalam memiliki kotak teks dan tombol untuk mengirimkan. Saya juga menambahkan gaya Bootstrap untuk membuat halaman kami terlihat lebih bagus.
Membaca file. Bagaimana TIDAK
Pendekatan yang paling mudah adalah membaca konten file di dalam penangan permintaan dan mengembalikan konten itu sebagai badan respons. Sesuatu seperti ini:
Dan omong-omong, itu akan berhasil. Anda dapat mencobanya sendiri: restart server dan memuat kembali halaman http://127.0.0.1:8080/
di browser Anda.

Jadi apa yang salah di sini? Dan mengapa tidak melakukan itu? Singkatnya, karena akan ada masalah jika sistem file mulai melambat.
Memblokir dan tidak memblokir panggilan
Biarkan saya menunjukkan kepada Anda apa yang saya maksud dengan "memblokir" panggilan, dan apa yang bisa terjadi ketika salah satu penangan permintaan berisi kode pemblokiran. Sebelum mengembalikan objek respons, tambahkan fungsi panggilan ke fungsi sleep()
:
Ini akan membuat penangan permintaan dibekukan selama 10 detik sebelum dapat mengembalikan respons dengan konten halaman HTML. Harap perhatikan bahwa kami tidak menyentuh penangan untuk alamat /upload
. Dengan memanggil fungsi sleep(10)
, saya meniru pelaksanaan semacam operasi pemblokiran.
Jadi apa yang kita miliki? Ketika browser meminta halaman /
, pawang menunggu 10 detik dan kemudian mengembalikan halaman HTML. Saat kami membuka /upload
alamat, penangannya harus segera mengembalikan respons dengan string 'Unggah halaman'.
Sekarang mari kita lihat apa yang terjadi dalam kenyataan. Seperti biasa, kami me-restart server. Sekarang, silakan buka jendela lain di browser Anda. Di bilah alamat, masukkan http://127.0.0.1:8080/upload , tetapi jangan langsung membuka halaman ini. Biarkan saja alamat ini di bilah alamat untuk saat ini. Lalu pergi ke jendela browser pertama dan buka halaman http://127.0.0.1:8080/ di dalamnya. Saat halaman ini dimuat (ingat bahwa ini akan memakan waktu 10 detik untuk melakukan ini), cepat pergi ke jendela kedua dan tekan "Enter" untuk memuat alamat yang tersisa di bilah alamat ( http://127.0.0.1:8080/upload ) .
Apa yang kita dapatkan? Ya, alamat /, seperti yang diharapkan, membutuhkan 10 detik untuk memuat. Namun, yang mengejutkan, halaman kedua mengambil jumlah waktu yang sama untuk memuat, meskipun kami tidak menambahkan panggilan sleep()
ke dalamnya. Adakah yang tahu mengapa ini terjadi?
ReactPHP berjalan dalam satu utas. Tampaknya dalam aplikasi asinkron, tugas dieksekusi secara paralel, tetapi dalam kenyataannya tidak demikian. Ilusi paralelisme diciptakan oleh suatu siklus peristiwa yang secara konstan beralih di antara berbagai tugas dan melaksanakannya. Tetapi pada titik waktu tertentu, hanya satu tugas yang selalu dilakukan. Ini berarti bahwa jika salah satu dari tugas-tugas ini memakan waktu terlalu lama, itu akan memblokir loop acara, yang tidak akan dapat mendaftarkan acara baru dan memanggil penangan untuk mereka. Dan itu pada akhirnya mengarah pada "pembekuan" seluruh aplikasi, itu hanya akan kehilangan sinkronisasi.
OK, tapi apa hubungannya dengan memanggil file_get_contents('pages/index.h')
? Masalahnya di sini adalah kita mengakses sistem file secara langsung. Dibandingkan dengan operasi lain, seperti bekerja dengan memori atau komputasi, bekerja dengan sistem file bisa sangat lambat. Misalnya, jika file tersebut ternyata terlalu besar, atau disk itu sendiri lambat, maka membaca file mungkin memakan waktu dan, sebagai akibatnya, memblokir event loop.
Dalam model sinkron standar, permintaan-respons tidak menjadi masalah. Jika klien meminta file yang terlalu berat, ia akan menunggu sampai file ini diunduh. Permintaan berat seperti itu tidak memengaruhi pelanggan lain. Tetapi dalam kasus kami, kami berhadapan dengan model berorientasi peristiwa asinkron. Kami telah meluncurkan server HTTP yang harus terus memproses permintaan masuk. Jika satu permintaan membutuhkan terlalu banyak waktu untuk diselesaikan, maka ini akan memengaruhi semua klien server lainnya.
Sebagai aturan, ingat:
- Anda tidak pernah dapat memblokir acara loop.
Jadi, bagaimana kita kemudian membaca file secara tidak sinkron? Dan di sini kita sampai pada aturan kedua:
- Ketika operasi pemblokiran tidak dapat dihindari, itu harus dimasukkan ke dalam proses anak dan melanjutkan eksekusi asinkron di utas utama.
Jadi, setelah kita belajar bagaimana tidak melakukannya, mari kita bahas solusi non-blocking yang benar.
Proses anak
Semua komunikasi dengan sistem file dalam aplikasi asinkron harus dilakukan dalam proses anak. Untuk mengelola proses anak dalam aplikasi ReactPHP, kita perlu menginstal komponen "Proses Anak" lainnya . Komponen ini memungkinkan Anda untuk mengakses fungsi sistem operasi untuk menjalankan perintah sistem apa pun di dalam proses anak. Untuk menginstal komponen ini, buka terminal di root proyek dan jalankan perintah berikut:
composer require react/child-process
Kompatibilitas Windows
Dalam sistem operasi Windows, utas STDIN, STDOUT, dan STDERR diblokir, yang berarti bahwa komponen Proses Anak tidak akan dapat bekerja dengan benar. Oleh karena itu, komponen ini terutama dirancang untuk bekerja hanya pada sistem nix. Jika Anda mencoba membuat objek kelas Proses pada sistem Windows, pengecualian akan dilempar. Tetapi komponen dapat bekerja di bawah Windows Subsystem untuk Linux (WSL) . Jika Anda ingin menggunakan komponen ini di Windows, Anda harus menginstal WSL.
Sekarang kita dapat menjalankan perintah shell di dalam proses anak. Buka file routes.php
, dan kemudian mari kita ubah handler untuk rute /
. Buat objek dari kelas React\ChildProcess\Process
, dan sebagai perintah, berikan ls
kepadanya untuk mendapatkan isi direktori saat ini:
Maka kita perlu memulai proses dengan memanggil metode start()
. Tangkapannya adalah bahwa metode start()
membutuhkan objek event loop. Tetapi dalam file routes.php
kita tidak memiliki objek ini. Bagaimana kami melewati loop acara dari index.php
ke rute langsung ke penangan permintaan? Solusi untuk masalah ini adalah "injeksi ketergantungan".
Injeksi Ketergantungan
Jadi, salah satu rute kami membutuhkan loop acara agar berfungsi. Dalam aplikasi kami, hanya satu komponen yang tahu tentang keberadaan rute - kelas Router
. Ternyata itu adalah tanggung jawabnya untuk menyediakan acara loop untuk rute. Dengan kata lain, router membutuhkan loop acara, atau tergantung pada loop acara. Bagaimana kita secara eksplisit menyatakan ketergantungan ini dalam kode? Bagaimana membuatnya mustahil untuk bahkan membuat router tanpa melewatkan perulangan event ke sana? Tentu saja, melalui konstruktor kelas Router
. Buka Router.php
dan tambahkan konstruktor ke kelas Router
:
use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Response; class Router { private $routes = []; private $loop; public function __construct(LoopInterface $loop) { $this->loop = $loop; }
Di dalam konstruktor, simpan loop peristiwa yang lewat di properti $loop
pribadi. Ini adalah injeksi ketergantungan ketika kami menyediakan kelas dengan objek yang dibutuhkannya untuk bekerja di luar.
Sekarang kita memiliki konstruktor baru ini, kita perlu memperbarui pembuatan router. Buka file index.php
dan perbaiki baris tempat kita membuat objek kelas Router
:
Selesai Kembali ke routes.php
. Seperti yang mungkin sudah Anda tebak, di sini kita dapat menggunakan ide yang sama dengan injeksi dependensi dan menambahkan loop peristiwa sebagai parameter kedua ke penangan kueri kami. Ubah panggilan balik pertama dan tambahkan argumen kedua: objek yang mengimplementasikan LoopInterface
:
Selanjutnya, kita perlu meneruskan event loop ke metode start()
dari proses anak. Dan di mana pawang mendapatkan loop acara? Dan itu sudah disimpan di dalam router di properti $loop
pribadi. Kita hanya perlu melewatinya ketika pawang dipanggil.
__invoke()
buka kelas Router
dan memperbarui metode __invoke()
, menambahkan argumen kedua ke panggilan pemanggil permintaan:
public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request, $this->loop); }
Itu saja! Ini mungkin injeksi ketergantungan yang cukup. Perjalanan yang agak besar dari siklus peristiwa terjadi, bukan? Dari file index.php
ke kelas Router
, dan kemudian dari kelas Router
ke file routes.php
tepat di dalam callback.
Jadi, untuk mengonfirmasi bahwa proses anak akan melakukan sihir non-pemblokirannya, mari kita ganti ls
sederhana dengan ping 8.8.8.8
lebih berat ping 8.8.8.8
. Mulai ulang server dan coba lagi untuk membuka dua halaman di dua jendela yang berbeda. Pertama, http://127.0.0.1:8080/
, dan kemudian /upload
. Kedua halaman terbuka dengan cepat, tanpa penundaan, meskipun perintah ping
dijalankan di penangan pertama di latar belakang. Ngomong-ngomong, ini berarti bahwa kita dapat memotong operasi yang mahal (misalnya, memproses file besar), tanpa memblokir aplikasi utama.
Ikat proses anak dan respons menggunakan utas
Mari kita kembali ke aplikasi kita. Jadi, kami membuat proses anak, memulainya, tetapi browser kami tidak menampilkan hasil operasi bercabang dua arah dengan cara apa pun. Mari kita perbaiki.
Bagaimana kita dapat berkomunikasi dengan proses anak? Dalam kasus kami, kami memiliki ls
menjalankan yang menampilkan konten direktori saat ini. Bagaimana kita mendapatkan kesimpulan ini, dan kemudian mengirimkannya ke jawaban? Jawaban singkatnya adalah: utas.
Mari kita bicara sedikit tentang proses. Perintah shell apa pun yang Anda jalankan memiliki tiga aliran data: STDIN, STDOUT, dan STDERR. Streaming ke output dan input standar, plus stream untuk kesalahan. Sebagai contoh, ketika kita menjalankan ls
, hasil dari perintah ini dikirim langsung ke STDOUT (pada layar terminal). Jadi, jika kita perlu mendapatkan output dari suatu proses, akses ke aliran output diperlukan. Dan ini sesederhana itu. Dalam membuat objek respons, ganti panggilan file_get_contents()
dengan $childProcess->stdout
:
return new Response( 200, ['Content-Type' => 'text/plain'], $childProcess->stdout );
Semua proses anak memiliki tiga properti yang berhubungan dengan stdio
stream: stdout
, stdin
, dan stderr
. Dalam kasus kami, kami ingin menampilkan output dari proses pada halaman web. Alih-alih string dalam konstruktor dari kelas Response
, kami melewatkan aliran sebagai argumen ketiga. Kelas Response
cukup pintar untuk menyadari bahwa ia menerima aliran dan memprosesnya.
Jadi, seperti biasa, kita me-reboot server dan melihat apa yang telah kita lakukan. Mari kita buka halaman http://127.0.0.1:8080/
di browser: Anda akan melihat daftar file dari folder root proyek.

Langkah terakhir adalah mengganti ls
dengan sesuatu yang lebih berguna. Kami memulai bab ini dengan merender file pages/index.html
menggunakan fungsi file_get_contents()
. Sekarang, kita dapat membaca file ini dengan sangat tidak sinkron, tanpa khawatir itu akan memblokir aplikasi kita. Ganti ls
dengan cat pages/index.html
.
Jika Anda tidak terbiasa dengan cat
, maka cat
ini digunakan untuk menyatukan dan menampilkan file. Paling sering, perintah ini digunakan untuk membaca file dan menampilkan isinya ke output standar. Perintah cat pages/index.html
membaca file cat pages/index.html
dan mencetak isinya ke STDOUT. Dan kami sudah mengirimkan stdout
sebagai badan tanggapan. Ini adalah versi final file routes.php
:
Akibatnya, semua kode ini hanya diperlukan untuk mengganti satu panggilan ke fungsi file_get_contents()
. Ketergantungan injeksi, melewati objek peristiwa loop, menambahkan proses anak, dan bekerja dengan utas. Semua ini hanya untuk menggantikan satu panggilan fungsi. Apakah itu sepadan? Jawaban: ya, itu sepadan. Ketika sesuatu dapat memblokir event loop, dan sistem file pasti bisa, pastikan itu pada akhirnya akan memblokir, dan pada saat yang paling tidak tepat.
Membuat proses anak setiap kali kita perlu mengakses sistem file mungkin terlihat seperti overhead tambahan yang akan mempengaruhi kecepatan dan kinerja aplikasi kita. Sayangnya, dalam PHP tidak ada cara lain untuk bekerja dengan sistem file secara tidak sinkron. Semua pustaka PHP asinkron menggunakan proses anak (atau ekstensi yang abstrak).
Pembaca Habra dapat membeli seluruh buku dengan diskon di tautan ini .
Dan kami mengingatkan Anda bahwa kami selalu mencari pengembang keren ! Ayo, kita bersenang-senang!