Socket select: примеры (PHP)

Использование socket_select для мониторинга сокетов в PHP
Раздел: Сокеты
socket_select(array &read, array &write, array &except, int tv_sec [, int tv_usec]): int|false
Функция socket_select и её назначение

Функция socket_select в PHP предназначена для мониторинга массивов сокетов на предмет изменения их статуса. Она позволяет реализовать асинхронную обработку множества соединений в одном потоке, что особенно востребовано в серверных приложениях, например, в чат-серверах или игровых серверах.

Аргументы функции

read — массив сокетов, которые проверяются на доступность для чтения (поступление данных).

write — массив сокетов, проверяемых на готовность к записи (возможность отправки данных).

except — массив сокетов, проверяемых на наличие исключительных ситуаций (ошибки).

tv_sec — секундная часть таймаута ожидания изменений.

tv_usec — микросекундная часть таймаута (дополнение к секундам).

Возвращаемое значение: количество сокетов, изменивших статус, либо false при ошибке.

Простые примеры использования
Ожидание данных от одного сокета
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '127.0.0.1', 8080);
$read = [$socket];
$write = $except = null;
$num_changed = socket_select($read, $write, $except, 5, 0);
if ($num_changed > 0) {
    echo 'Данные доступны для чтения';
}
?>
Данные доступны для чтения
Проверка возможности записи
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_nonblock($socket);
$write = [$socket];
$num_changed = socket_select(null, $write, null, 1);
if ($num_changed > 0) {
    echo 'Сокет готов к отправке данных';
}
?>
Сокет готов к отправке данных
Альтернативные функции в PHP

stream_select — работает с потоковыми ресурсами (файлы, сокеты через stream_socket). Удобна при использовании потокового контекста.

socket_poll — низкоуровневая функция для опроса сокетов, обеспечивает более тонкий контроль, но требует ручного управления структурами данных.

Асинхронное расширение Swoole — предоставляет event loop для работы с тысячами соединений без блокировок, но требует установки дополнительного модуля.

Аналоги в других языках
Python: select.select
import select
import socket

sock = socket.socket()
sock.connect(('127.0.0.1', 8080))
readable, writable, exceptional = select.select([sock], [], [], 5)
if readable:
    print('Данные доступны')
Данные доступны
JavaScript: библиотека net (Node.js)
const net = require('net');
const socket = net.createConnection(8080, '127.0.0.1');
socket.on('readable', () => {
    console.log('Данные доступны');
});
Данные доступны
C: системный вызов select()
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(socket_fd, &read_fds);
select(socket_fd+1, &read_fds, NULL, NULL, &timeout);

Отличие PHP-версии: работа с объектами сокетов вместо файловых дескрипторов.

Распространённые ошибки
Передача пустых массивов
<?php
$num_changed = socket_select([], [], [], 1);
var_dump($num_changed);
?>
Warning: socket_select(): No socket arrays provided in ...
bool(false)
Использование неинициализированных переменных
<?php
$read = [$socket];
$num_changed = socket_select($read, $write, $except, 1); // $write не определён
?>
Warning: socket_select(): Unable to select [4]: Interrupted system call
Изменение исходных массивов
<?php
$sockets = [$socket1, $socket2];
$read = $sockets;
socket_select($read, $write, $except, 1);
// $read теперь содержит только готовые сокеты
// $sockets остался неизменным
?>

Важно: функция модифицирует переданные массивы, оставляя только изменившиеся сокеты.

Изменения в версиях PHP

PHP 8.0: строгая типизация параметров — аргументы tv_sec и tv_usec теперь принимаются как int|null вместо int.

PHP 7.1: добавлена возможность передачи null в таймаут для бесконечного ожидания.

До PHP 5.4: микросекундная часть таймаута имела максимальное значение 1 000 000, сейчас ограничение снято.

Расширенные примеры
Мультиплексирование нескольких клиентов
Пример php
<?php
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '0.0.0.0', 9999);
socket_listen($server);
$clients = [$server];

while (true) {
    $read = $clients;
    $write = $except = null;
    
    if (socket_select($read, $write, $except, null) > 0) {
        foreach ($read as $socket) {
            if ($socket === $server) {
                $new_client = socket_accept($server);
                $clients[] = $new_client;
                echo 'Новое подключение';
            } else {
                $data = socket_read($socket, 1024);
                if ($data === '') {
                    socket_close($socket);
                    unset($clients[array_search($socket, $clients)]);
                } else {
                    echo 'Получено: ' . $data;
                }
            }
        }
    }
}
?>
Обработка таймаута с микросекундами
Пример php
<?php
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
$read = [$socket];
$start = microtime(true);
$result = socket_select($read, $write, $except, 1, 500000);
$end = microtime(true);

echo 'Ожидание: ' . ($end - $start) . ' секунд';
?>
Ожидание: 1.500128 секунд
Приоритетная обработка исключений
Пример php
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, 'example.com', 80);
$except = [$socket];
// Имитация исключительной ситуации
socket_shutdown($socket, 2);

if (socket_select($read, $write, $except, 0) > 0) {
    if (!empty($except)) {
        echo 'Обнаружена исключительная ситуация';
    }
}
?>
Обнаружена исключительная ситуация

PHP socket_select function comments

En
Socket select Runs the select() system call on the given arrays of sockets with a specified timeout