Using WebSockets With PHP

Posted by in Code

Mega Hovertank Wars

For my senior design project class, my group decided to try our hand at making a multiplayer 3D tank shooter game (called Mega Hovertank Wars) that plays in an HTML5-compliant browser. Along with one other team member (the other two were pretty much dead weight), we worked heartily on the game engine, but it turns out he didn’t have much experience with programming in anything except JavaScript. Obviously, that won’t cut it for writing a game server.

Luckily, I have a hefty amount of experience with PHP, and we were really set on writing our code so that it could run on just about any server or web hosting plan out there, otherwise I probably would have taken a C++ or Java approach.

My original plan was to use long-polling with AJAX since most other Comet implementations required packages to be installed on the server that usually weren’t allowed on most web hosting plans. However, through a combination of finding this to be too slow for effective gameplay and discovering that Chrome had websocket support, I decided to change over to using sockets, though sticking with PHP (just because I like it).

Client Side

Unfortunately, it turns out that since we had written our engine to use WebGL, we had to make the game run in Firefox 3.7a versions, aka Minefield, since that has the best WebGL support. The problem is that Minefield doesn’t have websocket support, so I had to use a bridge to use Flash sockets instead of websockets.

After perusing the web a bit, it turns out there was already a solution out there by gimite, aka Hiroshi Ichikawa, that uses a combination of other solutions floating around the Internet to provide websocket support to browsers that don’t yet provide it natively. You can check it out at GitHub, but it basically breaks down like this:

  • FABridge.js – a JavaScript file apparently provided by Adobe itself from back in 2006. It’s responsible for navigating AS instances.
  • WebSocketMain.swf – a Flash file you embed with swfobject.js. This is the file that actually connects via sockets with the socket server.
  • swfobject.js – this is the result of a project called SWFObject by bobbyvandersluis and TenSafeFrogs that aimed to provide a standards-friendly way to embed Flash content with JavaScript.
  • web_socket.js – this emulates websockets with the Flash object’s sockets for browsers that do not natively support websockets. Notice that if the WebSocket object exists, the file returns immediately

All you really have to do to set up is include these files into your webpage:

1
2
3
4
5
<script type="text/javascript" src="engine4.js"></script>
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript" src="FABridge.js"></script>
<script type="text/javascript" src="web_socket.js"></script>
<script type="text/javascript" src="jquery.min.js"></script>

I also include JQuery because I use it in other places in the game client page (which we’ll get into at another time). Next, all you have to do is initialize your socket and start writing your client code:

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
var ws;
var up = 0, down = 0, left = 0, right = 0;
var w = 0, a = 0, s = 0, d = 0;
var space = 0;
var gdata = 'userdata';
$(document).ready(function() {
    ws = new WebSocket("ws://192.168.1.67:12345/websocket/server.php");
    ws.onopen = function(e) {
        log('WebSocket - status ');
        ws.send('Initialize connection data');
    };
    ws.onmessage = function(e) {
        log('Received: ' + e.data);
    };
    ws.onclose = function(e) {
        log('Disconnected - status ');
    };
    $('#msg').focus();
    $(document).keydown(function(e) {
        var mapped = 0;
        switch(e.keyCode) {
            case 38: if(up == 0) up = mapped = 1; break;
            case 40: if(down == 0) down = mapped = 1; break;
            case 37: if(left == 0) left = mapped = 1; break;
            case 39: if(right == 0) right = mapped = 1; break;
            case 87: if(w == 0) w = mapped = 1; break;
            case 65: if(a == 0) a = mapped = 1; break;
            case 83: if(s == 0) s = mapped = 1; break;
            case 68: if(d == 0) d = mapped = 1; break;
            case 32: if(space == 0) space = mapped = 1; break;
        }
        if(mapped) send('1|'+gdata+'|'+up+'|'+down+'|'+left+'|'+right+'|'+w+'|'+a+'|'+s+'|'+d+'|'+space+
                '|'+Player.bSphere.x+'|'+Player.bSphere.y+'|'+Player.bSphere.z+'|'+Player.facing);
    });
    $(document).keyup(function(e) {
        var mapped = 1;
        switch(e.keyCode) {
            case 38: up = mapped = 0; break;
            case 40: down = mapped = 0; break;
            case 37: left = mapped = 0; break;
            case 39: right = mapped = 0; break;
            case 87: w = mapped = 0; break;
            case 65: a = mapped = 0; break;
            case 83: s = mapped = 0; break;
            case 68: d = mapped = 0; break;
            case 32: space = mapped = 0; break;
        }
        if(!mapped) send('1|'+gdata+'|'+up+'|'+down+'|'+left+'|'+right+'|'+w+'|'+a+'|'+s+'|'+d+'|'+space);
    });
});

