Cómo crear un servidor websockets en PHP

88

¿Hay tutoriales o guías que muestren cómo escribir yo mismo un servidor websockets simple en PHP? Intenté buscarlo en Google pero no encontré muchos. Encontré phpwebsockets pero ahora está desactualizado y no es compatible con el protocolo más nuevo. Intenté actualizarlo yo mismo pero no parece funcionar.

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

?>

y el cliente:

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

Si hay algo incorrecto en mi código, ¿pueden ayudarme a solucionarlo? Concole en firefox diceFirefox can't establish a connection to the server at ws://localhost:12345/.

EDITAR
Dado que hay mucho interés en esta pregunta, decidí brindarles lo que finalmente se me ocurrió. Aquí está mi código completo.

Dharman
fuente
1
Esta página enumera que ellos también tuvieron problemas con los phpwebsockets actuales e incluye los cambios que hicieron en el código src de ejemplos: net.tutsplus.com/tutorials/javascript-ajax/…
scrappedcola
1
Una biblioteca útil que se puede utilizar para aplicaciones WebSockets. incluye tanto el lado del cliente como de PHP. techzonemind.com/…
Jithin Jose
1
Creo que es mejor implementarlo en C ++.
Michael Chourdakis

Respuestas:

114

Estuve en el mismo barco que tú recientemente, y esto es lo que hice:

  1. He utilizado el phpwebsockets código como una referencia para la forma de estructurar el código del lado del servidor. (Parece que ya está haciendo esto y, como señaló, el código en realidad no funciona por una variedad de razones).

  2. Usé PHP.net para leer los detalles sobre cada función de socket utilizada en el código phpwebsockets. Al hacer esto, finalmente pude entender cómo funciona todo el sistema conceptualmente. Este fue un gran obstáculo.

  3. Leí el borrador real de WebSocket . Tuve que leer esto varias veces antes de que finalmente comenzara a asimilarlo. Es probable que tenga que volver a este documento una y otra vez durante todo el proceso, ya que es el único recurso definitivo con información correcta y actualizada. información sobre la API de WebSocket.

  4. Codifiqué el procedimiento de apretón de manos adecuado según las instrucciones del borrador en el n. ° 3. Esto no estuvo tan mal.

  5. Seguí recibiendo un montón de texto confuso enviado de los clientes al servidor después del apretón de manos y no podía entender por qué hasta que me di cuenta de que los datos están codificados y deben desenmascararse. El siguiente enlace me ayudó mucho aquí: (enlace original roto) Copia archivada .

    Tenga en cuenta que el código disponible en este enlace tiene varios problemas y no funcionará correctamente sin más modificaciones.

  6. Luego me encontré con el siguiente hilo SO, que explica claramente cómo codificar y decodificar correctamente los mensajes que se envían de un lado a otro: ¿Cómo puedo enviar y recibir mensajes de WebSocket en el lado del servidor?

    Este enlace fue realmente útil. Recomiendo consultarlo mientras mira el borrador de WebSocket. Ayudará a darle más sentido a lo que dice el borrador.

  7. Casi había terminado en este punto, pero tuve algunos problemas con una aplicación WebRTC que estaba creando usando WebSocket, así que terminé haciendo mi propia pregunta sobre SO, que finalmente resolví: ¿Qué son estos datos al final de la información del candidato de WebRTC?

  8. En este punto, prácticamente lo tenía todo funcionando. Solo tuve que agregar algo de lógica adicional para manejar el cierre de conexiones, y estaba listo.

Ese proceso me llevó unas dos semanas en total. La buena noticia es que ahora entiendo muy bien WebSocket y pude crear mis propios scripts de cliente y servidor desde cero que funcionan muy bien. Es de esperar que la culminación de toda esa información le proporcione suficiente orientación e información para codificar su propio script PHP de WebSocket.

¡Buena suerte!


Editar : esta edición es un par de años después de mi respuesta original, y aunque todavía tengo una solución funcional, no está realmente lista para compartir. Afortunadamente, alguien más en GitHub tiene un código casi idéntico al mío (pero mucho más limpio), por lo que recomiendo usar el siguiente código para una solución PHP WebSocket que funcione:
https://github.com/ghedipunk/PHP-Websockets/blob/master/ websockets.php


