How to Create a WebSocket Server in PHP with Ratchet for Real-Time Applications

How to Create a WebSocket Server in PHP with Ratchet for Real-Time Applications Хостинг
Содержание
  1. «рукопожатие» или handshake:
  2. Серверные сокеты в php
  3. Протокол вебсокетов
  4. Basically, no
  5. Create our application directory and files
  6. Create the websocket class
  7. How to create websockets server in php
  8. Suggestion
  9. Test the websocket server
  10. Tools needed to complete this tutorial
  11. Websocket-сервер на php | зона разработки
  12. Ws приложение чат
  13. Ws сервер панель управления улучшенная
  14. Автоматический запуск
  15. Анонимный ролевой флейм-чат
  16. Борьба с утечками памяти
  17. Демонстрация
  18. Заголовок клиента
  19. Заголовок сервера
  20. Запуск из консоли
  21. Запуск нескольких процессов для обработки соединений
  22. Интеграция с вашим фреймворком на примере yii
  23. Межпроцессное взаимодействие
  24. Можно ли запустить socket сервер на хостинге вместе с сайтом?
  25. Немного о разработке большого приложения
  26. Обмен пакетами
  27. Обмен сообщениями
  28. Отладка приложений на веб-сокетах на php
  29. Пишем простенький ws клиент на javascript
  30. Поднимаем websocket сервер на порту 8898
  31. Подсчёт количества пользователей он-лайн
  32. Поставленные цели:
  33. Проксирование вебсокетов с помощью nginx
  34. Разделение процессов на мастера и воркеров
  35. Фоновый процесс из php в ос windows
  36. Хранение логов
  37. Чат на веб-сокетах

«рукопожатие» или handshake:

Считываем значение Sec-WebSocket-Key из пришедшего заголовка от клиента, рассчитываем на его основе Sec-WebSocket-Accept и отправляем итоговый ответ:

Серверные сокеты в php

До этого момента я имел смутные представления о серверных сокетах. Почитав исходники нескольких библиотек для работы с вебсокетами я столкнулся с двумя схемами их реализаций:

используя расширение php «socket»:

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);//создаём сокет
socket_bind($socket, '127.0.0.1', 8000);//привязываем его к указанным ip и порту
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);//разрешаем использовать один порт для нескольких соединений
socket_listen($socket);//слушаем сокет

или используя расширение php «stream»:

$socket = stream_socket_server("tcp://127.0.0.1:8000", $errno, $errstr);

Я предпочёл второй вариант ввиду его краткости.

Итак, мы создали серверный сокет и теперь хотим обрабатывать новые соединения к нему, для этого опять же есть два варианта

while ($connect = stream_socket_accept($socket, -1)) {//ожидаем новое соединение (без таймаута) ...обрабатываем $connect
}

или с использованием stream_select

$connects = array();
while (true) { //формируем массив прослушиваемых сокетов: $read = $connects; $read[] = $socket; $write = $except = null; if (!stream_select($read, $write, $except, null)) {//ожидаем сокеты доступные для чтения (без таймаута) break; } if (in_array($socket, $read)) {//есть новое соединение $connect = stream_socket_accept($socket, -1);//принимаем новое соединение $connects[] = $connect;//добавляем его в список необходимых для обработки unset($read[ array_search($socket, $read) ]); } foreach($read as $connect) {//обрабатываем все соединения ...обрабатываем $connect unset($connects[ array_search($connect, $connects) ]); }
}

Пример простого http сервера с использованием stream_select, который на все запросы отвечает: Привет

