Navarr's Tech Side The Technical Side of my Life

1Jul/109

How to create a socket server in PHP

class::SocketServer

EDIT: If you need to submit bugs or want to improve my work, its now available on github!

Ever tried searching for information on how to properly create a multi-client socket server in PHP?  You’ll get plenty of results with outdated and messy source code, some of which won’t even work.

This was the my state a couple days ago when I decided that I wanted to build an IRC server.  The why is not important… (For the fun of building an IRC Server).  So I googled around a hell of a lot bit until I finally found some code that worked on its own, and quickly built a semi-functional IRC server using it, and headed off to sleep at 5am.

The next day I was very, very happy with the results of my hard labor, but it wasn’t good enough, so I started re-writing it from scratch as an Object, and thus I created class::IRCServer.

Today, once I felt that I was finished screwing around with my newly built IRCd, I decided to modify the function enough to be used on its own as a socket server, to share with the world.

And thus, class::SocketServer was created.

< ?php
	/*!	@class		SocketServer
		@author		Navarr Barnier
		@abstract 	A Framework for creating a multi-client server using the PHP language.
	 */
	class SocketServer
	{
		/*!	@var		config
			@abstract	Array - an array of configuration information used by the server.
		 */
		protected $config;
 
		/*!	@var		hooks
			@abstract	Array - a dictionary of hooks and the callbacks attached to them.
		 */
		protected $hooks;
 
		/*!	@var		master_socket
			@abstract	resource - The master socket used by the server.
		 */
		protected $master_socket;
 
		/*!	@var		max_clients
			@abstract	unsigned int - The maximum number of clients allowed to connect.
		 */
		public $max_clients = 10;
 
		/*!	@var		max_read
			@abstract	unsigned int - The maximum number of bytes to read from a socket at a single time.
		 */
		public $max_read = 1024;
 
		/*!	@var		clients
			@abstract	Array - an array of connected clients.
		 */
		public $clients;
 
		/*!	@function	__construct
			@abstract	Creates the socket and starts listening to it.
			@param		string	- IP Address to bind to, NULL for default.
			@param		int	- Port to bind to
			@result		void
		 */
		public function __construct($bind_ip,$port)
		{
			set_time_limit(0);
			$this->hooks = array();
 
			$this->config["ip"] = $bind_ip;
			$this->config["port"] = $port;
 
			$this->master_socket = socket_create(AF_INET, SOCK_STREAM, 0);
			socket_bind($this->master_socket,$this->config["ip"],$this->config["port"]) or die("Issue Binding");
			socket_getsockname($this->master_socket,$bind_ip,$port);
			socket_listen($this->master_socket);
			SocketServer::debug("Listenting for connections on {$bind_ip}:{$port}");
		}
 
		/*!	@function	hook
			@abstract	Adds a function to be called whenever a certain action happens.  Can be extended in your implementation.
			@param		string	- Command
			@param		callback- Function to Call.
			@see		unhook
			@see		trigger_hooks
			@result		void
		 */
		public function hook($command,$function)
		{
			$command = strtoupper($command);
			if(!isset($this->hooks[$command])) { $this->hooks[$command] = array(); }
			$k = array_search($function,$this->hooks[$command]);
			if($k === FALSE)
			{
				$this->hooks[$command][] = $function;
			}
		}
 
		/*!	@function	unhook
			@abstract	Deletes a function from the call list for a certain action.  Can be extended in your implementation.
			@param		string	- Command
			@param		callback- Function to Delete from Call List
			@see		hook
			@see		trigger_hooks
			@result		void
		 */
		public function unhook($command = NULL,$function)
		{
			$command = strtoupper($command);
			if($command !== NULL)
			{
				$k = array_search($function,$this->hooks[$command]);
				if($k !== FALSE)
				{
					unset($this->hooks[$command][$k]);
				}
			} else {
				$k = array_search($this->user_funcs,$function);
				if($k !== FALSE)
				{
					unset($this->user_funcs[$k]);
				}
			}
		}
 
		/*!	@function	loop_once
			@abstract	Runs the class's actions once.
			@discussion	Should only be used if you want to run additional checks during server operation.  Otherwise, use infinite_loop()
			@param		void
			@see		infinite_loop
			@result 	bool	- True
		*/
		public function loop_once()
		{
			// Setup Clients Listen Socket For Reading
			$read[0] = $this->master_socket;
			for($i = 0; $i < $this->max_clients; $i++)
			{
				if(isset($this->clients[$i]))
				{
					$read[$i + 1] = $this->clients[$i]->socket;
				}
			}
 
			// Set up a blocking call to socket_select
			if(socket_select($read,$write = NULL, $except = NULL, $tv_sec = 5) < 1)
			{
			//	SocketServer::debug("Problem blocking socket_select?");
				return true;
			}
 
			// Handle new Connections
			if(in_array($this->master_socket, $read))
			{
				for($i = 0; $i < $this->max_clients; $i++)
				{
					if(empty($this->clients[$i]))
					{
						$temp_sock = $this->master_socket;
						$this->clients[$i] = new SocketServerClient($this->master_socket,$i);
						$this->trigger_hooks("CONNECT",$this->clients[$i],"");
						break;
					}
					elseif($i == ($this->max_clients-1))
					{
						SocketServer::debug("Too many clients...   ");
					}
				}
 
			}
 
			// Handle Input
			for($i = 0; $i < $this->max_clients; $i++) // for each client
			{
				if(isset($this->clients[$i]))
				{
					if(in_array($this->clients[$i]->socket, $read))
					{
						$input = socket_read($this->clients[$i]->socket, $this->max_read);
						if($input == null)
						{
							$this->disconnect($i);
						}
						else
						{
							SocketServer::debug("{$i}@{$this->clients[$i]->ip} --> {$input}");
							$this->trigger_hooks("INPUT",$this->clients[$i],$input);
						}
					}
				}
			}
			return true;
		}
 
		/*!	@function	disconnect
			@abstract	Disconnects a client from the server.
			@param		int	- Index of the client to disconnect.
			@param		string	- Message to send to the hooks
			@result		void
		*/
		public function disconnect($client_index,$message = "")
		{
			$i = $client_index;
			SocketServer::debug("Client {$i} from {$this->clients[$i]->ip} Disconnecting");
			$this->trigger_hooks("DISCONNECT",$this->clients[$i],$message);
			$this->clients[$i]->destroy();
			unset($this->clients[$i]);
		}
 
		/*!	@function	trigger_hooks
			@abstract	Triggers Hooks for a certain command.
			@param		string	- Command who's hooks you want to trigger.
			@param		object	- The client who activated this command.
			@param		string	- The input from the client, or a message to be sent to the hooks.
			@result		void
		*/
		public function trigger_hooks($command,&$client,$input)
		{
			if(isset($this->hooks[$command]))
			{
				foreach($this->hooks[$command] as $function)
				{
					SocketServer::debug("Triggering Hook '{$function}' for '{$command}'");
					$continue = call_user_func($function,$this,$client,$input);
					if($continue === FALSE) { break; }
				}
			}
		}
 
		/*!	@function	infinite_loop
			@abstract	Runs the server code until the server is shut down.
			@see		loop_once
			@param		void
			@result		void
		*/
		public function infinite_loop()
		{
			$test = true;
			do
			{
				$test = $this->loop_once();
			}
			while($test);
		}
 
		/*!	@function	debug
			@static
			@abstract	Outputs Text directly.
			@discussion	Yeah, should probably make a way to turn this off.
			@param		string	- Text to Output
			@result		void
		*/
		public static function debug($text)
		{
			echo("{$text}\r\n");
		}
 
		/*!	@function	socket_write_smart
			@static
			@abstract	Writes data to the socket, including the length of the data, and ends it with a CRLF unless specified.
			@discussion	It is perfectly valid for socket_write_smart to return zero which means no bytes have been written. Be sure to use the === operator to check for FALSE in case of an error.
			@param		resource- Socket Instance
			@param		string	- Data to write to the socket.
			@param		string	- Data to end the line with.  Specify a "" if you don't want a line end sent.
			@result		mixed	- Returns the number of bytes successfully written to the socket or FALSE on failure. The error code can be retrieved with socket_last_error(). This code may be passed to socket_strerror() to get a textual explanation of the error.
		*/
		public static function socket_write_smart(&$sock,$string,$crlf = "\r\n")
		{
			SocketServer::debug("< -- {$string}");
			if($crlf) { $string = "{$string}{$crlf}"; }
			return socket_write($sock,$string,strlen($string));
		}
 
		/*!	@function	__get
			@abstract	Magic Method used for allowing the reading of protected variables.
			@discussion	You never need to use this method, simply calling $server->variable works because of this method's existence.
			@param		string	- Variable to retrieve
			@result		mixed	- Returns the reference to the variable called.
		*/
		function &__get($name)
		{
			return $this->{$name};
		}
	}
 
	/*!	@class		SocketServerClient
		@author		Navarr Barnier
		@abstract	A Client Instance for use with SocketServer
	 */
	class SocketServerClient
	{
		/*!	@var		socket
			@abstract	resource - The client's socket resource, for sending and receiving data with.
		 */
		protected $socket;
 
		/*!	@var		ip
			@abstract	string - The client's IP address, as seen by the server.
		 */
		protected $ip;
 
		/*!	@var		hostname
			@abstract	string - The client's hostname, as seen by the server.
			@discussion	This variable is only set after calling lookup_hostname, as hostname lookups can take up a decent amount of time.
			@see		lookup_hostname
		 */
		protected $hostname;
 
		/*!	@var		server_clients_index
			@abstract	int - The index of this client in the SocketServer's client array.
		 */
		protected $server_clients_index;
 
		/*!	@function	__construct
			@param		resource- The resource of the socket the client is connecting by, generally the master socket.
			@param		int	- The Index in the Server's client array.
			@result		void
		 */
		public function __construct(&$socket,$i)
		{
			$this->server_clients_index = $i;
			$this->socket = socket_accept($socket) or die("Failed to Accept");
			SocketServer::debug("New Client Connected");
			socket_getpeername($this->socket,$ip);
			$this->ip = $ip;
		}
 
		/*!	@function	lookup_hostname
			@abstract	Searches for the user's hostname and stores the result to hostname.
			@see		hostname
			@param		void
			@result		string	- The hostname on success or the IP address on failure.
		 */
		public function lookup_hostname()
		{
			$this->hostname = gethostbyaddr($this->ip);
			return $this->hostname;
		}
 
		/*!	@function	destroy
			@abstract	Closes the socket.  Thats pretty much it.
			@param		void
			@result		void
		 */
		public function destroy()
		{
			socket_close($this->socket);
		}
 
		function &__get($name)
		{
			return $this->{$name};
		}
 
		function __isset($name)
		{
			return isset($this->{$name});
		}
	}

