diff --git a/examples/cluster-httpd/README.md b/examples/cluster-httpd/README.md
new file mode 100644
index 0000000000..09edd05b9b
--- /dev/null
+++ b/examples/cluster-httpd/README.md
@@ -0,0 +1,31 @@
+
+# Socket.IO Chat with httpd & redis
+
+A simple chat demo for socket.io
+
+## How to use
+
+Install [Docker Compose](https://docs.docker.com/compose/install/), then:
+
+```
+$ docker-compose up -d
+```
+
+And then point your browser to `http://localhost:3000`.
+
+This will start four Socket.IO nodes, behind a httpd proxy which will loadbalance the requests (using a cookie for sticky sessions, see [cookie](http://httpd.apache.org/docs/2.4/fr/mod/mod_proxy_balancer.html)).
+
+Each node connects to the redis backend, which will enable to broadcast to every client, no matter which node it is currently connected to.
+
+```
+# you can kill a given node, the client should reconnect to another node
+$ docker-compose stop server-george
+```
+
+## Features
+
+- Multiple users can join a chat room by each entering a unique username
+on website load.
+- Users can type chat messages to the chat room.
+- A notification is sent to all users when a user joins or leaves
+the chatroom.
diff --git a/examples/cluster-httpd/docker-compose.yml b/examples/cluster-httpd/docker-compose.yml
new file mode 100644
index 0000000000..ad6e6f004a
--- /dev/null
+++ b/examples/cluster-httpd/docker-compose.yml
@@ -0,0 +1,51 @@
+
+httpd:
+ build: ./httpd
+ links:
+ - server-john
+ - server-paul
+ - server-george
+ - server-ringo
+ ports:
+ - "3000:80"
+
+server-john:
+ build: ./server
+ links:
+ - redis
+ expose:
+ - "3000"
+ environment:
+ - NAME=John
+
+server-paul:
+ build: ./server
+ links:
+ - redis
+ expose:
+ - "3000"
+ environment:
+ - NAME=Paul
+
+server-george:
+ build: ./server
+ links:
+ - redis
+ expose:
+ - "3000"
+ environment:
+ - NAME=George
+
+server-ringo:
+ build: ./server
+ links:
+ - redis
+ expose:
+ - "3000"
+ environment:
+ - NAME=Ringo
+
+redis:
+ image: redis:alpine
+ expose:
+ - "6379"
diff --git a/examples/cluster-httpd/httpd/Dockerfile b/examples/cluster-httpd/httpd/Dockerfile
new file mode 100644
index 0000000000..10fef8f3e2
--- /dev/null
+++ b/examples/cluster-httpd/httpd/Dockerfile
@@ -0,0 +1,2 @@
+FROM httpd:2.4-alpine
+COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
diff --git a/examples/cluster-httpd/httpd/httpd.conf b/examples/cluster-httpd/httpd/httpd.conf
new file mode 100644
index 0000000000..ee2dc5877f
--- /dev/null
+++ b/examples/cluster-httpd/httpd/httpd.conf
@@ -0,0 +1,52 @@
+
+Listen 80
+
+ServerName localhost
+
+LoadModule authn_file_module modules/mod_authn_file.so
+LoadModule authn_core_module modules/mod_authn_core.so
+LoadModule authz_host_module modules/mod_authz_host.so
+LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
+LoadModule authz_user_module modules/mod_authz_user.so
+LoadModule authz_core_module modules/mod_authz_core.so
+
+LoadModule headers_module modules/mod_headers.so
+LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
+LoadModule proxy_module modules/mod_proxy.so
+LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
+LoadModule proxy_http_module modules/mod_proxy_http.so
+LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
+LoadModule rewrite_module modules/mod_rewrite.so
+LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
+LoadModule unixd_module modules/mod_unixd.so
+
+User daemon
+Group daemon
+
+ErrorLog /proc/self/fd/2
+
+Header add Set-Cookie "SERVERID=sticky.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
+
+
+ BalancerMember "http://server-john:3000" route=john
+ BalancerMember "http://server-paul:3000" route=paul
+ BalancerMember "http://server-george:3000" route=george
+ BalancerMember "http://server-ringo:3000" route=ringo
+ ProxySet stickysession=SERVERID
+
+
+
+ BalancerMember "ws://server-john:3000" route=john
+ BalancerMember "ws://server-paul:3000" route=paul
+ BalancerMember "ws://server-george:3000" route=george
+ BalancerMember "ws://server-ringo:3000" route=ringo
+ ProxySet stickysession=SERVERID
+
+
+RewriteEngine On
+RewriteCond %{HTTP:Upgrade} =websocket [NC]
+RewriteRule /(.*) balancer://nodes_ws/$1 [P,L]
+RewriteCond %{HTTP:Upgrade} !=websocket [NC]
+RewriteRule /(.*) balancer://nodes_polling/$1 [P,L]
+
+ProxyTimeout 3
diff --git a/examples/cluster-httpd/server/Dockerfile b/examples/cluster-httpd/server/Dockerfile
new file mode 100644
index 0000000000..d1f9075727
--- /dev/null
+++ b/examples/cluster-httpd/server/Dockerfile
@@ -0,0 +1,15 @@
+FROM mhart/alpine-node:6
+
+# Create app directory
+RUN mkdir -p /usr/src/app
+WORKDIR /usr/src/app
+
+# Install app dependencies
+COPY package.json /usr/src/app/
+RUN npm install
+
+# Bundle app source
+COPY . /usr/src/app
+
+EXPOSE 3000
+CMD [ "npm", "start" ]
diff --git a/examples/cluster-httpd/server/index.js b/examples/cluster-httpd/server/index.js
new file mode 100644
index 0000000000..d5b56b2ce8
--- /dev/null
+++ b/examples/cluster-httpd/server/index.js
@@ -0,0 +1,82 @@
+// Setup basic express server
+var express = require('express');
+var app = express();
+var server = require('http').createServer(app);
+var io = require('socket.io')(server);
+var redis = require('socket.io-redis');
+var port = process.env.PORT || 3000;
+var serverName = process.env.NAME || 'Unknown';
+
+io.adapter(redis({ host: 'redis', port: 6379 }));
+
+server.listen(port, function () {
+ console.log('Server listening at port %d', port);
+ console.log('Hello, I\'m %s, how can I help?', serverName);
+});
+
+// Routing
+app.use(express.static(__dirname + '/public'));
+
+// Chatroom
+
+var numUsers = 0;
+
+io.on('connection', function (socket) {
+ socket.emit('my-name-is', serverName);
+
+ var addedUser = false;
+
+ // when the client emits 'new message', this listens and executes
+ socket.on('new message', function (data) {
+ // we tell the client to execute 'new message'
+ socket.broadcast.emit('new message', {
+ username: socket.username,
+ message: data
+ });
+ });
+
+ // when the client emits 'add user', this listens and executes
+ socket.on('add user', function (username) {
+ if (addedUser) return;
+
+ // we store the username in the socket session for this client
+ socket.username = username;
+ ++numUsers;
+ addedUser = true;
+ socket.emit('login', {
+ numUsers: numUsers
+ });
+ // echo globally (all clients) that a person has connected
+ socket.broadcast.emit('user joined', {
+ username: socket.username,
+ numUsers: numUsers
+ });
+ });
+
+ // when the client emits 'typing', we broadcast it to others
+ socket.on('typing', function () {
+ socket.broadcast.emit('typing', {
+ username: socket.username
+ });
+ });
+
+ // when the client emits 'stop typing', we broadcast it to others
+ socket.on('stop typing', function () {
+ socket.broadcast.emit('stop typing', {
+ username: socket.username
+ });
+ });
+
+ // when the user disconnects.. perform this
+ socket.on('disconnect', function () {
+ if (addedUser) {
+ --numUsers;
+
+ // echo globally that this client has left
+ socket.broadcast.emit('user left', {
+ username: socket.username,
+ numUsers: numUsers
+ });
+ }
+ });
+});
diff --git a/examples/cluster-httpd/server/package.json b/examples/cluster-httpd/server/package.json
new file mode 100644
index 0000000000..0fe83ecd3e
--- /dev/null
+++ b/examples/cluster-httpd/server/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "socket.io-chat",
+ "version": "0.0.0",
+ "description": "A simple chat client using socket.io",
+ "main": "index.js",
+ "author": "Grant Timmerman",
+ "private": true,
+ "license": "BSD",
+ "dependencies": {
+ "express": "4.13.4",
+ "socket.io": "^1.7.2",
+ "socket.io-redis": "^3.0.0"
+ },
+ "scripts": {
+ "start": "node index.js"
+ }
+}
diff --git a/examples/cluster-httpd/server/public/index.html b/examples/cluster-httpd/server/public/index.html
new file mode 100644
index 0000000000..9b2043d73f
--- /dev/null
+++ b/examples/cluster-httpd/server/public/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+ Socket.IO Chat Example
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/cluster-httpd/server/public/main.js b/examples/cluster-httpd/server/public/main.js
new file mode 100644
index 0000000000..03b3702c83
--- /dev/null
+++ b/examples/cluster-httpd/server/public/main.js
@@ -0,0 +1,286 @@
+$(function() {
+ var FADE_TIME = 150; // ms
+ var TYPING_TIMER_LENGTH = 400; // ms
+ var COLORS = [
+ '#e21400', '#91580f', '#f8a700', '#f78b00',
+ '#58dc00', '#287b00', '#a8f07a', '#4ae8c4',
+ '#3b88eb', '#3824aa', '#a700ff', '#d300e7'
+ ];
+
+ // Initialize variables
+ var $window = $(window);
+ var $usernameInput = $('.usernameInput'); // Input for username
+ var $messages = $('.messages'); // Messages area
+ var $inputMessage = $('.inputMessage'); // Input message input box
+
+ var $loginPage = $('.login.page'); // The login page
+ var $chatPage = $('.chat.page'); // The chatroom page
+
+ // Prompt for setting a username
+ var username;
+ var connected = false;
+ var typing = false;
+ var lastTypingTime;
+ var $currentInput = $usernameInput.focus();
+
+ var socket = io();
+
+ function addParticipantsMessage (data) {
+ var message = '';
+ if (data.numUsers === 1) {
+ message += "there's 1 participant";
+ } else {
+ message += "there are " + data.numUsers + " participants";
+ }
+ log(message);
+ }
+
+ // Sets the client's username
+ function setUsername () {
+ username = cleanInput($usernameInput.val().trim());
+
+ // If the username is valid
+ if (username) {
+ $loginPage.fadeOut();
+ $chatPage.show();
+ $loginPage.off('click');
+ $currentInput = $inputMessage.focus();
+
+ // Tell the server your username
+ socket.emit('add user', username);
+ }
+ }
+
+ // Sends a chat message
+ function sendMessage () {
+ var message = $inputMessage.val();
+ // Prevent markup from being injected into the message
+ message = cleanInput(message);
+ // if there is a non-empty message and a socket connection
+ if (message && connected) {
+ $inputMessage.val('');
+ addChatMessage({
+ username: username,
+ message: message
+ });
+ // tell server to execute 'new message' and send along one parameter
+ socket.emit('new message', message);
+ }
+ }
+
+ // Log a message
+ function log (message, options) {
+ var $el = $('').addClass('log').text(message);
+ addMessageElement($el, options);
+ }
+
+ // Adds the visual chat message to the message list
+ function addChatMessage (data, options) {
+ // Don't fade the message in if there is an 'X was typing'
+ var $typingMessages = getTypingMessages(data);
+ options = options || {};
+ if ($typingMessages.length !== 0) {
+ options.fade = false;
+ $typingMessages.remove();
+ }
+
+ var $usernameDiv = $('')
+ .text(data.username)
+ .css('color', getUsernameColor(data.username));
+ var $messageBodyDiv = $('')
+ .text(data.message);
+
+ var typingClass = data.typing ? 'typing' : '';
+ var $messageDiv = $('')
+ .data('username', data.username)
+ .addClass(typingClass)
+ .append($usernameDiv, $messageBodyDiv);
+
+ addMessageElement($messageDiv, options);
+ }
+
+ // Adds the visual chat typing message
+ function addChatTyping (data) {
+ data.typing = true;
+ data.message = 'is typing';
+ addChatMessage(data);
+ }
+
+ // Removes the visual chat typing message
+ function removeChatTyping (data) {
+ getTypingMessages(data).fadeOut(function () {
+ $(this).remove();
+ });
+ }
+
+ // Adds a message element to the messages and scrolls to the bottom
+ // el - The element to add as a message
+ // options.fade - If the element should fade-in (default = true)
+ // options.prepend - If the element should prepend
+ // all other messages (default = false)
+ function addMessageElement (el, options) {
+ var $el = $(el);
+
+ // Setup default options
+ if (!options) {
+ options = {};
+ }
+ if (typeof options.fade === 'undefined') {
+ options.fade = true;
+ }
+ if (typeof options.prepend === 'undefined') {
+ options.prepend = false;
+ }
+
+ // Apply options
+ if (options.fade) {
+ $el.hide().fadeIn(FADE_TIME);
+ }
+ if (options.prepend) {
+ $messages.prepend($el);
+ } else {
+ $messages.append($el);
+ }
+ $messages[0].scrollTop = $messages[0].scrollHeight;
+ }
+
+ // Prevents input from having injected markup
+ function cleanInput (input) {
+ return $('').text(input).text();
+ }
+
+ // Updates the typing event
+ function updateTyping () {
+ if (connected) {
+ if (!typing) {
+ typing = true;
+ socket.emit('typing');
+ }
+ lastTypingTime = (new Date()).getTime();
+
+ setTimeout(function () {
+ var typingTimer = (new Date()).getTime();
+ var timeDiff = typingTimer - lastTypingTime;
+ if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
+ socket.emit('stop typing');
+ typing = false;
+ }
+ }, TYPING_TIMER_LENGTH);
+ }
+ }
+
+ // Gets the 'X is typing' messages of a user
+ function getTypingMessages (data) {
+ return $('.typing.message').filter(function (i) {
+ return $(this).data('username') === data.username;
+ });
+ }
+
+ // Gets the color of a username through our hash function
+ function getUsernameColor (username) {
+ // Compute hash code
+ var hash = 7;
+ for (var i = 0; i < username.length; i++) {
+ hash = username.charCodeAt(i) + (hash << 5) - hash;
+ }
+ // Calculate color
+ var index = Math.abs(hash % COLORS.length);
+ return COLORS[index];
+ }
+
+ // Keyboard events
+
+ $window.keydown(function (event) {
+ // Auto-focus the current input when a key is typed
+ if (!(event.ctrlKey || event.metaKey || event.altKey)) {
+ $currentInput.focus();
+ }
+ // When the client hits ENTER on their keyboard
+ if (event.which === 13) {
+ if (username) {
+ sendMessage();
+ socket.emit('stop typing');
+ typing = false;
+ } else {
+ setUsername();
+ }
+ }
+ });
+
+ $inputMessage.on('input', function() {
+ updateTyping();
+ });
+
+ // Click events
+
+ // Focus input when clicking anywhere on login page
+ $loginPage.click(function () {
+ $currentInput.focus();
+ });
+
+ // Focus input when clicking on the message input's border
+ $inputMessage.click(function () {
+ $inputMessage.focus();
+ });
+
+ // Socket events
+
+ // Whenever the server emits 'login', log the login message
+ socket.on('login', function (data) {
+ connected = true;
+ // Display the welcome message
+ var message = "Welcome to Socket.IO Chat – ";
+ log(message, {
+ prepend: true
+ });
+ addParticipantsMessage(data);
+ });
+
+ // Whenever the server emits 'new message', update the chat body
+ socket.on('new message', function (data) {
+ addChatMessage(data);
+ });
+
+ // Whenever the server emits 'user joined', log it in the chat body
+ socket.on('user joined', function (data) {
+ log(data.username + ' joined');
+ addParticipantsMessage(data);
+ });
+
+ // Whenever the server emits 'user left', log it in the chat body
+ socket.on('user left', function (data) {
+ log(data.username + ' left');
+ addParticipantsMessage(data);
+ removeChatTyping(data);
+ });
+
+ // Whenever the server emits 'typing', show the typing message
+ socket.on('typing', function (data) {
+ addChatTyping(data);
+ });
+
+ // Whenever the server emits 'stop typing', kill the typing message
+ socket.on('stop typing', function (data) {
+ removeChatTyping(data);
+ });
+
+ socket.on('disconnect', function () {
+ log('you have been disconnected');
+ });
+
+ socket.on('reconnect', function () {
+ log('you have been reconnected');
+ if (username) {
+ socket.emit('add user', username);
+ }
+ });
+
+ socket.on('reconnect_error', function () {
+ log('attempt to reconnect has failed');
+ });
+
+ socket.on('my-name-is', function (serverName) {
+ log('host is now ' + serverName);
+ })
+
+});
diff --git a/examples/cluster-httpd/server/public/style.css b/examples/cluster-httpd/server/public/style.css
new file mode 100644
index 0000000000..3052d88e4c
--- /dev/null
+++ b/examples/cluster-httpd/server/public/style.css
@@ -0,0 +1,149 @@
+/* Fix user-agent */
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ font-weight: 300;
+ -webkit-font-smoothing: antialiased;
+}
+
+html, input {
+ font-family:
+ "HelveticaNeue-Light",
+ "Helvetica Neue Light",
+ "Helvetica Neue",
+ Helvetica,
+ Arial,
+ "Lucida Grande",
+ sans-serif;
+}
+
+html, body {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+ul {
+ list-style: none;
+ word-wrap: break-word;
+}
+
+/* Pages */
+
+.pages {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+
+.page {
+ height: 100%;
+ position: absolute;
+ width: 100%;
+}
+
+/* Login Page */
+
+.login.page {
+ background-color: #000;
+}
+
+.login.page .form {
+ height: 100px;
+ margin-top: -100px;
+ position: absolute;
+
+ text-align: center;
+ top: 50%;
+ width: 100%;
+}
+
+.login.page .form .usernameInput {
+ background-color: transparent;
+ border: none;
+ border-bottom: 2px solid #fff;
+ outline: none;
+ padding-bottom: 15px;
+ text-align: center;
+ width: 400px;
+}
+
+.login.page .title {
+ font-size: 200%;
+}
+
+.login.page .usernameInput {
+ font-size: 200%;
+ letter-spacing: 3px;
+}
+
+.login.page .title, .login.page .usernameInput {
+ color: #fff;
+ font-weight: 100;
+}
+
+/* Chat page */
+
+.chat.page {
+ display: none;
+}
+
+/* Font */
+
+.messages {
+ font-size: 150%;
+}
+
+.inputMessage {
+ font-size: 100%;
+}
+
+.log {
+ color: gray;
+ font-size: 70%;
+ margin: 5px;
+ text-align: center;
+}
+
+/* Messages */
+
+.chatArea {
+ height: 100%;
+ padding-bottom: 60px;
+}
+
+.messages {
+ height: 100%;
+ margin: 0;
+ overflow-y: scroll;
+ padding: 10px 20px 10px 20px;
+}
+
+.message.typing .messageBody {
+ color: gray;
+}
+
+.username {
+ font-weight: 700;
+ overflow: hidden;
+ padding-right: 15px;
+ text-align: right;
+}
+
+/* Input */
+
+.inputMessage {
+ border: 10px solid #000;
+ bottom: 0;
+ height: 60px;
+ left: 0;
+ outline: none;
+ padding-left: 10px;
+ position: absolute;
+ right: 0;
+ width: 100%;
+}