На днях опубликовал свой класс для развёртывания веб-сокет сервера.

Веб-сокеты — это технология, позволяющая устанавливать непрерывное соединение между клиентом и сервером. Особенность такой системы также в том, что сервер может по своей инициативе отправлять данные одному или нескольким клиентам. Это позволяет создавать real-time мессенджеры, онлайн-игры и прочие проекты.

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

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

Чтобы использовать класс, просто включите его в ваш скрипт

require_once('../src/websocket.class.php');

Сразу должен предупредить, что запускаться сервер будет не через браузер, как вы привыкли, а через консоль. В репозиторий на GitHub я включил примеры BAT и SH файлов для исполнения.

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

Чтобы уберечь сервер от падений, напишите данный код:

error_reporting(E_ERROR);
set_time_limit(0);
ob_implicit_flush();

Для начала нужно создать экземпляр данного класса:

$socket = new WebSocket('tcp://', '127.0.0.1', '7777');

Как видите, во время создания надо указать протокол, IP и порт будущего сервера. Обязательно убедитесь, что данный порт открыт и IP правильный.

При желании можете записывать логи в файл:

$socket->setOutput('ws-log.txt');

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

$socket->handler = function($connection, $data) 
{
 fwrite($connection, WebSocket::encode($data)); 
};

Наследуя добрые традиции JS, вы должны записать в переменную handler анонимную функцию. Она будет вызываться каждый раз, когда клиент отправит сообщение серверу. Если вы собираетесь прямо оттуда отправлять ответ, не забывайте кодировать данные при помощи статической функции WebSocket::encode.

Немножко отклонений. Соединение — это ресурс. Его можно читать и дополнять так же, как и обычный открытый файл — при помощи fwrite и fread.

Теперь о функции «сердцебиения». Данная функция будет вызываться во время каждой итерации (чаще, чем раз в секунду).

$socket->hearthbeat = function($connects)
{

}

Во время вызова функции в первом аргументе будет содержаться массив со всеми установленными соединениями. «Сердцебиение» может использоваться для регулярных проверок и других подобных целей.

Последнее, что нам пригодится — собственно запустить сервер. Делается это при помощи метода runServer:

$socket->runServer();

После этого будет запущен вечный цикл. Ваш веб-сокет сервер готов.

Буду очень благодарен любым отзывам или предложениям. Скрипт будет дополняться по мере возможности.

