Як створити сервер веб-сокетів в 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);
};

Якщо в моєму коді щось не так, ви можете допомогти мені це виправити? Говорить Конколе у ​​FirefoxFirefox 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/…
Jithin Jose

1
Я думаю, що краще впровадити це в C ++.
Michael Chourdakis,

Відповіді:


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 на можливість обробляти запити з різних джерел.


Thx, я спробую зробити це по-твоєму. Як ви оцінюєте продуктивність цього PHP-сервера?
Dharman

@Dharman, важко сказати, тому що я міг запустити його лише на своєму локальному хості. Звичайно, це чудово працює, але на реальному сервері з великим навантаженням я не знаю. Думаю, це могло б працювати досить добре, оскільки в моєму коді немає роздуття.
HartleySan

1
На даний момент у мене цього немає, але найближчим часом я планую написати підручник про весь процес із усім кодом. Після цього я розміщу посилання.
HartleySan

1
@HartleySan: Привіт! Мені було б дуже цікаво переглянути ваш код. Не могли б ви розмістити його в Інтернеті або надіслати особисто мені?
фрірін

Так, це скоро буде в Інтернеті. Вибачте всіх, хто просив про це. Я нещодавно був так зайнятий. Невдовзі я на це піду.
Хартлі,

26

Наскільки мені відомо, Ratchet - найкраще рішення PHP WebSocket, доступне на даний момент. Оскільки це відкрите джерело, ви можете побачити, як автор створив це рішення WebSocket за допомогою PHP.


2
Я додаю сюди своє рішення, яке використовує Ratchet і Silex: github.com/eole-io/sandstone. Я не знаю, чи знайдете це для вас корисним
Alcalyn

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, і я хотів би створити на зразок цього клас обгортки та надати функціонал сокета, бажано у vanilla php, використовуючи основне розширення

1

Потрібно перетворити ключ із шестнадцатеричного в 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";

Повідомте мене, якщо це допоможе.


1

Я деякий час був на вашому місці, і нарешті я використав node.js, оскільки він може робити гібридні рішення, такі як наявність веб-сервера та сервера сокетів в одному. Таким чином, серверний сервер php може надсилати запити через http на вузол веб-сервера, а потім транслювати його за допомогою websocket. Дуже ефективний шлях.


тож ми повинні використовувати клієнт 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, Відповідно до запитання вам потрібно пояснити з нуля, а не колишнє. який не працює.
Jaymin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.