Организация сетевого взаимодействия через сокеты в PHP
Сокетное соединение в PHP: обзор и реализация
Сокетное соединение позволяет взаимодействовать между процессами по сети. В PHP существует несколько подходов для создания серверов и клиентов на основе сокетов. Рассмотрим основные варианты, их цели, примеры кода и типичные трудности.
Базовый TCP-сервер и клиент с использованием функций socket_*
Как организовать простое клиент-серверное взаимодействие на PHP?
Самый прямой способ - использование набора функций socket_create, socket_bind, socket_listen и socket_accept. Этот метод подходит для учебных примеров и простых приложений, где не требуется высокая производительность.
// Сервер (server.php)
$host = '127.0.0.1';
$port = 9000;
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (!$socket) {
die('Ошибка создания сокета: ' . socket_strerror(socket_last_error()));
}
if (!socket_bind($socket, $host, $port)) {
die('Ошибка привязки: ' . socket_strerror(socket_last_error($socket)));
}
if (!socket_listen($socket, 5)) {
die('Ошибка прослушивания: ' . socket_strerror(socket_last_error($socket)));
}
echo "Сервер запущен на $host:$port\n";
while (true) {
$client = socket_accept($socket);
if (!$client) continue;
$data = socket_read($client, 1024);
echo "Получено: $data\n";
socket_write($client, "Echo: $data");
socket_close($client);
}
socket_close($socket);Php socket connection (сокетное соединение в php)
// Клиент (client.php)
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '127.0.0.1', 9000);
socket_write($socket, "Привет, сервер!");
$response = socket_read($socket, 1024);
echo "Ответ: $response\n";
socket_close($socket);Php connect to server (подключение к php серверу)
Типичные ошибки:
- Ошибка socket_create(): не установлено расширение sockets - проверьте php.ini (extension=sockets).
- socket_bind(): адрес уже используется - измените порт или дождитесь освобождения.
- socket_accept() блокирует выполнение - для обработки нескольких клиентов требуется многопоточность или неблокирующий режим.
Использование потоковых серверов (stream_socket_server / stream_socket_client)
Как создать сокетный сервер на основе потоков?
Функции stream_socket_server и stream_socket_client предоставляют более высокоуровневый интерфейс. Они поддерживают протоколы TCP, UDP, Unix-сокеты и упрощают работу с SSL/TLS. Подходит для приложений, где нужна гибкость в настройке протоколов.
$server = stream_socket_server('tcp://0.0.0.0:9001', $errno, $errstr);
if (!$server) {
die("Ошибка: $errstr ($errno)");
}
echo "Сервер запущен\n";
while ($client = stream_socket_accept($server)) {
$data = fread($client, 1024);
echo "Принято: $data\n";
fwrite($client, "Ответ: $data");
fclose($client);
}
fclose($server);Http connection php (http-соединение в php)
Проблемы: при большом количестве клиентов один поток блокирует очередь. Для масштабирования требуется использование stream_select или многопроцессорности.
Многопользовательский сервер с помощью stream_select
Как обрабатывать несколько клиентов одновременно без нескольких процессов?
Функция stream_select позволяет отслеживать активность на множестве потоков. Это неблокирующий способ обслуживания множества соединений в одном цикле. Применяется в чатах, игровых серверах с низкой нагрузкой.
$server = stream_socket_server('tcp://0.0.0.0:9002', $errno, $errstr);
if (!$server) die("Ошибка: $errstr");
$clients = [$server];
while (true) {
$read = $clients;
$write = null;
$except = null;
if (stream_select($read, $write, $except, null) === false) break;
if (in_array($server, $read)) {
$new = stream_socket_accept($server);
$clients[] = $new;
unset($read[array_search($server, $read)]);
}
foreach ($read as $client) {
$data = fread($client, 1024);
if ($data === false || feof($client)) {
fclose($client);
unset($clients[array_search($client, $clients)]);
continue;
}
foreach ($clients as $other) {
if ($other !== $server && $other !== $client) {
fwrite($other, $data);
}
}
}
}
Ошибки: если не проверить feof(), можно получить бесконечный цикл при разрыве соединения. stream_select() требует установки таймаута, иначе при отсутствии данных цикл блокируется.
Работа с UDP сокетами
Как установить соединение по протоколу UDP?
UDP - ненадёжный, но быстрый протокол. Используется для потокового видео, игр, DNS. Создание сокета с типом SOCK_DGRAM.
// UDP сервер
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_bind($socket, '0.0.0.0', 9999);
echo "UDP сервер запущен\n";
$buf = '';
$from = '';
$port = 0;
socket_recvfrom($socket, $buf, 1024, 0, $from, $port);
echo "Получено от $from:$port: $buf\n";
socket_sendto($socket, "Pong", 0, 0, $from, $port);
socket_close($socket);
Проблемы: потеря пакетов, отсутствие подтверждения доставки. Необходима реализация собственной логики подтверждения (ACK).
Высокопроизводительные решения (Swoole / ReactPHP)
Как построить высокопроизводительное сетевое приложение?
Расширение Swoole или библиотека ReactPHP предоставляют асинхронный ввод-вывод на основе событийного цикла. Используются для создания WebSocket-серверов, микросервисов с тысячами одновременных соединений. Требуют установки расширения (pecl install swoole).
// Swoole TCP сервер
$server = new Swoole\Server('0.0.0.0', 9501);
$server->on('connect', function ($serv, $fd) {
echo "Клиент $fd подключился\n";
});
$server->on('receive', function ($serv, $fd, $fromId, $data) {
$serv->send($fd, "Ответ: $data");
});
$server->on('close', function ($serv, $fd) {
echo "Клиент $fd отключился\n";
});
$server->start();
Проблемы: Swoole не совместим с некоторыми старыми версиями PHP, требует особой настройки для работы в общих хостингах. ReactPHP требует установки через Composer.
Расширенные примеры сокетных соединений в PHP
1. Эхо-сервер с поддержкой SSL/TLS
Для безопасного соединения можно обернуть поток в SSL с использованием stream_socket_enable_crypto. Сервер использует самоподписанный сертификат.
$context = stream_context_create([
'ssl' => [
'local_cert' => '/path/to/server.pem',
'allow_self_signed' => true,
'verify_peer' => false
]
]);
$server = stream_socket_server('tls://0.0.0.0:4433', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context);
if (!$server) die("$errstr ($errno)");
echo "SSL сервер запущен\n";
while ($client = stream_socket_accept($server, -1)) {
stream_set_blocking($client, true);
if (!stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) {
echo "Ошибка рукопожатия SSL\n";
fclose($client);
continue;
}
$data = fread($client, 4096);
fwrite($client, "Secure echo: $data");
fclose($client);
}
Запустите сервер, затем подключитесь командой: openssl s_client -connect 127.0.0.1:4433 Введите сообщение, сервер вернёт его с префиксом "Secure echo: ".
2. Неблокирующий сокет на socket_set_nonblock
Неблокирующий режим позволяет выполнять операции без ожидания. Пример клиента, который делает несколько попыток чтения с таймаутом.
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_nonblock($socket);
$result = @socket_connect($socket, '93.184.216.34', 80); // example.com
if ($result === false) {
$err = socket_last_error($socket);
if ($err !== SOCKET_EINPROGRESS) {
die("Ошибка подключения: " . socket_strerror($err));
}
}
// Ожидание подключения с использованием socket_select
$write = [$socket];
$read = null;
$except = null;
if (socket_select($read, $write, $except, 5) === false) {
die("select error");
}
if (in_array($socket, $write)) {
socket_write($socket, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
socket_set_block($socket); // переключаем в блокирующий для чтения ответа
$response = socket_read($socket, 4096);
echo substr($response, 0, 200); // первые 200 байт
}
socket_close($socket);
Выводит первые 200 символов HTTP-ответа от example.com.
3. Многопоточный сервер с использованием pcntl_fork
Для обработки каждого клиента в отдельном процессе. Подходит для Unix-систем. Внимание: при большом числе клиентов быстро расходуются ресурсы.
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '0.0.0.0', 9005);
socket_listen($server);
while (true) {
$client = socket_accept($server);
$pid = pcntl_fork();
if ($pid == -1) {
die("Ошибка fork");
} elseif ($pid == 0) {
// дочерний процесс
socket_close($server);
$data = socket_read($client, 1024);
socket_write($client, "From child: $data");
socket_close($client);
exit(0);
} else {
// родительский процесс
socket_close($client);
pcntl_waitpid($pid, $status, WNOHANG); // необязательно
}
}
socket_close($server);
При подключении каждого клиента сервер форкает дочерний процесс, который обрабатывает запрос и завершается. Родитель продолжает принимать новых клиентов.
4. Передача файлов через сокет (скачивание)
Сервер отправляет файл клиенту порциями. Обработка частичной записи и чтения.
// Сервер
$file = '/path/to/large.zip';
$size = filesize($file);
$fh = fopen($file, 'rb');
$server = stream_socket_server('tcp://0.0.0.0:9010', $errno, $errstr);
$client = stream_socket_accept($server);
fwrite($client, pack('N', $size)); // отправляем размер как 4-байтовое целое
while (!feof($fh)) {
$chunk = fread($fh, 8192);
fwrite($client, $chunk);
}
fclose($fh);
fclose($client);
fclose($server);
// Клиент
$socket = stream_socket_client('tcp://127.0.0.1:9010', $errno, $errstr, 30);
$sizeBin = fread($socket, 4);
$size = unpack('N', $sizeBin)[1];
$received = 0;
$out = fopen('received.zip', 'wb');
while ($received < $size && ($buf = fread($socket, 8192)) !== false) {
fwrite($out, $buf);
$received += strlen($buf);
}
fclose($out);
fclose($socket);
На стороне сервера указывается путь к файлу, клиент сохраняет его как received.zip. Подходит для передачи бинарных данных.
5. WebSocket рукопожатие (пример низкоуровневой реализации)
WebSocket начинается с HTTP-запроса Upgrade. Ниже показана минимальная обработка.
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '0.0.0.0', 8080);
socket_listen($server);
$client = socket_accept($server);
$request = socket_read($client, 4096);
echo $request;
if (preg_match('/Sec-WebSocket-Key: (.+)\r\n/', $request, $matches)) {
$key = $matches[1];
$acceptKey = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-5AB9DC11B85B', true));
$response = "HTTP/1.1 101 Switching Protocols\r\n"
. "Upgrade: websocket\r\n"
. "Connection: Upgrade\r\n"
. "Sec-WebSocket-Accept: $acceptKey\r\n\r\n";
socket_write($client, $response);
// Далее следует фреймовый обмен (не реализован)
}
socket_close($client);
socket_close($server);
На запрос WebSocket клиента (например, из браузера) сервер отправляет заголовок принятия. Для полноценной работы необходимо разбирать фреймы по RFC 6455.