function send(msg) {
    if(!msg) { alert('Message can not be empty\n' + msg); return; }
    try{ ws.send(msg); log('Sent: '+msg); } catch(ex){ log(ex); }
}

function quit() {
    log('Goodbye!');
    ws.close();
    ws = null;
}

function log(msg) {
    $('#log').append('<br />' + msg);
}

This, in and of itself, is based on the phpwebsocket project by georgenava. I kept his client-side example code largely intact because it turned out to actually serve as a pretty functional debugging console for my game server code. I’ve modified it, though, to also send keyboard input data. I actually turn this into JSON in my final code, but for now, this is good enough.

You’ll notice the key parts are line 7 which makes the websocket connection, line 8 which is the function that runs when the connection opens, line 12 which is the function that runs when the websocket receives a message, and line 15 which is the function that runs when the connection is terminated either client or server-side. This is where I put a solid chunk of my game client code, but I left it out so you could get the gist of what’s happening.

Server Side

Like I said before, this is based on georgenava’s phpwebsocket project. What I’ve kept mostly untouched are the WebSocket function (makes the socket), listen function (main loop that runs, waiting for data from the sockets), connect function, dohandshake function, getheaders function, and his helper functions like console, say, wrap, and unwrap. I mean, no need to fix what’s not broken, right?

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
<?php
class socketServer {
    private $ip;
    private $port;
    private $users;
    private $games;
    private $master;
    private $sockets;
    private $debug;
    private $dbc;
   
    function __construct($ip, $port) {
        $this->ip = $ip;
        $this->port = $port;
        $this->games = array();
        $this->users = array();
        $this->debug = true;
        $this->master = $this->WebSocket($ip, $port);
        $this->sockets = array($this->master);
        $this->dbc = new dbConnect();
        mysql_select_db('cs4311', $this->dbc->conn);
        $this->listen();
    }

