Como criar servidor websockets em PHP

88

Existem tutoriais ou guias mostrando como escrever para mim mesmo um servidor websockets simples em PHP? Tentei procurá-lo no google, mas não encontrei muitos. Encontrei phpwebsockets, mas agora está desatualizado e não oferece suporte ao protocolo mais recente. Eu tentei atualizá-lo sozinho, mas não 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;
}

?>

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

Se houver algo errado em meu código, você pode me ajudar a consertar? Concole no firefox dizFirefox can't establish a connection to the server at ws://localhost:12345/.

EDITAR
Como há muito interesse nesta questão, decidi fornecer a vocês o que eu finalmente descobri. Aqui está meu código completo.

Dharman
fonte
1
Esta página lista que eles também tiveram problemas com os phpwebsockets atuais e inclui as mudanças que eles fizeram em código exemplos src: net.tutsplus.com/tutorials/javascript-ajax/...
scrappedcola
1
Uma biblioteca útil que pode ser usada para aplicativos WebSockets. inclui o cliente e o lado do PHP. techzonemind.com/…
Jithin Jose
1
Acho melhor implementá-lo em C ++.
Michael Chourdakis

Respostas:

114

Eu estava no mesmo barco que você recentemente e aqui está o que fiz:

  1. Usei o código phpwebsockets como uma referência de como estruturar o código do lado do servidor. (Parece que você já está fazendo isso e, como observou, o código na verdade não funciona por vários motivos.)

  2. Usei o PHP.net para ler os detalhes sobre cada função de soquete usada no código do phpwebsockets. Ao fazer isso, finalmente consegui entender como todo o sistema funciona conceitualmente. Este foi um grande obstáculo.

  3. Eu li o rascunho real do WebSocket . Eu tive que ler isso um monte de vezes antes de finalmente começar a entender. Você provavelmente terá que voltar a este documento várias vezes ao longo do processo, pois é o único recurso definitivo com dados corretos e atualizados informações sobre a API WebSocket.

  4. Codifiquei o procedimento de handshake adequado com base nas instruções do rascunho no item 3. Isso não foi tão ruim.

  5. Continuei recebendo um monte de texto distorcido enviado dos clientes para o servidor após o aperto de mão e não consegui descobrir o motivo até perceber que os dados estão codificados e devem ser desmascarados. O link a seguir me ajudou muito aqui: (link original quebrado) Cópia arquivada .

    Observe que o código disponível neste link tem vários problemas e não funcionará corretamente sem outras modificações.

  6. Então me deparei com o seguinte segmento de SO, que explica claramente como codificar e decodificar adequadamente as mensagens enviadas e recebidas: Como posso enviar e receber mensagens WebSocket no servidor?

    Este link foi muito útil. Recomendo consultá-lo enquanto analisa o rascunho do WebSocket. Isso ajudará a dar mais sentido ao que o rascunho diz.

  7. Eu estava quase terminando neste ponto, mas tive alguns problemas com um aplicativo WebRTC que estava fazendo usando WebSocket, então acabei fazendo minha própria pergunta no SO, que eventualmente resolvi: Quais são esses dados no final das informações do candidato WebRTC?

  8. Nesse ponto, eu tinha praticamente tudo funcionando. Eu só tinha que adicionar alguma lógica adicional para lidar com o fechamento de conexões, e estava feito.

Esse processo levou cerca de duas semanas no total. A boa notícia é que agora entendo muito bem o WebSocket e pude fazer meus próprios scripts de cliente e servidor do zero que funcionam muito bem. Esperançosamente, o culminar de todas essas informações lhe dará orientação e informações suficientes para codificar seu próprio script WebSocket PHP.

Boa sorte!


Edit : Esta edição é alguns anos após minha resposta original e, embora eu ainda tenha uma solução de trabalho, ela não está realmente pronta para ser compartilhada. Felizmente, outra pessoa no GitHub tem código quase idêntico ao meu (mas muito mais limpo), então recomendo usar o seguinte código para uma solução PHP WebSocket funcional:
https://github.com/ghedipunk/PHP-Websockets/blob/master/ websockets.php