Edite # 2 : si bien todavía disfruto usando PHP para muchas cosas relacionadas con el lado del servidor, debo admitir que realmente me he acostumbrado mucho a Node.js recientemente, y la razón principal es porque está mejor diseñado desde el para manejar WebSocket que PHP (o cualquier otro lenguaje del lado del servidor). Como tal, descubrí recientemente que es mucho más fácil configurar Apache / PHP y Node.js en su servidor y usar Node.js para ejecutar el servidor WebSocket y Apache / PHP para todo lo demás. Y en el caso de que se encuentre en un entorno de alojamiento compartido en el que no pueda instalar / usar Node.js para WebSocket, puede usar un servicio gratuito como Herokupara configurar un servidor WebSocket Node.js y realizar solicitudes entre dominios desde su servidor. Solo asegúrese de hacer eso para configurar su servidor WebSocket para poder manejar solicitudes de origen cruzado.

HartleySan
fuente
Gracias, intentaré hacerlo a tu manera. ¿Cómo valora el rendimiento de este servidor PHP?
Dharman
@Dharman, es difícil de decir porque solo he podido ejecutarlo en mi host local. Por supuesto, funciona bien allí, pero en un servidor real con una carga pesada, no lo sé. Sin embargo, imagino que funcionaría bastante bien, ya que no hay hinchazón en mi código.
HartleySan
1
No lo tengo en este momento, pero en un futuro cercano, planeo escribir un tutorial sobre todo el proceso con todo el código. Una vez que haga eso, publicaré el enlace.
HartleySan
1
@HartleySan: ¡Hola! Me interesaría mucho ver su código. ¿Podrías ponerlo en línea o enviármelo personalmente?
2013
Sí, pronto estará en línea. Lo siento por todos los que lo han pedido. Estuve muy ocupado recientemente. Lo haré pronto.
HartleySan
26

Hasta donde yo sé, Ratchet es la mejor solución PHP WebSocket disponible en este momento. Y dado que es de código abierto , puede ver cómo el autor ha construido esta solución WebSocket usando PHP.

leggetter
fuente
2
Agrego aquí mi solución que usa Ratchet y Silex: github.com/eole-io/sandstone No sé si la encontrará útil
Alcalyn
8

¿Por qué no utilizar sockets http://uk1.php.net/manual/en/book.sockets.php ? Está bien documentado (no solo en contexto PHP) y tiene buenos ejemplos http://uk1.php.net/manual/en/sockets.examples.php

Lukasz Kujawa
fuente
2
Sí, puede tener una conexión continua entre sockets PHP simples y una página web, lo he probado varias veces.
WiMantis
@WiMantis: ¡Hola! ¿Podría poner un ejemplo de código que haga esto en línea, u opcionalmente enviármelo personalmente?
2013
¿Puedes usar esto junto con una conexión HTTP regular? Estaba construyendo un marco DDD y me gustaría crear como una clase contenedora además de esto y proporcionar funcionalidad de socket, preferiblemente en php vanilla mediante el uso de la extensión principal
1

Necesita convertir la clave de hexadecimal a dec antes de codificación base64 y luego enviarla para el protocolo de enlace.

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

Déjeme saber si esto ayuda.

usuario2288650
fuente
1

Estuve en tu lugar por un tiempo y finalmente terminé usando node.js, porque puede hacer soluciones híbridas como tener un servidor web y un servidor de socket en uno. Entonces, el backend php puede enviar solicitudes a través de http al servidor web del nodo y luego transmitirlas con websocket. Una forma muy eficaz de hacerlo.

MZ
fuente
así que tenemos que usar un cliente http para hacer una solicitud http desde php al servidor de nodo, ¿verdad?
Kiren Siva
Kiren Siva, correcto. Curl o similar, luego el nodo transmite un mensaje a través de 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);
    }
}

desde una terminal ejecute: php server.php

desde otra terminal ejecute: echo "hello woerld" | nc 127.0.0.1 8002

Dipak Yadav
fuente
1
¿Qué se supone que sea?
Dharman
8
Se trata de sockets, no de WebSockets. Hay una gran diferencia.
Chris
@techexpander, según la pregunta, debe explicar desde cero en lugar de ex. que no está funcionando.
Jaymin