Jak stworzyć serwer websockets w PHP

88

Czy są jakieś samouczki lub przewodniki pokazujące, jak samodzielnie napisać prosty serwer WebSockets w PHP? Szukałem go w Google, ale nie znalazłem wielu. Znalazłem phpwebsockets, ale jest już nieaktualne i nie obsługuje najnowszego protokołu. Próbowałem zaktualizować go samodzielnie, ale nie działa.

#!/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;
}

?>

a klient:

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);
};

Jeśli coś jest nie tak w moim kodzie, czy możesz mi pomóc to naprawić? Mówi Concole w FirefoksieFirefox can't establish a connection to the server at ws://localhost:12345/.

EDYCJA
Ponieważ zainteresowanie tym pytaniem jest duże, zdecydowałem się przedstawić to, co w końcu wymyśliłem. Oto mój pełny kod.

Dharman
źródło
1
Strona ta wymienia, że oni też mieli problemy z obecnych phpwebsockets i obejmuje zmiany zrobili w przykładach src kodu: net.tutsplus.com/tutorials/javascript-ajax/...
scrappedcola
1
Przydatna biblioteka, której można używać w aplikacjach WebSockets. zawiera zarówno stronę klienta, jak i PHP. techzonemind.com/…
Jithin Jose
1
Myślę, że lepiej zaimplementować go w C ++.
Michael Chourdakis,

Odpowiedzi:

114

Byłem na tej samej łodzi co ty ostatnio i oto co zrobiłem:

  1. Użyłem phpwebsockets kod jako punkt odniesienia dla jak struktura kodu po stronie serwera. (Wygląda na to, że już to robisz i jak zauważyłeś, kod w rzeczywistości nie działa z różnych powodów).

  2. Użyłem PHP.net, aby przeczytać szczegółowe informacje o każdej funkcji gniazda używanej w kodzie phpwebsockets. Robiąc to, w końcu byłem w stanie zrozumieć, jak cały system działa koncepcyjnie. To była dość duża przeszkoda.

  3. Przeczytałem aktualną wersję roboczą WebSocket . Musiałem to przeczytać kilka razy, zanim w końcu zaczęło się docierać. Prawdopodobnie będziesz musiał wracać do tego dokumentu wielokrotnie w trakcie całego procesu, ponieważ jest to jedyny ostateczny zasób z poprawnymi, aktualnymi informacje o interfejsie API WebSocket.

  4. Zakodowałem poprawną procedurę uzgadniania w oparciu o instrukcje w szkicu w # 3. To nie było takie złe.

  5. Po uzgodnieniu nadal otrzymywałem pakiet zniekształconych tekstów wysyłanych od klientów na serwer i nie mogłem zrozumieć, dlaczego, dopóki nie zdałem sobie sprawy, że dane są zakodowane i muszą zostać zdemaskowane. Poniższy link bardzo mi tutaj pomógł: (oryginalny link uszkodzony) Kopia zarchiwizowana .

    Pamiętaj, że kod dostępny pod tym linkiem ma wiele problemów i nie będzie działał poprawnie bez dalszych modyfikacji.

  6. Następnie natrafiłem na następujący wątek SO, który jasno wyjaśnia, jak prawidłowo kodować i dekodować wiadomości przesyłane tam iz powrotem: Jak mogę wysyłać i odbierać wiadomości WebSocket po stronie serwera?

    Ten link był naprawdę pomocny. Zalecam skonsultowanie się z nim podczas przeglądania wersji roboczej WebSocket. Pomoże to nadać sens temu, co mówi szkic.

  7. Na tym etapie prawie skończyłem, ale miałem pewne problemy z aplikacją WebRTC, którą tworzyłem za pomocą WebSocket, więc w końcu zadałem własne pytanie na temat SO, które ostatecznie rozwiązałem: Jakie są te dane na końcu informacji o kandydatach WebRTC?

  8. W tym momencie prawie wszystko działało. Musiałem tylko dodać dodatkową logikę do obsługi zamykania połączeń i gotowe.

Cały proces zajął mi około dwóch tygodni. Dobra wiadomość jest taka, że ​​teraz naprawdę dobrze rozumiem WebSocket i mogłem stworzyć od podstaw własne skrypty klienta i serwera, które działają świetnie. Mamy nadzieję, że zwieńczenie wszystkich tych informacji da ci wystarczające wskazówki i informacje do zakodowania własnego skryptu PHP WebSocket.

Powodzenia!


Edycja : ta zmiana nastąpiła kilka lat po mojej oryginalnej odpowiedzi i chociaż nadal mam działające rozwiązanie, nie jest ono naprawdę gotowe do udostępnienia. Na szczęście ktoś inny na GitHubie ma prawie identyczny kod jak mój (ale dużo czystszy), więc polecam użycie następującego kodu dla działającego rozwiązania PHP WebSocket:
https://github.com/ghedipunk/PHP-Websockets/blob/master/ websockets.php