#!/usr/bin/env php
<?php
$socket = stream_socket_server("tcp://0.0.0.0:8000", $errno, $errstr);
if (!$socket) { die("$errstr ($errno)n");
}
$connects = array();
while (true) { //формируем массив прослушиваемых сокетов: $read = $connects; $read []= $socket; $write = $except = null; if (!stream_select($read, $write, $except, null)) {//ожидаем сокеты доступные для чтения (без таймаута) break; } if (in_array($socket, $read)) {//есть новое соединение $connect = stream_socket_accept($socket, -1);//принимаем новое соединение $connects[] = $connect;//добавляем его в список необходимых для обработки unset($read[ array_search($socket, $read) ]); } foreach($read as $connect) {//обрабатываем все соединения $headers = ''; while ($buffer = rtrim(fgets($connect))) { $headers .= $buffer; } fwrite($connect, "HTTP/1.1 200 OKrnContent-Type: text/htmlrnConnection: closernrnПривет"); fclose($connect); unset($connects[ array_search($connect, $connects) ]); }
}
fclose($server);

Т.к. нам в дальнейшем нужно будет одновременно обрабатывать и серверный сокет на предмет новых соединений, и уже существующие подключения, на предмет новых сообщений, то остановимся на втором варианте.

Протокол вебсокетов

В этой статье хорошо описан протокол взаимодействия.Нас интересует два момента:

Basically, no

It is not likely for a shared hosting environment (i.e. Apache with VirtualHost config, PHP, MySQL, and a CPanel interface) to support your websocket application.

For websocket to work, you need to either:

Create our application directory and files

In your terminal, run the following commands to generate the project directory and all required files:

Create the websocket class

We’re now ready for some code! Return to your IDE and open app/socket.php. This file will house the class needed to implement how connections to our WebSocket server are handled. Paste the following code as shown below:

How to create websockets server in php

I was in the same boat as you recently, and here is what I did:

  1. I used the phpwebsockets code as a reference for how to structure the server-side code. (You seem to already be doing this, and as you noted, the code doesn’t actually work for a variety of reasons.)

  2. I used PHP.net to read the details about every socket function used in the phpwebsockets code. By doing this, I was finally able to understand how the whole system works conceptually. This was a pretty big hurdle.

  3. I read the actual WebSocket draft. I had to read this thing a bunch of times before it finally started to sink in. You will likely have to go back to this document again and again throughout the process, as it is the one definitive resource with correct, up-to-date information about the WebSocket API.

  4. I coded the proper handshake procedure based on the instructions in the draft in #3. This wasn’t too bad.

  5. I kept getting a bunch of garbled text sent from the clients to the server after the handshake and I couldn’t figure out why until I realized that the data is encoded and must be unmasked. The following link helped me a lot here: (

  6. original link broken) Archived copy.

    Please note that the code available at this link has a number of problems and won’t work properly without further modification.

  7. I then came across the following SO thread, which clearly explains how to properly encode and decode messages being sent back and forth: How can I send and receive WebSocket messages on the server side?

    This link was really helpful. I recommend consulting it while looking at the WebSocket draft. It’ll help make more sense out of what the draft is saying.

  8. I was almost done at this point, but had some issues with a WebRTC app I was making using WebSocket, so I ended up asking my own question on SO, which I eventually solved: What is this data at the end of WebRTC candidate info?

  9. At this point, I pretty much had it all working. I just had to add some additional logic for handling the closing of connections, and I was done.

That process took me about two weeks total. The good news is that I understand WebSocket really well now and I was able to make my own client and server scripts from scratch that work great.
Hopefully the culmination of all that information will give you enough guidance and information to code your own WebSocket PHP script.

Читайте также:  Бесплатные хостинги для создания сайтов: обзор лучших вариантов

Good luck!


Edit: This edit is a couple of years after my original answer, and while I do still have a working solution, it’s not really ready for sharing. Luckily, someone else on GitHub has almost identical code to mine (but much cleaner), so I recommend using the following code for a working PHP WebSocket solution:
https://shhost.ru/ghedipunk/PHP-Websockets/blob/master/websockets.php


Edit #2: While I still enjoy using PHP for a lot of server-side related things, I have to admit that I’ve really warmed up to Node.js a lot recently, and the main reason is because it’s better designed from the ground up to handle WebSocket than PHP (or any other server-side language). As such, I’ve found recently that it’s a lot easier to set up both Apache/PHP and Node.js on your server and use Node.js for running the WebSocket server and Apache/PHP for everything else. And in the case where you’re on a shared hosting environment in which you can’t install/use Node.js for WebSocket, you can use a free service like Heroku to set up a Node.js WebSocket server and make cross-domain requests to it from your server. Just make sure if you do that to set your WebSocket server up to be able to handle cross-origin requests.

