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

88

Есть ли какие-либо учебные пособия или руководства, показывающие, как написать простой сервер веб-сокетов на PHP? Я пытался найти его в Google, но не нашел много. Я нашел 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 "Server Started : ".date('Y-m-d H:i:s')."\n";
  echo "Master socket  : ".$master."\n";
  echo "Listening on   : ".$address." port ".$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'); // Send the message 'Ping' to the server
};

// Log errors
connection.onerror = function (error) {
  console.log('WebSocket Error ' + error);
};

// Log messages from the server
connection.onmessage = function (e) {
  console.log('Server: ' + e.data);
};

Если в моем коде что-то не так, вы можете помочь мне это исправить? Concole в firefox говоритFirefox can't establish a connection to the server at ws://localhost:12345/.

РЕДАКТИРОВАТЬ
Поскольку этот вопрос вызывает большой интерес, я решил предоставить вам то, что я наконец придумал. Вот мой полный код.

Дхарман
источник
1
На этой странице перечислены , что они тоже были проблемы с текущим phpwebsockets и включает в себя изменения , которые они сделали в примерах Src код: net.tutsplus.com/tutorials/javascript-ajax/...
scrappedcola
1
Полезная библиотека, которую можно использовать для приложений WebSockets. включить как клиентскую, так и PHP-сторону. techzonemind.com/…
Джитин Хосе
1
Думаю, лучше реализовать на C ++.
Майкл Чурдакис 05

Ответы:

114

Недавно я был в одной лодке с вами, и вот что я сделал:

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

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

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

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

  5. Я продолжал получать кучу искаженного текста, отправляемого от клиентов на сервер после рукопожатия, и я не мог понять почему, пока не понял, что данные закодированы и должны быть демаскированы. Следующая ссылка мне очень помогла: (исходная ссылка не работает) Архивная копия .

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

  6. Затем я наткнулся на следующий поток SO, в котором четко объясняется, как правильно кодировать и декодировать сообщения, отправляемые туда и обратно: Как я могу отправлять и получать сообщения WebSocket на стороне сервера?

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

  7. Я почти закончил на этом этапе, но у меня были некоторые проблемы с приложением WebRTC, которое я создавал с помощью WebSocket, поэтому я задал собственный вопрос по SO, который я в конечном итоге решил: что это за данные в конце информации кандидата WebRTC?

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

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

Удачи!


Изменить : это изменение сделано через пару лет после моего первоначального ответа, и, хотя у меня все еще есть рабочее решение, оно не совсем готово для обмена. К счастью, у кого-то еще на GitHub есть почти такой же код, что и мой (но намного чище), поэтому я рекомендую использовать следующий код для рабочего решения PHP WebSocket:
https://github.com/ghedipunk/PHP-Websockets/blob/master/ websockets.php


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

HartleySan
источник
Спасибо, попробую сделать по-твоему. Как вы оцениваете производительность этого PHP-сервера?
Дхарман
@Dharman, сложно сказать, потому что я смог запустить его только на моем локальном хосте. Конечно там нормально работает, но на реальном сервере с большой нагрузкой я не знаю. Я полагаю, что он будет работать достаточно хорошо, поскольку в моем коде нет раздутия.
HartleySan
1
В данный момент у меня его нет, но в ближайшем будущем я планирую написать учебник обо всем процессе со всем кодом. Как только я это сделаю, я отправлю ссылку.
HartleySan,
1
@HartleySan: Привет! Мне было бы очень интересно взглянуть на ваш код. Не могли бы вы выложить его онлайн или прислать мне лично?
Fightrin
Да, скоро он появится в сети. Извините всех, кто об этом попросил. Я был так занят в последнее время. Я скоро займусь этим.
HartleySan,
26

Насколько мне известно, Ratchet - лучшее решение PHP WebSocket, доступное на данный момент. И поскольку это открытый исходный код, вы можете увидеть, как автор построил это решение WebSocket с использованием PHP.

леггеттер
источник
2
Я добавляю сюда свое решение, в котором используются Ratchet и Silex: github.com/eole-io/sandstone. Не знаю,
пригодится
8

Почему бы не использовать сокеты http://uk1.php.net/manual/en/book.sockets.php ? Он хорошо документирован (не только в контексте PHP) и содержит хорошие примеры http://uk1.php.net/manual/en/sockets.examples.php

Лукаш Куджава
источник
2
Да, вы можете иметь постоянное соединение между обычными PHP-сокетами и веб-страницей, я тестировал это несколько раз.
WiMantis
@WiMantis: Привет! Не могли бы вы выложить пример кода, который делает это в Интернете, или, при желании, отправить его мне лично?
форрин
Можете ли вы использовать это вместе с обычным HTTP-соединением? Я создавал структуру DDD, и я хотел бы создать поверх
1

Необходимо преобразовать ключ из шестнадцатеричного в десятичный перед 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";

Позвольте мне знать, если это помогает.

user2288650
источник
1

Некоторое время я был на вашем месте и в конце концов остановился на node.js, потому что он может делать гибридные решения, такие как наличие веб-сервера и сервера сокетов в одном. Таким образом, бэкэнд php может отправлять запросы через http на веб-сервер узла, а затем транслировать его с помощью веб-сокета. Очень эффективный способ.

MZ
источник
так что мы должны использовать http-клиент, чтобы сделать http-запрос с php на сервер узла, верно?
Кирен Шива
Кирен Шива, правильно. Curl или что-то подобное, затем узел транслирует сообщение через веб-сокет
MZ
-2
<?php

// server.php

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

if($server == false) {
    throw new Exception("Could not bind to socket: $errorMessage");

}

for(;;) {
    $client = @stream_socket_accept($server);

    if($client) {
        stream_copy_to_stream($client, $client);
        fclose($client);
    }
}

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

из другого терминала запустить: echo "hello woerld" | NC 127.0.0.1 8002

Дипак Ядав
источник
1
Что это должно быть?
Дхарман
8
Это сокеты, а не WebSockets. Есть большая разница.
Крис
@techexpander, по вопросу вы должны объяснять с нуля, а не ex. который не работает.
Jaymin