Edycja nr 2 : Chociaż nadal lubię używać PHP do wielu rzeczy związanych z serwerem, muszę przyznać, że ostatnio bardzo rozgrzałem się do Node.js, a głównym powodem jest to, że jest lepiej zaprojektowany od gotowe do obsługi WebSocket niż PHP (lub jakikolwiek inny język po stronie serwera). W związku z tym niedawno odkryłem, że o wiele łatwiej jest skonfigurować zarówno Apache / PHP, jak i Node.js na serwerze i używać Node.js do uruchamiania serwera WebSocket i Apache / PHP do wszystkiego innego. A w przypadku, gdy korzystasz ze współdzielonego środowiska hostingowego, w którym nie możesz zainstalować / używać Node.js dla WebSocket, możesz skorzystać z bezpłatnej usługi, takiej jak Herokuaby skonfigurować serwer Node.js WebSocket i wysyłać do niego zapytania międzydomenowe z serwera. Po prostu upewnij się, że robisz to, aby ustawić serwer WebSocket tak, aby mógł obsługiwać żądania między źródłami.

HartleySan
źródło
Dzięki, spróbuję zrobić to po swojemu. Jak oceniasz wydajność tego serwera PHP?
Dharman,
@Dharman, trudno powiedzieć, ponieważ mogłem go uruchomić tylko na moim lokalnym hoście. Oczywiście działa tam dobrze, ale na prawdziwym serwerze z dużym obciążeniem nie wiem. Wyobrażam sobie jednak, że działałoby to całkiem dobrze, ponieważ w moim kodzie nie ma nadużyć.
HartleySan
1
W tej chwili nie mam tego, ale w najbliższej przyszłości planuję napisać tutorial o całym procesie z całym kodem. Kiedy to zrobię, opublikuję link.
HartleySan,
1
@HartleySan: Hello! Byłbym bardzo zainteresowany spojrzeniem na twój kod. Czy możesz umieścić to w Internecie, czy wysłać mi osobiście?
forwardrin
Tak, wkrótce będzie online. Przepraszam wszystkich, którzy o to prosili. Ostatnio byłem taki zajęty. Wkrótce się tym zajmę.
HartleySan
26

O ile mi wiadomo, Ratchet to najlepsze dostępne obecnie rozwiązanie PHP WebSocket. A ponieważ jest to oprogramowanie typu open source , możesz zobaczyć, jak autor zbudował to rozwiązanie WebSocket przy użyciu PHP.

leggetter
źródło
2
Dodaję tutaj moje rozwiązanie, które wykorzystuje Ratchet i Silex: github.com/eole-io/sandstone Nie wiem, czy uznasz to za przydatne
Alcalyn
8

Dlaczego nie użyć gniazd http://uk1.php.net/manual/en/book.sockets.php ? Jest dobrze udokumentowany (nie tylko w kontekście PHP) i ma dobre przykłady http://uk1.php.net/manual/en/sockets.examples.php

Łukasz Kujawa
źródło
2
Tak, możesz mieć ciągłe połączenie między zwykłymi gniazdami PHP a stroną internetową, testowałem to wiele razy.
WiMantis,
@WiMantis: Cześć! Czy mógłbyś podać przykład kodu, który robi to online, czy opcjonalnie wysłać go do mnie osobiście?
forwardrin
Czy możesz używać tego razem ze zwykłym połączeniem HTTP? Budowałem framework DDD i chciałbym na
1

Musisz przekonwertować klucz z hex na dec przed kodowaniem base64_encoding, a następnie wysłać go do uzgadniania.

$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";

Daj mi znać, jeśli to pomoże.

user2288650
źródło
1

Byłem w twoich butach przez jakiś czas i ostatecznie skończyłem na node.js, ponieważ może on tworzyć rozwiązania hybrydowe, takie jak posiadanie serwera WWW i gniazda w jednym. Tak więc backend php może wysyłać żądania przez http do serwera WWW węzła, a następnie rozgłaszać je za pomocą websocket. Bardzo skuteczny sposób.

MZ
źródło
więc musimy użyć klienta http, aby wykonać żądanie http z php do serwera węzła, prawda?
Kiren Siva
Kiren Siva, zgadza się. Curl lub coś podobnego, a następnie węzeł wysyła wiadomość przez websocket
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);
    }
}

z jednego terminala uruchom: php server.php

z innego terminala uruchom: echo "hello woerld" | nc 127.0.0.1 8002

Dipak Yadav
źródło
1
Co to ma być?
Dharman
8
To są gniazda, a nie WebSockets. Jest duża różnica.
Chris,
@techexpander, Zgodnie z pytaniem musisz wyjaśniać od zera zamiast ex. który nie działa.
Jaymin