วิธีสร้างเซิร์ฟเวอร์ websockets ใน PHP


89

มีบทช่วยสอนหรือคำแนะนำที่แสดงวิธีการเขียนเซิร์ฟเวอร์เว็บซ็อกเก็ตแบบง่าย ๆ ใน PHP หรือไม่ ผมลองหาใน google แล้วครับ แต่ไม่เจอมากมาย ฉันพบ 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);
};

หากมีอะไรผิดพลาดในรหัสของฉันคุณสามารถช่วยฉันแก้ไขได้หรือไม่? Concole ใน firefox พูดว่าFirefox can't establish a connection to the server at ws://localhost:12345/.

แก้ไข
เนื่องจากมีความสนใจในคำถามนี้มากฉันจึงตัดสินใจให้สิ่งที่ฉันคิดในที่สุด นี่คือรหัสเต็มของฉัน


1
หน้านี้แสดงว่าพวกเขามีปัญหากับ phpwebsockets ปัจจุบันเช่นกันและรวมถึงการเปลี่ยนแปลงที่ทำในตัวอย่างรหัส src: net.tutsplus.com/tutorials/javascript-ajax/…
scrappedcola

1
ไลบรารีที่มีประโยชน์ซึ่งสามารถใช้สำหรับแอปพลิเคชัน WebSockets รวมทั้งฝั่งไคลเอ็นต์และ PHP techzonemind.com/…
Jithin Jose

1
ฉันคิดว่ามันจะดีกว่าที่จะนำไปใช้ใน C ++
Michael Chourdakis

คำตอบ:


119

ฉันอยู่ในเรือลำเดียวกับคุณเมื่อไม่นานมานี้และนี่คือสิ่งที่ฉันทำ:

  1. ฉันใช้โค้ดphpwebsocketsเป็นข้อมูลอ้างอิงสำหรับวิธีการจัดโครงสร้างโค้ดฝั่งเซิร์ฟเวอร์ (ดูเหมือนคุณจะทำสิ่งนี้อยู่แล้วและอย่างที่คุณสังเกตเห็นว่าโค้ดใช้ไม่ได้จริงด้วยเหตุผลหลายประการ)

  2. ฉันใช้ PHP.net เพื่ออ่านรายละเอียดเกี่ยวกับทุกฟังก์ชั่นซ็อกเก็ตที่ใช้ในโค้ด phpwebsockets เมื่อทำเช่นนี้ในที่สุดฉันก็สามารถเข้าใจวิธีการทำงานของระบบทั้งหมดตามแนวคิด นี่เป็นอุปสรรคใหญ่ทีเดียว

  3. ผมอ่านที่เกิดขึ้นจริงร่าง WebSocket ฉันต้องอ่านสิ่งนี้หลายครั้งก่อนที่มันจะเริ่มจมลงในที่สุดคุณอาจจะต้องกลับไปที่เอกสารนี้ซ้ำแล้วซ้ำอีกตลอดกระบวนการเนื่องจากเป็นแหล่งข้อมูลขั้นสุดท้ายที่มีความถูกต้องและเป็นปัจจุบัน ข้อมูลเกี่ยวกับ WebSocket API

  4. ฉันเขียนรหัสขั้นตอนการจับมือที่ถูกต้องตามคำแนะนำในร่างใน # 3 นี่ก็ไม่เลวร้ายเกินไป

  5. ฉันได้รับข้อความที่อ่านไม่ออกจำนวนมากที่ส่งจากไคลเอนต์ไปยังเซิร์ฟเวอร์หลังจากการจับมือกันและฉันไม่สามารถหาสาเหตุได้จนกว่าฉันจะรู้ว่าข้อมูลถูกเข้ารหัสและต้องถูกเปิดโปง ลิงค์ต่อไปนี้ช่วยฉันได้มากที่นี่: (ลิงค์เดิมเสีย) สำเนาเก็บไว้

    โปรดทราบว่ารหัสที่มีอยู่ในลิงก์นี้มีปัญหาหลายประการและจะทำงานไม่ถูกต้องหากไม่มีการแก้ไขเพิ่มเติม

  6. จากนั้นฉันก็เจอเธรด SO ต่อไปนี้ซึ่งอธิบายวิธีการเข้ารหัสและถอดรหัสข้อความที่ส่งไปมาอย่างถูกต้อง: ฉันจะส่งและรับข้อความ WebSocket ทางฝั่งเซิร์ฟเวอร์ได้อย่างไร?

    ลิงค์นี้มีประโยชน์มาก ขอแนะนำให้ปรึกษาขณะดูแบบร่าง WebSocket จะช่วยให้เข้าใจได้มากขึ้นว่าร่างกำลังพูดอะไร

  7. ฉันเกือบจะเสร็จแล้ว แต่มีปัญหาบางอย่างกับแอป WebRTC ที่ฉันกำลังสร้างโดยใช้ WebSocket ดังนั้นฉันจึงถามคำถามของตัวเองเกี่ยวกับ SO ซึ่งในที่สุดฉันก็แก้ไขได้: ข้อมูลนี้คืออะไรในตอนท้ายของข้อมูลผู้สมัคร WebRTC

  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เพื่อตั้งค่าเซิร์ฟเวอร์ WebSocket Node.js และส่งคำขอข้ามโดเมนจากเซิร์ฟเวอร์ของคุณ ตรวจสอบให้แน่ใจว่าคุณทำเช่นนั้นเพื่อตั้งค่าเซิร์ฟเวอร์ WebSocket ของคุณให้สามารถจัดการคำขอข้ามแหล่งที่มาได้หรือไม่


