<?php
class WebSocket {
private $serv;
private $timeout;
private $client_descs = [];
private $on_connect = null;
private $on_disconnect = null;
private $on_msg = null;
private $on_timeout = null;
public function __construct($host, $port, $timeout = 10000000) {
$this->serv = new Server($host, $port);
$this->timeout = $timeout;
}
public function start() {
$this->serv->start($this, $this->timeout);
}
public function stop() {
$this->serv->stop();
}
public function disconnect($id) {
$this->serv->disconnect($id);
}
public function getClients() {
$clients = array();
foreach($this->client_descs as $id => $client)
$clients[] = $id;
return $clients;
}
public function on($action, Closure $func) {
switch($action) {
case 'connect':
$this->on_connect = $func;
break;
case 'disconnect':
$this->on_disconnect = $func;
break;
case 'msg':
$this->on_msg = $func;
break;
case 'timeout':
$this->on_timeout = $func;
break;
}
}
public function sendAll($msg) {
foreach($this->client_descs as $client) {
$client->send($msg);
}
}
public function send($id, $msg) {
$this->client_descs[$id]->send($msg);
}
public function sendHeartBeat() {
foreach($this->client_descs as $client) {
$client->heartbeat();
}
}
/* Functions needed by Server */
public function onConnect($id) {
echo "WebSocket: Client connected.\n";
$this->client_descs[$id] = new ClientDesc($this->serv, $id);
$this->client_descs[$id]->connect();
$callback = $this->on_connect;
if($callback !== null)
$callback($id);
}
public function onDisconnect($id) {
echo "WebSocket: Client disconnected.\n";
unset($this->client_descs[$id]);
$callback = $this->on_disconnect;
if($callback !== null)
$callback($id);
}
public function onData($id, $data) {
$msg_arr = $this->client_descs[$id]->onData($data);
$callback = $this->on_msg;
if($callback !== null)
foreach($msg_arr as $msg)
$callback($id, $msg);
}
public function onTimeout() {
$this->sendHeartbeat();
$callback = $this->on_timeout;
if($callback !== null)
$callback();
}
}
class ClientDesc {
private $id;
private $serv;
private $connected = false;
private $handshake_done = false;
private $reqBuf_remainlen = 0;
private $reqBuf = null;
private $payloadBuf = null;
private $heartbeat_waiting = null;
public function __construct($serv, $id) {
$this->serv = $serv;
$this->id = $id;
}
public function connect() {
$this->connected = true;
}
public function disconnect() {
if(!$this->connected)
return;
$req = $this->buildCloseReq('');
$this->serv->send($this->id, $req);
$this->serv->disconnect($this->id);
$this->connected = false;
}
public function heartbeat() {
if($this->heartbeat_waiting !== null) {
echo "Heartbeat with no answer, diconnecting...\n";
$this->disconnect();
return;
}
$this->heartbeat_waiting = sha1('salt.f4cvgr41' . decbin(154 + rand()), false);
$rq = $this->buildPingReq($this->heartbeat_waiting);
$this->serv->send($this->id, $rq);
}
private function buildReq($type, $msg) {
// We do not fragment.
$req = $this->uchartobin($type & 0xF | 0x80);
$len = strlen($msg);
if($len <= 125) {
$req .= $this->uchartobin($len & 0x7F);
} else if($len <= 0xFFFF) {
$req .= $this->uchartobin(126);
$req .= $this->u16tobin($len & 0xFFFF);
} else {
$req .= $this->uchartobin(127);
$req .= $this->u16tobin($len);
}
$req .= $msg;
return $req;
}
private function buildContReq($msg) {
return $this->buildReq(0x0, $msg);
}
private function buildTextReq($msg) {
return $this->buildReq(0x1, $msg);
}
private function buildBinReq($msg) {
return $this->buildReq(0x2, $msg);
}
private function buildCloseReq($msg) {
return $this->buildReq(0x8, $msg);
}
private function buildPingReq($msg) {
return $this->buildReq(0x9, $msg);
}
private function buildPongReq($msg) {
return $this->buildReq(0xA, $msg);
}
public function send($msg) {
$rq = $this->buildTextReq($msg);
$this->serv->send($this->id, $rq);
}
public function onData($data) {
$data_arr = array();
if(!$this->handshake_done) {
if(!$this->handshake($data))
$this->serv->disconnect($this->id);
$this->handshake_done = true;
return $data_arr;
}
if($this->reqBuf !== NULL) {
$this->reqBuf_remainlen -= strlen($data);
if($this->reqBuf_remainlen > 0) {
$this->reqBuf .= $data;
return $data_arr;
}
$data = $this->reqBuf . $data;
$this->reqBuf = NULL;
$this->reqBuf_remainlen = 0;
}
while(true) {
// Too short.
if(strlen($data) < 2)
return $data_arr;
$idx = 0;
$c = $this->bintouchar($data[$idx]);
$opcode = $c & 0xF;
$fin = $c >> 7;
$idx++;
$c = $this->bintouchar($data[$idx]);
$payload_len = $c & 0x7F;
$mask = ($c & 0x80) >> 7;
$idx++;
if(!$mask) {
$this->disconnect();
return $data_arr;
}
if($payload_len === 126) {
$n = substr($data, $idx, 2);
$idx += 2;
$payload_len = $this->bintou16($n);
} else if($payload_len === 127) {
$n = substr($data, $idx, 8);
$idx += 8;
$payload_len = $this->bintou64($n);
}
$mask_key = null;
if($mask) {
$mask_key = str_split(substr($data, $idx, 4));
$mask_key[0] = $this->bintouchar($mask_key[0]);
$mask_key[1] = $this->bintouchar($mask_key[1]);
$mask_key[2] = $this->bintouchar($mask_key[2]);
$mask_key[3] = $this->bintouchar($mask_key[3]);
$idx += 4;
}
$payload = substr($data, $idx, $payload_len);
if(strlen($payload) < $payload_len) {
$this->reqBuf = $data;
$this->reqBuf_remainlen = $payload_len - strlen($payload);
return $data_arr;
}
if($mask)
for($i = 0 ; $i < $payload_len ; $i++)
$payload[$i] = chr($this->bintouchar($payload[$i]) ^ $mask_key[$i % 4]);
switch($opcode) {
case 0: /* Continuation */
$this->payloadBuf .= $payload;
if($fin)
$payload = $this->payloadBuf;
else
$payload = null;
break;
case 1: /* Text */
case 2: /* Binary */
$this->payloadBuf = $payload;
if(!$fin)
$payload = null;
break;
case 8: /* Close */
// We must disconnect and return immediately.
$this->disconnect();
return $data_arr;
case 9: /* Ping */
$req = $this->buildPongReq($payload);
$this->serv->send($this->id, $req);
$payload = null;
break;
case 10: /* Pong */
// If the returned hash matches, reset heartbeat, otherwise just ignore it.
if($this->heartbeat_waiting === $payload)
$this->heartbeat_waiting = null;
$payload = null;
break;
}
if($payload) {
$this->payloadBuf = null;
$data_arr[] = $payload;
}
$data = substr($data, $idx + $payload_len);
if(strlen($data) === 0)
break;
}
return $data_arr;
}
private function handshake($header) {
$header = explode("\r\n", $header);
$req = explode(' ', $header[0]);
if(count($req) !== 3 || $req[0] !== 'GET')
return false;
$http = explode('/', $req[2]);
if($http[0] !== 'HTTP')
return false;
$httpver = explode('.', $http[1]);
$httpver_major = intval($httpver[0]);
$httpver_minor = intval($httpver[1]);
if($httpver_major < 1 || ($httpver_major < 1 && $httpver_minor < 1))
return false;
$hdr_upgrade = null;
$hdr_connection = null;
$hdr_ws_key = null;
$hdr_ws_version = null;
foreach($header as $entry) {
$entry = explode(':', $entry, 2);
if(count($entry) != 2)
continue;
$entry[1] = trim($entry[1]);
switch($entry[0]) {
case 'Upgrade':
$param = explode(', ', $entry[1]);
if(in_array('websocket', $param, true))
$hdr_upgrade = true;
break;
case 'Connection':
$param = explode(', ', $entry[1]);
if(in_array('Upgrade', $param, true))
$hdr_connection = true;
break;
case 'Sec-WebSocket-Key':
$hdr_ws_key = $entry[1];
break;
case 'Sec-WebSocket-Version':
if(intval($entry[1]) === 13)
$hdr_ws_version = true;
break;
}
}
if($hdr_upgrade === null
|| $hdr_connection === null
|| $hdr_ws_key === null
|| $hdr_ws_version === null)
return false;
$ans = "HTTP/1.1 101 Switching Protocols\r\n";
$ans .= "Upgrade: websocket\r\n";
$ans .= "Connection: Upgrade\r\n";
$ans .= 'Sec-WebSocket-Accept: ' . base64_encode(sha1($hdr_ws_key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)) . "\r\n";
$ans .= "\r\n";
$this->serv->send($this->id, $ans);
return true;
}
private function bintouchar($str) {
return unpack('Cb', $str)['b'];
}
private function bintou16($str) {
return unpack('nw', $str)['w'];
}
private function bintou64($str) {
$nums = unpack('Nhigh/Nlow', $str);
return ($nums['high'] << 32) | $nums['low'];
}
private function uchartobin($c) {
return pack('C', $c);
}
private function u16tobin($w) {
return pack('n', $w);
}
private function u64tobin($q) {
$low = $q & 0xFFFFFFFF;
$high = $q >> 32;
return pack('NN', $high, $low);
}
}
Vous devez être inscrit pour répondre à ce sujet.