Suggestion

To run your own websocket service, you should think about using Virtual Private Server services such as Amazon EC2, DigitalOcean VPS.

Test the websocket server

In your terminal, start the WebSocket server by running:

Tools needed to complete this tutorial

In order to complete this tutorial, the following prerequisites are needed:

Websocket-сервер на php | зона разработки

Думаю, многие слышали про такую технологию как вебсокеты. Это протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени. Что позволяет получить функционал привычных уже мессенджеров на сайте. Ну и вообще для мгновенного отображения каких-про действий на сайте, будь то лайк к фотографии, комментарий и т.п.

При слове «вебсокет» у многих сразу возникает ассоциация с NodeJS. Так как именно в этой среде исполнения часто реализуют указанный функционал. Но последователи PHP не оставляют попыток реализовать функционал ассинхронности. И, как мне кажется, фреймворк Workerman на сегодняшний день самое удобное, легкое и простое решение, реализующее указанный функционал. В своё время я пробовал phpdeamon, Ratchet и AmpPHP. Но все они как-то не вдохновляли. А Workerman покорил своей простотой и функциональностью. И что важно, он отлично подходит для использования его в качестве компонента MODX.

Чтобы пощупать его вживую, я поставил его на сайт через композер, взял скрипты серверной и клиентской частей из доки, чуть поправил, на сервере запустил скрипт серверной части

php server.php start

и всё. Вебсокеты готовы. Как это работает я продемонстрировал в небольшом видео.

Навесив UI для сообщений, получится шикарный компонент без NodeJS и сторонних comet-серверов.

20 августа 20201
 
2150

Ws приложение чат

Теперь, когда все инструменты готовы, можно заняться и творческими вещами, например, написанием простенького чата на ws. Код сервера пришлось значительно переписать и вы это увидите скачав архив ws сервера предложенный несколькими абзацами выше. Причиной для такой переработки послужила не реализация логики чата, которая очень проста, а приведение кода в порядок и необходимость реализации устойчивой работы под ОС Windows.

Ws сервер панель управления улучшенная

Всё же знал, что придётся столкнуться с тем чтобы разработать полноценную систему управления ws сервером, хотя и не хотел. Пользуйтесь.

Автоматический запуск

Для чата автоматический запуск может быть необходим просто для того, чтобы администратору не нужно было постоянно следить за тем, поднялся ws после перезагрузки Apache или нет. Т.к. скрипт ws-сервер чата простой, то достаточно реализовать при запуске чат клиента обращение по AJAX к скрипту сервера который всегда инициирует проверку состояния и осуществляет запуск если ws-сервер чата не запущен.

Но это применимо только для простых приложений типа чата. В случае с моим игровым проектом, я не использую автозапуск, т.к. там процесс запуска сложен сам по себе и может занимать до 40 секунд времени, поскольку игровые карты загружаются в память. Также к игровому проекту совершенно другой подход, при котором состояние игры должно мониториться более тщательно администратором и в случае проблем отправляться e-mail уведомление.

Как реализовать автозапуск ws-сервера? Реализуется простая функция на javascript на стороне клиента wsserverrun(), которая каждый раз при загрузке клиента делает AJAX-запрос к серверному скрипту wsstart.php отвечающему за запуск ws, который запускает ws в случае, если он отключен.

Анонимный ролевой флейм-чат

Правило: позовите друзей и поиграйте в ролевую игру, пытаясь угадать кто есть кто. Используйте команды /me, /to “Имя”. Пользователей он-лайн: Только вы.

Далее о том, как это было разработано.

Борьба с утечками памяти

Т.к. демон может быть долгоиграющим а проект находится в стадии разработки и активно дописывается, то, к сожалению, от утечек памяти никуда не убежать. Очевидно, что бороться с утечками можно и нужно логируя все действия демона и фиксируя затраченную память.