    private function WebSocket($address, $port) {
        $master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)     or die("socket_create() failed");
        socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1)    or die("socket_option() failed");
        socket_bind($master, $address, $port)                      or die("socket_bind() failed");
        socket_listen($master, 20)                                 or die("socket_listen() failed");
        echo "Server Started : " . date('Y-m-d H:i:s') . "\n";
        echo "Master socket  : " . $master."\n";
        echo "Listening on   : " . $address . " port " . $port . "\n\n";
        return $master;
    }
   
    private function listen() {
        while(true){
            $changed = $this->sockets;
            socket_select($changed, $write = NULL, $except = NULL, NULL);
            foreach($changed as $socket) {
                $this->checkTime();
                if($socket == $this->master){
                    $client = socket_accept($this->master);
                    if($client < 0) { $this->console('socket_accept() failed'); continue; }
                    else{ $this->connect($client); }
                }
                else {
                    $bytes = @socket_recv($socket, $buffer, 2048, 0);
                    if($bytes == 0) { $this->disconnect($socket); }
                    else {
                        $user = $this->getuserbysocket($socket);
                        if(!$user->handshake) { $this->dohandshake($user, $buffer); }
                        else{ $this->process($user, $buffer); }
                    }
                }
            }
        }
    }

    private function connect($socket) {
        $user = new User(uniqid(), $socket);
        array_push($this->users, $user);
        array_push($this->sockets, $socket);
        $this->console($socket . " CONNECTED!");
    }
   
    private function process($user, $msg) {
        $msg = $this->unwrap($msg);
        $this->say("< " . $msg);
        $data = explode('|', $msg);
        switch($data[0]) {
            case 0:
                $this->send($user->socket, '10|'.$data[1].'|'.$data[2].'|'.$data[3].'|'.$data[4].'|Received initialized user data');
                $user->gid = $data[1];
                $user->uip = $data[2];
                $user->uid = $data[3];
                $user->gud = $data[4];
                if(array_key_exists($data[1], $this->games) === FALSE) {
                    $game = new Game($data[1]);
                    $this->games[$data[1]] = $game;
                    $this->console('New game object ' . $data[1]);
                }
                $this->games[$data[1]]->addPlayer($data[3], $user->socket);
                $this->incrementStat(0, $data[3]);
                if($this->games[$data[1]]->isFull()) {
                    $players = $this->games[$data[1]]->getPlayers();
                    $msg = '0';
                    foreach($players as $player) {
                        $usr = $this->getuserbyuid($player['uid']);
                        $msg .= '|' . $usr->gud . '|' . $usr->uid;
                    }
                    $this->broadcast($data[1], 0, $user->socket, $msg);
                }
                break;
            case 1:
                $this->send($user->socket, 'Received input data!');
                $this->broadcast($data[1], $data[3], $user->socket, $msg);
                break;
            case 2:
                if($this->getuserbyuid($data[5]) != null) {
                    $this->incrementStat(1, $data[5]);
                    $this->incrementStat(2, $data[6]);
                    $this->broadcast($data[1], $data[3], $user->socket, $msg);
                }
                break;
            default:
                $this->console('Unknown command: ' . $msg);
                break;
        }
    }

    private function broadcast($gid, $uid, $socket, $msg = '') {
        $players = $this->games[$gid]->getPlayers();
        foreach($players as $player) {
            if($player['uid'] != $uid) $this->send($player['socket'], $msg);
        }
    }

    private function send($client, $msg) {
        $this->say("> " . $msg);
        $msg = $this->wrap($msg);
        socket_write($client, $msg, strlen($msg));
    }

    private function disconnect($socket) {
        $found = null;
        $n = count($this->users);
        for($i = 0; $i < $n; $i++) {
            if($this->users[$i]->socket == $socket) { $found = $i; break; }
        }
        if(!is_null($found)) {
            $this->broadcast($this->users[$found]->gid, $this->users[$found]->uid, 0, '2|'.$this->users[$found]->gid.'|'.$this->users[$found]->uip.'|'.$this->users[$found]->uid.'|'.$this->users[$found]->gud.'|'.$this->users[$found]->uid.'|0');
            $this->games[$this->users[$found]->gid]->dropPlayer($this->users[$found]->uid, $socket);
            if(count($this->games[$this->users[$found]->gid]->players) < 1) {
                $query = sprintf("DELETE FROM games WHERE gid = '%s'", $this->users[$found]->gid);
                if(!mysql_query($query, $this->dbc->conn))
                    $this->console('Failed to delete game ' . $this->users[$found]->gid . ' from the database: ' . mysql_error());
                unset($this->games[$this->users[$found]->gid]);
            }
            array_splice($this->users, $found, 1);
        }
        $index = array_search($socket, $this->sockets);
        socket_close($socket);
        $this->console($socket . " DISCONNECTED!");
        if($index >= 0) { array_splice($this->sockets, $index, 1); }
    }

    private function dohandshake($user, $buffer) {
        $this->console("\nRequesting handshake...");
        $this->console($buffer);
        list($resource, $host, $origin) = $this->getheaders($buffer);
        $this->console("Handshaking...");
        $upgrade  = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
            "Upgrade: WebSocket\r\n" .
            "Connection: Upgrade\r\n" .
            "WebSocket-Origin: " . $origin . "\r\n" .
            "WebSocket-Location: ws://" . $host . $resource . "\r\n" .
            "\r\n";
        socket_write($user->socket, $upgrade . chr(0), strlen($upgrade . chr(0)));
        $user->handshake = true;
        $this->console($upgrade);
        $this->console("Done handshaking...");
        return true;
    }

    private function getheaders($req) {
        $r = $h = $o = null;
        if(preg_match("/GET (.*) HTTP/"   ,$req,$match)) { $r = $match[1]; }
        if(preg_match("/Host: (.*)\r\n/"  ,$req,$match)) { $h = $match[1]; }
        if(preg_match("/Origin: (.*)\r\n/",$req,$match)) { $o = $match[1]; }
        return array($r, $h, $o);
    }

    private function getuserbysocket($socket) {
        $found = null;
        foreach($this->users as $user) {
            if($user->socket == $socket) { $found = $user; break; }
        }
        return $found;
    }
   
    private function getuserbyuid($uid = -1) {
        $found = null;
        foreach($this->users as $user) {
            if($user->uid == $uid) { $found = $user; break; }
        }
        return $found;
    }

    private function console($msg = "") { if($this->debug) { echo $msg . "\n"; } }
    private function     say($msg = "") { echo $msg . "\n"; }
    private function    wrap($msg = "") { return chr(0) . $msg . chr(255); }
    private function  unwrap($msg = "") { return substr($msg, 1, strlen($msg) - 2); }
}

