Как стать автором
Обновить

Комментарии 76

Спасибо за код!
может пригодится) Но я бы добавил хотя бы strip_tags)
Не думаю. Скорее всего это связано с настройками окружения на сервере. Локально я тестировал, открывая 10к сокетов одновременно и проблем не было. Сейчас разбираюсь, в чём проблема на сервере.
то что вы открыли 10К соединений никак не покажет вам как сервер держит нагрузку.
Будет круто, если расскажите сколько памяти и процессора «съел» PHP и остальное при 10K подключиях.
около 90 мб на 10к соединений, т.е. 9кб на одно соединение
Хороший показатель с памятью! А что с процессором?
если сообщений нет, то процессор вообще не жрёт.
если отправить сообщение с одного клиента на остальные 10к, то на долю секунды скачёк до 20% процессора.
если в чате зажимаю и не отпускаю enter, то нагрузка до 50% процессора.
Если в линуксе iptables будет настраивать криворукий админ, тогда показатели процессора могут стать печальней)
50% процессора циферка довольно относительная. Подозреваю что у вас двухядерная система и выжирается ядро целиком.
Пока что (на момент написания комментария!), увы, демонстрационный чат небезопасен — спокойно выполняет любой пришедший JS.
Автор не дремлет, уже не выполняется.
Через рандомные промежутки отключает с сообщением в консоль (Хром 28.0.1500.95 убунта):
WebSocket connection to 'ws://sharoid.ru:8000/' failed: Could not decode a text frame as UTF-8.
спасибо. как временное решение: на закрытие повесил перезапуск соекта, теперь не выбрасывает.
Как приятно, что не перевелись еще Кулибины! ) Так держать
В данной реализации (если я ничего не пропустил) не обрабатывается вариант, когда одно сообщение содержит более 100000 байт. Или же по каким-то причинам одновременно прочиталось два сообщения. Я на этот случай обычно использую входящий буфер на каждый коннект. Сервер складывает все, что пришло от клиента, в этот буфер, а дальше происходит уже анализ содержимого этого буфера.
опять же в дополнение к такой обработке появляется потребность убирать блокировку сокета, чтобы (пока ничего не пришло) могли обрабатываться сообщения, уже имеющиеся в буффере, либо могли выполняться какие-то внутренние действия
Какой смысл в своем буфере, если такой буфер уже есть в операционной системе? Если вам не хватает дефолтного размера буфера чтения/записи, никто не помешает вам указать свой размер.
Смысл в том, сообщение может приходить кусками. Пришедший кусок нельзя не читать — иначе он так и будет сигналить о готовности чтения каждый раз — но и обработать прочитанное еще нельзя.

Отсюда, кстати, и валятся периодически ошибки у автора.
if (($connect = stream_socket_accept($socket, -1)) && $info = handshake($connect)) {
    $connects[] = $connect;//добавляем его в список необходимых для обработки
    onOpen($connect, $info);//вызываем пользовательский сценарий
}
$connects[] = $connect;//добавляем его в список необходимых для обработки

2 раза добавляете один и тот же сокет.

 $data = fread($connect, 100000);
