Простой веб-сокет на PHP или веб сокеты с абсолютного 0

Или как работать с WebSocket на простом PHP хостинге

Или getting started with WebSocket PHP без phpDaemon

Здравствуйте! Простите за столь длинный заголовок, но, надеюсь, что новичкам вроде меня будет легче найти эту статью, ведь мне ничего подобного найти не удалось. Несколько недель назад я принял решение переработать игровой клиент и сервер своей игры Growing Crystals с AJAX, на WebSocket, но всё оказалось не просто непросто, а очень сложно. Поэтому я и решил написать статью, которая бы помогла самым что ни на есть начинающим разработчикам на WebSocket + PHP сэкономить несколько дней времени, максимально подробно объясняя каждый свой шаг по настройке и запуску первого WebSocket скрипта на PHP.

Что у меня есть: Денвер на локальной машине, на нём я веду разработку проекта и дешевый PHP хостинг, на котором я публикую свой проект для того, чтобы получить обратную связь от Интернет-пользователей.

Что я хочу: Без установки phpDaemon (phpd), NodeJS и прочих вещей на локальную машину и хостинг, продолжить разработку своего проекта, но теперь с WebSocket, в этой статье разберем простой WebSocket эхо сервер.

Чего я не хочу: Говоря о NodeJS, не хочется переписывать серерную логику с PHP на другой язык, тем более устанавливать NodeJS, хотя и люблю JavaScript больше чем PHP.

Важно: не путайте демона написанного на php, с фреймворком асинхронных приложений phpDaemon. Который, конечно же, обязательно потребуется в случае развития проекта и многократного роста нагрузки на хостинг. Но для начала работы с WebSocket на дешевом хостинге можно обойтись и без него.

Проверка поддержки сокетов на хостинге и в Денвере

— Что нужно сделать в первую очередь для начала работы с WebSocket?
— Проверить поддержку сокетов на хостинге и в Денвере.

Для этого создаём простенький файлик sockettest.php

<?
if(extension_loaded('sockets')) echo "WebSockets OK";
else echo "WebSockets UNAVAILABLE";
?>

Загружаем на хостинг и обращаемся к нему через браузер, и, здесь может быть самое неприятное, хостинг выдаст WebSockets UNAVAILABLE. В таком случае нужно искать другой хостинг, либо, если есть возможность поменять в настройках хостинга версию и комплектацию PHP на PHP с sockets, добиваться того, чтобы хотсер волшебным образом это сделал. Если хостер такой возможности не предоставляет, то тогда при выборе нового хостинга, нужно смотреть на php_info(); и по нему определить поддержку сокетов по наличию следующей строки. А для того, чтобы сокеты заработали в Денвере, пришлось немного покопаться.

sockets-enabled

В моём случае хостинг заработал с первой попытки, что в 2014 году вероятно на 80%.

Добавление веб-сокетов в Денвер

Теперь задача настроить Денвер. Мне довелось работать с разными сборками Денвера. Последняя из них Denwer3_Base_2013-06-02_a2.2.22_p5.3.13_m5.5.25_pma3.5.1_xdebug.exe, к сожалению, она и другие имеющиеся в настоящий момент сборки Денвера(дело в PHP) не поддерживают сокеты по умолчанию.

Но эта проблема решается путём поиска и установки подходящей php_sockets.dll. Для того, чтобы всё заработало, достаточно разместить dll файл в каталоге Денвера \usr\local\php5\ext\php_sockets.dll и отредактировать файл \usr\local\php5\php.ini убрав точку с запятой перед строкой

extension=php_sockets.dll

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

waring

это значит что вы используете не подходящую версию файла php_sockets.dll. Чтобы облегчить поиски предлагаю две версии — одна гарантированно подходит для PHP 5.2.12 (скачать), а вторая для гарантированно подходит для PHP 5.3.13 (скачать). Но если вам не подошел ни один из этих файлов, предлагаю скачать полный архив соответствующей версии php для windows с веб-сайта php.net и найти в архиве файл php_sockets.dll, который точно подойдёт.

Теперь можете запустить файлик sockettest.php через браузер и увидеть заветную строку «WebSockets OK», означающую что всё необходимое для работы с Web-Socket установлено и работает хорошо.

От сокета к веб-сокету

Теперь, когда модуль сокетов активирован в PHP, нам предстоит путь от написания простого сокет скрипта, до скрипта способного корректно общаться по протоколу веб-сокет. Дело в том, что PHP умеет только технически работать с сокетами: принимать соедние, заводить очередь, отправлять/принимать данные, находиться в режиме ожидания и другие технические вещи. Но в нём совершенно отсутствуют готовые заголовки и другие метаданные необходимые для веб-сокетов. Обычное сокет-соединение отличается от веб-сокетов тем, что мы можем отправлять любые сообщения в произвольном формате, в то время, как веб-сокет требует обязательной отправки заголовков вида «GET / HTTP/1.1\r\nUpgrade: websocket\r\n» и соблюдение достаточного количества других правил.