?>

The really important part is the listen function. It runs on an infinite while loop and listens to the socket for any incoming data. You’ll notice that in the socket_select function call (a built-in PHP method), the fourth parameter is NULL, meaning that it can block indefinitely to receive data. If it didn’t, we would end up disconnecting every connected client due to the else statement at line 47. It says that if the bytes received by the socket is equal to zero, then disconnect that client because it hasn’t sent any data and likely isn’t viewing the page anymore. This causes a slight problem with checking the time left for a game round, but that’s something we decided to live with for now.

Next, you should take notice of the dohandshake function. The way websockets work is that servers respond to GET requests that have the Connection field set to “Upgrade” (among other things) with a handshake response. They communicate a token so they can authenticate each other’s handshakes. After this, the client and server have full-duplex communication capabilities, perfect for our game.

I’ve removed a lot other functions from the SocketServer class so that we can focus on just the socket stuff. I’ve also chosen to not include the Game, User, and dbConnect classes since they don’t have anything whatsoever to do with sockets, but if you really want to see them, you can ask for them in the comments below. The process function, however, has been untouched just so you can see that the data sent from the JavaScript of the client is unchanged once it gets to the server.

To run the server, you’ve got instantiate the SocketServer from another PHP file with the proper IP and port that you’d like to run it from and then run the file from the server’s shell or command prompt. Also, in most server configurations, you’ve got to run a Flash socket policy server. You can head over to Jacqueline Kira Hamilton’s writeup on the topic and run her Perl script, otherwise connecting to your server may end in terminal sadness.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/php -q
<?php

error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();

include_once('classes/SocketServer.php');
include_once('classes/User.php');
include_once('classes/Game.php');
include_once('classes/dbConnect.php');

$ip = '129.118.120.190';
$port = 12345;

try{
  $mySocketServer = new SocketServer($ip, $port);
} catch (Project_Exception_AutoLoad $e) {
    echo "FATAL ERROR: CAN'T FIND SOCKET SERVER CLASS.";
}

?>

Anyways, that’s enough of that. If I feel like it, I may start to go over the game engine, but that is upwards of 1,000 lines of code, so that may never happen.