Web

Как создать сервер веб-сокетов на PHP

Я ищу простой код для создания сервера WebSocket. Я нашел фреймворк «phpwebsockets», но сейчас он устарел и не поддерживает новейший протокол. Я сам пробовал обновить его, но, похоже, у меня не получается сделать это.

#!/php -q

<?php  /*  >php -q server.php  */

 error_reporting(E_ALL);

set_time_limit(0);

ob_implicit_flush();

 $master  = WebSocket("localhost",12345);

$sockets = array($master);

$users   = array();

$debug   = false;

 

while(true){

  $changed = $sockets;

  socket_select($changed,$write=NULL,$except=NULL,NULL);

  foreach($changed as $socket){

    if($socket==$master){

      $client=socket_accept($master);

      if($client<0){ console("socket_accept() failed"); continue; }

      else{ connect($client); }

    }

    else{

      $bytes = @socket_recv($socket,$buffer,2048,0);

      if($bytes==0){ disconnect($socket); }

      else{

        $user = getuserbysocket($socket);

        if(!$user->handshake){ dohandshake($user,$buffer); }

        else{ process($user,$buffer); }

      }

    }

  }

}

 

//---------------------------------------------------------------

function process($user,$msg){

  $action = unwrap($msg);

  say("< ".$action);

  switch($action){

    case "hello" : send($user->socket,"hello human");                       break;

    case "hi"    : send($user->socket,"zup human");                         break;

    case "name"  : send($user->socket,"my name is Multivac, silly I know"); break;

    case "age"   : send($user->socket,"I am older than time itself");       break;

    case "date"  : send($user->socket,"today is ".date("Y.m.d"));           break;

    case "time"  : send($user->socket,"server time is ".date("H:i:s"));     break;

    case "thanks": send($user->socket,"you're welcome");                    break;

    case "bye"   : send($user->socket,"bye");                               break;

    default      : send($user->socket,$action." not understood");           break;

  }

}

 

function send($client,$msg){

  say("> ".$msg);

  $msg = wrap($msg);

  socket_write($client,$msg,strlen($msg));

}

 

function WebSocket($address,$port){

  $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");

  socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)  or die("socket_option() failed");

  socket_bind($master, $address, $port)                    or die("socket_bind() failed");

  socket_listen($master,20)                                or die("socket_listen() failed");

  echo "Сервер запущен : ".date('Y-m-d H:i:s')."\n";

  echo "Главный сокет  : ".$master."\n";

  echo "Прослушивание   : ".$address." порта ".$port."\n\n";

  return $master;

}

 

function connect($socket){

  global $sockets,$users;

  $user = new User();

  $user->id = uniqid();

  $user->socket = $socket;

  array_push($users,$user);

  array_push($sockets,$socket);

  console($socket." CONNECTED!");

}

 

function disconnect($socket){

  global $sockets,$users;

  $found=null;

  $n=count($users);

  for($i=0;$i<$n;$i++){

    if($users[$i]->socket==$socket){ $found=$i; break; }

  }

  if(!is_null($found)){ array_splice($users,$found,1); }

  $index = array_search($socket,$sockets);

  socket_close($socket);

  console($socket." DISCONNECTED!");

  if($index>=0){ array_splice($sockets,$index,1); }

}

 

function dohandshake($user,$buffer){

  console("\nRequesting handshake...");

  console($buffer);

  //list($resource,$host,$origin,$strkey1,$strkey2,$data) 

  list($resource,$host,$u,$c,$key,$protocol,$version,$origin,$data) = getheaders($buffer);

  console("Handshaking...");

 

    $acceptkey = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));

  $upgrade  = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $acceptkey\r\n";

 

  socket_write($user->socket,$upgrade,strlen($upgrade));

  $user->handshake=true;

  console($upgrade);

  console("Done handshaking...");

  return true;

}

 

function getheaders($req){

    $r=$h=$u=$c=$key=$protocol=$version=$o=$data=null;

    if(preg_match("/GET (.*) HTTP/"   ,$req,$match)){ $r=$match[1]; }

    if(preg_match("/Host: (.*)\r\n/"  ,$req,$match)){ $h=$match[1]; }

    if(preg_match("/Upgrade: (.*)\r\n/",$req,$match)){ $u=$match[1]; }

    if(preg_match("/Connection: (.*)\r\n/",$req,$match)){ $c=$match[1]; }

    if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$req,$match)){ $key=$match[1]; }

    if(preg_match("/Sec-WebSocket-Protocol: (.*)\r\n/",$req,$match)){ $protocol=$match[1]; }

    if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/",$req,$match)){ $version=$match[1]; }

    if(preg_match("/Origin: (.*)\r\n/",$req,$match)){ $o=$match[1]; }

    if(preg_match("/\r\n(.*?)\$/",$req,$match)){ $data=$match[1]; }

    return array($r,$h,$u,$c,$key,$protocol,$version,$o,$data);

}

 

function getuserbysocket($socket){

  global $users;

  $found=null;

  foreach($users as $user){

    if($user->socket==$socket){ $found=$user; break; }

  }

  return $found;

}

function     say($msg=""){ echo $msg."\n"; }

function    wrap($msg=""){ return chr(0).$msg.chr(255); }

function  unwrap($msg=""){ return substr($msg,1,strlen($msg)-2); }