// Somewhere before the endless loop

$last_gc_cycle = time() - (24 * 3600);
// Some more code
while (true) {
// The main code here

if (function_exists('gc_collect_cycles')) {
$time = time();
if ($time - $last_gc_cycle > 300) {
$last_gc_cycle = $time;
gc_collect_cycles();
}
}
}

upd 2022.08.04: следующая статья, обновленная панель и немного об опыте разработки и отладки.

Специально для тех, кто ищет хостинг для запуска своего проекта на веб-сокетах обсуждение по ссылке.

Демонстрация

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

Все исходники я оформил в виде библиотеки и выложил на github

Update: Если сообществу интересна эта тема, то следующая статья будет про то как сделать простую игру, в которой все участники будут находиться на одном игровом поле и взаимодействовать друг с другом в реальном времени (демка уже почти готова).

Третья часть статьи: От чата до игры: Battle City

Заголовок клиента

Про заголовок клиента говорить особо нечего, разве что остановлюсь на параметрах

OriginHostHost

содержит адрес сервера и порт, к которому подключается вебсокет.

Origin

— опциональное поле, используется как правило браузерами. Содержит имя вебсервера, со страницы которого запущен javascript для подключения к серверу (имхо, не проверял).

Заголовок сервера


А теперь плавно перейдем к тому, что входит в ответ сервера.

Читайте также:  Топ 9 хостингов сайтов c php и mysql в России 2022

Первая строка:

Запуск из консоли

Выполняем команду

php websocket.php

или

./websocket.php

(предварительно дав права на выполнение)


Если использовать

nohup

, например,

nohup ./websocket.php &

, то скрипт продолжит работать после закрытия консоли.

По-умолчанию есть два ограничения количества соединений на один процесс.


Как я уже писал, эти ограничения можно обойти, используя дочерние процессы (воркеры).

Запуск нескольких процессов для обработки соединений

Для работы простого сервера вебсокетов достаточно одного процесса, но чтобы увеличить количество одновременных соединений (и обойти ограничение 1024 одновременных соединения), а также для использования ресурсов всего процессора (а не только одного ядра), необходимо, чтобы сервер вебсокетов использовал несколько процессов (оптимально — количество процессов = количество ядер процессора).

Для запуска нескольких процессов мы будем использовать функцию pcntl_fork(). Она создаёт новый процесс (дочерний), который является практически полной копией процесса-родителя, выполняющего этот вызов. После вызова pcntl_fork() алгоритм разветвляется: в случае успешного выполнения функции pcntl_fork() она возвращает PID дочернего процесса родительскому, а NULL дочернему. Если создание форка закончилось неудачей, функция pcntl_fork() возвращает значение −1).

$pid = pcntl_fork(); //делаем форк
//далее весь код будет выполняться в обоих процессах
if ($pid == -1) { // Не удалось создать дочерний процесс
} elseif ($pid) { // Этот код выполнится родительским процессом
} else { // А этот код выполнится дочерним процессом, его PID можно узнать с помощью функции getmypid()
}

Про отличие родительского процесса от дочернего можно почитать на википедии.

Мы можем в цикле создавать столько дочерних процессов, сколько нам необходимо:

$childs = array();
for ($i=0; $i<4; $i ) { $pid = pcntl_fork(); //создаём форк if ($pid == -1) { die("error: pcntl_fork"); } elseif ($pid) { //родительский процесс $childs[] = $pid; //заполняем массив дочерними PID, они нам ещё пригодятся :) } else { //дочерний процесс break; //выходим из цикла, чтобы дочерние процессы создавались только из родителя }
}

Интеграция с вашим фреймворком на примере yii

Так как наш мастер прослушивает дополнительный сокет для связи с нашими скриптами (в примере выше был

unix:///tmp/websocket.sock

), мы можем в любом месте нашего сайта или в кроне соединиться с этим сокетом и отправить сообщение, которое мастер разошлёт всем воркерам, а они, в свою очередь, все клиентам:

$service = stream_socket_client ('unix:///tmp/websocket.sock', $errno, $errstr);
fwrite($service, 'всем привет');


