Реалізація багатопотокового сервера на PHP

Дана публікація не претендує на повноту вирішення поставленого питання. Сервер розробляється виключно в ознайомлювальних цілях. Багато важливі питання, такі як, наприклад, обробка помилок сокетів, опущені. Для реалізації багатопотокового сервера ми будемо використовувати, звичайно ж, потоки. Дуже часто доводиться бачити фразу, що, мовляв, в PHP потоків немає. Так от, це неправда. Потоки є, але реалізовані в окремому розширенні pthreads.

Для початку нам знадобиться збірка PHP, скомпільована з прапором thread safety. Я використовую Windows для роботи, тому скачав готовий пакет тут. Потрібно лише правильно вибрати розрядність ОС, потрібну версію PHP і, звичайно ж, Thread Safe версію. Впродовж статті буде передбачатися, що архів з PHP ми розпакували в C:\php директорію. Далі нам потрібно встановити розширення pthreads. Йдемо сюди і вибираємо версію, відповідну завантаженою версією PHP і розрядності системи. З архіву копіюємо файл php_pthreads.dll в директорію C:\php\ext файл pthreadVC2.dll в директорії C:\php і C:\Windows\System32. В директорії C:\php перейменовуємо файл php.ini-development в php.ini і додаємо в нього такий рядок:

extension=php_pthreads.dll

Також знаходимо і раскоменчиваем директиву extension_dir і виставляємо їй значення «C:\php\ext» (у мене в версії PHP7 відносні шляхи не заробили). Відкриваємо " командний рядок і перевіряємо:

C:\php\php.exe -v

В кінці першого рядка виводу ми повинні побачити позначку (ZTS). Переходимо безпосередньо до реалізації сервера. Створюємо файл (в моєму випадку він буде розташовуватися за адресою C:\server.php. Для початку створимо сокет, який буде слухати порт 8080 на нашій локальній машині.

$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '127.0.0.1', 8080);
socket_listen($server);

Далі створюємо пул воркеров.

$pool = new Pool(10, Worker::class);

Перший аргумент встановлює максимальну кількість діючих потоків, другий-ім'я класу воркера. Для більш визначених завдань можна описати свій клас, успадкувавши його від класу Worker. Ми ж будемо використовувати оригінальний клас. Забігаючи вперед скажу, що в класі потоку встановлений воркер можна отримати через $this->worker.

Далі реалізуємо клас, який буде виконуватися в окремому потоці. Клас повинен спадкуватися від Threaded.

class Task extends Threaded
{
protected $socket;

public function __construct($socket)
{
$this->socket = $socket;
}

public function run()
{
if (!empty($this->socket)) {
$response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 12\r\n\r\nHello world!";
socket_write($this->socket, $response, strlen($response));
// при спробі закриття сокету я отримую помилку zend_mm_heap currupted, тому цю частину в тестовому вирішенні опускаю 
//socket_close($this->socket);
}
}
}

Наш клас приймає в конструкторі сокет з'єднання з клієнтом. Так само дії, що виконуються в потоці, повинні бути описані в методі run(). У моєму випадку це відповідь клієнту базових заголовків і тексту «Hello world!».

Далі ми будемо циклічно намагатися приймати з'єднання від клієнта, і, в разі успіху, створювати окремий потік і передавати туди дескриптор сокета.


$servers = [$server];

while (true) {
$read = $servers;

if (socket_select($read, $write, $except, 0) >= 0 && in_array($server, $read)) {
$task = new Task(socket_accept($server));
$pool->submit($task);
}
}

Оскільки ми використовуємо нескінченний цикл, я зареєструю функцію, яка виконається при завершенні роботи скрипта і зупинить роботу пулу. Функцію слід реєструвати до початку циклу.


register_shutdown_function(function () use ($server, $pool) {
if (!empty($server)) {
socket_close($server);
}

$pool->shutdown();
});

Власне все. Запускаємо сервер в командному рядку і пробуємо відкрити в браузері localhost:8080.

cd C:\
C:\php\php.exe server.php

Нижче наводжу повний код сервера.


<?php

$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '127.0.0.1', 8080);
socket_listen($server);

$pool = new Pool(10, Worker::class);

class Task extends Threaded
{
protected $socket;

public function __construct($socket)
{
$this->socket = $socket;
}

public function run()
{
if (!empty($this->socket)) {
$response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 12\r\n\r\nHello world!";
socket_write($this->socket, $response, strlen($response));
}
}
}

register_shutdown_function(function () use ($server, $pool) {
if (!empty($server)) {
socket_close($server);
}

$pool->shutdown();
});

$servers = [$server];

while (true) {
$read = $servers;

if (socket_select($read, $write, $except, 0) >= 0 && in_array($server, $read)) {
$task = new Task(socket_accept($server));
$pool->submit($task);
}
}

Спасибі за увагу!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.