├── .gitignore ├── code ├── .bowerrc ├── config.js ├── Makefile ├── public │ ├── css │ │ └── custom.css │ ├── js │ │ └── communication.js │ └── index.html ├── bower.json ├── package.json ├── app.js ├── message_handler.js └── rabbitMQ_messaging.js ├── blog ├── images │ └── final_app.png └── post.md └── slides ├── rabbitmq_models.png └── slides /.gitignore: -------------------------------------------------------------------------------- 1 | code/node_modules 2 | code/public/bower_components 3 | -------------------------------------------------------------------------------- /code/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /code/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: process.env.NODE_PORT ? process.env.NODE_PORT : 3000 3 | }; 4 | -------------------------------------------------------------------------------- /blog/images/final_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-pettigrew/scaling-socket-io-talk/HEAD/blog/images/final_app.png -------------------------------------------------------------------------------- /slides/rabbitmq_models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/john-pettigrew/scaling-socket-io-talk/HEAD/slides/rabbitmq_models.png -------------------------------------------------------------------------------- /code/Makefile: -------------------------------------------------------------------------------- 1 | start_mq: 2 | docker run -d --name socketio_demo -p 15672:15672 -p 5672:5672 rabbitmq 3 | 4 | .PHONY: start_mq 5 | -------------------------------------------------------------------------------- /code/public/css/custom.css: -------------------------------------------------------------------------------- 1 | #chat-container{ 2 | position: fixed; 3 | bottom: 0; 4 | width: 70%; 5 | } 6 | .chat-message{ 7 | font-size: 2em; 8 | text-align: center; 9 | } 10 | -------------------------------------------------------------------------------- /code/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-app", 3 | "description": "A chat app.", 4 | "main": "app.js", 5 | "authors": [ 6 | "John Pettigrew " 7 | ], 8 | "license": "MIT", 9 | "homepage": "", 10 | "private": true, 11 | "dependencies": { 12 | "jquery": "^3.0.0", 13 | "moment": "^2.13.0", 14 | "skeleton": "skeleton-css#^2.0.4" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-app", 3 | "version": "1.0.0", 4 | "description": "A chat app.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "John Pettigrew ", 10 | "dependencies": { 11 | "amqplib": "^0.4.2", 12 | "express": "^4.13.4", 13 | "socket.io": "^1.4.6" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /slides/slides: -------------------------------------------------------------------------------- 1 | Scaling A Websocket Application with RabbitMQ 2 | 3 | John Pettigrew 4 | http://john.pettigrew.rocks/ 5 | @johnbpettigrew 6 | 7 | Socket.io - "enables real-time bidirectional event-based communication" 8 | 9 | demo 10 | 11 | RabbitMQ - Message queue. 12 | 13 | @rabbitmq_models.png 14 | 15 | Message Flow 16 | producer -> exchange -> queue -> consumer 17 | 18 | demo 19 | 20 | Slides & code are on Github. Also blog post. 21 | 22 | end 23 | 24 | John Pettigrew 25 | http://john.pettigrew.rocks/ 26 | @johnbpettigrew 27 | -------------------------------------------------------------------------------- /code/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var http = require('http').Server(app); 4 | var io = require('socket.io')(http); 5 | var messageHandler = require('./message_handler'); 6 | var config = require('./config'); 7 | 8 | //Serve static files 9 | app.get('/', function(req, res){ 10 | res.sendFile(__dirname+'/public/index.html'); 11 | }); 12 | app.use('/public', express.static('public')); 13 | 14 | //Handle messaging 15 | messageHandler(io); 16 | 17 | http.listen(config.port, function(){ 18 | console.log(`Server running on port ${config.port}`); 19 | }); 20 | -------------------------------------------------------------------------------- /code/public/js/communication.js: -------------------------------------------------------------------------------- 1 | ( 2 | function(){ 3 | 4 | var messageInput = '#chat-input'; 5 | var messageSubmit = '#chat-send'; 6 | var messageList = '#chat-list'; 7 | 8 | var socket = io(); 9 | 10 | //messages to server 11 | $(messageSubmit).click(function(){ 12 | 13 | var msg = $(messageInput).val(); 14 | if(!msg){ 15 | return; 16 | } 17 | 18 | sendMessage(msg); 19 | $(messageInput).val(''); 20 | }); 21 | 22 | //messages from server 23 | socket.on('message', displayMessage); 24 | 25 | function sendMessage(msg){ 26 | socket.emit('message', msg) 27 | } 28 | 29 | function displayMessage(msg){ 30 | $(messageList).append(getMessageHTML(msg)) 31 | } 32 | 33 | function getMessageHTML(msg){ 34 | return '
  • ' + msg.text + ' '+ moment(new Date(msg.date)).format('MMMM Do YYYY, h:mm:ss a') + '' + '
  • ' 35 | } 36 | } 37 | )(); 38 | -------------------------------------------------------------------------------- /code/message_handler.js: -------------------------------------------------------------------------------- 1 | var rabbitMQHandler = require('./rabbitMQ_messaging'); 2 | 3 | module.exports = messageHandler; 4 | 5 | function messageHandler(io){ 6 | // rabbitMQHandler('amqp://localhost', function(err, options){ 7 | 8 | // if(err){ 9 | // throw err; 10 | // } 11 | 12 | // options.onMessageReceived = onMessageReceived; 13 | 14 | io.on('connection', websocketConnect); 15 | 16 | function websocketConnect(socket){ 17 | 18 | console.log('New connection') 19 | 20 | socket.on('disconnect', socketDisconnect); 21 | socket.on('message', socketMessage); 22 | 23 | function socketDisconnect(e){ 24 | console.log('Disconnect ', e); 25 | } 26 | 27 | function socketMessage(text){ 28 | var message = {text: text, date: new Date()}; 29 | io.emit('message', message) 30 | // options.emitMessage(message); 31 | } 32 | } 33 | 34 | function onMessageReceived(message){ 35 | 36 | io.emit('message', message) 37 | } 38 | 39 | // }); 40 | } 41 | -------------------------------------------------------------------------------- /code/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat App 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 | -------------------------------------------------------------------------------- /code/rabbitMQ_messaging.js: -------------------------------------------------------------------------------- 1 | var amqp = require('amqplib/callback_api'); 2 | 3 | module.exports = rabbitMQMessages; 4 | 5 | function rabbitMQMessages(address, callback){ 6 | //connect to RabbitMQ 7 | amqp.connect(address, function amqpConnectCallback(err, conn){ 8 | if(err){ 9 | return callback(err); 10 | } 11 | 12 | //create a channel 13 | conn.createChannel(function(err, ch){ 14 | if(err){ 15 | return callback(err); 16 | } 17 | 18 | 19 | ch.assertExchange('messages', 'fanout', {durable: false}); 20 | 21 | //setup a queue for receiving messages 22 | ch.assertQueue('', {exclusive: true}, function(err, q){ 23 | if(err){ 24 | return callback(err); 25 | } 26 | 27 | 28 | ch.bindQueue(q.queue, 'messages', ''); 29 | 30 | var options = { 31 | emitMessage: emitMessage, 32 | onMessageReceived: onMessageReceived 33 | }; 34 | 35 | //listen for messages 36 | ch.consume(q.queue, function(msg){ 37 | options.onMessageReceived(JSON.parse(msg.content.toString())); 38 | }, {noAck: true}); 39 | 40 | callback(null, options); 41 | 42 | function emitMessage(message){ 43 | 44 | ch.publish('messages', '', new Buffer(JSON.stringify(message))); 45 | } 46 | 47 | function onMessageReceived(){ 48 | console.log('Message received. Nothing to do.'); 49 | } 50 | }); 51 | 52 | 53 | }); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /blog/post.md: -------------------------------------------------------------------------------- 1 | [Socket.io](http://socket.io) is extremely powerful when it comes to communicating between the browser and a server in real-time. However, the problem of scaling quickly arises with the situations of very high numbers of clients or the need to implement load balancing. This problem can be easily and effectively addressed with [RabbitMQ](https://www.rabbitmq.com). This method also allows for a very extendable architecture when the project's goals inevitably grow and/or change. We will go over some quick basics for these tools as well as extend an existing chat application to use RabbitMQ and multiple node processes. The demo application is available on [my Github page](https://github.com/john-pettigrew/scaling-socket-io-talk). 2 | 3 | ## Socket.io 4 | [Socket.io](http://socket.io) is a library that implements the websocket protocol. Websockets are meant for two way communication and are often used between a server and a web browser. This is a sharp contrast to the standard way a browser communicates with a server. Typically, a web browser makes requests over 'http' or 'https' and the server responds. When you type in "https://google.com" there is a server that receives your browser's request and does its best to send back a document. Data (such as JSON) can be sent over AJAX requests. This requires the web browser to ask for the information. If the browser needs to wait for new information, it has to poll and ask the server for the updated information every X number of seconds. 5 | 6 | With websockets however, communication is free to take place between a web browser and a server. This means that the server can push information to the web browser and vice versa. This type of communication is great for chat apps, simple games, and real time dashboards. 7 | 8 | ## RabbitMQ 9 | [RabbitMQ](https://www.rabbitmq.com) is a message queue. There are many models for building applications that use RabbitMQ. Just take a look at [their tutorials](https://www.rabbitmq.com/getstarted.html) for some samples. For example, you might use the worker model for a web application that has some long running task like resizing an image. The RabbitMQ server can even implement acknowledgments to make sure the resize completes even if the worker process crashes mid way through completing. It can simply route the job to another worker. However, I won't be covering acknowledgments in this post. 10 | 11 | In this post, our chat application will use a publish and subscribe model. We will use it to send to and listen for messages from our chat application. Our chat servers will not need to know about each other. They will only need to know the IP address of RabbitMQ. RabbitMQ also offers a nice web UI and allows for clustering if our application ever requires it. Our application acts as a "producer" when it sends messages to RabbitMQ. These messages are sent to an exchange. This exchange routes messages to queues and then our application acts as a "consumer" and reads them. 12 | 13 | producer -> exchange -> queue -> consumer 14 | 15 | ## Our Base 16 | For this demo, I have created a small chat application to extend to using RabbitMQ. It is available on [my Github page](https://github.com/john-pettigrew/scaling-socket-io-talk). Currently, it uses Express JS as a server to serve our chat page and Socket.io for our messaging. Socket.io will actually handle the work of getting our browser connected via websockets. Let's take a look at the "[message_handler.js](https://github.com/john-pettigrew/scaling-socket-io-talk/blob/master/code/message_handler.js)" file. 17 | ```js 18 | io.on('connection', websocketConnect); 19 | 20 | function websocketConnect(socket){ 21 | 22 | console.log('New connection') 23 | 24 | socket.on('disconnect', socketDisconnect); 25 | socket.on('message', socketMessage); 26 | 27 | function socketDisconnect(e){ 28 | console.log('Disconnect ', e); 29 | } 30 | 31 | function socketMessage(text){ 32 | var message = {text: text, date: new Date()}; 33 | io.emit('message', message) 34 | } 35 | } 36 | ``` 37 | Here we are telling Socket.io to wait for connections. Once connected, we wait for a disconnect or a message from a socket. Once a message is received, we simply emit the message out to all listening clients. 38 | 39 | [Our client code](https://github.com/john-pettigrew/scaling-socket-io-talk/blob/master/code/public/js/communication.js) is also very simple. 40 | ```js 41 | var messageInput = '#chat-input'; 42 | var messageSubmit = '#chat-send'; 43 | var messageList = '#chat-list'; 44 | 45 | var socket = io(); 46 | 47 | //messages to server 48 | $(messageSubmit).click(function(){ 49 | 50 | var msg = $(messageInput).val(); 51 | if(!msg){ 52 | return; 53 | } 54 | 55 | sendMessage(msg); 56 | $(messageInput).val(''); 57 | }); 58 | 59 | //messages from server 60 | socket.on('message', displayMessage); 61 | 62 | function sendMessage(msg){ 63 | socket.emit('message', msg) 64 | } 65 | 66 | function displayMessage(msg){ 67 | $(messageList).append(getMessageHTML(msg)) 68 | } 69 | 70 | function getMessageHTML(msg){ 71 | return '
    • ' + msg.text + ' '+ moment(new Date(msg.date)).format('MMMM Do YYYY, h:mm:ss a') + '' + '
    • ' 72 | } 73 | ``` 74 | I use a small amount of jQuery to listen for the user to submit a chat message and to add new messages from the server to the page. 75 | Note: I am not doing any input sanitization for the demo app. 76 | 77 | ## Extending with RabbitMQ 78 | We can start setting up this application to scale by creating a file to handle talking to RabbitMQ for us (["rabbitMQ_messaging.js"](https://github.com/john-pettigrew/scaling-socket-io-talk/blob/master/code/rabbitMQ_messaging.js)). 79 | 80 | ```js 81 | var amqp = require('amqplib/callback_api'); 82 | 83 | module.exports = rabbitMQMessages; 84 | 85 | function rabbitMQMessages(address, callback){ 86 | //connect to RabbitMQ 87 | amqp.connect(address, function amqpConnectCallback(err, conn){ 88 | if(err){ 89 | return callback(err); 90 | } 91 | ``` 92 | Here we start by importing an amqp library to communicate with RabbitMQ. Then we export our function that will setup our connection to RabbitMQ. 93 | 94 | Next, we create a channel. This is what we talk to RabbitMQ through. 95 | 96 | ```js 97 | //create a channel 98 | conn.createChannel(function(err, ch){ 99 | if(err){ 100 | return callback(err); 101 | } 102 | ``` 103 | 104 | From here, we need to assert our exchange. Our exchange is what our application will send our chat messages to in RabbitMQ. We chose the 'fanout' method to tell rabbitMQ that we want our message delivered to several clients. 105 | 106 | ```js 107 | ch.assertExchange('messages', 'fanout', {durable: false}); 108 | 109 | //setup a queue for receiving messages 110 | ch.assertQueue('', {exclusive: true}, function(err, q){ 111 | if(err){ 112 | return callback(err); 113 | } 114 | ch.bindQueue(q.queue, 'messages', ''); 115 | ``` 116 | 117 | We use "assertQueue" with an empty string to define a temporary queue as described [here](). Finally, we bind our queue and our exchange. This tells the exchange to send our chat messages to this queue. Now we can start sending and receiving messages. 118 | 119 | ```js 120 | var options = { 121 | emitMessage: emitMessage, 122 | onMessageReceived: onMessageReceived 123 | }; 124 | 125 | //listen for messages 126 | ch.consume(q.queue, function(msg){ 127 | options.onMessageReceived(JSON.parse(msg.content.toString())); 128 | }, {noAck: true}); 129 | 130 | callback(null, options); 131 | 132 | function emitMessage(message){ 133 | 134 | ch.publish('messages', '', new Buffer(JSON.stringify(message))); 135 | } 136 | 137 | function onMessageReceived(){ 138 | console.log('Message received. Nothing to do.'); 139 | } 140 | ``` 141 | 142 | We create an "options" object that will contain our functions for sending and receiving messages. Using this method, we can replace the "onMessageReceived" function to do something more useful later. 143 | 144 | Now that we have built this file, lets modify our "message_handler.js" file to use RabbitMQ. 145 | ```js 146 | var rabbitMQHandler = require('./rabbitMQ_messaging'); 147 | 148 | ... 149 | 150 | rabbitMQHandler('amqp://localhost', function(err, options){ 151 | 152 | if(err){ 153 | throw err; 154 | } 155 | ``` 156 | We start by importing our file and passing in our address string for our message queue. Next, we replace the "onMessageReceived" function. 157 | 158 | ```js 159 | options.onMessageReceived = onMessageReceived; 160 | 161 | ... 162 | 163 | function onMessageReceived(message){ 164 | 165 | io.emit('message', message) 166 | } 167 | 168 | ``` 169 | 170 | Since this function is now sending to clients, we need our application to send messages to RabbitMQ. 171 | 172 | ```js 173 | function socketMessage(text){ 174 | var message = {text: text, date: new Date()}; 175 | // io.emit('message', message) 176 | options.emitMessage(message); 177 | } 178 | ``` 179 | 180 | ## Adding More Servers 181 | Now, lets test adding a few node servers. We can see that our applications are talking to each other by starting a few on different ports. My demo application is reading a "NODE_PORT" environment variable to know which port to run on. 182 | ```bash 183 | #run each of these in a different terminal 184 | NODE_PORT=3000 node app.js 185 | ... 186 | NODE_PORT=3001 node app.js 187 | ... 188 | NODE_PORT=3002 node app.js 189 | ``` 190 | ![final working application](https://raw.githubusercontent.com/john-pettigrew/scaling-socket-io-talk/master/blog/images/final_app.png) 191 | 192 | ## Recap 193 | For smaller applications, scaling in the way that we have discussed may not be necessary. The chat application could be further extended if logging or some other service was required by letting other node applications subscribe to these events in RabbitMQ. We went over some of the basics of RabbitMQ with Socket.io and applied them to a chat application to help it scale. If you thought this was awesome, please share it! 194 | 195 | Until next time, 196 | 197 | John 198 | --------------------------------------------------------------------------------