С использованием компонента yii это будет выглядеть вот так:

Yii::app()->websocket->send('всем привет');

Межпроцессное взаимодействие

Для взаимодействия между родительским и дочерним процессом мы будем использовать сокеты, а именно связанные сокеты:

Функция

stream_socket_pair()

создаёт пару связанных неразличимых потоковых сокетов. Таким образом мы можем писать в один сокет, а считывать данные из второго.

$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); //получаем массив из связанных сокетов
fwrite($pair[0], 'тест'); //пишем в первый сокет
fread($pair[1], mb_strlen('тест')); //читаем из второго

Теперь совмещаем этот код с форками и получаем:

$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); //создаём связанные сокеты
$pid = pcntl_fork(); //делаем форк
//далее весь код будет выполняться в обоих процессах
if ($pid == -1) { die("error: pcntl_fork");
} elseif ($pid) { //родительский процесс fclose($pair[0]); //закрываем один из сокетов в родителе $child = $pair[1]; //второй будем использовать для связи с потомком
} else { //дочерний процесс fclose($pair[1]); //закрываем второй из сокетов в потомке $parent = $pair[0]; //первый будем использовать для связи с родителем
}

Итоговый код для создания множества дочерних процессов:

$parent = null;
$childs = array();
for ($i=0; $i<5; $i ) { $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); //создаём связанные сокеты $pid = pcntl_fork(); //создаём форк if ($pid == -1) { die("error: pcntl_fork"); } elseif ($pid) { //родительский процесс fclose($pair[0]); //закрываем один из сокетов в родителе $childs[] = $pair[1]; //второй будем использовать для связи с потомком } else { //дочерний процесс fclose($pair[1]); //закрываем второй из сокетов в потомке $parent = $pair[0]; //первый будем использовать для связи с родителем break; //выходим из цикла, чтобы дочерние процессы создавались только из родителя }
}

В результате работы этого кода в родителе массив

$childs

будет содержать в себе все сокеты для связи с потомками, а потомки для связи с родителем будут использовать

$parent

Можно ли запустить socket сервер на хостинге вместе с сайтом?

В вашем примере использования websocket обращение идет к порту 8081. Соответственно на данном порту должно находиться какое-то приложение, которое будет отвечать на этот запрос.

Если вы делали по статье, то у вас должен быть запущен «демон», который будет слушать этот порт. Т.е. это не обычный скрипт, а по сути приложение, аналогичное веб-серверу.

Если же говорить о хостинге — тут многое зависит от того где вы будете размещать ваш сайт. Если это обычный шаред хостинг — то вряд ли получится. Но надо уточнять у поддержки хостинга. Если речь идет о VPS — то тут все в ваших руках.

И да — используемый порт (в вашем случае 8081) надо открыть на firewall для входящих соединений.

Немного о разработке большого приложения

Постараюсь в несколько слов рассказать о своём опыте. Ранее у меня уже было приложение которое работало на AJAX. Переход оказалось осуществить достаточно просто. В классе websocketserver_class я завёл экземпляр главного класса игрового проекта game_class.

В game_class я заменил получение данных из переменных $_GET на получение данных из переменных передаваемых в него из websocketserver_class в качестве аргументов. Также websocketserver_class передавал еще и id игрока с которым произошло событие, поскольку процесс всегда загружен в памяти и для него не могло существовать сессий игроков.

С помощью полученного id game_class опознавал игрока, восстанавливал данные игрока в класс player_class, производил над ним определенные манипуляции в итоге сохраняя данные из player_class в БД или файл или память. Таким образом переход с AJAX на ws оказался действительно очень прост. Вот, кстати, как стала выглядеть диаграмма классов после перехода на ws.

Обмен пакетами

Вот тут конечно они сильно замудрили. Фрейм выглядит в документации так:

 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - - - - ------- - ------------- ------------------------------- |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | - - - - ------- - ------------- - - - - - - - - - - - - - - - | Extended payload length continued, if payload len == 127 | - - - - - - - - - - - - - - - ------------------------------- | |Masking-key, if MASK set to 1 | ------------------------------- ------------------------------- | Masking-key (continued) | Payload Data | -------------------------------- - - - - - - - - - - - - - - - : Payload Data continued ... : - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | Payload Data continued ... | --------------------------------------------------------------- 


