Примеры
Этот пример показывает работу простого сервера. Измените переменные address и port в соответствии с вашими настройками и выполните. Затем вы можете соединиться с сервером с командой, похожей на: telnet 192.168.1.53 10000 (где адрес и порт должны соответствовать вашим настройкам). Всё, что вы наберёте на клавиатуре, будет затем выведено на сервере и отправлено вам обратно. Для отключения наберите ‘выход’.
#!/usr/local/bin/php -q
error_reporting ( E_ALL );
/* Позволяет скрипту ожидать соединения бесконечно. */
set_time_limit ( 0 );
/* Включает скрытое очищение вывода так, что мы видим данные
* как только они появляются. */
ob_implicit_flush ();
$address = ‘192.168.1.53’ ;
$port = 10000 ;
if (( $sock = socket_create ( AF_INET , SOCK_STREAM , SOL_TCP )) === false ) echo «Не удалось выполнить socket_create(): причина: » . socket_strerror ( socket_last_error ()) . «\n» ;
>
if ( socket_bind ( $sock , $address , $port ) === false ) echo «Не удалось выполнить socket_bind(): причина: » . socket_strerror ( socket_last_error ( $sock )) . «\n» ;
>
if ( socket_listen ( $sock , 5 ) === false ) echo «Не удалось выполнить socket_listen(): причина: » . socket_strerror ( socket_last_error ( $sock )) . «\n» ;
>
do if (( $msgsock = socket_accept ( $sock )) === false ) echo «Не удалось выполнить socket_accept(): причина: » . socket_strerror ( socket_last_error ( $sock )) . «\n» ;
break;
>
/* Отправляем инструкции. */
$msg = «\nДобро пожаловать на тестовый сервер PHP. \n» .
«Чтобы отключиться, наберите ‘выход’. Чтобы выключить сервер, наберите ‘выключение’.\n» ;
socket_write ( $msgsock , $msg , strlen ( $msg ));
do if ( false === ( $buf = socket_read ( $msgsock , 2048 , PHP_NORMAL_READ ))) echo «Не удалось выполнить socket_read(): причина: » . socket_strerror ( socket_last_error ( $msgsock )) . «\n» ;
break 2 ;
>
if (! $buf = trim ( $buf )) continue;
>
if ( $buf == ‘выход’ ) break;
>
if ( $buf == ‘выключение’ ) socket_close ( $msgsock );
break 2 ;
>
$talkback = «PHP: Вы сказали ‘ $buf ‘.\n» ;
socket_write ( $msgsock , $talkback , strlen ( $talkback ));
echo » $buf \n» ;
> while ( true );
socket_close ( $msgsock );
> while ( true );
Пример #2 Пример использования сокетов: Простой клиент TCP/IP
Этот пример показывает использование простого одноразового HTTP-клиента. Он просто соединяется со страницей, отправляет запрос HEAD, выводит ответ и завершает работу.
echo «
Соединение TCP/IP
\n» ;
/* Получаем порт сервиса WWW. */
$service_port = getservbyname ( ‘www’ , ‘tcp’ );
/* Получаем IP-адрес целевого хоста. */
$address = gethostbyname ( ‘www.example.com’ );
/* Создаём сокет TCP/IP. */
$socket = socket_create ( AF_INET , SOCK_STREAM , SOL_TCP );
if ( $socket === false ) echo «Не удалось выполнить socket_create(): причина: » . socket_strerror ( socket_last_error ()) . «\n» ;
> else echo «OK.\n» ;
>
echo «Пытаемся соединиться с ‘ $address ‘ на порту ‘ $service_port ‘. » ;
$result = socket_connect ( $socket , $address , $service_port );
if ( $result === false ) echo «Не удалось выполнить socket_connect().\nПричина: ( $result ) » . socket_strerror ( socket_last_error ( $socket )) . «\n» ;
> else echo «OK.\n» ;
>
$in = «HEAD / HTTP/1.1\r\n» ;
$in .= «Host: www.example.com\r\n» ;
$in .= «Connection: Close\r\n\r\n» ;
$out = » ;
echo «Отправляем HTTP-запрос HEAD. » ;
socket_write ( $socket , $in , strlen ( $in ));
echo «OK.\n» ;
echo «Читаем ответ:\n\n» ;
while ( $out = socket_read ( $socket , 2048 )) echo $out ;
>
echo «Закрываем сокет. » ;
socket_close ( $socket );
echo «OK.\n\n» ;
?>
User Contributed Notes 3 notes
You can easily extend the first example to handle any number of connections instead of jsut one
#!/usr/bin/env php
error_reporting ( E_ALL );
/* Permitir al script esperar para conexiones. */
set_time_limit ( 0 );
/* Activar el volcado de salida implícito, así veremos lo que estamo obteniendo
* mientras llega. */
ob_implicit_flush ();
$address = ‘127.0.0.1’ ;
$port = 10000 ;
if (( $sock = socket_create ( AF_INET , SOCK_STREAM , SOL_TCP )) === false ) echo «socket_create() falló: razón: » . socket_strerror ( socket_last_error ()) . «\n» ;
>
if ( socket_bind ( $sock , $address , $port ) === false ) echo «socket_bind() falló: razón: » . socket_strerror ( socket_last_error ( $sock )) . «\n» ;
>
if ( socket_listen ( $sock , 5 ) === false ) echo «socket_listen() falló: razón: » . socket_strerror ( socket_last_error ( $sock )) . «\n» ;
>
//clients array
$clients = array();
do $read = array();
$read [] = $sock ;
$read = array_merge ( $read , $clients );
// Set up a blocking call to socket_select
if( socket_select ( $read , $write = NULL , $except = NULL , $tv_sec = 5 ) < 1 )
// SocketServer::debug(«Problem blocking socket_select?»);
continue;
>
// Handle new Connections
if ( in_array ( $sock , $read ))
if (( $msgsock = socket_accept ( $sock )) === false ) echo «socket_accept() falló: razón: » . socket_strerror ( socket_last_error ( $sock )) . «\n» ;
break;
>
$clients [] = $msgsock ;
$key = array_keys ( $clients , $msgsock );
/* Enviar instrucciones. */
$msg = «\nBienvenido al Servidor De Prueba de PHP. \n» .
«Usted es el cliente numero: < $key [ 0 ]>\n» .
«Para salir, escriba ‘quit’. Para cerrar el servidor escriba ‘shutdown’.\n» ;
socket_write ( $msgsock , $msg , strlen ( $msg ));
// Handle Input
foreach ( $clients as $key => $client ) < // for each client
if ( in_array ( $client , $read )) if ( false === ( $buf = socket_read ( $client , 2048 , PHP_NORMAL_READ ))) echo «socket_read() falló: razón: » . socket_strerror ( socket_last_error ( $client )) . «\n» ;
break 2 ;
>
if (! $buf = trim ( $buf )) continue;
>
if ( $buf == ‘quit’ ) unset( $clients [ $key ]);
socket_close ( $client );
break;
>
if ( $buf == ‘shutdown’ ) socket_close ( $client );
break 2 ;
>
$talkback = «Cliente < $key >: Usted dijo ‘ $buf ‘.\n» ;
socket_write ( $client , $talkback , strlen ( $talkback ));
echo » $buf \n» ;
>
Работа с Веб-сокетами на PHP
PHP — едва ли первое, что придет в голову, когда стоит задача поднять сервер веб-сокетов. Практически каждая статья в интернете будет пестрить предложениями использовать для этого NodeJS, Python или Go. Но поскольку PHP — это однозначно первое, что приходит в голову, когда речь идет о веб-приложениях, почему бы не попробовать?
На самом деле, запуск сервера веб-сокетов на PHP довольно прост. Существует превосходная библиотека Ratchet, позволяющая работать на любом фреймворке (или вовсе без него) полноценно и легко.
Казалось бы, на этом разговор можно заканчивать, но мы неизбежно столкнемся с некоторыми ограничениями и проблемами, связанными с архитектурой конечного приложения и природой самого протокола веб-сокетов.
Авторизация
По умолчанию, сервер веб-сокетов открыт для любого подключения. Конечно, можно поставить сетевые ограничения по доменам или IP адресам, но для веб-приложения — это, мягко говоря, не эффективный подход. В обычной ситуации мы используем для таких ограничений тот или иной вариант сервиса авторизации — токены, сессии и т.д. Здесь же проблема в том, что мы не сможем отправить через протокол ws:// ни HTTP заголовок, ни cookies. Значительная часть привычных методов, таким образом, не сработает.
Архитектура
Основное приложение != сервер веб-сокетов. Для работы с ними всегда необходимо держать в голове, что мы имеем дело с двумя отдельными приложениями, вне зависимости от того, насколько тесно они взаимодействуют между собой. На первый взгляд это может показаться незначительным нюансом, однако такое положение вещей требует особого внимания к подготовке интерфейсов для интеграции основного приложения и сервера веб-сокетов. Ко всему прочему, это порождает еще одну проблему.
База данных
Поскольку сервер веб-сокетов — это отдельное от основного бэкенда приложение, он ничего не знает о существующей базе данных. Сложно представить себе современное приложение на PHP, написанное без использование какого-либо фреймворка и ORM, так что перед разработчиком встанет дополнительная задача интегрировать службы, сервисы и библиотеки для работы с БД в сторонний скрипт.
Решения
Для каждой из названных проблем вполне возможно отыскать соответствующее решение. Некоторые из потенциальных решений могут показаться шероховатыми, но главное, что они рабочие.
Авторизуем пользователей
В процессе подключения к серверу веб-сокетов существует этап, на котором исходный HTTP запрос преобразуется в WS запрос. Используемая нами библиотека Ratchet сохраняет этот начальный запрос в объекте Connection. Хотя возможности подцепить Bearer заголовок к запросу нет (для клиентского приложения запрос строится сразу как ws://websocket-server), мы можем передать токен (например, JWT) в параметрах запроса. При использовании HTTPS — это вполне безопасный способ передачи.
В итоге, запрос на подключение может выглядеть примерно так:
Строку параметров затем можно извлечь из упомянутого ранее объекта Connection.
После извлечения токен может использоваться в любом уже применяющемся механизме авторизации, реализованном в основном приложении.
Интегрируем базу данных
В 9 из 10 случаев основное приложение будет написано на одном из популярных фреймворков вроде Laravel или Symfony. Все, что нам необходимо реализовать в такой ситуации — внедрение службы, отвечающей за ORM, в конструктор сервера веб-сокетов. При условии, что для запуска сервера используется консольная команда, использующая компонент Symfony Console, мы можем сделать это в два этапа: первоначальной инъекцией в конструктор консольной команды, а оттуда передачей в конструктор основного класса веб-сокетов.
Разделяем приложения
Раз уж мы вынуждены расценивать основное приложение и сервер веб-сокетов как два отдельных компонента, ничто не мешает нам использовать API основного приложения внутри сервера веб-сокетов. Пожалуй, самый распространенный сценарий — сохранение сообщений в БД и последующая отдача их фронтенд-приложению.
В целом, после внедрения ORM в обработчик веб-сокетов, мы могли бы выполнять все это с помощью обычных CRUD-операций. Но гораздо более эффективным решением было бы использовать уже готовый API. Почему? Во-первых, это позволит избежать дублирования кода (ровно такие же CRUDы используются в контроллерах, отвечающих за API). Во-вторых, таким способом мы укладываемся в общую архитектуру разделенных компонентов, даже внутри монолитного решения. Более того, имея одновременно токен из исходного запроса и внедренный ORM, мы получаем возможность авторизовывать действия и валидировать данные при абсолютно каждом событии веб-сокетов, а это уже полноценная имперсонификация пользователя.
Выводы
PHP все еще может быть не первым вариантом для работы с веб-сокетами, но на нем все еще вполне возможен запуск и эксплуатация полноценного сервера веб-сокетов со всеми необходимыми соображениями безопасностями и прозрачной архитектуры.