题意:如何在 PHP 中创建 WebSockets 服务器
问题背景:
I am looking for a simple code to create a WebSocket server. I found phpwebsockets but it is outdated now and doesn't support the newest protocol. I tried updating it myself but it doesn't seem to work.
我在寻找一个简单的代码来创建 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;
}
?>
and the client:
以及客户端
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);
};
If there is anything wrong in my code can you help me fix it? Console in Firefox says:
如果我的代码有任何问题,你能帮我修复吗?Firefox 控制台显示
Firefox can't establish a connection to the server at ws://localhost:12345/.
问题解决:
I was in the same boat as you recently, and here is what I did:
我最近也遇到过和你一样的问题,这是我做的
-
I used the phpwebsockets code as a reference for how to structure the server-side code. (You seem to already be doing this, and as you noted, the code doesn't actually work for a variety of reasons.) 我使用了
phpwebsockets
代码作为参考,了解如何构建服务器端代码。(你似乎已经在这么做了,正如你提到的,代码因为各种原因实际上并不能正常工作。) -
I used PHP.net to read the details about every socket function used in the phpwebsockets code. By doing this, I was finally able to understand how the whole system works conceptually. This was a pretty big hurdle. 我使用 PHP.net 阅读了
phpwebsockets
代码中使用的每个套接字函数的详细信息。通过这样做,我终于能够从概念上理解整个系统是如何工作的。这是一个相当大的障碍 -
I read the actual WebSocket draft. I had to read this thing a bunch of times before it finally started to sink in. You will likely have to go back to this document again and again throughout the process, as it is the one definitive resource with correct, up-to-date information about the WebSocket API. 我阅读了实际的 WebSocket 草案。在我反复读了几遍之后,才开始真正理解。你在整个过程中很可能需要一遍又一遍地回到这份文档,因为它是唯一一个包含正确、最新的 WebSocket API 信息的权威资源
-
I coded the proper handshake procedure based on the instructions in the draft in #3. This wasn't too bad. 我根据第 3 条草案中的说明编写了正确的握手过程。这并不太难
-
I kept getting a bunch of garbled text sent from the clients to the server after the handshake and I couldn't figure out why until I realized that the data is encoded and must be unmasked. The following link helped me a lot here: (original link broken) Archived copy. 在握手之后,我不断收到从客户端发送到服务器的乱码文本,我无法弄明白为什么,直到我意识到数据是经过编码的,并且必须解掩码。以下链接对我帮助很大:(原始链接已损坏)存档副本
Please note that the code available at this link has a number of problems and won't work properly without further modification. 请注意,链接中的代码存在一些问题,未经进一步修改无法正常工作
-
I then came across the following SO thread, which clearly explains how to properly encode and decode messages being sent back and forth: 然后我遇到了以下的 Stack Overflow 讨论,它清楚地解释了如何正确地编码和解码发送来回的消息How can I send and receive WebSocket messages on the server side?
This link was really helpful. I recommend consulting it while looking at the WebSocket draft. It'll help make more sense out of what the draft is saying. 这个链接非常有帮助。我建议在查看 WebSocket 草案时参考它,它将帮助你更好地理解草案中的内容
-
I was almost done at this point, but had some issues with a WebRTC app I was making using WebSocket, so I ended up asking my own question on SO, which I eventually solved: 到这时我差不多完成了,但在使用 WebSocket 制作 WebRTC 应用时遇到了一些问题,所以我最终在 Stack Overflow 上提了一个问题,最后解决了它What is this data at the end of WebRTC candidate info?
-
At this point, I pretty much had it all working. I just had to add some additional logic for handling the closing of connections, and I was done. 到这个时刻,我几乎已经完成了所有工作。我只需要添加一些额外的逻辑来处理连接的关闭,然后就完成了
That process took me about two weeks total. The good news is that I understand WebSocket really well now and I was able to make my own client and server scripts from scratch that work great. Hopefully the culmination of all that information will give you enough guidance and information to code your own WebSocket PHP script.
这个过程大约花了我两周的时间。好消息是,现在我对 WebSocket 有了很深的理解,并且我能够从零开始编写出自己的客户端和服务器脚本,运行得非常好。希望所有这些信息的总结能够为你提供足够的指导和信息,帮助你编写自己的 WebSocket PHP 脚本
Good luck!
Edit: This edit is a couple of years after my original answer, and while I do still have a working solution, it's not really ready for sharing. Luckily, someone else on GitHub has almost identical code to mine (but much cleaner), so I recommend using the following code for a working PHP WebSocket solution: 编辑:这是我原始回答后的几年更新,虽然我仍然有一个有效的解决方案,但它还不太适合分享。幸运的是,GitHub 上有其他人编写了几乎与我相同的代码(但更清晰),所以我建议使用以下代码来实现一个可用的 PHP WebSocket 解决方案
PHP-Websockets/websockets.php at master · ghedipunk/PHP-Websockets · GitHub
Edit #2: While I still enjoy using PHP for a lot of server-side related things, I have to admit that I've really warmed up to Node.js a lot recently, and the main reason is because it's better designed from the ground up to handle WebSocket than PHP (or any other server-side language). As such, I've found recently that it's a lot easier to set up both Apache/PHP and Node.js on your server and use Node.js for running the WebSocket server and Apache/PHP for everything else. And in the case where you're on a shared hosting environment in which you can't install/use Node.js for WebSocket, you can use a free service like Heroku to set up a Node.js WebSocket server and make cross-domain requests to it from your server. Just make sure if you do that to set your WebSocket server up to be able to handle cross-origin requests.
虽然我仍然喜欢在很多与服务器端相关的事情中使用 PHP,但我必须承认,最近我对 Node.js 的喜爱大大增加,主要原因是 Node.js 从底层设计上就比 PHP(或其他任何服务器端语言)更适合处理 WebSocket。因此,我最近发现,在服务器上设置 Apache/PHP 和 Node.js 变得更加容易,可以使用 Node.js 来运行 WebSocket 服务器,而使用 Apache/PHP 处理其他所有事务。如果你处于一个共享主机环境,无法安装/使用 Node.js 来处理 WebSocket,你可以使用像 Heroku 这样的免费服务来设置一个 Node.js WebSocket 服务器,并从你的服务器向它发起跨域请求。只要确保在这样做时,设置你的 WebSocket 服务器能够处理跨域请求