Как увидел, так прям сразу лень как-то стало разбираться… но всеже пришлось. Кстати, для тех кто хочет детально разобраться в этом на

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

готовые функции

hybi10Decode()hybi10Encode()

, которые показали себя, как исправно работающие. Тамже в функции

handshake()

описан метод получения параметров заголовка клиента.

Читайте также:  Поднимите свой бизнес с помощью исключительных услуг локального хостинга


Учтите также, что после рукопожатий, клиент отправляет серверу только маскированные фреймы, а сервер клиенту только немаскированные, то есть где бит MASK = 0.

В процессе я столкнулся еще с одной проблемой, после рукопожатий и ответа сервера на «hello» клиента, хром выдал следующее:

Обмен сообщениями

После получения данных из вебсокета нам нужно их раскодировать, а при отправке закодировать.Всё в той же статье хорошо описано кодирование сообщений, но нам по-сути нужны только две функции: decode и encode.

Пример реализации функций decode и encode

function decode($data)
{ $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('b', ord($data[0])); $secondByteBinary = sprintf('b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; // unmasked frame is received: if (!$isMasked) { return array('type' => '', 'payload' => '', 'error' => 'protocol error (1002)'); } switch ($opcode) { // text frame: case 1: $decodedData['type'] = 'text'; break; case 2: $decodedData['type'] = 'binary'; break; // connection close frame: case 8: $decodedData['type'] = 'close'; break; // ping frame: case 9: $decodedData['type'] = 'ping'; break; // pong frame: case 10: $decodedData['type'] = 'pong'; break; default: return array('type' => '', 'payload' => '', 'error' => 'unknown opcode (1003)'); } if ($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = bindec(sprintf('b', ord($data[2])) . sprintf('b', ord($data[3]))) $payloadOffset; } elseif ($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; $tmp = ''; for ($i = 0; $i < 8; $i ) { $tmp .= sprintf('b', ord($data[$i 2])); } $dataLength = bindec($tmp) $payloadOffset; unset($tmp); } else { $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = $payloadLength $payloadOffset; } /** * We have to check for large frames here. socket_recv cuts at 1024 bytes * so if websocket-frame is > 1024 bytes we have to wait until whole * data is transferd. */ if (strlen($data) < $dataLength) { return false; } if ($isMasked) { for ($i = $payloadOffset; $i < $dataLength; $i ) { $j = $i - $payloadOffset; if (isset($data[$i])) { $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } } $decodedData['payload'] = $unmaskedPayload; } else { $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } return $decodedData;
}
function encode($payload, $type = 'text', $masked = false)
{ $frameHead = array(); $payloadLength = strlen($payload); switch ($type) { case 'text': // first byte indicates FIN, Text-Frame (10000001): $frameHead[0] = 129; break; case 'close': // first byte indicates FIN, Close Frame(10001000): $frameHead[0] = 136; break; case 'ping': // first byte indicates FIN, Ping frame (10001001): $frameHead[0] = 137; break; case 'pong': // first byte indicates FIN, Pong frame (10001010): $frameHead[0] = 138; break; } // set mask and payload length (using 1, 3 or 9 bytes) if ($payloadLength > 65535) { $payloadLengthBin = str_split(sprintf('4b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 255 : 127; for ($i = 0; $i < 8; $i ) { $frameHead[$i 2] = bindec($payloadLengthBin[$i]); } // most significant bit MUST be 0 if ($frameHead[2] > 127) { return array('type' => '', 'payload' => '', 'error' => 'frame too large (1004)'); } } elseif ($payloadLength > 125) { $payloadLengthBin = str_split(sprintf('6b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 254 : 126; $frameHead[2] = bindec($payloadLengthBin[0]); $frameHead[3] = bindec($payloadLengthBin[1]); } else { $frameHead[1] = ($masked === true) ? $payloadLength 128 : $payloadLength; } // convert frame-head to string: foreach (array_keys($frameHead) as $i) { $frameHead[$i] = chr($frameHead[$i]); } if ($masked === true) { // generate a random mask: $mask = array(); for ($i = 0; $i < 4; $i ) { $mask[$i] = chr(rand(0, 255)); } $frameHead = array_merge($frameHead, $mask); } $frame = implode('', $frameHead); // append payload to frame: for ($i = 0; $i < $payloadLength; $i ) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } return $frame;
}

Отладка приложений на веб-сокетах на php

Первое на что нужно обратить внимание, это на то, что ошибки разрабатываемого кода могут быть нескольких видов, но самые распространённые с которыми может столкнуться разработчик:

  • ошибка интерпретации PHP кода (E_PARSE) при ней выдаётся сообщение о невозможности запуска PHP кода указывая на строку и тип возникшей ошибки в исходном коде;
  • ошибка которая может возникнуть во время выполнения программы (E_ERROR), как правило выдаётся ошибка PHP Fatal error с описанием того, что конкретно привело к этой ошибке.

Пишем простенький ws клиент на javascript

Теперь, когда браузер подключится к WebSocket-серверу, он отправит ему сообщение.
В свою очередь, сервер получит сообщение, прочитает его, и отправит обратно браузеру,
после чего браузер выведет обычный alert(), показав dump изначально отправленного сообщения
в диалоговом окне браузера.

Пример в виде набора готовых файлов

Поднимаем websocket сервер на порту 8898

Затем запускаем получившийся скрипт из консоли:

Подсчёт количества пользователей он-лайн

Тоже очень простая задача, для этого в классе websocketserver_class заводим приватную переменную $online, задаём значение 0 в конструкторе.

При подключении пользователя

$this->online ; 

при отключении соответственно

$this->online --; 

Единственное, что теперь необходимо сообщать количество он-лайн пользователей всем подключенным клиентам, что я и делаю, передавая каждый раз всем сообщение, когда значение $this->online изменяется

Поставленные цели:

1) разобраться с серверными сокетами в php2) разобраться с протоколом вебсокетов3) написать с нуля простой сервер вебсокетов