Есть еще один важный нюанс, теперь касающийся PHP — скрипт обрабатывающий сокет-соединения отличается от обычного скрипта PHP, который многократно выполняется от начала до конца при разгрузке страницы обычно менее чем за 0,1 секунды. Отличаются скрипты работы с WebSocket-ами тем, что длительность их выполнения должна быть бесконечной. Т.е. мы должны инициировать выполнение PHP-скрипта содержащего бесконечный цикл, в котором происходит получение/отправка сообщений по протоколу веб-сокет. На самом деле с этим не должно быть никаких проблем, т.к. существует большое количество способов снять ограничение по времени выполнения PHP скрипта, помимо этого можно запустить скрипт в бэкграунде пользуясь консолью сервера. На PHP вполне возможно даже создание полноценного демона. Существуют готовые решения, о которых я упоминал выше, например PhpDaemon, но сегодня речь не о них.

Для начала, чтобы убедиться что мне не мешают файрволлы, всё настроено правильно и связь между клиентом и сервером может быть установлена, я решил написать и протестировать небольшой PHP скрипт выполняющий роль сокет-сервера (именно сокет, а не веб-сокет!), устанавливающий соединение и отвечающий всем фразой «Hello, Client!». Но для его тестирования нужно несколько клиентов (веб-сокет клиент, чтобы понять базовое отличие от простого сокета и обычный telnet).

Клиент на html+JavaScript для веб-сокет соединения

Для тестирования скрипта сокет-сервера нам понадобится несколько клиентов, один из них telnet (я использовал Putty), второй, веб-сокет клиент, написанный на html+JavaScript. Вы можете не устанавливать себе Telnet, его я использовал исключительно, чтобы разобраться в том, что отправляет сервер и почему я не могу это увидеть в браузере. Приведу код веб-сокет клиента, написанного на html+JavaScript.

socket.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Siple Web-Socket Client</title>
</head>
<body>
<br /><br />

<script src="socket.js" type="text/javascript"></script>

Server address:
<input id="sock-addr" type="text" value="ws://echo.websocket.org"><br />
Message:
<input id="sock-msg" type="text">

<input id="sock-send-butt" type="button" value="send">
<br />
<br />
<input id="sock-recon-butt" type="button" value="reconnect"><input id="sock-disc-butt" type="button" value="disconnect">
<br />
<br />

Полученные сообщения от веб-сокета:
<div id="sock-info" style="border: 1px solid"> </div>

</body>
</html>

Веб-сокет клиент должен иметь возможность подключаться/отключаться к веб-сокетам, отправлять сообщения, выводить полученные ответы. По умолчанию, в качестве веб-сокет сервера выступает ws://echo.websocket.org, т.к. это гарантированно работающий ws(веб-сокет, далее везде ws) echo сервер, на котором можно убедиться в работоспособности нашего веб-сокет клиента.

socket.js

"use strict"; //All my JavaScript written in Strict Mode http://ecma262-5.com/ELS5_HTML.htm#Annex_C

(function () {
// ======== private vars ========
var socket;

////////////////////////////////////////////////////////////////////////////
var init = function () {

socket = new WebSocket(document.getElementById("sock-addr").value);

socket.onopen = connectionOpen;
socket.onmessage = messageReceived;
//socket.onerror = errorOccurred;
//socket.onopen = connectionClosed;

document.getElementById("sock-send-butt").onclick = function () {
socket.send(document.getElementById("sock-msg").value);
};


document.getElementById("sock-disc-butt").onclick = function () {
connectionClose();
};

document.getElementById("sock-recon-butt").onclick = function () {
socket = new WebSocket(document.getElementById("sock-addr").value);
socket.onopen = connectionOpen;
socket.onmessage = messageReceived;
};

};


function connectionOpen() {
socket.send("Connection with \""+document.getElementById("sock-addr").value+"\" Подключение установлено обоюдно, отлично!");
}

function messageReceived(e) {
console.log("Ответ сервера: " + e.data);
document.getElementById("sock-info").innerHTML += (e.data+"<br />");
}

function connectionClose() {
socket.close();
document.getElementById("sock-info").innerHTML += "Соединение закрыто <br />";

}


return {
////////////////////////////////////////////////////////////////////////////
// ---- onload event ----
load : function () {
window.addEventListener('load', function () {
init();
}, false);
}
}
})().load();

Логика скрипта JavaScript также максимально проста. При загрузке пытаемся подключиться по адресу ws сервера по умолчанию. Выполняем функции приёма отправки сообщений. Я специально для простоты понимания кода не использую jQuery и другие библиотеки. В скрипте используются команды создания веб-сокета (что означает автоматическое подключение), отправки сообщения и закрытия.

var socket = new WebSocket(address); //Создание и подключение к address
socket.send(msg); //Отправка сообщения msg
socket.close(); //Закрытие соединения

и обработчики событий onopen, onmessage. Которые вызываются при открытии соединения и при получении сообщения соответственно. К сожалению, если вы создадите эти два файла и запустите клиента через Денвер с localhost, то у вас возникнут проблемы с кодировкой т.к. Денвер по умолчанию использует CP1251, не забывайте явно прописать UTF-8 в .htaccess. Или скачайте архив данного ws клиента, содержащий также и сокет-сервер.

