<?php
/*
PHP socket server, by aaaaaa123456789, version 1.0
October 17, 2013
This code is released to the public domain under the Creative Commons CC0 license.
You can find the full text at: http://creativecommons.org/publicdomain/zero/1.0/legalcode
*/
const SERVER_SPLIT_CHAR = 0; // split on the specified character
const SERVER_SPLIT_LENGTH = 1; // split on the specified length
const SERVER_SPLIT_BYTES_AFTER_BYTE = 2; // split after the amount of bytes specified by a certain value (8-bit)
const SERVER_SPLIT_BYTES_AFTER_16LE = 3; // split after the amount of bytes specified by a certain value (16-bit, little endian)
const SERVER_SPLIT_BYTES_AFTER_32LE = 4; // split after the amount of bytes specified by a certain value (32-bit, little endian)
const SERVER_SPLIT_BYTES_AFTER_16BE = 5; // split after the amount of bytes specified by a certain value (16-bit, big endian)
const SERVER_SPLIT_BYTES_AFTER_32BE = 6; // split after the amount of bytes specified by a certain value (32-bit, big endian)
const SERVER_SPLIT_NEWLINES = 7; // split on any newline
const SERVER_SPLIT_NEWLINES_NO_BLANKS = 8; // split on any sequence of newlines (i.e., split on newlines and don't send empty lines)
const SERVER_SPLIT_PACKET = 9; // don't split, just consider the packet received (whatever size) as the appropriate size
const SERVER_ERROR_INVALID_PORT = -1;
const SERVER_ERROR_CANNOT_CREATE_SOCKET = -2;
const SERVER_ERROR_CANNOT_READ = -3;
const SERVER_ERROR_LISTENING_SOCKET_KILLED = -4;
const SERVER_ERROR_ALREADY_RUNNING = -5;
const SERVER_SEND_ALL = 0; // sends the buffer passed as is, without change
const SERVER_SEND_LINE = -1; // sends until the first newline, including it
const SERVER_SEND_LINE_NO_NEWLINE = -2; // sends until the first newline, stripping it
const SERVER_SEND_LINE_LF = -3; // sends until the first newline (not included), and adds a \n to it
const SERVER_SEND_LINE_CR = -4; // sends until the first newline (not included), and adds a \r to it
const SERVER_SEND_LINE_CRLF = -5; // sends until the first newline (not included), and adds a \r\n to it
class Server {
private $_clients;
private $_groups;
private $_delim;
private $_split;
private $_callbacks;
private $_listening;
private $_running;
private $_clientstemp;
private $_packetlimit;
private $_setters = array();
private $_getters = array(
'running',
'clients',
'groups',
'newGroupID'
);
private $_accessors = array(
'openCallback',
'closeCallback',
'receiveCallback',
'overflowCallback',
'delimiter',
'splitMode',
'packetLimit'
);
private function setCallback ($event, $callback) {
if (isset($this -> _callbacks[$event]))
$current = $this -> _callbacks[$event];
else
$current = false;
if ($callback === null) return $current;
if ($callback === false) {
if ($current !== false) unset($this -> _callbacks[$event]);
return false;
}
if (!is_callable($callback)) return null;
$this -> _callbacks[$event] = $callback;
return $callback;
}
private function createClient ($socket) {
$newclient = array();
$newclient['socket'] = $socket;
$newclient['buffer'] = "";
foreach ($this -> _groups as $group => $count)
$newclient[$group] = false;
do
$newID = mt_rand();
while (($newID <= 0) or ($newID > 2147483647));
while (isset($this -> _clients[$newID])){
$newID ++;
if (($newID <= 0) or ($newID > 2147483647)) $newID = 1;
}
$this -> _clients[$newID] = $newclient;
return $newID;
}
private function removeClient ($client) {
if (!isset($this -> _clients[$client])) return false;
$clientdata = $this -> _clients[$client];
unset($this -> _clients[$client]);
socket_close($clientdata['socket']);
foreach ($this -> _groups as $group => $count)
if ($clientdata[$group]) {
$this -> _groups[$group] --;
if (!$this -> _groups[$group])
$this -> deleteGroup($group);
}
if (isset($this -> _callbacks[$client])) unset($this -> _callbacks[$client]);
return true;
}
private function getSockets () {
$result = array($this -> _listening);
foreach ($this -> _clients as $client)
$result[] = $client['socket'];
return $result;
}
private function closeEverything () {
foreach ($this -> _clients as $client => $info)
$this -> closeClient($client);
if ($this -> _listening !== null) {
socket_close($this -> _listening);
$this -> _listening = null;
}
$this -> _groups = null;
$this -> _clients = null;
$this -> _clientstemp = null;
$this -> _running = false;
return true;
}
private function processIncoming () {
for ($retries = 3; $retries and (($socket = socket_accept($this -> _listening)) === false); $retries --);
if ($socket === false) return false;
$id = $this -> createClient($socket);
if (isset($this -> _callbacks['open']))
call_user_func($this -> _callbacks['open'], $id, $this);
return true;
}
private function triggerClientOverflow ($client) {
if (isset($this -> _callbacks['overflow'])) {
call_user_func($this -> _callbacks['overflow'], $client, $this);
return;
}
$this -> removeClient($client);
if (isset($this -> _callbacks['close']))
call_user_func($this -> _callbacks['close'], $client, $this);
}
private function checkBuffers ($size) {
$limit = $this -> _packetlimit;
if ($limit <= 0) return;
foreach ($this -> _clients as $client => $data) {
if (!isset($this -> _clients[$client]['buffer'])) continue;
if (strlen($this -> _clients[$client]['buffer']) > $limit)
$this -> triggerClientOverflow($client);
}
}
private function getValue ($buffer, $position, $bitwidth, $bigendian = false) {
$count = (int) ($bitwidth / 8);
if (strlen($buffer) < ($count)) return null;
$partial = substr($buffer, $position, $count);
if (strlen($partial) < $count) return null;
if ($bigendian) return $this -> getValue(strrev($partial), 0, 8 * $count);
$result = 0;
while ($partial != "") {
$char = ord($partial);
$partial = substr($partial, 1);
$result <<= 8;
$result += $char;
}
return $result;
}
private function processReceived ($client, $data) {
if (isset($this -> _callbacks[$client]))
call_user_func($this -> _callbacks[$client], $data, $this, $client);
else if (isset($this -> _callbacks['receive']))
call_user_func($this -> _callbacks['receive'], $data, $client, $this);
}
private function splitReceived ($client, $buffer) {
$mode = $this -> _split;
if (isset($this -> _clients[$client]['buffer'])) {
$buffer = $this -> _clients[$client]['buffer'] . $buffer;
unset($this -> _clients[$client]['buffer']);
}
switch ($mode) {
case SERVER_SPLIT_PACKET:
$this -> processReceived($client, $buffer);
return;
case SERVER_SPLIT_NEWLINES:
case SERVER_SPLIT_NEWLINES_NO_BLANKS:
while ((($crpos = strpos($buffer, "\r")) !== false) | (($lfpos = strpos($buffer, "\n")) !== false)) {
if ($crpos === false) $crpos = 2147483647; // good ol' high value technique
if ($lfpos === false) $lfpos = 2147483647;
if ($crpos < $lfpos) {
$cut = $crpos;
$skip = ($lfpos == ($crpos + 1)) ? 2 : 1;
} else {
$cut = $lfpos;
$skip = 1;
}
$line = substr($buffer, 0, $cut);
$buffer = substr($buffer, $cut + $skip);
if (($mode == SERVER_SPLIT_NEWLINES_NO_BLANKS) and ($line == "")) continue;
$this -> processReceived($client, $line);
}
break;
case SERVER_SPLIT_CHAR:
while (($split = strpos($buffer, chr($this -> _delim))) !== false) {
$line = substr($buffer, 0, $split);
$buffer = substr($buffer, $split + 1);
$this -> processReceived($client, $line);
}
break;
case SERVER_SPLIT_LENGTH:
$length = strlen($buffer);
for ($pos = 0; $pos < $length; $pos += $this -> _delim)
$this -> processReceived($client, substr($buffer, $pos, $this -> _delim));
$buffer = substr($buffer, $pos, $length - $pos);
break;
case SERVER_SPLIT_BYTES_AFTER_BYTE:
$bytes = 1;
$be = false;
goto split_bytes;
case SERVER_SPLIT_BYTES_AFTER_16LE:
$bytes = 2;
$be = false;
goto split_bytes;
case SERVER_SPLIT_BYTES_AFTER_16BE:
$bytes = 2;
$be = true;
goto split_bytes;
case SERVER_SPLIT_BYTES_AFTER_32LE:
$bytes = 4;
$be = false;
goto split_bytes;
case SERVER_SPLIT_BYTES_AFTER_32BE:
$bytes = 4;
$be = true;
split_bytes:
while ($buffer != "") {
$length = strlen($buffer);
$base = $this -> _delim + $bytes;
if ($length < $base) break;
$offset = $this -> getValue($buffer, $this -> _delim, $bytes * 8, $be);
$bs = $base + $offset;
if ($length < $bs) break;
$block = substr($buffer, 0, $bs);
$buffer = substr($buffer, $bs);
$this -> processReceived($client, $block);
}
}
if (($this -> _packetlimit > 0) and (strlen($buffer) > $this -> _packetlimit))
$this -> triggerClientOverflow($client);
else if ($buffer != "")
$this -> _clients[$client]['buffer'] = $buffer;
}
private function splitAgain () {
foreach ($this -> _clients as $client => $data) {
if (!isset($this -> _clients[$client]['buffer'])) continue;
$this -> splitReceived($client, "");
}
}
private function processReceiving ($client) {
if (!isset($this -> _clients[$client])) return false;
$socket = $this -> _clients[$client]['socket'];
$length = (($split = $this -> _split) == SERVER_SPLIT_PACKET) ? $this -> _delim : 2048;
if ($length <= 0) $length = 2048; // default
$buffer = str_repeat("\0", $length);
$length = socket_recv($socket, $buffer, $length, 0);
if (!$length) return false;
$buffer = substr($buffer, 0, $length);
$this -> splitReceived($client, $buffer);
return true;
}
private function searchClientBySocket ($socket) {
foreach ($this -> _clients as $client => $data)
if ($data['socket'] == $socket)
return $client;
return false;
}
private function closeClient ($client) {
if (!isset($this -> _clients[$client])) return false;
if (isset($this -> _callbacks[$client]))
$callback = $this -> _callbacks[$client];
else
$callback = null;
$data = $this -> _clients[$client];
$this -> removeClient($client);
$this -> _clientstemp[$client] = $data;
if ($callback !== null)
call_user_func($callback, null, $this, $client);
else if (isset($this -> _callbacks['close']))
call_user_func($this -> _callbacks['close'], $client, $this);
unset($this -> _clientstemp[$client]);
return true;
}
private function createGroup ($group) {
if (!is_int($group)) return false;
if ($group <= 0) return false;
if (isset($this -> _groups[$group])) return false;
$this -> _groups[$group] = 0;
foreach ($this -> _clients as $client => $data)
$this -> _clients[$client][$group] = false;
return true;
}
private function parseData ($data, $mode) {
if (is_int($data)) {
if (($data >= 0) and ($data < 256))
return ord($data);
return false;
} else if (is_array($data)) {
$result = "";
foreach ($data as $item) {
if (!is_int($item)) return false;
if (($item < 0) or ($item >= 256)) return false;
$result .= chr($item);
}
return $result;
} else if (!is_string($data))
return false;
else {
if (is_float($mode)) $mode = (int) $mode;
if (($mode === SERVER_SEND_ALL) or ($mode === null)) return $data;
if (!is_int($mode)) return false;
if ($mode < -5) return false;
if ($mode > 0) {
$length = strlen($data);
if ($length == $mode) return $data;
if ($length > $mode) return substr($data, 0, $mode);
$data .= str_repeat("\0", $mode - $length);
return $data;
}
$pos1 = strpos($data, "\r");
$pos2 = strpos($data, "\n");
if (($pos1 === false) and ($pos2 === false)) {
$substring = $data;
$linebreak = "";
} else {
if ($pos1 < $pos2) {
$substring = substr($data, 0, $pos1);
if ($pos2 == ($pos1 + 1))
$linebreak = "\r\n";
else
$linebreak = "\r";
} else {
$substring = substr($data, 0, $pos2);
$linebreak = "\n";
}
}
switch ($mode) {
case SERVER_SEND_LINE: return $substring . $linebreak;
case SERVER_SEND_LINE_NO_NEWLINE: return $substring;
case SERVER_SEND_LINE_CR: return $substring . "\r";
case SERVER_SEND_LINE_LF: return $substring . "\n";
case SERVER_SEND_LINE_CRLF: return $substring . "\r\n";
}
}
return false;
}
private function sendData ($socket, $data) {
if ($data === false) return false;
$remainder = strlen($data);
$attempts = 0;
while ($remainder and ($attempts < 3)) {
$sent = socket_send($socket, $data, $remainder, 0);
if (!$sent) {
$attempts ++;
continue;
}
$remainder -= $sent;
if ($remainder <= 0) return true;
$data = substr($data, $sent);
}
return false;
}
public function __construct () {
$this -> _listening = null;
$this -> _split = SERVER_SPLIT_NEWLINES_NO_BLANKS;
$this -> _delim = 0;
$this -> _running = false;
$this -> _clients = array();
$this -> _clientstemp = array();
$this -> _callbacks = array();
$this -> _groups = array();
$this -> _packetlimit = 0;
}
// returns the status of the server (running or not)
public function running () {
return $this -> _running;
}
// opens the server, in the specified port; this function will return when the server dies
public function open ($port) {
if ($this -> _running) return SERVER_ERROR_ALREADY_RUNNING;
if (!is_int($port)) return SERVER_ERROR_INVALID_PORT;
if (($port < 1) or ($port > 65535)) return SERVER_ERROR_INVALID_PORT;
$this -> _listening = socket_create_listen($port, SOMAXCONN);
if ($this -> _listening === false) {
$this -> _listening = null;
return SERVER_ERROR_CANNOT_CREATE_SOCKET;
}
$this -> _clients = array();
$this -> _clientstemp = array();
$this -> _groups = array();
$this -> _running = true;
while ($this -> _running) {
$sockets = $this -> getSockets();
$w = $e = null;
$repetitions = 0;
do
$status = socket_select($sockets, $w, $e, null);
while (($status === false) and ((++ $repetitions) < 5));
if ($status === false) {
$this -> closeEverything();
return SERVER_ERROR_CANNOT_READ;
}
foreach ($sockets as $socket) {
if ($socket === $this -> _listening)
$status = $this -> processIncoming();
else {
$client = $this -> searchClientBySocket($socket);
$status = $this -> processReceiving($client);
}
if (!$status) {
if ($socket == $this -> _listening) {
$this -> closeEverything();
return SERVER_ERROR_LISTENING_SOCKET_KILLED;
} else
$this -> closeClient($client);
}
}
}
$this -> closeEverything();
return 0;
}
// closes the server, killing all outstanding client connections
public function close () {
$x = $this -> _running;
$this -> _running = false;
return $x;
}
// sets or gets the value of the delimiter between packets (a character for SERVER_SPLIT_CHAR, a length for SERVER_SPLIT_LENGTH, or a
position)
public function delimiter ($newdelim = null) {
if (is_float($newdelim)) $newdelim = (int) $newdelim;
if (is_int($newdelim) and ($newdelim >= 0))
$this -> _delim = $newdelim;
elseif (is_string($newdelim) and (strlen($newdelim) == 1))
$this -> _delim = ord($newdelim);
if ($newdelim !== null) $this -> splitAgain();
return ($this -> _split == SERVER_SPLIT_CHAR) ? @chr($this -> _delim) : $this -> _delim;
}
// sets or gets the splitting mode according to the defined constants
public function splitMode ($newmode = null) {
if (is_int($newmode) and ($newmode >= 0) and ($newmode <= 9)) {
$this -> _split = $newmode;
$this -> splitAgain();
}
return $this -> _split;
}
// sets or gets the packet size limit, in bytes (0 = no limit)
public function packetLimit ($newlimit = null) {
if (is_int($newlimit) and ($newlimit >= 0)) {
$this -> _packetlimit = $newlimit;
if ($newlimit > 0) $this -> checkBuffers($newlimit);
}
return $this -> _packetlimit;
}
// sends a message, according to the send mode, to a certain client or an array of them
// the message can also be a single byte-long value or an array of them, in which case the mode is ignored
public function send ($client, $data, $mode = null) {
if (!$this -> _running) return false;
if (is_array($client)) {
$errors = array();
foreach ($client as $each)
if ($this -> send($each, $data, $mode) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
if (!isset($this -> _clients[$client])) return false;
return $this -> sendData($this -> _clients[$client]['socket'], $this -> parseData($data, $mode));
}
// broadcasts a message to all clients (group = 0), all but a client (group = -client), or those who are in a certain group (group > 0)
public function broadcast ($data, $group = null, $mode = null) {
if (!$this -> _running) return false;
$errors = array();
if (($group === null) or ($group === 0))
foreach ($this -> _clients as $client => $info) {
if ($this -> send($client, $data, $mode) !== true)
$errors[] = $client;
}
elseif (!is_int($group))
return false;
elseif ($group < 0)
foreach ($this -> _clients as $client => $info) {
if ($client == (- $group)) continue;
if ($this -> send($client, $data, $mode) !== true)
$errors[] = $client;
}
else if (!isset($this -> _groups[$group]))
return false;
else
foreach ($this -> _clients as $client => $info) {
if (!$info[$group]) continue;
if ($this -> send($client, $data, $mode) !== true)
$errors[] = $client;
}
if (!count($errors)) return true;
return $errors;
}
// closes the connection with a client (or an array of them), disconnecting it
public function disconnect ($client) {
if (!$this -> _running) return false;
if (is_array($client)) {
$errors = array();
foreach ($client as $each)
if ($this -> disconnect($each) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
$this -> removeClient($client);
return true;
}
// closes the connection for multiple clients ($group has the same meaning as for the broadcast() method)
public function disconnectMultiple ($group = null) {
if (!$this -> _running) return false;
$errors = array();
if (($group === null) or ($group === 0))
foreach ($this -> _clients as $client => $data) {
if ($this -> disconnect($client) !== true)
$errors[] = $client;
}
else if (!is_int($group))
return false;
else if ($group < 0)
foreach ($this -> _clients as $client => $data) {
if (!($client + $group)) continue;
if ($this -> disconnect($client) !== true)
$errors[] = $client;
}
elseif (!isset($this -> _groups[$group]))
return false;
else
foreach ($this -> _clients as $client => $data) {
if (!$data[$group]) continue;
if ($this -> disconnect($client) !== true)
$errors[] = $client;
}
if (!count($errors)) return true;
return $errors;
}
// returns all clients currently connected to the server
public function clients () {
if (!$this -> _running) return false;
return array_keys($this -> _clients);
}
// returns the groups a client has joined
public function groupsOf ($client) {
if (!$this -> _running) return null;
if (isset($this -> _clientstemp[$client])) {
$joined = array();
foreach ($this -> _groups as $group => $count)
if ($this -> _clientstemp[$client][$group])
$joined[] = $group;
return $joined;
}
if (!isset($this -> _clients[$client])) return null;
$joined = array();
foreach ($this -> _groups as $group => $count)
if ($this -> _clients[$client][$group])
$joined[] = $group;
return $joined;
}
// joins a client (or an array of them) to a group (or an array of them)
public function joinGroup ($client, $group) {
if (!$this -> _running) return false;
if (is_array($client)) {
$errors = array();
foreach ($client as $each)
if ($this -> joinGroup($each, $group) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
if (is_array($group)) {
$errors = array();
foreach ($group as $each)
if ($this -> joinGroup($client, $each) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
if (!isset($this -> _clients[$client])) return false;
if (isset($this -> _groups[$group])) {
$this -> _clients[$client][$group] = true;
$this -> _groups[$group] ++;
return true;
}
$status = $this -> createGroup($group);
if (!$status) return false;
$this -> _clients[$client][$group] = true;
$this -> _groups[$group] = 1;
return true;
}
// removes a client (or an array of them) from a group (or an array of them)
public function leaveGroup ($client, $group) {
if (!$this -> _running) return false;
if (is_array($client)) {
$errors = array();
foreach ($client as $each)
if ($this -> leaveGroup($each, $group) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
if (is_array($group)) {
$errors = array();
foreach ($group as $each)
if ($this -> leaveGroup($client, $each) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
if (!isset($this -> _clients[$client])) return false;
if (!isset($this -> _groups[$group])) return false;
$this -> _clients[$client][$group] = false;
$this -> _groups[$group] --;
if ($this -> _groups[$group] == 0) $this -> deleteGroup($group);
return true;
}
// deletes an entire group (or an array of them)
public function deleteGroup ($group) {
if (!$this -> _running) return false;
if (is_array($group)) {
$errors = array();
foreach ($group as $each)
if ($this -> deleteGroup($each) !== true)
$errors[] = $each;
if (!count($errors)) return true;
return $errors;
}
if (!isset($this -> _groups[$group])) return false;
foreach ($this -> _clients as $clientid => $client)
unset($this -> _clients[$clientid][$group]);
unset($this -> _groups[$group]);
return true;
}
// returns all existing groups
public function groups () {
if (!$this -> _running) return null;
return array_keys($this -> _groups);
}
// returns an available ID for a new group
public function newGroupID () {
if (!$this -> _running) return null;
do
$newID = mt_rand();
while (($newID <= 0) or ($newID > 2147483647));
while (isset($this -> _groups[$newID])){
$newID ++;
if (($newID <= 0) or ($newID > 2147483647)) $newID = 1;
}
return $newID;
}
// sets or gets the callback for when a new connection is opened, the callback receives the new client's ID and the server object itself
// the callback is removed if it is set to false
public function openCallback ($callback = null) {
return $this -> setCallback('open', $callback);
}
// sets or gets the callback for when a connection is closed, the callback receives the old client's ID and the server object itself
// the callback is removed if it is set to false
public function closeCallback ($callback = null) {
return $this -> setCallback('close', $callback);
}
// sets or gets the callback for when a message is received, the callback receives the message, the client's ID and the server object
itself
// the callback is removed if it is set to false
public function receiveCallback ($callback = null) {
return $this -> setCallback('receive', $callback);
}
// sets or gets the callback for when a message is received for a certain client (the callback is removed if it is set to false)
// the callback receives the message (null for a close), the server object itself and the client's ID
public function clientCallback ($client, $callback = null) {
if (!$this -> _running) return false;
if (!isset($this -> _clients[$client])) return false;
return $this -> setCallback($client, $callback);
}
// sets or gets the callback for when a connection sends too much data and overflows the packet limit
// the callback receives the client's ID and the server object itself, and the data that is to be received can no longer be trusted to be
// properly split. If set to false, a default callback that closes the connection and calls the closing callback is used
public function overflowCallback ($client, $callback = null) {
return $this -> setCallback('overflow', $callback);
}
public function __get ($property) {
if (!in_array($property, array_merge($this -> _getters, $this -> _accessors)))
trigger_error("Invalid property invoked: $property", E_USER_ERROR);
return $this -> $property();
}
public function __set ($property, $value) {
if (!in_array($property, array_merge($this -> _setters, $this -> _accessors)))
trigger_error("Invalid or read-only property invoked: $property", E_USER_ERROR);
return $this -> $property($value);
}
}
?>
Comments