Navarr's Tech Side The Technical Side of my Life

1Jul/100

How to create a socket server in PHP

class::SocketServer

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.

28Jun/101

Magical Typesetting in PHP

So, well working for Route 50 I came up with a fantastic idea for “typesetting” that well exceeded the norm.  Something we constantly have issues with is what type of string was sent to MySQL originally (some of us have different conventional ideas about where escaping HTML should be located.) as well as outputting that string in its correct format.

Me, with my fantastic idea, came up with a couple variables classes that I put in a file named class_typesetting.php.  The version on gist.github is slightly modified from the original version on the server.

It creates three classes, GenericVariable, String, and Number.  So far we haven’t used GenericVariable, but since the introduction of the classes I’ve taken it upon myself to introduce them to any new code I write.  When we create Core v5 (which will Objectify everything) strings taken from SQL will automatically be re-stored as String class variables.

First, lets examine some useful functionality.

<?php
$title = new String($_POST[“title”]); // <strong>Hello 'World'</strong>
?>
HTML Output: <?= $title->html ?> (&lt;strong&gt;Hello 'World'&lt;/strong&gt;)
Text Output: <?= $title->text ?> (Hello 'World')
SQL Output: <?= $title->sql ?> (<strong>Hello \'World\'</strong>)
HTML Attribute Output: <?= $title->html_attr ?>(&lt;strong&gt;Hello &#039;World&#039;&lt;/strong&gt;)

This allows for quick and easy access to the variables without having to worry about escaping them.

I recommend you hit the download link (class_typesetting.php) and play around with it.  Tell me about anything that’s not working correctly and if possible implement it in your future code.  (This means I’m putting this code in the “Public Domain”).

20Feb/100

simpleTAPI is Broken

Apparently I’ve completely broken simpleTAPI somewhere between Build 27 and Build 30.  I thought I had fixed it with Build 29, but it seems that I was mistaken.

In lieu of this, I am putting simpleTAPI on a temporary hiatus.  I will be re-constructing it from scratch (though, probably looking back and using a good bit of the original code).  The next version should have several configurable options, and will hopefully interact with the Twitter API much better than the previous versions.

Build 30 was supposed to return results as an array([“TAPI”] => data, [“result”] => data).  But all I’m getting from it at the moment is “Unable to Authenticate User.”

Those wanting to use simpleTAPI should use Build 27, though you will have to deal with some minor quirks in the way results are returned.  (the TAPI array is simply appended to the results array, making things slightly complicated if you don’t unset($result[“TAPI”]);

What will be simpleTAPI 0.4 should have better error handling, better return data, and better built-in caching.  I’m also hoping to build in support for xAuth and Delegated OAuth, if at all possible.  (Though probably not since simpleTAPI is built upon another OAuth library).

So, I’m asking for any and all feature requests.  Is there something about simpleTAPI you don’t like or want to be improved?  Please, post in the comments below!

29Nov/090

Google Voice OMS Code on Github

I pushed the 子猫ちゃん Google Voice OMS service’s code to github, so you can now download it – albeit, to make it work it’ll take a lot of hacking and a lot more editing.

Either way, I’ve gotten no donations and no offers for free SSL hosting, so it looks like this project just will not be seeing the light of day.  It’s a shame, I worked a long time to make it work, and it’s obviously something a lot of business professionals would be able to find a use for.

Oh well, you can find the project on github.

Remember to abide by the Usage License!

25Nov/090

Google Voice in Outlook

If you’re a regular reader to my blog, I’m sure you read yesterday’s post about how Google Voice could gain a head in the business world.  At that time, my dream of connecting Google Voice and Outlook via OMS was far from completion, with the only work I’d managed to accomplish being a simple reading over of the related technologies.

Well, late last night a certain gear clicked in my brain, and I spent the entire night awake and coding PHP on a local XAMPP server.  But my end result was fruitful – I finished successfully coding an Outlook Mobile Service that allows the delivery of SMS through the Google Voice system.

Here is a video showing it off:

I’m not yet prepared to release the source code for this, though.  (Messy, Messy, Mess! as Double D would say).  There’s a lot in my mind about it, it took a lot of work and I’m not ready to see forks and duplicate services pop up.  (Sorry guys =S).  Be on the look out for follow up posts that describe some of the technologies I had to learn to make this possible.

Oh, also – If you’d like; Help sponsor this project (I can’t afford to make it public ATM) with either Free (VERIFIED) SSL Hosting for a subdomain of a domain I own [contact me], or the money to make it public using my current host ($62.40/yr) [donate through my host].  I would be most appreciative if you could offer either of these to get this thing up and running!

22Nov/090

simpleTAPI v0.2.1 – Build 16 (Twitter API Library)

I’ve renamed the Twitter API Library to “simpleTAPI.”  Yes, I’m not very good at names when it comes to this sort of thing.  We’ve jumped forward two builds since my last post here.

Build 15

  • The addition of a quick variable, bool Twitter::geo_enabled.
    Returns TRUE if the user has turned on geo functionality, FALSE if not.

Version 0.2.1

  • Re-Organized classes.  Separated TwitterOAuth and OAuth into separate files, and moved them along with Twitter into a “twitter” folder.  All classes can be loaded simply by including Twitter.lib.php.

Build 16

  • Fixed a minor inconsistency in TWML where different functions returned different links to a twitter user’s profile.
  • Fixed a bug where specifying screennameonly=TRUE for TWML::name resulted in an empty hyperlink.

Examples

  • Started work on Example files to teach how to use simpleTAPI.  Currently, the only one included is a basic Update script.  This file includes logging in, updating a status, and returning the same status as well as some basic TWML examples.

So, enjoy!  I will continue to improve this library.  Please remember to post all issues and feature requests either on this blog or the github page.

19Nov/090

Twitter API Library Build 13 (Breaking Change)

I pushed Build 13 today.  This build adds the recent addition of descriptions to a user’s list.

This change breaks:

  • TwitterAPI::lists_create and
  • TwitterAPI::lists_update

The new profiles for these commands are as follows:

TwitterAPI::lists_create( str $name [, str $description = NULL [, bool $privacy = TWITTER_PRIVACY_PUBLIC ] ] )

TwitterAPI::lists_update( str $name [, str $new_name = NULL [, str $description = NULL [, bool $privacy = NULL ] ] ] )

17Nov/090

Twitter API Library Build 12

If you’ve been watching this blog, you’ll notice I skipped Build 11 – It was small, and was trumped by its quick replacement – Build 12.  Builds 11 and 12 fixed five previous known issues, while build 12 fixed a non-issue with build 11.  Fixes listed below:

  • The following now work with all users:
    • Twitter::get_sn_from_id()
    • Twitter::get_name_from_id()
    • Twitter::get_id_from_sn()
    • TWML::name()
    • TWML::profile_pic()
  • Addition of User Cache commands for use by TWML and the get_x_from_y() commands.

As always, the most recent build is available at github.

17Nov/090

Twitter API Library Version 0.2 Build 10

I pushed Version 2 Build 10 of the Twitter API Library to github.  This new version still does not fix any of the previous errors, so the changelog is quite minor.

  • Updated to TwitterOAuth v0.2-beta
    • "fixes several bugs"
    • Support for OAuth 1.0a
  • Added a boolean return to require_login().
16Nov/090

Twitter API Library Build 9

I’ve pushed Build 9 of the Twitter API Library to github today.  Interesting thing, it adds a few missing API features but does not yet fix any of the previous problems.

New features are all Authenticated API Calls:

  • search
  • trends
  • trends_current
  • trends_daily
  • trends_weekly
  • users_search
  • trends_available
  • trends_location

Yes, that means that we’ve added support for the following portions of the twitter api:

  • Search
  • Trends
  • User Search
  • Local Trends

Enjoy, and as always please report any and all errors you get.