Код сокет-сервера на PHP я приводить не буду, т.к. он есть в архиве и снабжен комментариями. Я искусственно ограничил время работы приёмки соединений 100 секундами, чтобы скрипт он не оставался в памяти, закрывал все соединения и не приходилось при внесении в него изменений постоянно перезагружать Денвер. При чем, по прошествии 100 секунд, скрипт не прекратит свою работу, а будет ждать подключения, после которого его работа будет завершена, а все сокеты благополучно закрыты. Также, для тестирования, я использую localhost и порт 889.

	socket_bind($socket, '127.0.0.1', 889);//привязываем его к указанным ip и порту

Файлы из архива можно поместить в корневую папку localhost Денвера. Алгоритм тестирования:

  1. Запускаем http://localhost/socket.html, он автоматически подключится к ws echo серверу ws://echo.websocket.org, можно отправить несколько сообщений, которые сервер должен отправить обратно, ведь это же эхо-сервер. Если это работает — отлично, с клиентом всё в порядке, если нет, проверьте все ли вы файлы разместили в одном каталоге, запущен ли у вас Денвер, и есть ли соединение с сетью Интернет.
  2. Откройте в этой же вкладке где у вас открыт ws-клиент JavaScript консоль (В GoogleChrome 34.8.1847.137 m и в FireFox это делается почти одинаково меню->инструменты->консоль Java Script или Ctrl+Shift+J, но лично я использую для этого консоль FireBug). Отправьте еще несколько сообщений. Вы увидите какие сообщения приходят от сервера. Вы можете нажать disconnect и после этого отправить несколько сообщений, вы убедитесь что сообщения не уходят, т.к. связь с сервером ws://echo.websocket.org разорвана.
  3. Запускаем в новой вкладке браузера наш сокет-сервер http://localhost/simpleworking.php. Желательно чтобы вы сразу могли видеть и вкладку клиента с консолью и вкладку сервера. Должно появиться
    GO() ... 
    socket_create ...OK 
    socket_bind ...OK 
    Listening socket... OK 
    

    Что означает, что сервер готов к входящим соединениям.

  4. Во вкладке клиента в поле Server address вводим ws://127.0.0.1:889 и нажимаем reconnect, мы видим что на клиенте ничего не происходит, а на сервере появляются сообщения вида
    Client "Resource id #3" has connected
    Send to client "Hello, Client!"... OK 
    Waiting... OK 
    

    Что говорит нам о том, что технически соединение с сокетом на сервере установлено, а на клиенте ожидается соединение по протоколу веб-сокет, и оно не установлено, т.к. браузером не получены соответствующие заголовки протокола ws. При попытке отправить сообщение с клиента, в консоли должна появиться ошибка о том, что соедние с ws не установлено. К сожалению, скрипт в GoolgeChrome остановится и для новых попыток подключения придётся перезагружать страницу с веб-клиентом. В FireFox скрипт продолжает выполняться. Обязательно сделайте reconnect спустя 100 секунд, после запуска скрипта сервера, чтобы дать ему благополучно завершиться.

    Client "Resource id #4" has connected
    Send to client "Hello, Client!"... OK 
    time = 309.8900001049return 
    go() ended
    Closing connection... OK 
    
  5. Чтобы окончательно убедиться в том, что сервер отвечает, что его сообщения не блокируются файрволом, запустите скрипт сервера http://localhost/simpleworking.php запустите telnet и попытайтесь подключиться к 127.0.0.1:889, только это нужно сделать не позднее 100 секунд, с момента запуска сервера, пока он не закрыл соединения и не завершил скрипт.

По telnet должен придти ответ «Hello, Client!», что свидетельствует о том, что всё работает в штатном режиме и связь с сервером двухсторонняя.

Ответ сокет сервера при попытке подключения по Telnet

Ответ сокет сервера при попытке подключения по Telnet

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

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

Теперь остаётся научить наш сокет-сервер общаться как веб-сокет сервер, корректно работать в фоне (демон), и, самое главное, на дешевом хостинге. Немного теории: разрешив серверу выводить на экран сообщение, которое получаем от клиента при попытке подключения, добавив перед строкой $msg = «Hello, Client!»; код

	echo "Client \"".$accept."\" says:<br /><pre style=\"border:1px solid;\">";
echo socket_read($accept,512);
echo "</pre>";

можно увидеть, что попытка установить соединение по протоколу веб-сокет всегда сопровождается отправлением клиентом заголовка с обязательным соблюдением формата протокола WebScoket. Тег pre для вывода заголовков выбран не случайно, т.к. в заголовках большое значение играют переносы строк. Такие заголовки я получаю от браузеров, когда пытаюсь подключиться к нашему ws://127.0.0.1:889.

FireFox

GET / HTTP/1.1
Host: localhost:889
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://localhost
Sec-WebSocket-Key: ndHtpnSPk2H0qOeP6ry46A==
Cookie: vc=3; __gads=ID=9eabc58c385071c7:T=1400699204:S=ALNI_Ma_g9PZBXpi_MLKDBsao3LQiGx-EA
Connection: keep-alive, Upgrade
Pragma:

GoogleChrome

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: localhost:889
Origin: http://localhost
Pragma: no-cache
Cache-Control: no-cache
Sec-WebSocket-Key: zNMTfmc+C9UK6Ztmv4cE5g==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits, x-webkit-deflate-frame
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36