Комментарии (53)


  1. andrewnester
    15.09.2015 20:44
    +3

    Чем Вас Ratchet не устроил?


    1. mikechips
      15.09.2015 20:46
      -7

      Да я и не пробовал его. Просто написал своё.


      1. andrewnester
        15.09.2015 20:51
        +7

        Хорошее решение


        1. mikechips
          15.09.2015 20:52
          -3

          Если честно, во время написания своего я не слышал о Ратчет… у меня тут минимализм, а там мощная продуманная платформа. Кому что надо…


          1. andrewnester
            15.09.2015 20:57
            +1

            Предположим с клиента делается запрос на получение каких-то данных, серверу предположим надо на это 500мс. Таких запросов одновременно от 20,50,100 клиентов. Что будет с Вашим сервером?


            1. mikechips
              15.09.2015 21:31
              -6

              Я тестил лишь при 15-ти реальных пользователях, всё работало бесперебойно. Тесты потом ещё сделаю


              1. igordata
                15.09.2015 21:47

                Интересно, что с ним будет при over 9000 подключениях =)


              1. andrewnester
                15.09.2015 22:11
                +8

                итак, код сервера

                include_once __DIR__ . "/src/websocket.class.php";
                
                error_reporting(E_ERROR);
                set_time_limit(0);
                ob_implicit_flush();
                
                $socket = new WebSocket('tcp://', '127.0.0.1', '7777');
                
                $socket->handler = function($connection, $data)
                {
                    sleep(1); // some long operation 
                    fwrite($connection, WebSocket::encode($data));
                };
                
                $socket->runServer();
                


                Результаты тестов
                1 запрос
                image


  1. grayfolk
    15.09.2015 22:16
    +6

    Чтобы уберечь сервер от падений, напишите данный код:
    set_time_limit(0);

    Если уж на то пошло, то
    When running PHP from the command line the default setting is 0.


    1. mikechips
      15.09.2015 23:01
      -2

      Да, вы правы, пожалуй. Правда есть извращенцы, которые запускают сокет-сервер через браузер… он работать-то будет, но тайм-лимит там будет стоять…


  1. haskel
    15.09.2015 22:31
    +12

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


  1. AlexLeonov
    15.09.2015 23:21
    +5

    Чтобы использовать класс, просто включите его в ваш скрипт


    PSR, неймспейсы и composer придумали трусы, для рисковых парней есть require_once?

    Каждый раз, когда вижу такое, смотрю на календарь. Нет, я не ошибся. У меня 2015 год. А у вас?


    1. zviryatko
      16.09.2015 09:39
      +1

      Мм… Это еще что, мне тут недавно объясняли что namespace это прямая замена для require.


      1. Temirkhan
        16.09.2015 10:57

        Может Вы не так поняли, и человек имел ввиду autoload, основанный на namespace'ах?


        1. zviryatko
          16.09.2015 11:04

          Естественно autoload, только он имел ввиду что лучше использовать require вместо use. А на вопрос когда таких строк становится слишком много отвечал что подключает через require файл со списком других require. А про пользу расширения пространства имен он и слышать не хотел.


    1. mikechips
      16.09.2015 15:18
      -1

      Проекты вообще разные, в некоторых аутолоадер не используется, и тем более в данном случае существует лишь один класс… аутолоадер принципиально не нужен, если у кого-то проекты с множеством файлов — пусть используют свою автозагрузку, я ведь не могу предугадать все случаи


  1. zelenin
    15.09.2015 23:28
    +6

    require_once('../src/websocket.class.php');

    после этого сразу перешел к комментариям :-)


    1. Temirkhan
      16.09.2015 13:04

      Как говорил Печкин, в целях повышения самообразования, что не так с конструкцией require?)


      1. zelenin
        16.09.2015 13:05
        -4

        неиспользование в проекте composer


        1. Metus
          16.09.2015 13:37
          +1

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

          Тем более что composer -V:
          Composer version 1.0-dev (b4bd5774dc7c2a3be18d31d55583492a7c7c2ebe) 2015-09-16 10:01:25


          1. zelenin
            16.09.2015 13:42
            -4

            1. а) потому что его библиотеку нельзя установить через композер б) потому что никто не будет использовать его библиотеку без других
            2. ой-ой, как страшно — станадарт де-факто в индустрии представляется дев-версией :-(


            1. Mendel
              16.09.2015 14:18

              это ТЕСТОВЫЙ пример, хоть и оформленный в виде библиотеки.
              очень хорошо что не потянешь через композер. Будешь думать лишний раз — использовать ли этот концепт-пруф на продакте.


              1. zelenin
                16.09.2015 14:26

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


                1. Mendel
                  16.09.2015 14:52

                  ну это да, но на это топикстартеру уже указали и не раз, и с более конкретными замечаниями, а не с оформлением…


                  1. zelenin
                    16.09.2015 17:49

                    это не оформление — это уровень.


                    1. Mendel
                      16.09.2015 19:40

                      Ну меня лет семь назад тоже так носом тыкали в мои велосипеды. Тогда не понимал…
                      Иногда можно и так писать, даже профессионалу. Но нужно понимать КОГДА это можно.
                      К примеру в документациях часто ужасы пишут, но это примеры, а не сниппеты.
                      Вот у меня сейчас простенький «проект» килобайт на 150 кода.
                      Примерно вот так вот и написан. Никаких автозагрузчиков, нормального наследования, структуры, слоев, MVC? о чем вы?)
                      И да, я даже в некоторых реальных задачах его использую. Но это даже не прототип. Я просто разбираюсь с форматами, протоколами и т.п. Там реально в живой проект уйдут одни константы и тому подобное.
                      Я конечно соблюдаю стандарты кодирования которые я сам же себе и принимал, но тут руку сбивать себе дороже.
                      Но в плане структуры…


                      1. zelenin
                        16.09.2015 19:59

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


                        1. Mendel
                          16.09.2015 20:58

                          Согласен, но человеку уже столько раз это сказали, что смысл тыкать7)


  1. SerafimArts
    16.09.2015 00:43
    +1

    Дабы не перелопачивать исходник, небольшой вопрос — какой из протоколов вебсокетов он поддерживает? RFC6455, Hixie76, HyBi10?


  1. Ramzeska
    16.09.2015 00:54
    -9

    Увы это велосипед. PHP отличный язык, но не предназначен работать 24/7, не поддерживает многопоточность и рано или позно начнет память течь, а сервер грохнется. Есть куда более живучие решения на node.js или go.
    В защиту скажу что это полная ерунда на вебсокет-сервере запускать долгие задачи — вебсокет должен быстро пулять сообщения от отправителя (сервера, демона, пользователей). На этом его задача должна заканчиваться.
    Кстати ваше решение как раз будет очень неплохим для кастомных вещей типа веб-консоли или чего-то подобного, где не нужен хайлоад и нужна развертка с минимумом зависимостей.

    Оффтоп про питон
    В старые добрые времена(года 3-4 назад), когда вебсокет протоколов было где-то штук пять я юзал торнадо и обновлял библиотеку раз в месяц, потому что протокол постоянно обновлялся. Питон еле-еле справлялся с нагрузкой — приходилось запускать 4 штуки что бы обработать 1500 коннектов — память текла в неизвестном направлении после 400-500 коннектов…


    1. Rathil
      16.09.2015 01:43
      +3

      Не согласен. Дописывали phpDaemon до нормального состояния, 3 года назад. Даже род нагрузкой, спустя месяц, и на мегобайт не протек.


      1. Ramzeska
        16.09.2015 11:18

        Я пробовал phpdaemon, код там ужасен, высыпал тонну ошибок, когда я наконец-то его починил, то понял что он не поддерживает современные протоколы(на тот момент). Я забил на него и перешел на торнадо.


    1. SerafimArts
      16.09.2015 02:06

      Не предназначен был во времена 4ки, когда память действительно подтекала.


      1. Ramzeska
        16.09.2015 11:29

        Память в редких случаях течет и сейчас. Тут вопрос о чистоте кода. При написании таких приложений надо в циклах проверять память и если что-то течет — то бегать по коду и искать место где поставить unset(). Сборщик мусора в php совсем не такой как в других языках — он от всего не спасет.


        1. SerafimArts
          16.09.2015 17:27

          Имею ввиду что в 4ке без контроля пользователя (разработчика), а своими силами улетучивалась. Да и наличие объектного пула (начиная с php 5.3 вроде, по аналогии с интеджер-пулом джавы), всяких Spl структур, вроде ObjectStorage, FixedArray, Heap и т.д. добавляют намного больше контроля над памятью в «реалтайм» приложениях.


    1. symbix
      16.09.2015 02:23

      Вебсокет-сервер на Ratchet запущен уже третий месяц, ничего не течет, что я делаю не так?


      1. Ramzeska
        16.09.2015 11:24

        Какой пиковый онлайн?


        1. symbix
          16.09.2015 14:40

          Реально пока было чуть больше 200, на нагрузочных тестах тянуло 3k на куда более слабом сервере, чем тот, что в продакшене — а для задачи этого с запасом.

          Отсутствию циклических ссылок уделялось особое внимание, каэш.


          1. SerafimArts
            16.09.2015 17:35

            Хм, а как так получилось в тестах 3к, если ратчет отсекает больше 255 одновременных подключений по-дефолту? Треды\форки? Или какой-то хитрый способ запиливания нескольких процессов с разными айпишниками, балансировщиком и общением через шаред мемори?


  1. AlexGx
    16.09.2015 03:34
    +1

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

    ошибся веткой, комментарий для Ramzeska


    1. Ramzeska
      16.09.2015 11:46

      Может я действительно уже устарел и не в курсе? В php нет инструментов ни для многопоточной работы, ни для микропотоков (типа goroutines)
      Расскажите как в одном php процессе обрабатывать websocket сообщения и выполнять долгие задачи не повесив всех клиентов?


      1. Mendel
        16.09.2015 12:07

        форк к примеру.
        Но тут правильно сказали, что вебсокет не разумно загружать тяжелыми задачами, лучше выполнять их отдельно.
        Вы в аяксе как делаете? отправил запрос, когда пришел ответ, обработали? ну вот так и тут делайте.
        демон принимает запросы извне, и передает им ответы. Если добавить тут еще один интерфейс работающий с бекэендом выполняющим тяжелые задачи, то ничего так уж принципиально не поменяется…


      1. SerafimArts
        16.09.2015 13:00
        +2

        1) Генераторы\корутины (yield\Generator) http://php.net/manual/ru/language.generators.syntax.php
        2) Thread http://php.net/manual/ru/class.thread.php
        3) event, libebent, etc
        4) pcntl fork
        5) exec\system на крайний случай + redis\shared memory\etc


        1. Ramzeska
          16.09.2015 16:51

          1. Генераторы мягко говоря не особо то
          2. pthread — интересная библиотека, давно появилась? Норм работает? Память не жрет?
          3. libevent — очень узкая специализация, да и работать с ней не очень то просто и удобно… зато тянет очень много коннектов
          4. ну форк понятно, жрет много памяти, 10к клиентов так не подключить
          5. exec/system кстати я бы не называл крайним случаем — наилучший вариант поднять rabbitmq или redis pub/sub и на него цеплять кучу микросервисов.
          Мой вебсокет-сервер именно так и работает — все сообщения летают через редисовый канал, две-три штуки втыкаются в редис и распределяют клиентов между собой. Аналогично к редису цепляются мелкие демоны, выполняющие разные задания. Пхп из фронтенда кидает сообщения в редис, а вебсокет-серверы сами разберутся кому что доставить.


          1. SerafimArts
            16.09.2015 17:40

            Ну «на крайний» я погорячился в случае п.5, действительно. Сами года 2 назад организовывали вебсокеты таким же образом. Только в том случае были несколько воркеров Event Machine'ы (специфика окружения), общение через редиску как раз, у неё эвенты есть и перманентная работа в памяти, так что работает (работало) «на ура».


  1. pewpew
    16.09.2015 08:12
    +1

    Автор — молодец.
    Хвалю за велосипедостроение. Я считаю, любой велосипед — это тернистый, но СВОЙ путь к пониманию темы.
    На продакшин такое я бы ставить рискнул, но разобраться с веб сокетами на привычном языке (если до этого не было опыта) вполне позволяет.


    1. AmdY
      16.09.2015 19:53

      Не хочется разочаровывать вашу веру в людей, но я слазил посмотреть исходники и сразу напрягла помесь английских комментатриев с русскими. Погулив нашёл ряд похожих материалов от разных авторов
      http://bb3x.ru/blog/delaem-vebsoketyi-na-php-s-nulya/
      https://gist.github.com/Xeoncross/8b5fc1deb46f13f23ffa
      https://github.com/brebvix/php-websocket/blob/master/socket.php


      1. pewpew
        16.09.2015 20:05

        Нда… Это почти копипаста.
        Причём первый код опубликован от некоей Анны, второй — копипаста первого на гитхабе год назад. Написаны функционально.
        Третей — обремлён в класс за авторством какого-то Назара полгода назад и почти полностью совпадает с кодом топикстартера.
        Лицензии не указаны, и автор счёл, что код можно использовать как угодно, опубликовав свой вариант под лицензией MIT.
        Куда катится мир…


        1. mikechips
          16.09.2015 20:46
          -1

          Если честно, я просто использовал один урок, на его основе создав шаблон. Решения выше впервые вижу


  1. Rhaps107
    16.09.2015 08:32
    +1

    На половине методов нет модификаторов доступа, опечатка в слове heartbeat, реализация только одного протокола вебсокетов, без возможности расширения, использование цикла вместо event polling (увеличивает нагрузку), из композера поставить нельзя, я правильно понимаю?

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


  1. Groove
    16.09.2015 08:51
    -1

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

    А вот как сделать так, чтобы с сервера отправить сообщение в канал

    Пример: кроном раз в минуту забирается результаты матчей с удаленного сервера. Записываются в базу.
    Те, кто откроет страницу — им результаты покажутся из сгенерированной страницы.
    Как уведомить тех, кто не обновлял страницу?


    На реалплексоре Котерова такую задачу решали на раз-два, но таскать его и устанавливать за собой проблематичнее, чем установка того же BrainSocket из композера и запуск сервера.


    1. Groove
      16.09.2015 09:13

      toster.ru/q/248997 перенес в вопросы ответы коммент


    1. andrewnester
      16.09.2015 09:20

      Как насчет просто отправить данные клиенту по открытому сокет-каналу? Есть еще такая вещь broadcast сообщения