В данной статье рассмотрим работу с библиотекой phpws, которая нужна для организации приложений или WEB — приложений на основе сокетов и запустим парочку стандартных примеров, которые представлены на странице репозитория GitHub данного проекта.
Примечание. Сокеты у нас будут работать, как на серверной части, так и на клиентской. На серверной части этим займется стандартный WebSocket, который появился в HTML5, а работу на серверной части, где у нас PHP будет выполнять библиотека phpws. Есть много подобных библиотек, пожалуй, особенно следует отметить Ratchet, который мне показался громоздким для моего маленького проекта и я остановился на phpws.
Нам нужен Composer
Очень удобная штука, которая облегчит всю работу с зависимостями и библиотеками, которые включены в проекты. По всеобщему стандарту кодирования или, проще говоря, по правильному написанию кода все библиотеки, пакеты, зависимости или проекты принято хранить в репозиториях исходных кодов, которые потом подключаются в проект за парочку команд через менеджеры пакетов или через менеджеры зависимостей. Для каждого языка есть свой менеджер или почти для каждого, поэтому, вооружимся данным инструментом и установим его в систему командой в Linux
$ curl -s https://getcomposer.org/installer | php
Мы его скачали, но команды composer не будут выполняться через PATH, поэтому переместим скачанное в /usr/local/bin
$ mv composer.phar /usr/local/bin/composer
Выполняем команду и получаем результат в виде инструкций и команд Composer, что говорит об удачной установке
$ composer
Для Windows и Mac можно посмотреть инструкцию на офф-сайте.
Примечание. Все зависимости, которые нужно подключать надо указывать в файле composer.json в корне проекта, который скачает, обновит и соберет все зависимости в одну папку vendor, из которого потом можно загружать через автозагрузчик классов. У Composer есть свое хранилище пакетов и библиотек и называется Packagist, который позволяет указывать vendor/package и он будет установлен. Да, можно указывать конкретные адреса svn/git репозиториев в composer.json, но это неудобно. Намного удобнее иметь какой-то центральный пункт, где есть соответствия пакетов с их адресами репозиториев. Это Packagist.
Нам нужна библиотека phpws
Для подключения к проекту, нам нужно зайти в корень папки проекта или в подпапку, если это будет частью проекта и там установить данную библиотеку, но сначала надо создать в этом месте composer.json, который выполним потом через консоль командами composer и он прочитав, все нам установит. Для этого создаем данный файл со следующим содержимым
{ "repositories": [ { "type": "vcs", "url": "https://github.com/Devristo/phpws" } ], "require": { "devristo/phpws": "dev-master" } }
В данном случае, мы указали, что скачивать прямо с репозитория GitHub без посредничества Packagist.
Выполним данный файл командой
$ composer install
После чего в папке появится подпапка vendor со скачанными библиотеками и нам остается их подключить и использовать.
Нам нужны базовые понимания работы WebSocket с PHP
И так, на минуточку разберемся, что делать со скачанными библиотеками и как их использовать, углубляться не буду, поэтому, если на пальцах, то нам нужно 2 файла:
- client.html — файл клиентской части, который видит тот, кто за браузером. В нем с сокетом работает JavaScript;
- server.php — собственно, наш сокет-сервер, который обрабатывает все запросы от клиента и отвечает ему обработанными обтветами.
Для соединения нам надо указать схему соединения или протокол связи, ip — адрес сервера. Если удаленный сервер, то надо указать ip — адрес хоста или VPS, а если локальный, то localhost, который равен адресу 127.0.0.0 и указываем еще порт, на котором служба сервера будет запущена под собственным PID. Все эти данные указываются при создании экземпляра соединения.
Для клиентской части:
var socket = "ws://127.0.0.0:12345/";
Для серверной части:
$server = new WebSocketServer("tcp://127.0.0.0:12345", $loop, $logger);
Стандартный пример вывода текущего времени сервера с обновлением до секунды
Для работы данного примера нужно единожды запустить файл server.php через консоль и после выполнения данного скрипта запуститься сокет-сервер со своим PID
Что делает пример? В примере показано, как до долей секунды сокет обновляет информацию времени на сервер и выдает его клиенту
Клиентская часть:
<html> <head> <title>Timers</title> </head> <body> <h1>Server Time</h1> <div>Status: <span id="status"></span></div> <div>Time: <span style="font-size:bold;" id="time"></span></div> </body> </html>
и
var socket = new WebSocket("ws://localhost:12345"); socket.onopen = function(msg){ document.getElementById("status").innerHTML = 'Online'; }; socket.onclose = function(msg){ document.getElementById("status").innerHTML = 'Offline'; } socket.onmessage = function(msg){ document.getElementById("time").innerHTML = msg.data; };
Серверная часть:
#!/php -q <?php require_once("vendor/autoload.php"); use Devristo\Phpws\Server\WebSocketServer; $loop = \React\EventLoop\Factory::create(); // Create a logger which writes everything to the STDOUT $logger = new \Zend\Log\Logger(); $writer = new Zend\Log\Writer\Stream("php://output"); $logger->addWriter($writer); // Create a WebSocket server using SSL $server = new WebSocketServer("tcp://127.0.0.0:12345", $loop, $logger); // Each 0.5 seconds sent the time to all connected clients $loop->addPeriodicTimer(0.5, function() use($server, $logger){ $time = new DateTime(); $string = $time->format("Y-m-d H:i:s"); $logger->notice("Broadcasting time to all clients: $string"); foreach($server->getConnections() as $client) $client->sendString($string); }); // Bind the server $server->bind(); // Start the event loop $loop->run();
Стандартный пример простого чата
Показан пример простого чата. Визуально он имеет вид, как на картинке
Клиентская часть:
<!DOCTYPE html> <html> <head> <title>WebSocket TEST</title> </head> <body onload="init()"> <h3>WebSocket Test</h3> <div id="log"></div> <label>Message <input id="msg" type="text" onkeypress="onkey(event)"/></label> <button onclick="send()">Send</button> <button onclick="quit()">Quit</button> <div>Server will echo your response!</div> </body> </html>
var socket; function createSocket(host) { if ('WebSocket' in window) return new WebSocket(host); else if ('MozWebSocket' in window) return new MozWebSocket(host); throw new Error("No web socket support in browser!"); } function init() { var host = "ws://127.0.0.0:12345/chat"; try { socket = createSocket(host); log('WebSocket - status ' + socket.readyState); socket.onopen = function(msg) { log("Welcome - status " + this.readyState); }; socket.onmessage = function(msg) { log(msg.data); }; socket.onclose = function(msg) { log("Disconnected - status " + this.readyState); }; } catch (ex) { log(ex); } document.getElementById("msg").focus(); } function send() { var msg = document.getElementById('msg').value; try { socket.send(msg); } catch (ex) { log(ex); } } function quit() { log("Goodbye!"); socket.close(); socket = null; } function log(msg) { document.getElementById("log").innerHTML += "<br>" + msg; } function onkey(event) { if (event.keyCode == 13) { send(); } }<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Серверная часть:
#!/php -q <?php // Set timezone of script to UTC inorder to avoid DateTime warnings in // vendor/zendframework/zend-log/Zend/Log/Logger.php date_default_timezone_set('UTC'); require_once("vendor/autoload.php"); // Run from command prompt > php chat.php use Devristo\Phpws\Framing\WebSocketFrame; use Devristo\Phpws\Framing\WebSocketOpcode; use Devristo\Phpws\Messaging\WebSocketMessageInterface; use Devristo\Phpws\Protocol\WebSocketTransportInterface; use Devristo\Phpws\Server\IWebSocketServerObserver; use Devristo\Phpws\Server\UriHandler\WebSocketUriHandler; use Devristo\Phpws\Server\WebSocketServer; /** * This ChatHandler handler below will respond to all messages sent to /chat (e.g. ws://localhost:12345/chat) */ class ChatHandler extends WebSocketUriHandler { /** * Notify everyone when a user has joined the chat * * @param WebSocketTransportInterface $user */ public function onConnect(WebSocketTransportInterface $user){ foreach($this->getConnections() as $client){ $client->sendString("User {$user->getId()} joined the chat: "); } } /** * Broadcast messages sent by a user to everyone in the room * * @param WebSocketTransportInterface $user * @param WebSocketMessageInterface $msg */ public function onMessage(WebSocketTransportInterface $user, WebSocketMessageInterface $msg) { $this->logger->notice("Broadcasting " . strlen($msg->getData()) . " bytes"); foreach($this->getConnections() as $client){ $client->sendString("User {$user->getId()} said: ".$msg->getData()); } } } class ChatHandlerForUnroutedUrls extends WebSocketUriHandler { /** * This class deals with users who are not routed */ public function onConnect(WebSocketTransportInterface $user){ //do nothing $this->logger->notice("User {$user->getId()} did not join any room"); } public function onMessage(WebSocketTransportInterface $user, WebSocketMessageInterface $msg) { //do nothing $this->logger->notice("User {$user->getId()} is not in a room but tried to say: {$msg->getData()}"); } } $loop = \React\EventLoop\Factory::create(); // Create a logger which writes everything to the STDOUT $logger = new \Zend\Log\Logger(); $writer = new Zend\Log\Writer\Stream("php://output"); $logger->addWriter($writer); // Create a WebSocket server $server = new WebSocketServer("tcp://127.0.0.0:12345", $loop, $logger); // Create a router which transfers all /chat connections to the ChatHandler class $router = new \Devristo\Phpws\Server\UriHandler\ClientRouter($server, $logger); // route /chat url $router->addRoute('#^/chat$#i', new ChatHandler($logger)); // route unmatched urls durring this demo to avoid errors $router->addRoute('#^(.*)$#i', new ChatHandlerForUnroutedUrls($logger)); // Bind the server $server->bind(); // Start the event loop $loop->run();
Запускать данный пример, надо, как и предыдущий — единожды через консоль запускаем файл server.php и через браузер входим в клиентскую часть client.html, подключив скрипт script.js .
Работа с PHP — сокет сервером из консоли
Для того, чтобы обновлять код сервера и перезапускать очень удобно использовать команды для остановки и перезапуска файла сервера PHP через консоль иначе может происходить казусная ситуация, когда вроде бы правляешь код сервера, а он выполняет старый.
Сначала выводим PID процесса запущенного сокет-сервера. Его мы узнаем посмотрев список запущенных сокетов через их порты через команду:
netstat --tcp --listening --program
Находя из списка нужный PID убиваем его через команду:
kill %pid%
Идеально, если закроем WebSocket через клиентскую JavaScript часть командой перед запуском «убийства» PID:
socket.close(); socket = null;