A simple server-side dashboard for Union Platform

If you are doing any sort of client-server development and haven’t heard of Union, you’re missing out. Union  is a development platform for creating multiuser applications. It contains a server written in Java and client libraries in AS3 (Reactor), Javascript with Websocket (Orbitermicro) and even server-side with Node.js. The communication protocol is XML-based and open so anyone can write a custom client in any language. In fact, in this article I’ll show you how Union can talk with PHP. Union also has built-in support for persistent accounts (using Derby – IBM’s Cloudscape database open-sourced) and you can even extend its functionality with Java- or Javascript-based modules.

PHP Dashboard for Union Platform

PHP Dashboard for Union Platform

I’m using Union for my own chess game and I wanted to be able to show site visitors some server stats. This info is available normally only through the special password-protected ‘Administrative Gateway’ and I couldn’t risk having it in a javascript or flash client. Further, the idea was to have that info always available, even if the user cannot connect due to some strange configuration (Union can run on port 80 in Socket or HTTP mode, so theoretically any client who can browse the web should be able to connect; still, you never know…). Even further, a server-side client would allow for some nice features, such as logging the server’s vital stats and producing nice charts.

Even if you aren’t using Union, the code below may still be educational for you as it shows some real socket communication in PHP.

So, let’s start by defining the multiplayer server info:

$address  = 'multiplayer.server.com';
$port     = 9110;
$password = 'mypassword';

To connect to the server on the Admin Gateway, we’ll use this bit of code:

$socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false)
    $err = 'ERR'.socket_last_error($socket).' ('.socket_strerror(socket_last_error($socket)).')';
else
{
	/* set socket options - read timeout after 30 seconds, reuse address */
	socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>30, "usec"=>0));
	socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
 
	/* connect to server */
	$result = @socket_connect($socket, $address, $port);
	if ($result === false)
		$err ='ERR'.socket_last_error($socket).' ('.socket_strerror(socket_last_error($socket)).')';
	else
	{
		// we're connected
	}
}

OK, so we’re in. Now we must do the Union Handshake, which is basically sending a specially-crafted XML and waiting for a specific response.

In fact, here are all the steps:

  1. Connect to server
  2. Send a CLIENT_HELLO message
  3. Receive a SERVER_HELLO message
  4. Receive several CLIENT_METADATA and CLIENT_ATTR_UPDATE messages
  5. Receive a CLIENT_READY message
  6. Authenticate as Administrator
  7. Receive authentication result
  8. Send a server info request
  9. Receive server info
  10. Disconnect

So you see, it’s a linear sequence. Our client simply has to go through two states, Send and Listen. There are many ways to do this, some more elegant than the others, but to keep this short and simple, I chose a very simple method: a do-while loop in which the client sends a message then listens for an answer. When it gets it, it sees what it is and decides what’s the next message to send.

The messages themselves are XML, but we won’t do any XML parsing (for that see my Twitter tutorial) and we’ll just treat them as strings.

Here are the requests we’ll make (see how they all have a null char at the end):

//requests
$hello_request   = "<U><M>u65</M><L><A>PHP</A><A>1</A><A>1.9.2</A></L></U>\0";
$login_request   = "<U><M>a1</M><L><A>$password</A></L></U>\0";
$info_request    = "<U><M>a6</M><L></L></U>\0";

And these are the responses we’re expecting – as you can see we simply ignore the metadata stuff and we don’t even check the full string, but just to see that we got the message type (those IDs in tags):

//responses
$ready_message   = "<U><M>u63";
$success_message = "<U><M>a2</M><L><A>SUCCESS";
$fail_message    = "<U><M>a2</M><L><A>AUTHORIZATION_FAILED";
$info_message    = '<U><M>a7';

Our loop will look like this:

do
{
	@socket_write($socket, $request, strlen($request));
 
	$read = @socket_read($socket, 1024);
	if ($read === FALSE)
	{
		/* read error, exit loop */
		$err =  'ERR'.socket_last_error($socket).' ('.socket_strerror(socket_last_error($socket)).')';
		break;
	}
	else
	if ($read  == '')
	{
		/* empty result, exit loop */
		$err = 'EOT';
		break;
	}
	else
	if (strpos($read, $ready_message) !== FALSE)
	{
		/* we received the READY message, send the login info */
		$request = $login_request;
		continue;
	}
	else
	if (strpos($read, $success_message) !== FALSE)
	{
		/* login successful, send the server info request */
		$request = $info_request;
		continue;
	}
	else
	if (strpos($read, $fail_message) !== FALSE)
	{
		/* login failed, exit loop */
		$err = 'AUTH';
		break;
	}
	else
	if (strpos($read, $info_message) !== FALSE)
	{
		/* server info received */
		$server_info = substr($read, strpos($read, $info_message));
		break;
	}
}
while(true);

This structure with no less than 6 IF/ELSE statements is downright ugly. If you intend to a more complex client, you should definitely parse the response XML. I’m showing it to you because it’s straightforward and doesn’t require me to explain other concepts.

So, first we write to the socket, then receive. We check the answer for errors. If it’s a valid answer, we are testing it to see if it’s one of the 4 responses we expect (hello, auth ok, auth fail and info). In each case we decide what to do next: if we like the response we set a different request and start over, if there was an error or obtain the info, we break the loop. Piece of cake.

Next, close the socket and check if we got what we needed or if there was an error:

socket_close($socket);
if ($server_info == '')
{
	echo $err;
	exit;
}

Finally, we must process the $server_info var. This is done easily with a very simple regexp:

preg_match_all('|<a>(.*?)</a>|', $server_info, $matches, PREG_PATTERN_ORDER);
$uptime  = $matches[1][2]; // server uptime in miliseconds
$memory  = $matches[1][4]; // memory used in bytes
$load    = $matches[1][5]; // average server load [0..1]
$clients = $matches[1][6]; // number of connected clients

You now can do whatever you want with the data you just obtained – make a nice dashboard, write it to a log and so on. The image at the top of the article is my actual dashboard. I made the gauges in Flash rotating the indicator over 100 frames and I exported the movie as a sequence of numbered PNGs, so if the CPU usage is 20%, all I have to do is show “20.png”. With the number of users I applied a non-linear function to fit any number of users in the 0-100 interval.

A word of warning: It would be a bad idea to expose this dashboard to the public, because each time someone executes the script, a new connection is made to the server, potentially leading to denial of service. What you should do is implement a simple caching mechanism, so that it first tries to retrieve the values from a text file and the connection is done if either no data is present or if the data is older than 1-2 minutes; after the info is retrieved, it’s saved in the cache.

Here’s the complete code (except for the caching part, which is left as an exercise to the reader):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
<?php
 
/* server address, server admin port and password */
$address  = 'server.com';
$port     = 9110;
$password = 'password';
 
$err         = '';
$server_info = '';
 
/* requests and responses */
$hello_request   = "<U><M>u65</M><L><A>PHP</A><A>1</A><A>1.9.2</A></L></U>\0";
$ready_message   = "<U><M>u63";
$login_request   = "<U><M>a1</M><L><A>$password</A></L></U>\0";
$success_message = "<U><M>a2</M><L><A>SUCCESS";
$fail_message    = "<U><M>a2</M><L><A>AUTHORIZATION_FAILED";
$info_request    = "<U><M>a6</M><L></L></U>\0";
$info_message    = '<U><M>a7';
 
/* create the socket */
$socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket !== false)
{
	/* set socket options - read timeout after 30 seconds, reuse address */
	socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>10, "usec"=>0));
	socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
 
	/* connect to server */
	$result = @socket_connect($socket, $address, $port);
	if ($result !== false) 
	{
		/* set initial request for server handshake*/
		$request = $hello_request;
 
		/* loop to write and read data for as long as needed */
		do
		{
			@socket_write($socket, $request, strlen($request));
 
			$read = @socket_read($socket, 1024);
			if ($read === FALSE)
			{
				/* read error, exit loop */
				$err =  'ERR'.socket_last_error($socket).' ('.socket_strerror(socket_last_error($socket)).')';
				break;
			}
			else
			if ($read  == '')
			{
				/* empty result, exit loop */
				$err = 'EOT';
				break;
			}
			else
			if (strpos($read, $ready_message) !== FALSE)
			{
				/* we received the READY message, send the login info */
				$request = $login_request;
				continue;
			}
			else
			if (strpos($read, $success_message) !== FALSE)
			{
				/* login successful, send the server info request */
				$request = $info_request;
				continue;
			}
			else
			if (strpos($read, $fail_message) !== FALSE)
			{
				/* login failed, exit loop */
				$err = 'AUTH';
				break;
			}
			else
			if (strpos($read, $info_message) !== FALSE)
			{
				/* server info received */
				$server_info = substr($read, strpos($read, $info_message));
				break;
			}
		}
		while(true);
	}
	else
		$err ='ERR'.socket_last_error($socket).' ('.socket_strerror(socket_last_error($socket)).')';
}
else
    $err = 'ERR'.socket_last_error($socket).' ('.socket_strerror(socket_last_error($socket)).')';
 
if ($server_info != '')
{
	preg_match_all('|<A>(.*?)</A>|', $server_info, $matches, PREG_PATTERN_ORDER);
	$uptime  = $matches[1][2];
	$memory  = $matches[1][4];
	$load    = $matches[1][5];
	$clients = $matches[1][6];
 
	$uptime_text = format_milliseconds($uptime);
	$memory_text = format_megabytes($memory);
	$load_text   = format_percent($load);
 
	echo "$clients,$memory_text,$load_text,$err\n";
}
else
	echo "$err\n";
 
socket_close($socket);
 
function format_milliseconds($milliseconds) 
{
    $seconds = floor($milliseconds / 1000);
    $minutes = floor($seconds / 60);
    $hours = floor($minutes / 60);
    $days = floor($hours / 24);
 
    $seconds = $seconds % 60;
    $minutes = $minutes % 60;
    $hours   = $hours % 24;
 
    $time = sprintf('%u days %u:%02u:%02u', $days, $hours, $minutes, $seconds);
    return rtrim($time, '0');
}
 
function format_megabytes($bytes)
{
	return number_format($bytes/(1024*1024),2)."MB";
}
 
function format_percent($n)
{
	return ($n*100)."%";
}
?>

The script above will output the server stats in a simple line. For convenience, I added some helper functions to show the update in days, hours and minutes, memory in MB, etc.

In the near future, I’ll show you how you can use this to log, monitor and graph all this info.

Armand Niculescu, BEng, MSM, is a 34 year old Art Director at Media Division. and he enjoys working with visual arts for film, web and print.