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