if (!$data) { //соединение было закрыто

А вот и нет. Клиент может прислать «0».
Используйте strlen($data).

Еще ваш код, как и 90% мануалов по сокетам, не учитывает 3 вещи:
1. при чтении данные могли прийти не полностью
2. блокирующая запись (сокет может быть не готов)
3. данные записались не полностью

Лечить так:
1. буфер чтения
2. разобраться с использованием параметра $write для stream_select
3. буфер записи

Хорошие исходники для медитации:
github.com/reactphp/event-loop/blob/master/StreamSelectLoop.php
github.com/igorw/webserver-zceu
я для чтения из сокетов предпочитаю использовать более «низкоуровневые» вызовы *_recv
тогда отключение можно поймать, когда после селекта мы из сокета вычитываем 0 байт.
strlen($data) — это и есть количество байт, если там 0, значит клиент отключился.
Вы используете socket_recv? Оно работает с врапперами (ssl://)?
strlen — зачем нужен лишний вызов?
врапперы — очевидно, что нет. Тут уже все зависит от задачи, есть ли необходимость во врапперах или нет.
strlen — зачем нужен лишний вызов?

Вы правы (я и не спорил). В случае с socket_recv можно сэкономить на вызове strlen.

врапперы — очевидно, что нет

Мне было не очевидно, т.к. я socket_recv не использовал. Поэтому Вам и задал вопрос.
именно по этой причине (зависимость от использования сетевого протокола, ssl и т.д.) стоит использовать потоки, а не низкоуровневое api. Хотя все опять же зависит от задачи.
Всегда казалось что предпочтительнее использовать потоки…
спасибо, код в статье подправил.
я сначала хотел в статье написать ещё пример с использованием libevent, но решил, что для статьи, в которой «делаем простой сервер вебсокетов» этого будет слишком много.
Советую взглянуть на pecl/event, он предоставляет более высокоуровневый и объектный интерфейс. Документации, правда, толком нет, но примеров из bitbucket-а вполне достаточно, если есть опыт работы с либевентом.
Да, я его тоже пробовал, написал примеры, но в эту статью не стал впихивать.
В следующей статье приведу пример реализации на event и libevent для сравнения, кому что больше понравится.
клиент не может прислать 0, данные кодируются по протоколу вебсокета — если клиент отправит 0, то придёт ������
по-этому можно не делать strlen($data), а достаточно if (!$data)
Первое правило безопасности программ — данные, полученные из внешнего источника, могут быть какими угодно.
UPD: второе правило безопасности — на любую, даже самую мелкую, ошибку всегда найдется хакер, который положит все нафиг с ее помощью.
if (!$data) {@fclose($client);}
куда уже безопаснее? если пользователь в обход протокола запишет «0», то соединение будет разорвано.
если бы хакер прислал «0», то он не положил бы чат, а разорвал соединение.
чат зависал из-за того что использовалась fgets($client);, а она ожидает конец строки или таймаут.
т.к. конца строки вы не приходило, то она «зависала».
А если этот нолик случайно окажется пересланным в первом байте между мастером и воркером? Кончено, для создания такой ситуации нужно гнать ОЧЕНЬ много нулей через чат — но и эффект будет интересным…
клиент не может прислать 0, данные кодируются по протоколу вебсокета — если клиент отправит 0, то придёт ������
по-этому можно не делать strlen($data), а достаточно if (!$data)

В Вашей конкретной реализации — возможно.

В общем случае сервер состоит из:
1. цикл обработки событий (event loop) (абстрактный, не зависящий от протокола)
2. обработчик конкретного протокола

Цикл читает данные и передает в обработчик. Обработка закрытия/обрыва соединения находится в цикле (в общем случае).
Исходя из этих пунктов, !$data в цикле обработки использовать не стоит.
Т.к. цикл обработки использует stream_select, то сервер может получать данные по кускам.
Т.е. сообщение

Это стоит 1000$

Может прийти в виде

Эт
о с
тоит 1
0
00$

И на предпоследнем куске обработчик, проверяющий !$data, ошибочно закроет соединение. Каким бы способом сообщение не кодировалось, если там встречается «0» — соединение будет обработано неверно.
Про несколько процессов-воркеров будет крайне интересно увидеть статью.
алгоритмы prefork-серверов слабо зависят от языка реализации. Суть везде та же.
Не забывайте, что Вам нужно следить за жизнью этого демона. Рестартовать при падении, перезагружать, собирать логи и т.д. Тут у Вас, опять же, 2 пути:
1. Написать обвязку самому.
2. Использовать готовые решения.

Я Вам рекомендую использовать supervisord.org/ — это Вас лишит лишней головной боли.
Пример конфига можно увидеть тут: webadvent.org/2009/daemonize-your-php-by-sean-coates
Монстрообразность указанных в начале статьи библиотек обусловлена, как минимум наличием 4х протоколов вебсокетов (в вашем примере только один) и возможностью завести библиотеки вместе с libevent и pcntl (форки\многопроцессовость).
Мы используем HttpPushStreamModule для работы с сокетами из пхп. Поддерживает, к слову, много чего, включая деградацию до long-pooling.
А где можно поглядеть на пример взаимодействия php с этим модулем?
Где на пример взаимодействия с php посмотреть, не подскажу. Могу сказать, что взаимодействие сводится к http-запросам (например, записать что-то в канал = отправить запрос), но это, наверное, и так понятно.
Меня интересует вот что:

Publisher отправляет сообщение. Я его получаю из:
$_POST
php://input
STDIN
Какая-то библиотека

Откуда?

Дальше. Есть несколько subscribers. Я хочу отправить им это сообщение. Я пишу его в:
Сокет
Пайп
STDOUT
echo

Куда?
Хотите получить сообщение (сообщения) из канала — отправляете запрос.
Хотите отправить сообщение подписчикам — отправляете запрос.
Как именно отправить http-запрос из приложения — вам решать.
Хотите получить сообщение (сообщения) из канала — отправляете запрос.

Это polling получается. Я имел ввиду использование WebSockets… Что-то я запутался. Надо пробовать на практике.

В любом случае, спасибо за наводку.
Нет, клиент (из браузера) коннектится через веб-сокеты к каналу, открытому nginx. Приложение (бэк-энд) читает и пишет в канал, используя http запросы.
не будет ли проблемой то, что handshake выполняется сразу же после accept? Не будет ли fgets блокировать поток? Я имею в виду ситуацию, при которой любой сможет положить ваш comet-сервер просто установив соединение с сервером но не посылая никаких данных…
Будет и блокирует. Я проверил.
Чат замер. Уже весь Хабр проверяет.
На самом деле это легко чинится. Просто нужно вынести handshake в цикл и разруливать необходимость оного через socket_select. А еще не плохо вводить таймауты, что бы отбрасывать долговисящие пустые соединения.
сделал рукопожатие неблокирующим, как вы и написали.
итоговый код в статье обновил.
имеет смысл открыть для себя библиотеки libev/libevent/libevent2
$isMasked = ($secondByteBinary[0] == '1') ? true : false;

wat?
Вешается чат довольно таки просто — telnet sharoid.ru 8000…
добавил неблокирующую запись и неблокирующее рукопожатие.
итоговый код в статье обновил.
теперь ваша команда не сработает.
это потому что использовалась функция fgets($client);, а она ожидает конец строки или таймаут.
т.к. конца строки вы не передавали, то она «зависала».
поменял на fread? теперь это не сработает, исправил итоговый код в статье.
… а проверку на пустоту массива вы так и не исправили…
while true; do nc sharoid.ru 8000 < /dev/zero ; done
phpdaemon и пр. не от хорошей жизни такие «монструозные», а из за того, что, в том числе, над безопасностью думают.
это статья про то как работают вебсокеты, вы слишком ответственно отнеслись к тестированию :)
мне казалось, что часа вам будет достаточно, чтобы убедиться в своей правоте и перестать глушить мой сервер, который я использую не только для тестов, но видимо я ошибался.
В смысле? Так я один раз попробовал, отправил комментарий и забыл. Кто то ещё запустил что-ли? Ну извините, возможно стоило в личку написать.
Поставьте Nginx на фронт — он с недавних пор хорошо websocket проксирует и такой флуд легко отсекает. Плюс сможете websocket на 80 порту держать.
Причем тут phpdaemon? На самом деле, вы успешно провели атаку на замеченную ранее ошибку с некорректной проверкой длины.
Если вы хотите довести этот пример до возможности использования в реальной жизни — то настоятельно рекомендую прочитать вот эту статью: www.kegel.com/c10k.html
Вот Вам еще пара ссылок по сокетам и select:
linux.die.net/man/3/select
beej.us/guide/bgnet/output/html/singlepage/bgnet.html

Там примеры на C под UNIX, но они здорово помогают глубже и полнее понять асинхронную работу с select.

P.S.
Путь «велосипеда» — это правильный путь. Правильный для обучения.
Но чем больше начинаешь понимать всяких нюансов, тем чаще тебя начинает посещать мысль, что не такие уже и монструозные, на самом деле, те готовые решения, использования которых ты решил избежать.
Рекоммендовал бы еще рассмотреть случай, когда в сетевой поток могла бы прекратиться запись до того, как будет записано все.
Пример расписан в оффициальной документации us2.php.net/manual/ru/function.fwrite.php
Я с PHP года 4 уже не работал, так что могу не знать нюансов, сужу по косвенным признакам… Вы написали, что переделали работу с сокетами на неблокирующую, но я нигде в коде не вижу вызовов stream_set_blocking да и read/write/fgets у вас используются так, как использовались бы с блокирующими сокетами. Мне кажется они у вас блокирующие…
Пример — вызов fgets (его вы уже выпилили) возвращает в 3-х случаях: сокет закрыт, достигнут лимит или найден "\n". Как она работает с неблокирующими сокетами вообще не представляю, но совершенно точно что нужно удостовериться, что последний символ в считанных данных это "\n".

Дальше fwrite: на неблокирующих сокетах он не обязан записывать всё, что ему сказали записать — сколько он запишет зависит от размера буферов, так что нужно делать что-то вроде
if (length($data) != ($written = fwrite($data))) {
    $data = substr($data, $written),
}
и дописывать остаток когда сокет снова станет записываемым.

Насчёт read та же фигня — в неблокирующем режиме он может вернуть хоть один байт (привет if(!$data)) хоть всё вплоть до $length. Так что всегда нужно проверять достаточно ли данных считалось и если нет — сохранять в буфер и дожидаться, пока сокет снова станет readable.

Тот факт, что сокеты у вас всё-же блокирующие, позволяет, по идее, заблокировать воркера послав, например, всего один байт, в то время как сервер делает read($fd, 10000). Хотя насчёт этого не совсем уверен, возможно в PHP там промежуточные буферы какие то есть.
NodeJS + socket.io, нет?
foreach($read as $connect) {//обрабатываем все соединения ...обрабатываем $connect
разве не foreach($read as $connects)?
$read это массив сокетов, по сути соединений. Так что у автора все правильно.
Понял. Благодарю за разъяснение, мозги в первой половине дня не в ту сторону повёрнуты)
$read — это массив сокетов, готовых для чтения
Как запускать сторонние скрипты из функций onMessage?
fwrite($connect, encode(Test::prt(decode($data)['payload'])));


Метод prt делает конкатенацию входящего сообщения с заданной строкой.
public function prt($m){
     return $m . 'work!';
}


В результате корректно работает раз от раза:
work!
work!
fdwork!
work!
work!
fdwork!


В чем причина?
Почему бы не использовать wsphp? Нативно, шустро, не ест память…

ну в моем случае причины такие:


  • не нашел ссылку на исходники (не zip там а vcs какая)
  • не нашел ишус трекера
  • не знаю кто автор
  • C/C++ я знаю плохо, go знаю лучше.
  • если мне нужно будет ws <-> http rpc то я возьму pushpin у которого есть нормальное комьюнити и документация.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.