Еще до того, как я начал изучать веб-сокеты, мне казалось, что работа с веб-сокетами будет напоминать работу с AJAX, когда мы отправляли запросы серверу используя JavaScript XMLHttpRequest(), но в реальности задача оказалась намного сложнее, и в первую очередь потому, что WebSocket это протокол находящийся на одном уровне с протоколом HTTP (но с некоторыми нюансами) в сетевой модели OSI, что означает, нам на стороне сервера, нужно реализовывать функционал похожий на обработку HTTP (который всегда выполнял Apache). Продолжая сравнения с AJAX, в AJAX нам не нужно обеспечивать работу протокола HTTP, т.к. это всё делает браузер и веб-сервер Apache. Мы просто отправляем и получаем сообщения. Но, использование ws хоть и накладывает на нас достаточно большой объём работы связанный с реализацией сервера на PHP, оно также даёт и преимущества: сокращение объёма передаваемых данных и рост производительности серверного ПО. Для успешного общения с клиентами по протоколу WebSocket сервер должен строго выполнять требования стандарта RFC6455, подробно описывающего форматы заголовков ответов. Все сообщения отправляемые по протоколу WebSocket можно разделить на несколько видов: handsnake (рукопожатие при установлении связи), ping-pong(проверка связи) и data-transfer(передача данных). Также есть более краткое описание протокола в общих чертах на русском. Для того, чтобы обеспечить полноценное корректное общение сервера и клиента по протоколу веб-сокет, необходима реализация функций на PHP получения и разбора заголовков от клиента, составление заголовков сервером, составление ключа и ряд других. Изучив материалы представленные на хабре и других ресурсах, удалось найти реализованные годные функции, единственным смущающим различием явлилось то, что большинство авторов используют потоковые функции работы с сокетами, они считаются более компактными, но, при этом, более ресурсоёмкими в связи с использованием буфера. Для перехода на потоковые функции работы с сокетами, не требуется установка никаких дополнительных модулей кроме уже установленных, т.к. потоки встроены в PHP 5 по умолчанию. Потоковые функции работы с сокетами всегда начинаются с stream_socket_*. При использовании потоковых функций, желательно убедиться в phpinfo() в поддержке необходимого транспорта для потоков.

Registered Stream Socket Transports: tcp, udp. 

В случае если такой информации в phpinfo() нет или в случае возникновения других проблем, обратитесь к документации, но проблема решается банальным обновлением Денвера на актуальную версию.
Еще важным является тот факт, что большинство систем ограничивает возможность создания сокета на порту ниже чем 1024, поэтому во всех дальнейших программах для сокета будет использоваться порт 8889.
Взяв за основу исходные коды функций кодирования и декодирования заголовков протокола WebSocket, удалось реализовать полноценный ws echo сервер. Алгоритм его работы также прост как и в предыдущем случае: создаём ws сервер используя функцию stream_socket_server, запускаем бесконечный цикл в котором проверяем наличие новых соединений и при получении нового соединения размещаем его в массиве $connects, также запускаем второй цикл, который пробегает по всем соединениям и закрывает отвалившиеся и получает сообщения от открытых соединений. Также в скрипт PHP мною добавлено три функции, которые я оформил как обработчиков событий.

function onOpen($connect, $info) {
echo "open OK<br />\n";
}

function onClose($connect) {
echo "close OK<br />\n";
}

function onMessage($connect, $data) {
$f = decode($data);
echo "Message:";
echo $f['payload'] . "<br />\n";
fwrite($connect, encode($f['payload']));
}

Которые ничего не делают, за исключением вывода сообщений о событиях на экран. И при получении сообщения от клиента, раскодируем его и отправляем его текст, предварительно закодировав, обратно. Архив ws клиента и ws echo сервера, ws клиент не изменился. Можно протестировать ws echo server скачав архив, и разместив его в корневой папке localhost-а Денвера. Подключиться клиентом к адресу ws://127.0.0.1:8889.
Тонкости связанные с запуском ws сервера в фоне (демон), и запуск на хостинге мы разберем в следующей статье. Буду рад комментариям и отзывам.

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