class::SocketServer does all the functions necessary for a server.  It binds to the IP address and starts listening to the port.  Its easy to specify a maximum number of clients to allow, and the way its coded makes it easily modified.

Here is an example of a server (using this class) that listens for a user to send a string, and then echoes the reverse of that string back to the user.

< ?php
	// This is PHP5 Code, by the way.
 
	require_once("SocketServer.class.php"); // Include the Class File
	$server = new SocketServer(null,31337); // Create a Server binding to the default IP address (null) and listen to port 31337 for connections
	$server->max_clients = 10; // Allow no more than 10 people to connect at a time
	$server->hook("CONNECT","handle_connect"); // Run handle_connect everytime someone connects
	$server->hook("INPUT","handle_input"); // Run handle_input whenever text is sent to the server
	$server->infinite_loop(); // Run Server Code Until Process is terminated.
 
	/*
	 * All hooked functions are sent the parameters $server (The server class), $client (the connection), and $input (anything sent, if anything was sent)
	 * You should save the variables $server and $client using an ampersand (&) to make sure they are references to the objects and not duplications.
	 */
	function handle_connect(&$server,&$client,$input)
	{
		SocketServer::socket_write_smart($client->socket,"String? ",""); // Outputs 'String? ' without a Line Ending
	}
	function handle_input(&$server,&$client,$input)
	{
		$trim = trim($input); // Trim the input, Remove Line Endings and Extra Whitespace.
 
		if(strtolower($trim) == "quit") // User Wants to quit the server
		{
			SocketServer::socket_write_smart($client->socket,"Oh... Goodbye..."); // Give the user a sad goodbye message, meany!
			$server->disconnect($client->server_clients_index); // Disconnect this client.
			return; // Ends the function
		}
 
		$output = strrev($trim); // Reverse the String
 
		SocketServer::socket_write_smart($client->socket,$output); // Send the Client back the String
		SocketServer::socket_write_smart($client->socket,"String? ",""); // Request Another String
	}

In essence, this class allows you to handle sockets in PHP. Beautifully handle sockets in PHP, that is.