Edit # 2 : Embora eu ainda goste de usar PHP para muitas coisas relacionadas ao lado do servidor, tenho que admitir que realmente me familiarizei com o Node.js recentemente, e o principal motivo é porque ele é melhor projetado para lidar com WebSocket em vez de PHP (ou qualquer outra linguagem do lado do servidor). Como tal, descobri recentemente que é muito mais fácil configurar Apache / PHP e Node.js em seu servidor e usar Node.js para executar o servidor WebSocket e Apache / PHP para todo o resto. E no caso em que você está em um ambiente de hospedagem compartilhada em que não pode instalar / usar Node.js para WebSocket, você pode usar um serviço gratuito como Herokupara configurar um servidor Node.js WebSocket e fazer solicitações entre domínios a partir de seu servidor. Apenas certifique-se de fazer isso para configurar seu servidor WebSocket para poder lidar com solicitações de origem cruzada.

HartleySan
fonte
Thx, vou tentar fazer do seu jeito. Como você avalia o desempenho deste servidor PHP?
Dharman
@Dharman, é difícil dizer porque só consegui executá-lo no meu host local. Claro que funciona bem lá, mas em um servidor real com uma carga pesada, não sei. Eu imagino que funcionaria muito bem, pois não há inchaço no meu código.
HartleySan
1
Não estou com isso no momento, mas em um futuro próximo, estou planejando escrever um tutorial sobre todo o processo com todo o código. Depois de fazer isso, postarei o link.
HartleySan
1
@HartleySan: Olá! Eu estaria muito interessado em examinar seu código. Você poderia colocá-lo online ou enviá-lo para mim pessoalmente?
forthrin de
Sim, em breve estará online. Desculpe a todos que pediram por isso. Estive tão ocupado recentemente. Eu vou entrar nisso em breve.
HartleySan
26

Tanto quanto eu sei, Ratchet é a melhor solução PHP WebSocket disponível no momento. E como é de código aberto, você pode ver como o autor construiu essa solução WebSocket usando PHP.

leggetter
fonte
2
Adiciono aqui minha solução que usa Ratchet e Silex: github.com/eole-io/sandstone Não sei se você achará útil
Alcalyn
8

Por que não usar sockets http://uk1.php.net/manual/en/book.sockets.php ? Está bem documentado (não apenas no contexto de PHP) e tem bons exemplos http://uk1.php.net/manual/en/sockets.examples.php

Lukasz Kujawa
fonte
2
Sim, você pode ter uma conexão contínua entre soquetes de PHP simples e uma página da web, eu testei várias vezes.
WiMantis
@WiMantis: Olá! Você poderia colocar um exemplo de código que faça isso online ou, opcionalmente, enviar para mim pessoalmente?
forthrin
Você pode usar isso junto com uma conexão HTTP regular? Eu estava construindo uma estrutura DDD e gostaria de criar como uma classe de wrapper em cima disso e fornecer funcionalidade de soquete, de preferência em php vanilla usando extensão de núcleo
1

É necessário converter a chave de hex para dec antes de base64_encoding e enviá-la para handshake.

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

Avise-me se isso ajudar.

user2288650
fonte
1

Eu estava no seu lugar por um tempo e finalmente acabei usando node.js, porque ele pode fazer soluções híbridas como ter um servidor web e socket em um. Assim, o backend php pode enviar solicitações através de http para o servidor web do nó e então transmiti-lo com websocket. Maneira muito eficiente de ir.

MZ
fonte
então temos que usar um cliente http para fazer a solicitação de http do php para o servidor do nó certo?
Kiren Siva
Kiren Siva, correto. Curl ou similar, o nó transmite uma mensagem através do 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);
    }
}

de um terminal executado: php server.php

de outro terminal executado: echo "hello woerld" | nc 127.0.0.1 8002

Dipak Yadav
fonte
1
O que isso significa?
Dharman
8
São sockets, não WebSockets. Existe uma grande diferença.
Chris
@techexpander, De acordo com a pergunta, você deve explicar do zero em vez de ex. que não está funcionando.
Jaymin