Мои статьи про PHP демонов и веб-сокеты

  • Dj Dance

    Приветствую, наконец-то кто-то внятно написал работающие примеры для нубов. Спасибо. Поскорее напишите продолжение!

  • Shone_dz

    Спасибо за все три публикации посвященные вебсокетам.

  • Thomas Moon

    Артур, респект тебе и спасибо за хорошо разжеванную статью по сокетам и веб-сокетам!
    Впервые затронул данную тему и как-то было тяжело понять с какой стороны вообще подходить, теперь начинает проясняться.
    Единственное, некоторые ссылки, например на dll файлы, некорректны и там нужно немного подредактировать
    Ну и прогонять через ворд статьи на предмет очепяток:)

    • Thomas, спасибо за правки — исправил ссылки, теперь обязаны работать, и все очепятки которые нашел — тоже теперь должны работать )). Буду рад если укажете очепятки которые не нашел, к сожалению их компилятором проверить нельзя и без них WP запускается )

  • Хорошая статья но в simpleworking.php код обрезан , дай ссылку на нормальный файл
    и лучше бы сразу установку на хостинге описал вместо тестирования на локалке

    • Василий, код полный и работающий. (Скачан более 50 раз). Разработка сразу на хостинге неудобна. Работа с серверами хостинга описана в следующих статьях по этой теме.

      • посмотри simpleworking.php код заканчивается на строке 81 здесь тег php ?> не закрывается и страница в браузере не грузится, а в
        echows.php заканчивается на строке 284 и опять тега закрытия php ?> нет

        • Василий, прошу обратить внимание что закрывающие теги в php файлах необязательны. Цитирую документацию php http://php.net/manual/ru/language.basic-syntax.phptags.php

          «Если файл содержит только код PHP, предпочтительно опустить закрывающий тег в конце файла. Это помогает избежать добавлени случайных символов пробела или перевода строки после закрывающего тега PHP, которые могут послужить причиной нежелательных эффектов, так как PHP начинает выводить данные в буфер при отсутствии намерения у программиста выводить какие-либо данные в этой точке скрипта.»

          Рекомендую включить отображение ошибок в Apache чтобы понять почему код не работает.

    • И так скажи спасибо, что разжевал и дал руками попробовать, убедиться, что оно прекрасно работает и несложно настраивается.

  • dimka11

    при попытке подключения через telnet к 127.0.0.1:8889 пишет,что сервер не найден.(скачал архив ,выложенный в коцне статьи.) что не так?(

    • Интересно узнать что в это время видно в окне сервера. Если
      GO() …
      socket_create …OK
      socket_bind …OK
      Listening socket… OK
      значит см. файрволл.

      • dimka11

        Я наконец-то смог подключиться к серверу через этот порт.
        echows. php выдает такое:
        try to start…
        main while…
        new connection…
        connect=Resource id #4, info=Array
        OK
        open OK
        main while…
        Message:Connection with «ws://127.0.0.1:8889» Подключение установлено обоюдно, отлично!
        main while…
        Все ведь правильно?
        При подключении через Putty соединение вроде бы устанавливается, но никакого ответа типа «Hello,client!» не приходит. Так и должно быть?

        • Судя по сообщениям сервера, он видит это подключение. Почему в Putty не выводится сообщение мне сказать трудно. Подключение установлено, это точно.

          • dimka11

            Я понял,почему не получалось до этого: ecows.php не был запущен.
            А этот случай,как я понял,в след. статье разбирается?
            И обязательно размещать вс сервер на платном хостинге?
            И да,спасибо,что ответили) А то я был в тупике

          • Пожалуйста прежде чем задавать вопросы, прочитайте внимательно все статьи.

          • dimka11

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

          • dimka11

            Так, еще вот проблема с telnet. Через вс клиент-то все нормально,подключается.
            А через telnet соединение не устанавливается.
            На вс сервере такая ошибка: Notice: Undefined offset: 1 in Z:homelocalhostwwwechows.php on line 76
            Строка 76 $info[‘uri’] = $header[1];
            Выходит,telnet не отправляет нужные заголовки,поэтому эта проблема возникает?

  • Илья

    Ну я вообще получается нуб нубом!!! Ну памажите все же!!!

    На локальной машине стоит Apache и PHP с библиотекой socket.
    Создал два локальных домена http://x.local (клиент) и http://xserver.local (сервер)
    Проверяю WebSocket показывает: http://prntscr.com/69i319
    Фаэрвол отключал.

    При обращении к ws://xserver.local естественно ничего не работает…
    Почему у всех работает?)) Как так протокол ws начинает сам по себе работать?

    • Илья, вопрос слишком абстрактный. В статье написано как тестировать, начиная от библиотеки socket, заканчивая связью.

      • Илья

        Да у меня наверное вопрос с непониманием работы протокола ws…
        Вы обращаетесь к урлу ws:/….. И как я понимаю он у всех работает. У меня же к сожалению нет… Вот и хочу понять почему…
        Скорее всего не могу правильно сформулировать и поэтому гугл тут не поможет)

  • Саня Коваль

    У вас в функции getstatusp ошибка, ps -aux -p $pid выдает список всех процессов независимо от заданного pid и соответственно при некорректном завершении работы сервера он больше не запускается. В данном случае достаточно ps -p $pid. По крайней мере в моём случае было так (сервер на Ubuntu).

    • Саня Коваль

      Ой, не к той статье комент оставил, он к продолжению относится.

      • Главное что работает. Но man ps для конкретной ОС лучше посмотреть.

  • Сергей

    Спасибо за подробную статью!! Как раз осваиваю веб-сокеты; столкнулся с проблемой того, что их не пропускает корпоративный прокси. Пока единственное решение, которое нашел — wss, но дело в том, что помимо обычных данных предполагается передача потока видео с камер, а его шифрование, боюсь, создаст слишком большую нагрузку на сервер… использование ноды или nginx тоже не предполагается начальством. Нету ли других способов решения этой проблемы?

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

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

      • Сергей

        Спасибо)) Наверное нужно подключать к этому делу еще и админа и думать сообща.. Пока буду разбираться с заголовками и портами. Вот еще нашел относительно свежие статьи на тему веб-сокетов http://habrahabr.ru/company/ifree/blog/209864/ (там три статьи и файлы на гитхабе, демки почему-то не работают), может интересно будет… но — уже не так подробно)

  • oploshka

    Ради удовлетворения и углубления своих познаний решил приступить к изучению Этого чуда. С одной стороны все просто, с другой все же есть подводные камни. в частности можно подправить js файл:
    document.getElementById(«sock-send-butt»).onclick = function () {
    socket.send(document.getElementById(«sock-msg»).value);
    };
    это не совсем хорошо, так как человек может отключится от сокета и пытаться что то послать.
    document.getElementById(«sock-send-butt»).onclick = function () {
    if(socket.readyState == 1){
    socket.send(document.getElementById(«sock-msg»).value);
    } else { alert(‘соединение не установленно’);}
    };
    взял это отсюда https://developer.mozilla.org/en-US/docs/Web/API/WebSocket, в опере работает.

  • Блин, реально спасибо. Я читал, искал смотрел и 90% теории понимал. Но как-то по детски не мог ничего запустить на хостинге потому что не знал то ли я делаю не так, то ли хостинг имеет ограничения. На Денвере запустил быстро благодаря твоим примерам, хостера попросил открыть порт. Супер.

  • Владимир Пурик

    Еще раз спасибо за цикл статей по WebSocket. Очень полезно!!!!

    Кому интересно столкнулся при реализации своего проекта, с тем что Safari for Win присылает заголовки в редакции черновика протокола номер 76.

    [method] => GET
    [uri] => /
    [Upgrade] => WebSocket
    [Connection] => Upgrade
    [Host] =>удалил адрес
    [Origin] => удалил адрес
    [Cookie] => currency=a; koefEUR=1; koefRUB=0.0162; koefUSD=0.9190; lang=ru_RU; PHPSESSID=871506b7b970b8c2865a41c630042bba
    [Sec-WebSocket-Key1] => 102 *2833377
    [Sec-WebSocket-Key2] => 2 3 B0 59 4 132 7
    [ip] => 91.243.212.133
    [port] => 8889

    соответственно надо дорабатывать функцию handshake

  • Радик Камалов

    У меня затор на 3м пункте. Пытаюсь запустить на хостинге. В итоге в консоли клиента выходит ошибка:
    failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

    • Есть возможность сменить хостинг?

      http://petukhovsky.com/web-socket-php-hosting/

      • Радик Камалов

        Думаете ошибка из-за хостинга? Использую beget. VPS тариф start

        • failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED
          это ошибка невозможности подключения к сокетам для JS
          значит нужно последовательно пройтись, понять где проблема.
          1. Прочитать следующие статьи, там как раз есть про хостинг.
          2. Запустить скрипт на хостинге, проверить нету ли ошибок (на экране, в логах). Если нет, то дальше.
          3. Проверить телнетом возможность соединение.

          Если всё работает значит проблема в клиенте, но 99% что выявите ошибку на 2м или 3м шаге.

          Для простоты отладки можно использовать ws server admin panel v.0.3. & chat v.0.1., качайте последний архив

        • Сергей Катков

          Мне помолго простое открытие порта на сервере 🙂

  • Здравствуйте!Всего второй день как узнал о существовании термина «веб сокеты».
    С кодом клиентской части вопросов пока не возникает,а вот как сделать чтобы серверная часть срабатывала-не доходит до меня никак.Есть хостинг от Radiushost,однако занимался лишь примитивной разработкой сайтов html+php+javascript..Может поделитесь ресурсом который пошагово объяснит куда тыкать,что нажимать и куда что вводить,чтобы все заработало??Прям совсем для «тупых»))

  • yambbkru

    У вас для чтения данных на сервере используется функция fread. Но у нее есть ограничения: она может считать не более, чем 8192 байт. Что делать, если нужно передать больше? Конструкция типа
    $contents = »;
    while (!feof($handle)) {
    $contents .= fread($handle, 8192);
    }
    не помогла.

  • Kirill

    Добрый день!
    Очень признателен за опубликованный материал!

    В ходе тестирования появились проблемы и никак не пойму в чём дело, надеюсь откликнитесь!
    С сокетами как таковыми работал достаточно часто, но с веб и на php столкнулся впервые

    Проблема на стороне сервера.

    Как я понимаю на строчке $accept = @socket_accept($socket); скрипт должен ждать

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

    При этом, оповестив о ней, работа продолжается, т.к. никакого ветвления алгоритма в этом случае нет, и пытается выполнить socket_write($accept, $msg);

    Разумеется это ему не удаётся, т.к. $accept вернул ошибку.

    Поэтому есть несколько вопросов:

    1) Поведение socket_accept() вполне логично, если бы @socket создавался неблокирующим. Из документации следует, должна была быть использована функция socket_set_nonblock. Но в коде такой не нет. Решил явно указать, что сокет блокирующий с помощью функции socket_set_block($socket) сразу после его создания — результат аналогичный — не блокирует.

    2) Код с отправкой сообщения клиенту правильнее наверное было бы поместить в блок после проверки $accept, чтобы он не пытался в случае ошибки отправлять какие-либо сообщения

    3) Не осознал для чего присутствует строка usleep(100);

    Наверняка упустил какую то деталь, и прошу помочь с решением.

    И ещё забыл упомянуть, что на функции socket_bind() и socket_listen() у меня выдаёт предупреждение. Описания предупреждения никакого нет. Что он хочет мне этим сказать, я понять не могу.

    • Kirill, сомневаюсь в ценности своего стандартного совета, и, тем не менее.

      1. Рекомендую настроить отображение ошибок display_errors и пытаться поймать ошибку.

      2. Пройти все шаги описанной статьи последовательно, соблюдая всё до мелочей. Потому как решение гарантированно работает и опробовано большим количеством людей. Если всё заработает — нужно аккуратно последовательно идти от работающей схемы (исходники прикрепленные к этой статье) к вашей версии отслеживая изменения и пытаясь определить место появления дефекта.

  • Maxim Zhukov

    У меня вопрос новичка.

    Под рукой есть домашний NAS-сервер, со статическим IP, видимым в интернете.
    На сервере стоит phpMyAdmin.
    Есть примитивная страница html+php, залитая на этом сервере в директорию web.

    На странице есть код php, который:
    1. дергает показания удаленного датчика атмосферного давления по API;
    2. записывает это показание новой строкой в один и тот же файл CSV;
    3. обновляет страницу каждые 10 минут и код в пунктах 1 и 2 повторяется.

    Естественно, если я закрою браузер, то код обрабатываться не будет.
    Наверное тут нужен демон на сервере?
    Куда что писать? Если у меня есть уже код, то куда его надо «просто скопировать»?
    Что ковырять? куда смотреть?

    Заранее признателен!

    • Прочитайте о демонах в следующей статье
      http://petukhovsky.com/simple-web-socket-on-php-daemon/

      • Maxim Zhukov

        спасибо. Задачу решил на Raspberry Pi, которая по случаю была заказана. На рабочем NAS-сервере cron так и не заработал. Видимо есть ограничения.

  • Bagaev Dmitry

    Доброго времени суток. Сделал все как надо и даже удалось нормально работать с сокетами. Но прошло три дня и я где-то [накосячил], теперь постоянно в консоль получаю ошибку:

    WebSocket connection to ‘ws://websocketserver/server.php’ failed: Error during WebSocket handshake: Unexpected response code: 200

    Подскажи, пожалуйста, что поломалось и как исправить.

    Скрипт запускаю через консоль винды C://xampp/htdocs/php/php.exe C://xampp/htdocs/websocketserver.php/www/server.php

    Порты открыты, файервол выключен.

    .htaccess AddDefaultCharset utf-8

    JS: socket = new WebSocket(‘ws://websocketserver/server.php’);
    PHP: $bind = socket_bind($socket, ‘127.0.0.1’, 8889);

    • Только стандартный рецепт. Пошаговая отладка начиная с билда в котором всё работало.

      Но вообще в JS нужно указывать порт по которому подключаетесь, хотя это не относится к текущей проблеме на сервере.
      socket = new WebSocket(‘ws://websocketserver:8889’);

      • Bagaev Dmitry

        @socket = new WebSocket(‘ws://websocketserver:8889’);@
        поставил порт в JS ничего не вышло, все равно.

        Ну во-первых стоит уточнить, на локальном хосте (точнее на виртуальном локальном:websocketserver), получилось только на порте :80 (это я точно помню!)

        Когда все заработало, я со спокойной совестью пошел спать.

        Сегодня разобрался с серваком (Ubuntu) DigitalOcean, и впаял все это дело на сервак, в процессе, возможно, где-то что-то потерял, хотя насколько я помню, я менял только IP и Порты в конфигах. Следовательно после манипуляций и стала возникать эта ошибка, как на локальном хосте, так и на серваке DigitalOcean

        P.S. Насчет JS сам так думал, и даже ставил туда порт

        • JS в вашем случае ни как не влияет, но я указал как это делать правильно.

          Так ёпрст. 80 это порт http протокола — не удивительно что сервер ругается когда к нему летят http handsnake от браузера. В моей статье написано какой диапазон портов допустимо использовать для ws. Почему не получается на других портах тоже примерно понятно, они закрыты у VDS. Настраивайте файрволл. У DigitalOcean своих фильтров на порты быть не должно.

          • Bagaev Dmitry

            На порт 8889 получаю следующее,

            WebSocket connection to ‘ws://[IP]:8889/’ failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

          • А я о чём пишу?

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

            Читайте внимательнее статью, пользуйтесь телнетом чтобы проверить порты.

          • Bagaev Dmitry

            А разве вот это не говорит о том, что с портами все хорошо?

          • Локальному соединению может быть всё ОК. Но вы же говорите что вы не можете достучаться к WS серверу на VDS по порту отличному от 80.

          • Bagaev Dmitry

            Так и есть. Может быть посмотрите что не так на самом серваке?

            Могу скинуть рут доступ по SSH. А то я уже совсем голову сломал. Ну или подскажите, что делать.

          • Делайте пошагово всё что описано в этой статье. Но скорее всего дело в том, что закрыты необходимые порты на вашей VDS-ке. Если так то смотрите что в iptables и firewall конфигурационных файлах, если в них всё ОК, но не удаётся подключится на нужный порт по телнету, пишите провайдеру.

          • Bagaev Dmitry

            Ошибка была в том, что сокет соединение на серваке нужно поднимать по белому ip, и также по белому ip нужно подключаться с указанием порта.

            НЕВЕРНО:
            $socket = stream_socket_server(«tcp://127.0.01:8889», $errno, $errstr);

            ВЕРНО:
            $socket = stream_socket_server(«tcp://88.34.12.54:8889», $errno, $errstr);
            (ip написал наугад)
            P.S. Также подключение не срабатывает если использовать alias на ip. Например: $socket = stream_socket_server(«tcp://socketserverblabla.ru:8889», $errno, $errstr);

            Может быть как-то можно подключить по alias?

            P.S.S. Можно добавить в статью, так как это не указано.

          • Только закончил перезд на новый VDS. Правильно писать чтобы во вне, и на локалхост был виден: 0.0.0.0:8889 Видимо на старом VDS работало из-за того, что порты с локалхоста были ручками проброшены наружу.

  • Андрей

    Здравствуйте!
    Спасибо за хорошую статью!
    Подскажите, запускаю сервер, клиент
    клиент коннектит — в консоле видно, но js выплевывает ошибку, что не может подключится
    скрин https://yadi.sk/d/OUoWxrL0tzaWb

    • Похоже что что-то не так с файрволом, в котором дропаются исходящие пакеты, либо с протоколом на клиенте.

      • Андрей

        Спасибо за быстрый ответ, решил. файрвол

  • Андрей

    Добрый день.
    Нужен ваш совет!
    есть система, внутри ее отправка сообщений между юзерами. Сейчас для уведомлений используется костыльное решение — раз в 3с ajax запрос.

    Я правильно понимаю, что ws сервер не может инициализировать отправку сообщения юзеру В при приеме сообщения от юзера А ?

    Надо делать внутри сервера цикл с паузой и проверкой сообщений для конкретного юзера (кто приконнектился)? или есть другое решение?

    • Андрей

      нашел решение подменой коннекта

  • Maxim

    Я смогу держать сервер сокетов на локальной машине? Просто я запускал когда-то игровые сервера и все работало

    • Конечно, что мешает. Apache + PHP и всё будет работать. Подробно написано в статьях как и что настраивать.

  • Viktor

    Приветствую, спасибо за статью. Все прекрасно работает. Только вот не пойму как сделать следующее:
    в двух вкладках открыты соединения допустим test_1.html и test_2.html обе страници подключились в одному вебсокету, но почемуто веб сокет отправляет сообщения только одной странице не дублирую на вторую, хотя соедеение одно. Мож конечно я что то не допонял. Как сделать вещание информации всем подключенным к веб сокету?

    • Виктор, к сожалению не знаю почему у вас так происходит. В моём приложении (не чате) все вкладки отрабатывали одинаково.

    • Саша Ром

      Вам надо заменить строку :
      onMessage($connect, $data);//вызываем пользовательский сценарий
      на такой цикл:
      foreach ($connects as $oneConnect) {
      onMessage($oneConnect, $data);//вызываем пользовательский сценарий
      }

  • dimaaa15

    На денвере все работает нормально. А как запустить сокеты на ubuntu? Подскажете?

    • Посмотрите все мои статьи по веб-сокетам, должно помочь.У меня с *nix всегда было проще, чем с Windows+Денвер.

      • dimaaa15

        Все равно не получается запустить демон (echows.php), ни через браузер, ни через консоль. Кстати через консоль с & сразу ошибка (Остановлено). Без & ошибок нет, только в логах ERROR socket unavailable Cannot assign requested address(0)

        • Какие порты используете? Попробуйте в другом диапазоне. У вас VDS?

          • dimaaa15

            Ну порт один вроде: 8889. У меня виртуальная машина ubuntu, на VDS тоже пробовал, таже самая ошибка….

  • Сергей Семенов

    Все это хорошо, вроде все работает, но есть один вопрос? Вы не проверяли, что будет происходить на стороне сервера, если клиент прервет соединение некорректно(т.е. отвалится канал связи — WiFi,Ethernet,Internrt)? Так вот, произойдет следующее — в начале сервер будет работать в обычном режиме, далее начнет жутко тормозить и выкидывать ошибки про запись в несуществующий ресурс, через 20 минут, сервер разорвет соединение с несуществующим сокетом автоматически! В связи с этим вопрос, как определить и удалить отключенный ресурс из массива $connects, не дожидаясь автоматического разъединения и не вешать сервер?

    • Хороший вопрос, странно, но я не сталкивался с такой проблемой даже на большом количестве соединений. Если найдёте способ, пишите, интересно взглянуть.

  • брат Марио

    Пользуюсь wsphp.net — намного проще и понятнее…

  • брат Марио

    С недавних пор пользуюсь wsphp.net — пока полет нормальный 🙂 Рекоментдую.

  • searching

    Спасибо за статью.
    Изучил php, sql, mysql, и т.д, пишу скрипты, но нет полной картины как все работает на сервере, читая справочник четкое ощущение что не хватает базовых, фундаментальных знаний.

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

    • Не знаю, трудно сказать, просто читайте самоучители, учитесь, общайтесь, смотрите гитхаб, и самое главное, пишите работающий полезный в прикладном плане код.

      • searching

        Нет понимания сути, и не хватает фундаментальных знаний, возможно проблемы из за пробелов в администрировании linux, и apache, а так же работе сети, возможно еще что то ?