Проксирование вебсокетов с помощью nginx

Nginx поддерживает проксирование вебсокетов начиная с версии 1.3.13. Благодаря nginx можно обрабатывать соединения к серверу вебсокетов на том же порту, что и сайт, а также ограничить количество открытых вебсокетов с одного ip и другие полюбившиеся вам плюшки.

Пример nginx-конфига, который это позволяет:

Разделение процессов на мастера и воркеров

Так как дочерние процессы в нашей реализации не связаны друг с другом напрямую и могут взаимодействовать только через родителя, то целесообразно разделение обязанностей между родителем и потомками:

Также воркер у нас будет заниматься пересылкой сообщений из скриптов со страниц сайта или из крона. Для этого мы создадим дополнительный сокет, и добавим его в массив, прослушиваемых сокетов. Например, можно создать unix-сокет:

$service = stream_socket_server('unix:///tmp/websocket.sock', $errorNumber, $errorString);

Фоновый процесс из php в ос windows

Решение оказалось не однозначным — изначально везде где нужно была добавлена проверка версии ОС на которой запущен PHP скрипт, и в зависимости от ОС следовало выполнение команды запуска ws сервера (демона).

if (strtoupper(substr(PHP_OS,0,3)) === 'WIN') { //Действия под виндой
exec("w:usrlocalphp5php.exe -q w:homelocalhostwwwwsws.php");
} else exec("php -q ws.php &");

Хранение логов

В комментариях под прошлой статьёй был вопрос о том, как организовать логирование чата в БД. Реализация этого очень проста, единственное, что вместо запроса INSERT в БД я сохраняю данные в лог-файл. Для этого я написал простую функцию chatlogmsg($msg), которая сохраняет всё в отдельный файл chatlog.html.

Чат на веб-сокетах

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

Оцените статью
Хостинги