Сокеты в PHP. Использование WebSocket с phpws

👁 265 просмотров

В данной статье рассмотрим работу с библиотекой 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 файла:

  1. client.html — файл клиентской части, который видит тот, кто за браузером. В нем с сокетом работает JavaScript;
  2. 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;