<?
//require_once('./daemonize.php');
require_once('./users.php');
abstract class WebSocketServer {
protected $userClass = 'WebSocketUser'; // redefine this if you want a custom user class. The custom user class should inherit from WebSocketUser.
protected $maxBufferSize;
protected $master;
protected $sockets = array();
protected $users = array();
protected $heldMessages = array();
protected $interactive = true;
protected $headerOriginRequired = false;
protected $headerSecWebSocketProtocolRequired = false;
protected $headerSecWebSocketExtensionsRequired = false;
function __construct($addr, $port, $bufferLength = 1024) {
$this->maxBufferSize = $bufferLength * 1024 + 8;
$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Failed: socket_create()");
socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die("Failed: socket_option()");
socket_bind($this->master, $addr, $port) or die("Failed: socket_bind()");
socket_listen($this->master,20) or die("Failed: socket_listen()");
$this->sockets['m'] = $this->master;
$this->stdout("Server started\nListening on: $addr:$port\nMaster socket: ".$this->master);
}
abstract protected function process($user,$message); // Called immediately when the data is recieved.
abstract protected function connected($user); // Called after the handshake response is sent to the client.
abstract protected function closed($user); // Called after the connection is closed.
protected function connecting($user) {
// Override to handle a connecting user, after the instance of the User is created, but before
// the handshake has completed.
}
protected function send($user, $message) {
if ($user->handshake) {
$message = $this->frame($message,$user);
$result = @socket_write($user->socket, $message, strlen($message));
}
else {
// User has not yet performed their handshake. Store for sending later.
$holdingMessage = array('user' => $user, 'message' => $message);
$this->heldMessages[] = $holdingMessage;
}
}
protected function tick() {
// Override this for any process that should happen periodically. Will happen at least once
// per second, but possibly more often.
}
protected function _tick() {
// Core maintenance processes, such as retrying failed messages.
foreach ($this->heldMessages as $key => $hm) {
$found = false;
foreach ($this->users as $currentUser) {
if ($hm['user']->socket == $currentUser->socket) {
$found = true;
if ($currentUser->handshake) {
unset($this->heldMessages[$key]);
$this->send($currentUser, $hm['message']);
}
}
}
if (!$found) {
// If they're no longer in the list of connected users, drop the message.
unset($this->heldMessages[$key]);
}
}
}
/**
* Main processing loop
*/
public function run() {
while(true) {
if (empty($this->sockets)) {
$this->sockets['m'] = $this->master;
}
$read = $this->sockets;
$write = $except = null;
$this->_tick();
$this->tick();
@socket_select($read,$write,$except,1);
foreach ($read as $socket) {
if ($socket == $this->master) {
$client = socket_accept($socket);
if ($client < 0) {
$this->stderr("Failed: socket_accept()");
continue;
}
else {
$this->connect($client);
$this->stdout("Client connected. " . $client);
}
}
else {
$numBytes = @socket_recv($socket, $buffer, $this->maxBufferSize, 0);
if ($numBytes === false) {
$sockErrNo = socket_last_error($socket);
switch ($sockErrNo)
{
case 102: // ENETRESET -- Network dropped connection because of reset
case 103: // ECONNABORTED -- Software caused connection abort
case 104: // ECONNRESET -- Connection reset by peer
case 108: // ESHUTDOWN -- Cannot send after transport endpoint shutdown -- probably more of an error on our part, if we're trying to write after the socket is closed. Probably not a critical error, though.
case 110: // ETIMEDOUT -- Connection timed out
case 111: // ECONNREFUSED -- Connection refused -- We shouldn't see this one, since we're listening... Still not a critical error.
case 112: // EHOSTDOWN -- Host is down -- Again, we shouldn't see this, and again, not critical because it's just one connection and we still want to listen to/for others.
case 113: // EHOSTUNREACH -- No route to host
case 121: // EREMOTEIO -- Rempte I/O error -- Their hard drive just blew up.
case 125: // ECANCELED -- Operation canceled
$this->stderr("Unusual disconnect on socket " . $socket);
$this->disconnect($socket, true, $sockErrNo); // disconnect before clearing error, in case someone with their own implementation wants to check for error conditions on the socket.
break;
default:
$this->stderr('Socket error: ' . socket_strerror($sockErrNo));
}
}
elseif ($numBytes == 0) {
$this->disconnect($socket);
$this->stderr("Client disconnected. TCP connection lost: " . $socket);
}
else {
$user = $this->getUserBySocket($socket);
if (!$user->handshake) {
$tmp = str_replace("\r", '', $buffer);
if (strpos($tmp, "\n\n") === false ) {
continue; // If the client has not finished sending the header, then wait before sending our upgrade response.
}
$this->doHandshake($user,$buffer);
}
else {
//split packet into frame and send it to deframe
$this->split_packet($numBytes,$buffer, $user);
}
}
}
}
}
}
protected function connect($socket) {
$user = new $this->userClass(uniqid('u'), $socket);
$this->users[$user->id] = $user;
$this->sockets[$user->id] = $socket;
$this->connecting($user);
}
protected function disconnect($socket, $triggerClosed = true, $sockErrNo = null) {
$disconnectedUser = $this->getUserBySocket($socket);
if ($disconnectedUser !== null) {
unset($this->users[$disconnectedUser->id]);
if (array_key_exists($disconnectedUser->id, $this->sockets)) {
unset($this->sockets[$disconnectedUser->id]);
}
if (!is_null($sockErrNo)) {
socket_clear_error($socket);
}
if ($triggerClosed) {
$this->stdout("Client disconnected. ".$disconnectedUser->socket);
$this->closed($disconnectedUser);
socket_close($disconnectedUser->socket);
}
else {
$message = $this->frame('', $disconnectedUser, 'close');
@socket_write($disconnectedUser->socket, $message, strlen($message));
}
}
}
protected function doHandshake($user, $buffer) {
$magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
$headers = array();
$lines = explode("\n",$buffer);
foreach ($lines as $line) {
if (strpos($line,":") !== false) {
$header = explode(":",$line,2);
$headers[strtolower(trim($header[0]))] = trim($header[1]);
}
elseif (stripos($line,"get ") !== false) {
preg_match("/GET (.*) HTTP/i", $buffer, $reqResource