ขอบคุณฉันจะพยายามทำในแบบของคุณ คุณให้คะแนนประสิทธิภาพของเซิร์ฟเวอร์ PHP นี้อย่างไร?
ธรรมทาน

@Dharman มันยากที่จะพูดเพราะฉันสามารถเรียกใช้บน localhost ของฉันเท่านั้น แน่นอนว่ามันใช้งานได้ดี แต่บนเซิร์ฟเวอร์จริงที่มีภาระงานหนักฉันไม่รู้ ฉันคิดว่ามันจะทำงานได้ดีพอสมควรเนื่องจากโค้ดของฉันไม่มีการขยายตัว
HartleySan

1
ตอนนี้ฉันยังไม่มีมัน แต่ในอนาคตอันใกล้นี้ฉันกำลังวางแผนที่จะเขียนบทช่วยสอนเกี่ยวกับกระบวนการทั้งหมดด้วยโค้ดทั้งหมด เมื่อทำเสร็จแล้วฉันจะโพสต์ลิงก์
HartleySan

1
@HartleySan: สวัสดี! ฉันสนใจที่จะดูรหัสของคุณมาก คุณสามารถวางออนไลน์หรือส่งให้ฉันเป็นการส่วนตัวได้ไหม
farrin

ใช่มันจะออนไลน์เร็ว ๆ นี้ ขอโทษทุกคนที่ถามมา ฉันเพิ่งยุ่งมากเมื่อไม่นานมานี้ ฉันจะไปเร็ว ๆ นี้
HartleySan

26

เท่าที่ฉันทราบRatchetเป็นโซลูชัน PHP WebSocket ที่ดีที่สุดในขณะนี้ และเนื่องจากเป็นโอเพ่นซอร์สคุณจึงสามารถดูได้ว่าผู้เขียนสร้างโซลูชัน WebSocket นี้โดยใช้ PHP ได้อย่างไร


2
ฉันเพิ่มโซลูชันของฉันที่ใช้ Ratchet และ Silex ที่นี่: github.com/eole-io/sandstoneฉันไม่รู้ว่าคุณจะพบว่ามันมีประโยชน์หรือไม่
Alcalyn

8

ทำไมไม่ใช้ซ็อกเก็ตhttp://uk1.php.net/manual/en/book.sockets.php ? มีการบันทึกไว้อย่างดี (ไม่ใช่เฉพาะในบริบท PHP) และมีตัวอย่างที่ดีhttp://uk1.php.net/manual/en/sockets.examples.php


2
ใช่คุณสามารถเชื่อมต่ออย่างต่อเนื่องระหว่างซ็อกเก็ต PHP ธรรมดาและหน้าเว็บฉันได้ทดสอบหลายครั้งแล้ว
WiMantis

@WiMantis: สวัสดี! คุณช่วยใส่ตัวอย่างโค้ดที่ทำแบบนี้ทางออนไลน์หรืออาจจะส่งให้ฉันเป็นการส่วนตัว
ออกมา

คุณสามารถใช้สิ่งนี้ควบคู่ไปกับการเชื่อมต่อ HTTP ปกติได้หรือไม่ ฉันกำลังสร้างเฟรมเวิร์ก DDD และฉันต้องการสร้างเหมือนคลาส wrapper ที่ด้านบนของสิ่งนี้และให้ฟังก์ชันการทำงานของซ็อกเก็ตโดยเฉพาะอย่างยิ่งใน vanilla php โดยใช้ส่วนขยายหลัก

2

ฉันอยู่ในรองเท้าของคุณมาสักพักแล้วและในที่สุดก็ลงเอยด้วยการใช้ node.js เพราะมันสามารถทำโซลูชันแบบไฮบริดเช่นมีเว็บและเซิร์ฟเวอร์ซ็อกเก็ตในที่เดียว ดังนั้น php แบ็กเอนด์สามารถส่งคำขอผ่าน http ไปยังเว็บเซิร์ฟเวอร์โหนดแล้วออกอากาศด้วย websocket วิธีที่มีประสิทธิภาพมากที่จะไป


ดังนั้นเราต้องใช้ไคลเอนต์ http เพื่อทำการร้องขอ http จาก php ไปยังเซิร์ฟเวอร์โหนดใช่ไหม
Kiren Siva

คิเรนสิวะถูกต้อง Curl หรือที่คล้ายกันจากนั้นโหนดจะส่งข้อความผ่าน websocket
MZ

1

ต้องแปลงคีย์จากฐานสิบหกเป็นถอดรหัสก่อน base64_encoding จากนั้นจึงส่งสำหรับการจับมือ

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

โปรดแจ้งให้เราทราบหากสิ่งนี้ช่วยได้


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

จากการรันเทอร์มินัลเดียว: php server.php

จากเทอร์มินัลรันอื่น: echo "hello woerld" | nc 127.0.0.1 8002


1
นั่นหมายถึงอะไร?
ธรรมทาน

8
นี่คือซ็อกเก็ตไม่ใช่ WebSockets มีความแตกต่างกันมาก
คริส

@techexpander ตามคำถามที่คุณต้องอธิบายตั้งแต่เริ่มต้นแทนที่จะเป็นเช่น ซึ่งใช้งานไม่ได้
Jaymin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.