One of the most pervasive and least understood technologies that underpin the internet is socket programming. It’s been a dark art for decades, but the recent standardisation of web sockets (in relation to HTML 5) has made this type of programming a little easier to get into.
This tutorial depends heavily on client-side resources. We’re developing a Laravel 4 application which has lots of server-side aspects; but it’s also a chat app. There be scripts!
For this; we’re using Bootstrap and EmberJS. Download Bootstrap at: http://getbootstrap.com/ and unpack it into your public folder. Where you put the individual files makes little difference, but I have put the scripts in public/js, the stylesheets in public/css and the fonts in public/fonts. Where you see those paths in my source-code; you should substitute them with your own.
Next up, download EmberJS at: http://emberjs.com/ and unpack it into your public folder. You’ll also need the Ember.Data script at: http://emberjs.com/guides/getting-started/obtaining-emberjs-and-dependencies/.
For the server-side portion of dependencies, we need to download a library called Ratchet. I’ll explain it shortly, but in the meantime we need to add it to our composer.json file:
1 "require" : { 2 "laravel/framework" : "4.0.*", 3 "cboden/Ratchet" : "0.3.*" 4 }, Follow that up with:
1 composer update We’ll now have access to the Ratchet library for client-server communication, Bootstrap for styling the interface and EmberJS for connecting these two things together.
Before we can understand Ratchet, we need to understand ReactPHP. ReactPHP was born out of the need to develop event-based, asynchronous PHP applications. If you’ve worked with Node.JS you’ll feel right at home developing applications with ReactPHP; as they share a similar approaches to code. We’re not going to develop our chat application in ReactPHP, but it’s a dependency for Ratchet…
One of the many ways in which real-time client-server applications are made possible is by what’s called socket programming. Believe it or not; most of what you do on the internet depends on socket programming. From simple browsing to streaming — your computer opens a socket connection to a server and the server sends data back through it.
PHP supports this type of programming but PHP websites have not typically been developed with this kind of model in mind. PHP developers have preferred the typical request/response model, and it’s comparatively easier than low-level socket programming.
Enter ReactPHP. One of the requirements for building a fully-capable socket programming framework is creating what’s called an Event Loop. ReactPHP has this and Ratchet uses it, along with the Publish/Subscribe model to accept and maintain open socket connections.
ReactPHP wraps the low-level PHP functions into a nice socket programming API and Ratchet wraps that API into another API that’s even easier to use.
Let’s get to the code! We’re going to need an interface (kind of like a wireframe) so we know what to build with our application. Let’s set up a simple view and plug it into EmberJS.
Let’s change the default routes.php file to load a custom view:
1 <?php 2 3 Route::get("/", function() 4 { 5 return View::make("index/index"); 6 }); Then we need to add this view:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <link 6 rel="stylesheet" 7 type="text/css" 8 href="{{ asset("css/bootstrap.3.0.0.css") }}" 9 /> 10 <link 11 rel="stylesheet" 12 type="text/css" 13 href="{{ asset("css/bootstrap.theme.3.0.0.css") }}" 14 /> 15 <title>Laravel 4 Chat</title> 16 </head> 17 <body> 18 <script type="text/x-handlebars"> 19 @{{outlet}} 20 </script> 21 <script 22 type="text/x-handlebars" 23 data-template-name="index" 24 > 25 <div class="container"> 26 <div class="row"> 27 <div class="col-md-12"> 28 <h1>Laravel 4 Chat</h1> 29 <table class="table table-striped"> 30 @{{#each}} 31 <tr> 32 <td> 33 @{{user}} 34 </td> 35 <td> 36 @{{text}} 37 </td> 38 </tr> 39 @{{/each}} 40 </table> 41 </div> 42 </div> 43 <div class="row"> 44 <div class="col-md-12"> 45 <div class="input-group"> 46 <input 47 type="text" 48 class="form-control" 49 /> 50 <span class="input-group-btn"> 51 <button class="btn btn-default"> 52 Send 53 </button> 54 </span> 55 </div> 56 </div> 57 </div> 58 </div> 59 </script> 60 <script 61 type="text/javascript" 62 src="{{ asset("js/jquery.1.9.1.js") }}" 63 ></script> 64 <script 65 type="text/javascript" 66 src="{{ asset("js/handlebars.1.0.0.js") }}" 67 ></script> 68 <script 69 type="text/javascript" 70 src="{{ asset("js/ember.1.1.1.js") }}" 71 ></script> 72 <script 73 type="text/javascript" 74 src="{{ asset("js/ember.data.1.0.0.js") }}" 75 ></script> 76 <script 77 type="text/javascript" 78 src="{{ asset("js/bootstrap.3.0.0.js") }}" 79 ></script> 80 <script 81 type="text/javascript" 82 src="{{ asset("js/shared.js") }}" 83 ></script> 84 </body> 85 </html> You’ll notice I’ve specified shared.css and shared.js files — the CSS file is blank, but the JavaScript file contains:
1 // 1 2 var App = Ember.Application.create(); 3 4 // 2 5 App.Router.map(function() { 6 this.resource("index", { 7 "path" : "/" 8 }); 9 }); 10 11 // 3 12 App.Message = DS.Model.extend({ 13 "user" : DS.attr("string"), 14 "text" : DS.attr("string") 15 }); 16 17 // 4 18 App.ApplicationAdapter = DS.FixtureAdapter.extend(); 19 20 // 5 21 App.Message.FIXTURES = [ 22 { 23 "id" : 1, 24 "user" : "Chris", 25 "text" : "Hello World." 26 }, 27 { 28 "id" : 2, 29 "user" : "Wayne", 30 "text" : "Don't dig it, man." 31 }, 32 { 33 "id" : 3, 34 "user" : "Chris", 35 "text" : "Meh." 36 } 37 ]; If you’re an EmberJS noob, like me, then it will help to understand what each piece of this script is doing.
When you browse to the base URL of the application; you should now see an acceptably styled list of message objects, along with a heading and input form. Let’s make it dynamic!
You should also see some console log messages (depending on your browser) which show that EmberJS is running.
Following on from a previous tutorial; we’re going to be creating a service provider which will provide the various classes for our application. Create a new workbench:
1 php artisan workbench formativ/chat This will produce (amongst other things) a service provider template. I’ve added a few IoC bindings to it:
1 <?php 2 3 namespace Formativ\Chat; 4 5 use Evenement\EventEmitter; 6 use Illuminate\Support\ServiceProvider; 7 use Ratchet\Server\IoServer; 8 9 class ChatServiceProvider 10 extends ServiceProvider 11 { 12 protected $defer = true; 13 14 public function register() 15 { 16 $this->app->bind("chat.emitter", function() 17 { 18 return new EventEmitter(); 19 }); 20 21 $this->app->bind("chat.chat", function() 22 { 23 return new Chat( 24 $this->app->make("chat.emitter") 25 ); 26 }); 27 28 $this->app->bind("chat.user", function() 29 { 30 return new User(); 31 }); 32 33 $this->app->bind("chat.command.serve", function() 34 { 35 return new Command\Serve( 36 $this->app->make("chat.chat") 37 ); 38 }); 39 40 $this->commands("chat.command.serve"); 41 } 42 43 public function provides() 44 { 45 return [ 46 "chat.chat", 47 "chat.command.serve", 48 "chat.emitter", 49 "chat.server" 50 ]; 51 } 52 } The first binding is a simple alias to the EvenementEventEmitter class (which Ratchet requires). We bind it here as we cannot guarantee that Ratchet will continue to use EvenementEventEmitter and we’ll need a reliable way to unit test possible alternatives in the future.
Let’s look closer at the second and third bindings. The first is to the Chat class. It implements the ChatInterface interface:
1 <?php 2 3 namespace Formativ\Chat; 4 5 use Evenement\EventEmitterInterface; 6 use Ratchet\ConnectionInterface; 7 use Ratchet\MessageComponentInterface; 8 9 interface ChatInterface 10 extends MessageComponentInterface 11 { 12 public function getUserBySocket(ConnectionInterface $socket); 13 public function getEmitter(); 14 public function setEmitter(EventEmitterInterface $emitter); 15 public function getUsers(); 16 } It’s interesting to note that PHP actually supports interfaces which extend other interfaces. This is useful if you want to expect a certain level of functionality, provided by a third-party library (such as SPL), but want to add your own requirements on top.
The concrete implementation looks like this:
1 <?php 2 3 namespace Formativ\Chat; 4 5 use Evenement\EventEmitterInterface; 6 use Exception; 7 use Ratchet\ConnectionInterface; 8 use SplObjectStorage; 9 10 class Chat 11 implements ChatInterface 12 { 13 protected $users; 14 protected $emitter; 15 protected $id = 1; 16 17 public function getUserBySocket(ConnectionInterface $socket) 18 { 19 foreach ($this->users as $next) 20 { 21 if ($next->getSocket() === $socket) 22 { 23 return $next; 24 } 25 } 26 27 return null; 28 } 29 30 public function getEmitter() 31 { 32 return $this->emitter; 33 } 34 35 public function setEmitter(EventEmitterInterface $emitter) 36 { 37 $this->emitter = $emitter; 38 } 39 40 public function getUsers() 41 { 42 return $this->users; 43 } 44 45 public function __construct(EventEmitterInterface $emitter) 46 { 47 $this->emitter = $emitter; 48 $this->users = new SplObjectStorage(); 49 } 50 51 public function onOpen(ConnectionInterface $socket) 52 { 53 $user = new User(); 54 $user->setId($this->id++); 55 $user->setSocket($socket); 56 57 $this->users->attach($user); 58 $this->emitter->emit("open", [$user]); 59 } 60 61 public function onMessage( 62 ConnectionInterface $socket, 63 $message 64 ) 65 { 66 $user = $this->getUserBySocket($socket); 67 $message = json_decode($message); 68 69 switch ($message->type) 70 { 71 case "name": 72 { 73 $user->setName($message->data); 74 $this->emitter->emit("name", [ 75 $user, 76 $message->data 77 ]); 78 break; 79 } 80 81 case "message": 82 { 83 $this->emitter->emit("message", [ 84 $user, 85 $message->data 86 ]); 87 break; 88 } 89 } 90 91 foreach ($this->users as $next) 92 { 93 if ($next !== $user) 94 { 95 $next->getSocket()->send(json_encode([ 96 "user" => [ 97 "id" => $user->getId(), 98 "name" => $user->getName() 99 ], 100 "message" => $message 101 ])); 102 } 103 } 104 } 105 106 public function onClose(ConnectionInterface $socket) 107 { 108 $user = $this->getUserBySocket($socket); 109 110 if ($user) 111 { 112 $this->users->detach($user); 113 $this->emitter->emit("close", [$user]); 114 } 115 } 116 117 public function onError( 118 ConnectionInterface $socket, 119 Exception $exception 120 ) 121 { 122 $user = $this->getUserBySocket($socket); 123 124 if ($user) 125 { 126 $user->getSocket()->close(); 127 $this->emitter->emit("error", [$user, $exception]); 128 } 129 } 130 } It’s fairly simple: the (delegate) onOpen and onClose methods handle creating new User objects and disposing of them. The onMessage method translates JSON-encoded message objects into required actions and responds back to the other socket connections with further details.
Additionally; the UserInterface interface and User class look like this:
1 <?php 2 3 namespace Formativ\Chat; 4 5 use Evenement\EventEmitterInterface; 6 use Ratchet\ConnectionInterface; 7 use Ratchet\MessageComponentInterface; 8 9 interface UserInterface 10 { 11 public function getSocket(); 12 public function setSocket(ConnectionInterface $socket); 13 public function getId(); 14 public function setId($id); 15 public function getName(); 16 public function setName($name); 17 } 1 <?php 2 3 namespace Formativ\Chat; 4 5 use Ratchet\ConnectionInterface; 6 7 class User 8 implements UserInterface 9 { 10 protected $socket; 11 protected $id; 12 protected $name; 13 14 public function getSocket() 15 { 16 return $this->socket; 17 } 18 19 public function setSocket(ConnectionInterface $socket) 20 { 21 $this->socket = $socket; 22 return $this; 23 } 24 25 public function getId() 26 { 27 return $this->id; 28 } 29 30 public function setId($id) 31 { 32 $this->id = $id; 33 return $this; 34 } 35 36 public function getName() 37 { 38 return $this->name; 39 } 40 41 public function setName($name) 42 { 43 $this->name = $name; 44 return $this; 45 } 46 } The User class is a simple wrapper for a socket resource and name string. The way we’ve chosen to implement the Ratchet server requires that we have a class which implements the MessageComponentInterface interface; and this interface specifies that ConnectionInterface objects are passed back and forth. There’s no way to identify these, by name (and id), so we’re adding that functionality with the extra layer.
All these classes lead us to the artisan command which will kick things off:
1 <?php 2 3 namespace Formativ\Chat\Command; 4 5 use Illuminate\Console\Command; 6 use Formativ\Chat\ChatInterface; 7 use Formativ\Chat\UserInterface; 8 use Ratchet\ConnectionInterface; 9 use Ratchet\Http\HttpServer; 10 use Ratchet\Server\IoServer; 11 use Ratchet\WebSocket\WsServer; 12 use Symfony\Component\Console\Input\InputOption; 13 use Symfony\Component\Console\Input\InputArgument; 14 15 class Serve 16 extends Command 17 { 18 protected $name = "chat:serve"; 19 protected $description = "Command description."; 20 protected $chat; 21 22 protected function getUserName($user) 23 { 24 $suffix = " (" . $user->getId() . ")"; 25 26 if ($name = $user->getName()) 27 { 28 return $name . $suffix; 29 } 30 31 return "User" . $suffix; 32 } 33 34 public function __construct(ChatInterface $chat) 35 { 36 parent::__construct(); 37 38 $this->chat = $chat; 39 40 $open = function(UserInterface $user) 41 { 42 $name = $this->getUserName($user); 43 $this->line(" 44 <info>" . $name . " connected.</info> 45 "); 46 }; 47 48 $this->chat->getEmitter()->on("open", $open); 49 50 $close = function(UserInterface $user) 51 { 52 $name = $this->getUserName($user); 53 $this->line(" 54 <info>" . $name . " disconnected.</info> 55 "); 56 }; 57 58 $this->chat->getEmitter()->on("close", $close); 59 60 $message = function(UserInterface $user, $message) 61 { 62 $name = $this->getUserName($user); 63 $this->line(" 64 <info>New message from " . $name . ":</info> 65 <comment>" . $message . "</comment> 66 <info>.</info> 67 "); 68 }; 69 70 $this->chat->getEmitter()->on("message", $message); 71 72 $name = function(UserInterface $user, $message) 73 { 74 $this->line(" 75 <info>User changed their name to:</info> 76 <comment>" . $message . "</comment> 77 <info>.</info> 78 "); 79 }; 80 81 $this->chat->getEmitter()->on("name", $name); 82 83 $error = function(UserInterface $user, $exception) 84 { 85 $message = $exception->getMessage(); 86 87 $this->line(" 88 <info>User encountered an exception:</info> 89 <comment>" . $message . "</comment> 90 <info>.</info> 91 "); 92 }; 93 94 $this->chat->getEmitter()->on("error", $error); 95 } 96 97 public function fire() 98 { 99 $port = (integer) $this->option("port"); 100 101 if (!$port) 102 { 103 $port = 7778; 104 } 105 106 $server = IoServer::factory( 107 new HttpServer( 108 new WsServer( 109 $this->chat 110 ) 111 ), 112 $port 113 ); 114 115 $this->line(" 116 <info>Listening on port</info> 117 <comment>" . $port . "</comment> 118 <info>.</info> 119 "); 120 121 $server->run(); 122 } 123 124 protected function getOptions() 125 { 126 return [ 127 [ 128 "port", 129 null, 130 InputOption::VALUE_REQUIRED, 131 "Port to listen on.", 132 null 133 ] 134 ]; 135 } 136 } The reason for us adding the event emitter to the equation should now be obvious — we need a way to tie into the delegated events, of the Chat class, without leaking the abstraction we gain from it. In other words; we don’t want the Chat class to know of the existence of the artisan command. Similarly; we don’t want the artisan command to know of the onOpen, onMessage, onError and onMessage methods so instead we use a publish/subscribe model for notifying the command of changes. The result is a clean abstraction.
The fire() method gets (or defaults) the port and starts the Ratchet web socket server.
To connect to the web socket server; we need to add a bit of vanilla JavaScript:
1 try { 2 if (!WebSocket) { 3 console.log("no websocket support"); 4 } else { 5 var socket = new WebSocket("ws://127.0.0.1:7778/"); 6 7 socket.addEventListener("open", function (e) { 8 console.log("open: ", e); 9 }); 10 11 socket.addEventListener("error", function (e) { 12 console.log("error: ", e); 13 }); 14 15 socket.addEventListener("message", function (e) { 16 console.log("message: ", JSON.parse(e.data)); 17 }); 18 19 console.log("socket:", socket); 20 21 window.socket = socket; 22 } 23 } catch (e) { 24 console.log("exception: " + e); 25 } We’ve wrapped this code in a try-catch block as not all browsers support Web Sockets yet. There are a number of libraries which will shim this functionality, but their use is outside the scope of this tutorial.
This code will attempt to open a socket connection to 127.0.0.1:7778 (the address and port used in the serve command) and write some console messages depending on the events that are emitted. You’ll notice we’re also assigning the socket instance to the window object; so we can send some debugging commands through it.
This allows us to see both the server-side of things, as well as the client-side…
Getting our interface talking to our socket server is relatively straightforward. We begin by disabling our fixture data and modifying our model slightly:
1 App.Message = DS.Model.extend({ 2 "user_id" : DS.attr("integer"), 3 "user_name" : DS.attr("string"), 4 "user_id_class" : DS.attr("string"), 5 "message" : DS.attr("string") 6 }); 7 8 App.ApplicationAdapter = DS.FixtureAdapter.extend(); 9 10 App.Message.FIXTURES = [ 11 // { 12 // "id" : 1, 13 // "user" : "Chris", 14 // "text" : "Hello World." 15 // }, 16 // { 17 // "id" : 2, 18 // "user" : "Wayne", 19 // "text" : "Don't dig it, man." 20 // }, 21 // { 22 // "id" : 3, 23 // "user" : "Chris", 24 // "text" : "Meh." 25 // } 26 ]; Now the index template will show only the heading and form elements, but no chat messages. In order to populate these; we need to store a reference to the application data store:
1 var store; 2 3 App.IndexRoute = Ember.Route.extend({ 4 "init" : function() { 5 store = this.store; 6 }, 7 "model" : function () { 8 return store.find("message"); 9 } 10 }); We store this reference because we will need to push rows into the store once we receive them from the open socket. This leads us to the changes to web sockets:
1 try { 2 var id = 1; 3 4 if (!WebSocket) { 5 console.log("no websocket support"); 6 } else { 7 8 var socket = new WebSocket("ws://127.0.0.1:7778/"); 9 var id = 1; 10 11 socket.addEventListener("open", function (e) { 12 // console.log("open: ", e); 13 }); 14 15 socket.addEventListener("error", function (e) { 16 console.log("error: ", e); 17 }); 18 19 socket.addEventListener("message", function (e) { 20 21 var data = JSON.parse(e.data); 22 var user_id = data.user.id; 23 var user_name = data.user.name; 24 var message = data.message.data; 25 26 switch (data.message.type) { 27 28 case "name": 29 $(".name-" + user_id).html(user_name); 30 break; 31 32 case "message": 33 store.push("message", { 34 "id" : id++, 35 "user_id" : user_id, 36 "user_name" : user_name || "User", 37 "user_id_class" : "name-" + user_id, 38 "message" : message 39 }); 40 break; 41 42 } 43 44 }); 45 46 // console.log("socket:", socket); 47 48 window.socket = socket; // debug 49 } 50 } catch (e) { 51 console.log("exception: " + e); 52 } We start by defining an id variable, to store the id’s of message objects as they are passed through the socket. Inside the onMessage event handler; we parse the JSON data string and determine the type of message being received. If it’s a name message (a user changing their name) then we update all the table cells matching the user’s server id. If it’s a normal message object; we push it into the data store.
This gets us part of the way, since console message commands will visually affect the UI. We still need to wire up the input form…
1 App.IndexController = Ember.ArrayController.extend({ 2 "command" : null, 3 "actions" : { 4 "send" : function(key) { 5 6 if (key && key != 13) { 7 return; 8 } 9 10 var command = this.get("command") || ""; 11 12 if (command.indexOf("/name") === 0) { 13 socket.send(JSON.stringify({ 14 "type" : "name", 15 "data" : command.split("/name")[1] 16 })); 17 } else { 18 socket.send(JSON.stringify({ 19 "type" : "message", 20 "data" : command 21 })); 22 } 23 24 this.set("command", null); 25 } 26 } 27 }); 28 29 App.IndexView = Ember.View.extend({ 30 "keyDown" : function(e) { 31 this.get("controller").send("send", e.keyCode); 32 } 33 }); We create IndexController and IndexView objects. The IndexView object intercepts the keyDown event and passes it to the IndexController object. The first bit of logic tells the send() method to ignore all keystrokes that aren’t the enter key. This means enter will trigger the send() method.
It continues by checking for the presence of a /name command switch. If that’s present in the input value (via this.get(“command”)) then it sends a message to the server to change the user’s name. Otherwise it sends a normal message to the server. In order for the UI to update for the person sending the message; we need to also slightly modify the Chat class:
1 public function onMessage(ConnectionInterface $socket, $message) 2 { 3 $user = $this->getUserBySocket($socket); 4 $message = json_decode($message); 5 6 switch ($message->type) 7 { 8 case "name": 9 { 10 $user->setName($message->data); 11 $this->emitter->emit("name", [ 12 $user, 13 $message->data 14 ]); 15 break; 16 } 17 18 case "message": 19 { 20 $this->emitter->emit("message", [ 21 $user, 22 $message->data 23 ]); 24 break; 25 } 26 } 27 28 foreach ($this->users as $next) 29 { 30 // if ($next !== $user) 31 // { 32 $next->getSocket()->send(json_encode([ 33 "user" => [ 34 "id" => $user->getId(), 35 "name" => $user->getName() 36 ], 37 "message" => $message 38 ])); 39 // } 40 } 41 } The change we’ve made is to exclude the logic which prevented messages from being sent to the user from which they came. All messages will essentially be sent to everyone on the server now.
The final change is to the index template, as we changed the model structure and need to adjust for this in the template:
1 <script 2 type="text/x-handlebars" 3 data-template-name="index" 4 > 5 <div class="container"> 6 <div class="row"> 7 <div class="col-md-12"> 8 <h1>Laravel 4 Chat</h1> 9 <table class="table table-striped"> 10 @{{#each message in model}} 11 <tr> 12 <td @{{bind-attr class="message.user_id_class"}}> 13 @{{message.user_name}} 14 </td> 15 <td> 16 @{{message.message}} 17 </td> 18 </tr> 19 @{{/each}} 20 </table> 21 </div> 22 </div> 23 <div class="row"> 24 <div class="col-md-12"> 25 <div class="input-group"> 26 @{{input 27 type="text" 28 value=command 29 class="form-control" 30 }} 31 <span class="input-group-btn"> 32 <button 33 class="btn btn-default" 34 @{{action "send"}} 35 > 36 Send 37 </button> 38 </span> 39 </div> 40 </div> 41 </div> 42 </div> 43 </script> You’ll notice, apart from us using different field names; that we’ve change how the loop is done, the class added to each “label” cell and how the input field is generated.
The reason for the change to the loop structure is simply so that you can see another way to represented enumerable data in handlebars. Where previously we used a simple {{#each}} tag, we’re now being more explicit about the data we want to iterate.
We add a special class on each “label” cell as we need to target these cells and change their contents, in the event that a user decides to change their name.
Finally, we change how the input field is generated because we need to bind its value to the IndexController’s command property. This format allows that succinctly.
For persistent sockets to remain open, Apache needs to keep a single process thread occupied. This is a problem as it will consume vast amounts of RAM over a shorter time period than non-persistent connections would. For this reason; I highly recommend using either Nginx or an event-based language to create your chat application.
It’s not that Apache sucks; this is just not the sort of thing it was designed for. Nginx, on the other hand, is event-based and doesn’t hold onto a whole thread while it waits for activity through the open socket.