Организация сетевого взаимодействия через сокеты в PHP

Раздел: Программирование на 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.

Сокетное соединение в PHP - comments

En
Php socket connection (php)