PHPでWebSocketサーバーを作成する方法


88

PHPで簡単な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 can't establish a connection to the server at ws://localhost:12345/.

編集
この質問には多くの関心があるので、私は最終的に思いついたものをあなたに提供することにしました。これが私の完全なコードです。


1
このページには、現在のphpwebsocketsにも問題があったことがリストされており、サンプルのsrcコードで行った変更が含まれています:net.tutsplus.com/tutorials/javascript-ajax/…–
スクラップ

1
WebSocketアプリケーションに使用できる便利なライブラリ。クライアント側とPHP側の両方を含みます。techzonemind.com/…–
Jithin Jose

1
C ++で実装したほうがいいと思います。
MichaelChourdakis19年

回答:


113

私は最近あなたと同じボートに乗っていました、そしてこれが私がしたことです:

1)サーバー側のコードを構造化する方法のリファレンスとしてphpwebsocketsコードを使用しました。(あなたはすでにこれを行っているようです、そしてあなたが指摘したように、コードは実際にはさまざまな理由で機能しません。)

2)PHP.netを使用して、phpwebsocketsコードで使用されているすべてのソケット関数の詳細を読み取りました。そうすることで、ようやくシステム全体がどのように機能しているかを概念的に理解することができました。これはかなり大きなハードルでした。

3)実際のWebSocketドラフトを読みました(投稿ごとに2つ以上のリンクを投稿することはできないため、Web検索を行ってください)。最終的に沈み始める前に、私はこのことを何度も読まなければなりませんでした。これは正しい、最新の1つの決定的なリソースであるため、プロセス全体で何度もこのドキュメントに戻る必要があります。 WebSocketAPIに関する情報。

4)#3のドラフトの指示に基づいて、適切なハンドシェイク手順をコーディングしました。これはそれほど悪くはありませんでした。

5)ハンドシェイク後、クライアントからサーバーに大量の文字化けしたテキストが送信され続けましたが、データがエンコードされており、マスクを解除する必要があることに気付くまで、理由がわかりませんでした。次のリンクはここで私を大いに助けました:http//srchea.com/blog/2011/12/build-a-real-time-application-using-html5-websockets/

このリンクで入手できるコードには多くの問題があり、さらに変更しないと正しく機能しないことに注意してください。

6)次に、次のSOスレッドに出くわしました。これは、送受信されるメッセージを適切にエンコードおよびデコードする方法を明確に説明しています。サーバー側でWebSocketメッセージを送受信するにはどうすればよいですか。

このリンクは本当に役に立ちました。WebSocketドラフトを見ながら参照することをお勧めします。ドラフトが言っていることをより理解するのに役立ちます。

7)この時点でほぼ完了しましたが、WebSocketを使用して作成しているWebRTCアプリで問題が発生したため、SOについて独自の質問をすることになり、最終的に解決しました。質問と回答を参照するには、「SO WebRTC候補情報の最後にあるこのデータは何ですか?」をWeb検索してください。(引用符なし)。

8)この時点で、私はほとんどすべてが機能していました。接続の終了を処理するためのロジックを追加するだけで、完了しました。

そのプロセスには、合計で約2週間かかりました。幸いなことに、私はWebSocketを非常によく理解しており、独自のクライアントスクリプトとサーバースクリプトを最初から作成して、うまく機能させることができました。うまくいけば、そのすべての情報の集大成が、独自のWebSocketPHPスクリプトをコーディングするのに十分なガイダンスと情報を提供するでしょう。幸運を!

編集:この編集は私の最初の答えから数年後です、そして私はまだ実用的な解決策を持っていますが、それは本当に共有する準備ができていません。幸い、GitHubの他の誰かが私のものとほぼ同じコードを持っているので(ただし、はるかにクリーンです)、動作するPHP WebSocketソリューションには次のコードを使用することをお勧めします:https
//github.com/ghedipunk/PHP-Websockets/blob/master/ websockets.php

編集#2:サーバーサイド関連の多くのことにPHPを使用することを今でも楽しんでいますが、最近Node.jsにかなりウォームアップしたことを認めなければなりません。主な理由は、PHPがより適切に設計されているためです。 PHP(または他のサーバーサイド言語)よりもWebSocketを処理するために基礎を固めます。そのため、最近、サーバーでApache / PHPとNode.jsの両方をセットアップし、WebSocketサーバーの実行にNode.jsを使用し、その他すべてにApache / PHPを使用する方がはるかに簡単であることがわかりました。また、Node.js for WebSocketをインストール/使用できない共有ホスティング環境を使用している場合は、Herokuなどの無料サービスを使用してNode.js WebSocketサーバーをセットアップし、クロスドメインを作成できます。サーバーからのリクエスト。


Thx、私はあなたのやり方でそれをやってみます。このPHPサーバーのパフォーマンスをどのように評価しますか?
ダルマン2013年

@Dharman、ローカルホストでしか実行できなかったので、言うのは難しいです。もちろん、そこでは問題なく動作しますが、負荷の高い実サーバーではわかりません。私のコードには肥大化がないので、それでもかなりうまくいくと思います。
HartleySan 2013年

1
現時点では準備ができていませんが、近い将来、すべてのコードを使用してプロセス全体についてのチュートリアルを作成する予定です。それができたら、リンクを投稿します。
HartleySan 2013年

1
@HartleySan:こんにちは!私はあなたのコードを見ることに非常に興味があります。オンラインで公開するか、個人的に送っていただけませんか。
フォースリン2013

はい、まもなくオンラインになります。それを求めたすべての人に申し訳ありません。私は最近とても忙しいです。すぐに乗ります。
HartleySan 2013


8

ソケットhttp://uk1.php.net/manual/en/book.sockets.phpを使用しないのはなぜですか?これは(PHPのコンテキストだけでなく)十分に文書化されており、良い例がありますhttp://uk1.php.net/manual/en/sockets.examples.php


2
はい、プレーンPHPソケットとWebページの間で継続的に接続できます。私はそれを複数回テストしました。
WiMantis 2013

@WiMantis:こんにちは!これをオンラインで行うコード例を掲載するか、オプションで個人的に送信していただけますか?
フォースリン2013

これを通常のHTTP接続と一緒に使用できますか?私はDDDフレームワークを構築していて、この上にラッパークラスのように作成し、できればコア拡張機能を使用してバニラphpでソケット機能を提供したいと思います

1

base64_encodingの前に、キーを16進数から12進数に変換してから、ハンドシェイクに送信する必要があります。

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

これが役立つかどうか教えてください。


1

私はしばらくの間あなたの立場にあり、最終的にnode.jsを使用することになりました。これは、Webサーバーとソケットサーバーを1つにまとめるなどのハイブリッドソリューションを実行できるためです。したがって、phpバックエンドはhttpを介してノードWebサーバーにリクエストを送信し、それをWebSocketでブロードキャストできます。非常に効率的な方法です。


したがって、httpクライアントを使用してphpからノードサーバーへのhttpリクエストを行う必要がありますか?
Kirenシヴァ

キレン・シヴァ、正解です。カールまたは類似は、そのノードの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);
    }
}

1つのターミナルから実行:php server.php

別のターミナルの実行から:echo "hello woerld" | nc 127.0.0.1 8002


1
それはどういう意味ですか?
ダルマン2013年

8
これはソケットであり、WebSocketではありません。大きな違いがあります。
クリス

@techexpander、質問ごとに、exではなく最初から説明する必要があります。これは機能していません。
ジェイミン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.