如何在PHP中创建WebSockets服务器

128

我正在寻找一个简单的代码来创建WebSocket服务器。 我发现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);
};

如果我的代码有任何问题,你能帮我修复吗?在 Firefox 控制台上显示:

Firefox 无法与 ws://localhost:12345/ 建立连接。


1
此页面列出了他们也在当前的phpwebsockets上遇到了问题,并包括他们在示例源代码中所做的更改:http://net.tutsplus.com/tutorials/javascript-ajax/start-using-html5-websockets-today/ - scrappedcola
为什么不使用套接字[http://uk1.php.net/manual/en/book.sockets.php]?它有很好的文档(不仅在PHP上下文中),并且有良好的示例[http://uk1.php.net/manual/en/sockets.examples.php]。 - Lukasz Kujawa
1
一个有用的库,可用于WebSockets应用程序。包括客户端和PHP端。http://www.techzonemind.com/php-websocket-library-two-way-real-time-communication/ - Jithin Jose
1
我认为最好用C++来实现它。 - Michael Chourdakis
1
由于这个问题引起了很多关注,我决定提供我最终想出的解决方案。这是我的完整代码。 - Dharman
5个回答

167
我最近和你一样遇到了同样的问题,这是我所做的事情:
  1. 我参考了 phpwebsockets 代码来构建服务器端代码结构,你似乎也在这样做,但是如你所指出的,该代码实际上由于各种原因无法正常工作。

  2. 我使用 PHP.net 来查看 phpwebsockets 代码中使用的 每个套接字函数 的详细信息。通过这样做,我最终能够理解整个系统的概念。这是一个相当大的障碍。

  3. 我阅读了实际的 WebSocket 草案。我不得不多次阅读这份文档,才开始逐渐理解。在整个过程中,您可能需要反复参考此文档,因为它是唯一具有正确和最新的 WebSocket API 信息的权威资源。

  4. 我根据第三点草案中的说明编写了适当的握手程序。这并不太困难。

  5. 在握手之后,我一直收到从客户端发送的一堆乱码,并且一直无法找出原因,直到意识到数据被编码并且必须取消掩码。以下链接在这里帮助了我很多:原始link已经失效Archived copy

    请注意,此链接提供的代码存在许多问题,必须进行进一步修改才能正常工作。

  6. 然后我发现了以下 SO 主题,它清楚地解释了如何正确地对来回发送的消息进行编码和解码:
    How can I send and receive WebSocket messages on the server side?

    这个链接非常有帮助。在查看 WebSocket 草案时,建议参考此链接。它将有助于更好地理解草案中所说的内容。

  7. 当时我的 WebRTC 应用程序使用 WebSocket 存在一些问题,因此最终我在 SO 上提出了自己的问题,并解决了它:
    What is this data at the end of WebRTC candidate info?

  8. 到这里,我基本上已经全部完成了。我只需要添加一些额外的逻辑来处理连接的关闭,就可以了。

那个过程总共花了我大约两周的时间。好消息是,现在我非常了解WebSocket,并且能够自己从头编写出很好用的客户端和服务器脚本。 希望所有这些信息的综合将为您提供足够的指导和信息,以编写自己的WebSocket PHP脚本。 祝你好运!

编辑:这个编辑是在我最初的回答几年后,虽然我仍然有一个可用的解决方案,但它并不真正准备分享。幸运的是,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服务器设置为能够处理跨源请求。

2
我目前还没有准备好,但在不久的将来,我计划撰写一个关于整个过程以及所有代码的教程。一旦完成,我会发布链接。 - HartleySan
1
@HartleySan:你好!我非常有兴趣查看你的代码。你能把它放到网上或者私信给我吗? - forthrin
是的,它很快就会上线了。对于所有询问过它的人,我感到抱歉。最近我非常忙碌,但我会尽快处理它。 - HartleySan
@KirenSiva,我认为这个人对你的问题回答得很好:https://dev59.com/_WEh5IYBdhLWcg3wdzWR#22411059。话虽如此,我不确定使用情况,但通常情况下,您会想要直接从浏览器向WebSocket服务器发送WebSocket请求,而不是从PHP向WebSocket服务器发送请求。就像我上面推荐的那样,Node.js比PHP更适合处理这个问题,并且在任何服务器上安装和设置都相对容易。 - HartleySan
@HartleySan 我有个问题,希望你能回答。你说你是从零开始编写了WebSocket服务器,那么你是否需要使用一些外部库或框架? - Wais Kamal
显示剩余5条评论

35

据我所知,Ratchet是目前最好的PHP WebSocket解决方案。而且,由于它是开源的,您可以看到作者如何使用PHP构建这个WebSocket解决方案。


3
我在这里添加了我的解决方案,使用了Ratchet和Silex:https://github.com/eole-io/sandstone。我不知道你是否会觉得它有用。 - Alcalyn

7

我花费了数小时寻找PHP + WebSockets的最小解决方案,直到我发现了这篇文章:

超级简单的PHP WebSocket示例

不需要任何第三方库。

下面是如何实现它:创建一个包含以下内容的index.html文件:

<html>
<body>
    <div id="root"></div>
    <script>
        var host = 'ws://<<<IP_OF_YOUR_SERVER>>>:12345/websockets.php';
        var socket = new WebSocket(host);
        socket.onmessage = function(e) {
            document.getElementById('root').innerHTML = e.data;
        };
    </script>
</body>
</html>

在命令行中启动php websockets.php(是的,它将是一个不断运行的PHP脚本事件循环),然后在浏览器中打开此websockets.php文件。


这是介绍websockets如何与PHP配合使用的好方法,但实现在真实世界中可能并不值得,因为它使用了while(true) .. sleep(1)(称为“长轮询”,不好)。另一方面,[Ratchet](http://socketo.me/)在其核心中使用[ZeroMQ](https://zeromq.org/),可以正确处理它。 - evilReiko
1
那是一个很好的例子。它漂亮而简单地展示了如何监听连接并向Web浏览器发送数据。 我希望还会有一个例子,说明如何从Web浏览器接收数据到服务器。 当我在握手数据后使用socket_read时,数据似乎不是ASCII码。 - LovelyHanibal
1
@evilReiko 不不不,这并不是长轮询;while / sleep(1) 只是一个示例,让服务器每秒向客户端发送示例消息,只是 WebSocket 的演示。在实际应用中,您根本不需要在服务器代码中使用 sleep(1) - Basj
@Basj 噢,你是对的! - evilReiko

3

我曾经有过和你一样的经历,最终选择了node.js,因为它可以实现混合解决方案,例如在一个服务器上同时开启web和socket服务器。这样php后台就能够通过http向node web服务器提交请求,并通过websocket广播出去。这是非常高效的一种方式。


所以我们必须使用一个HTTP客户端从PHP向Node服务器发出HTTP请求,对吗? - Kiren S
Kiren Siva,正确。使用Curl或类似工具,然后Node通过WebSocket广播消息。 - M.Z.

3
需要在进行base64编码之前将密钥从十六进制转换为十进制,然后发送握手信号。
$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";

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接