function console($msg=""){ global $debug; if($debug){ echo $msg."\n"; } }

 

class User{

  var $id;

  var $socket;

  var $handshake;

}

?>

 

И клиент:

var connection = new WebSocket('ws://localhost:12345');

connection.onopen = function () {

  connection.send('Ping'); // Отправляем сообщение 'Ping' на сервер

};

// Журнал ошибок

connection.onerror = function (error) {

  console.log('WebSocket Error ' + error);

};

// Журнал сообщений с сервера

connection.onmessage = function (e) {

  console.log('Server: ' + e.data);

};

 Если в моем коде что-то не так, вы можете помочь мне это исправить? Console в Firefox выводит следующее:

Firefox не может установить соединение с сервером по адресу ws: // localhost: 12345 /.

 

  Ответ 1

Недавно я был в той же ситуации, что и вы, и вот что я сделал:

  1. Я использовал код phpwebsockets в качестве ссылки на то, как структурировать код на стороне сервера (похоже, вы уже делаете это, и, как вы отметили, код, на самом деле, не работает по целому ряду причин).

  2. Я использовал PHP.net, чтобы прочитать подробности о каждой функции сокета, используемой в коде phpwebsockets. Сделав это, я наконец-то смог понять, как концептуально работает вся система. Это было довольно серьезное препятствие.

  3. Я просмотрел реальный проект WebSocket. Мне пришлось прочитать его несколько раз, прежде чем я понял, как все работает. Скорее всего, вам придется возвращаться к этому документу снова и снова на протяжении всего процесса, поскольку это единственный окончательный ресурс с правильной и актуальной информацией об API WebSocket.

  4. Я закодировал правильную процедуру соединения, основываясь на инструкциях, приведенных в проекте в #3 документации. Это было не так уж плохо.

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

  6. Обратите внимание, что код имеет ряд проблем и не будет работать должным образом без дальнейшей модификации.

  7. Затем я понял, как правильно кодировать и декодировать сообщения, отправляемые туда и обратно. Также я понял, как надо отправлять и получать сообщения WebSocket на стороне сервера.

  8. На этом этапе у меня практически все работало. Мне нужно было только добавить некоторую дополнительную логику для обработки закрытия соединений, и все было готово.

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

Дополнение 1:

У меня есть рабочее решение, но оно не совсем готово для работы. К счастью, у другого человека на GitHub есть почти идентичный моему код (но намного чище), поэтому я рекомендую использовать следующий код для рабочего решения PHP WebSocket:

https://github.com/ghedipunk/PHP-Websockets/blob/master/websockets.php

 

Дополнение 2:

Хотя мне по-прежнему нравится использовать PHP для многих серверных задач, я должен признать, что в последнее время мне очень нравится использовать Node.js, и главная причина в том, что он с самого начала был лучше разработан для работы с WebSocket, чем PHP (или любой другой серверный язык). Поэтому в последнее время я обнаружил, что гораздо проще установить на свой сервер Apache/PHP и использовать Node.js для работы сервера WebSocket, а Apache/PHP для всего остального. А в случае, если вы находитесь на виртуальном хостинге, где вы не можете установить/использовать Node.js для WebSocket, вы можете воспользоваться бесплатным сервисом, таким как Heroku, чтобы установить Node.js WebSocket-сервер и выполнять междоменные запросы к нему с вашего сервера. Только убедитесь, что ваш WebSocket-сервер способен обрабатывать кросс-доменные запросы.

 

Ответ 2

Вот как это сделать: создайте файл index.html, содержащий следующее:

<html>

<body>

    <div id="root"></div>

    <script>

        var host = 'ws://<<<IP_OF_YOUR_SERVER>>>:12345/websockets.php';

        var socket = new WebSocket(host);

        socket.onmessage = function(e) {

            document.getElementById('root').innerHTML = e.data;

        };

    </script>

</body>

</html>

 и откройте его в браузере сразу после запуска php websockets.php в командной строке (это будет цикл событий, постоянно выполняющийся PHP-скрипт) с этим файлом.

 

Ответ 3

<?php

// server.php

$server = stream_socket_server("tcp://127.0.0.1:8001", $errno, $errorMessage);

if($server == false) {

    throw new Exception("Не удалось подключиться к сокету: $errorMessage");

}

for(;;) {

    $client = @stream_socket_accept($server);

    if($client) {

        stream_copy_to_stream($client, $client);

        fclose($client);

    }

}

Из одного терминала запустите: php server.php

Из другого терминала выполните: echo "hello world" | nc 127.0.0.1 8002

 

Ответ 4

Необходимо преобразовать ключ из hex в dec перед вызовом base64_encoding и затем отправить его для соединения.

$hashedKey = sha1($key. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true);

$rawToken = "";

    for ($i = 0; $i < 20; $i++) {

      $rawToken .= chr(hexdec(substr($hashedKey,$i*2, 2)));

    }

$handshakeToken = base64_encode($rawToken) . "\r\n";

$handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $handshakeToken\r\n";

 

Схожие статьи

Web

Создание шаблона проектирования Singleton в PHP5

Web

Ошибка при открытии потока в PHP: отсутствует файл или каталог

Web

Что это — stdin, stdout и stderr?

Нейронная сеть Google: что нового и какие возможности у сети Гугла?
Web

Нейронная сеть Google: что нового и какие возможности у сети Гугла?

×