Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: socketio/socket.io
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0abbd4da50c3fbffa5e32359b269e96c156c6ef2
Choose a base ref
...
head repository: socketio/socket.io
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: baa6804440fe5dc98fcbd5249dee2f4fa6ef1b8a
Choose a head ref

Commits on Dec 30, 2016

  1. Copy the full SHA
    255b845 View commit details
  2. Copy the full SHA
    d821900 View commit details
  3. Copy the full SHA
    c46d448 View commit details
  4. Copy the full SHA
    a27802e View commit details

Commits on Jan 4, 2017

  1. Copy the full SHA
    e04de3c View commit details

Commits on Jan 6, 2017

  1. Copy the full SHA
    f260439 View commit details

Commits on Jan 10, 2017

  1. Copy the full SHA
    01b262f View commit details
  2. Copy the full SHA
    0be865f View commit details
  3. Copy the full SHA
    131a2be View commit details
  4. Copy the full SHA
    accd0bd View commit details
  5. Copy the full SHA
    706ca2a View commit details
  6. Copy the full SHA
    b754cff View commit details

Commits on Jan 16, 2017

  1. [perf] Reset properties instead of deleting them (#2826)

    Deleting properties is a v8 'optimization killer'
    Tom Atkinson authored and darrachequesne committed Jan 16, 2017
    Copy the full SHA
    e24434a View commit details
  2. [perf] Use shared instance of the encoder (#2825)

    Tom Atkinson authored and darrachequesne committed Jan 16, 2017
    Copy the full SHA
    ad658b8 View commit details
  3. [fix] Properly close the connection on error (#2681)

    Tom Atkinson authored and darrachequesne committed Jan 16, 2017
    Copy the full SHA
    2258a6a View commit details
  4. Copy the full SHA
    8eaba08 View commit details
  5. Copy the full SHA
    9888529 View commit details
  6. Copy the full SHA
    f7eed6e View commit details
  7. Copy the full SHA
    e28b475 View commit details
  8. Copy the full SHA
    23c9dd3 View commit details

Commits on Jan 22, 2017

  1. [fix] Use path.resolve by default and require.resolve as a fallback (#…

    …2797)
    
    Browserify doesn't support require.resolve, and as a consequence, makes nexe fail the compilation. This PR attempts to get the path of the socket.io-client file via path.resolve and falls back to the original require.resolve if this file cannot be found.
    a-lucas authored and darrachequesne committed Jan 22, 2017
    Copy the full SHA
    3b5f433 View commit details

Commits on Jan 23, 2017

  1. Copy the full SHA
    3d695c6 View commit details

Commits on Jan 24, 2017

  1. Copy the full SHA
    3b92cc2 View commit details

Commits on Feb 1, 2017

  1. Copy the full SHA
    e1facd5 View commit details
  2. Copy the full SHA
    54ff591 View commit details

Commits on Feb 13, 2017

  1. Copy the full SHA
    5b79ab1 View commit details

Commits on Feb 16, 2017

  1. [chore] Bump engine.io to version 2.0.2 (#2864)

    Includes the following (from engine.io changelog):
    
    * [fix] Initialize the WebSocket server in the Server constructor (#476)
    * [chore] Bump ws to version 1.1.2 (vulnerability fix) (#480)
    sgress454 authored and darrachequesne committed Feb 16, 2017
    Copy the full SHA
    4d8f68c View commit details

Commits on Feb 17, 2017

  1. Copy the full SHA
    5ae06e6 View commit details

Commits on Feb 22, 2017

  1. Copy the full SHA
    2d5b002 View commit details

Commits on Feb 26, 2017

  1. Copy the full SHA
    01a4623 View commit details

Commits on Mar 18, 2017

  1. Copy the full SHA
    e40accf View commit details

Commits on Mar 27, 2017

  1. Copy the full SHA
    03f3bc9 View commit details

Commits on Mar 29, 2017

  1. Copy the full SHA
    c5b7738 View commit details
  2. Copy the full SHA
    240b154 View commit details

Commits on Apr 3, 2017

  1. Copy the full SHA
    f1b39a6 View commit details

Commits on Apr 19, 2017

  1. Copy the full SHA
    199eec6 View commit details

Commits on Apr 24, 2017

  1. Copy the full SHA
    87b06ad View commit details

Commits on Apr 28, 2017

  1. 1
    Copy the full SHA
    a086588 View commit details

Commits on May 8, 2017

  1. Copy the full SHA
    0d07c47 View commit details
  2. Copy the full SHA
    1980fb4 View commit details
  3. Copy the full SHA
    6c0705f View commit details
  4. [chore] Release 2.0.0

    darrachequesne committed May 8, 2017
    Copy the full SHA
    3367eaa View commit details
  5. Copy the full SHA
    a005690 View commit details
  6. [chore] Release 2.0.1

    darrachequesne committed May 8, 2017
    Copy the full SHA
    832b8fc View commit details

Commits on May 22, 2017

  1. [fix] Fix timing issues with middleware (#2948)

    Using a middleware could previously lead to a connecting client
    receiving a connect event from the server before the server triggers
    its own connect event.
    darrachequesne authored May 22, 2017
    3
    Copy the full SHA
    2b21690 View commit details

Commits on Jun 1, 2017

  1. [chore] Release 2.0.2

    darrachequesne committed Jun 1, 2017
    Copy the full SHA
    a10dc8d View commit details

Commits on Jun 3, 2017

  1. Copy the full SHA
    2b10f1b View commit details

Commits on Jun 8, 2017

  1. Copy the full SHA
    9a014e2 View commit details

Commits on Jun 12, 2017

  1. [fix] Fix middleware initialization (#2969)

    Fix "TypeError: Cannot convert undefined or null to object" when a
    middleware is added before the engine is properly attached.
    darrachequesne authored Jun 12, 2017
    Copy the full SHA
    94df7bc View commit details
  2. [fix] Reset rooms object before broadcasting (#2970)

    It seems packets could be delivered to wrong room in some case, if the
    _rooms array was not reset before the next emit.
    darrachequesne authored Jun 12, 2017
    Copy the full SHA
    db0c699 View commit details
Showing with 8,081 additions and 1,380 deletions.
  1. +8 −1 .github/ISSUE_TEMPLATE.md
  2. +24 −0 .github/workflows/ci.yml
  3. +1 −0 .gitignore
  4. +0 −28 .travis.yml
  5. +27 −0 CHANGELOG.md
  6. +0 −638 History.md
  7. +1 −1 LICENSE
  8. +0 −8 Makefile
  9. +164 −410 Readme.md
  10. +880 −0 docs/API.md
  11. +15 −0 docs/README.md
  12. +64 −0 docs/emit.md
  13. +9 −8 examples/chat/index.js
  14. +27 −27 examples/chat/public/main.js
  15. +31 −0 examples/cluster-haproxy/README.md
  16. +51 −0 examples/cluster-haproxy/docker-compose.yml
  17. +2 −0 examples/cluster-haproxy/haproxy/Dockerfile
  18. +31 −0 examples/cluster-haproxy/haproxy/haproxy.cfg
  19. +15 −0 examples/cluster-haproxy/server/Dockerfile
  20. +87 −0 examples/cluster-haproxy/server/index.js
  21. +17 −0 examples/cluster-haproxy/server/package.json
  22. +28 −0 examples/cluster-haproxy/server/public/index.html
  23. +286 −0 examples/cluster-haproxy/server/public/main.js
  24. +149 −0 examples/cluster-haproxy/server/public/style.css
  25. +31 −0 examples/cluster-httpd/README.md
  26. +51 −0 examples/cluster-httpd/docker-compose.yml
  27. +2 −0 examples/cluster-httpd/httpd/Dockerfile
  28. +52 −0 examples/cluster-httpd/httpd/httpd.conf
  29. +15 −0 examples/cluster-httpd/server/Dockerfile
  30. +82 −0 examples/cluster-httpd/server/index.js
  31. +17 −0 examples/cluster-httpd/server/package.json
  32. +28 −0 examples/cluster-httpd/server/public/index.html
  33. +286 −0 examples/cluster-httpd/server/public/main.js
  34. +149 −0 examples/cluster-httpd/server/public/style.css
  35. +31 −0 examples/cluster-nginx/README.md
  36. +51 −0 examples/cluster-nginx/docker-compose.yml
  37. +3 −0 examples/cluster-nginx/nginx/Dockerfile
  38. +35 −0 examples/cluster-nginx/nginx/nginx.conf
  39. +15 −0 examples/cluster-nginx/server/Dockerfile
  40. +82 −0 examples/cluster-nginx/server/index.js
  41. +17 −0 examples/cluster-nginx/server/package.json
  42. +28 −0 examples/cluster-nginx/server/public/index.html
  43. +286 −0 examples/cluster-nginx/server/public/main.js
  44. +149 −0 examples/cluster-nginx/server/public/style.css
  45. +50 −0 examples/custom-parsers/README.md
  46. +21 −0 examples/custom-parsers/package.json
  47. +13 −0 examples/custom-parsers/public/index.html
  48. +8 −0 examples/custom-parsers/src/client1.js
  49. +11 −0 examples/custom-parsers/src/client2.js
  50. +11 −0 examples/custom-parsers/src/client3.js
  51. +11 −0 examples/custom-parsers/src/client4.js
  52. +125 −0 examples/custom-parsers/src/custom-parser.js
  53. +55 −0 examples/custom-parsers/src/server.js
  54. +15 −0 examples/custom-parsers/support/webpack.config.js
  55. +18 −0 examples/webpack-build-server/README.md
  56. +16 −0 examples/webpack-build-server/lib/index.js
  57. +15 −0 examples/webpack-build-server/package.json
  58. +9 −0 examples/webpack-build-server/support/webpack.config.js
  59. +20 −0 examples/webpack-build/README.md
  60. +13 −0 examples/webpack-build/index.html
  61. +12 −0 examples/webpack-build/lib/index.js
  62. +21 −0 examples/webpack-build/package.json
  63. +2 −0 examples/webpack-build/support/noop.js
  64. +8 −0 examples/webpack-build/support/webpack.config.js
  65. +33 −0 examples/webpack-build/support/webpack.config.json-parser.js
  66. +31 −0 examples/webpack-build/support/webpack.config.slim.js
  67. +17 −0 examples/whiteboard/README.md
  68. +16 −0 examples/whiteboard/index.js
  69. +19 −0 examples/whiteboard/package.json
  70. +23 −0 examples/whiteboard/public/index.html
  71. +106 −0 examples/whiteboard/public/main.js
  72. +44 −0 examples/whiteboard/public/style.css
  73. +0 −69 gulpfile.js
  74. +29 −11 lib/client.js
  75. +101 −15 lib/index.js
  76. +70 −40 lib/namespace.js
  77. +39 −0 lib/parent-namespace.js
  78. +93 −56 lib/socket.js
  79. +3,352 −0 package-lock.json
  80. +12 −20 package.json
  81. +315 −48 test/socket.io.js
9 changes: 8 additions & 1 deletion .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@

*Note*: for support questions, please use one of these channels: [stackoverflow](http://stackoverflow.com/questions/tagged/socket.io) or [slack](https://socketio.slack.com)
**Note**: for support questions, please use one of these channels: [stackoverflow](http://stackoverflow.com/questions/tagged/socket.io) or [slack](https://socketio.slack.com)

For bug reports and feature requests for the **Swift client**, please open an issue [there](https://github.com/socketio/socket.io-client-swift).

For bug reports and feature requests for the **Java client**, please open an issue [there](https://github.com/socketio/socket.io-client-java).

### You want to:

@@ -8,12 +12,15 @@

### Current behaviour

*What is actually happening?*

### Steps to reproduce (if the current behaviour is a bug)

**Note**: the best way (and by that we mean **the only way**) to get a quick answer is to provide a failing test case by forking the following [fiddle](https://github.com/socketio/socket.io-fiddle).

### Expected behaviour

*What is expected?*

### Setup
- OS:
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI

on:
push:
pull_request:

jobs:
test-node:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [10.x, 12.x, 14.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
env:
CI: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -11,3 +11,4 @@ node_modules
coverage
.idea
dist
.nyc_output
28 changes: 0 additions & 28 deletions .travis.yml

This file was deleted.

27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# [2.5.0](https://github.com/socketio/socket.io/compare/2.4.1...2.5.0) (2022-06-26)


### Bug Fixes

* fix race condition in dynamic namespaces ([05e1278](https://github.com/socketio/socket.io/commit/05e1278cfa99f3ecf3f8f0531ffe57d850e9a05b))
* ignore packet received after disconnection ([22d4bdf](https://github.com/socketio/socket.io/commit/22d4bdf00d1a03885dc0171125faddfaef730066))
* only set 'connected' to true after middleware execution ([226cc16](https://github.com/socketio/socket.io/commit/226cc16165f9fe60f16ff4d295fb91c8971cde35))
* prevent the socket from joining a room after disconnection ([f223178](https://github.com/socketio/socket.io/commit/f223178eb655a7713303b21a78f9ef9e161d6458))



## [2.4.1](https://github.com/socketio/socket.io/compare/2.4.0...2.4.1) (2021-01-07)


### Reverts

* fix(security): do not allow all origins by default ([a169050](https://github.com/socketio/socket.io/commit/a1690509470e9dd5559cec4e60908ca6c23e9ba0))


# [2.4.0](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0) (2021-01-04)


### Bug Fixes

* **security:** do not allow all origins by default ([f78a575](https://github.com/socketio/socket.io/commit/f78a575f66ab693c3ea96ea88429ddb1a44c86c7))
* properly overwrite the query sent in the handshake ([d33a619](https://github.com/socketio/socket.io/commit/d33a619905a4905c153d4fec337c74da5b533a9e))
638 changes: 0 additions & 638 deletions History.md

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(The MIT License)

Copyright (c) 2014-2016 Automattic <dev@cloudup.com>
Copyright (c) 2014-2018 Automattic <dev@cloudup.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
8 changes: 0 additions & 8 deletions Makefile

This file was deleted.

574 changes: 164 additions & 410 deletions Readme.md

Large diffs are not rendered by default.

880 changes: 880 additions & 0 deletions docs/API.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

## Table of Contents

#### Getting started

- [Write a chat application](http://socket.io/get-started/chat/)

#### API Reference

- [Server API](API.md)
- [Client API](https://github.com/socketio/socket.io-client/blob/master/docs/API.md)

#### Advanced topics

- [Emit cheatsheet](emit.md)
64 changes: 64 additions & 0 deletions docs/emit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

## Emit cheatsheet

```js

io.on('connect', onConnect);

function onConnect(socket){

// sending to the client
socket.emit('hello', 'can you hear me?', 1, 2, 'abc');

// sending to all clients except sender
socket.broadcast.emit('broadcast', 'hello friends!');

// sending to all clients in 'game' room except sender
socket.to('game').emit('nice game', "let's play a game");

// sending to all clients in 'game1' and/or in 'game2' room, except sender
socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");

// sending to all clients in 'game' room, including sender
io.in('game').emit('big-announcement', 'the game will start soon');

// sending to all clients in namespace 'myNamespace', including sender
io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');

// sending to a specific room in a specific namespace, including sender
io.of('myNamespace').to('room').emit('event', 'message');

// sending to individual socketid (private message)
io.to(<socketid>).emit('hey', 'I just met you');

// sending with acknowledgement
socket.emit('question', 'do you think so?', function (answer) {});

// sending without compression
socket.compress(false).emit('uncompressed', "that's rough");

// sending a message that might be dropped if the client is not ready to receive messages
socket.volatile.emit('maybe', 'do you really need it?');

// specifying whether the data to send has binary data
socket.binary(false).emit('what', 'I have no binaries!');

// sending to all clients on this node (when using multiple nodes)
io.local.emit('hi', 'my lovely babies');

// sending to all connected clients
io.emit('an event sent to all connected clients');

};

```

**Note:** The following events are reserved and should not be used as event names by your application:
- `error`
- `connect`
- `disconnect`
- `disconnecting`
- `newListener`
- `removeListener`
- `ping`
- `pong`
17 changes: 9 additions & 8 deletions examples/chat/index.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
// Setup basic express server
var express = require('express');
var app = express();
var path = require('path');
var server = require('http').createServer(app);
var io = require('../..')(server);
var port = process.env.PORT || 3000;

server.listen(port, function () {
server.listen(port, () => {
console.log('Server listening at port %d', port);
});

// Routing
app.use(express.static(__dirname + '/public'));
app.use(express.static(path.join(__dirname, 'public')));

// Chatroom

var numUsers = 0;

io.on('connection', function (socket) {
io.on('connection', (socket) => {
var addedUser = false;

// when the client emits 'new message', this listens and executes
socket.on('new message', function (data) {
socket.on('new message', (data) => {
// we tell the client to execute 'new message'
socket.broadcast.emit('new message', {
username: socket.username,
@@ -29,7 +30,7 @@ io.on('connection', function (socket) {
});

// when the client emits 'add user', this listens and executes
socket.on('add user', function (username) {
socket.on('add user', (username) => {
if (addedUser) return;

// we store the username in the socket session for this client
@@ -47,21 +48,21 @@ io.on('connection', function (socket) {
});

// when the client emits 'typing', we broadcast it to others
socket.on('typing', function () {
socket.on('typing', () => {
socket.broadcast.emit('typing', {
username: socket.username
});
});

// when the client emits 'stop typing', we broadcast it to others
socket.on('stop typing', function () {
socket.on('stop typing', () => {
socket.broadcast.emit('stop typing', {
username: socket.username
});
});

// when the user disconnects.. perform this
socket.on('disconnect', function () {
socket.on('disconnect', () => {
if (addedUser) {
--numUsers;

54 changes: 27 additions & 27 deletions examples/chat/public/main.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ $(function() {

var socket = io();

function addParticipantsMessage (data) {
const addParticipantsMessage = (data) => {
var message = '';
if (data.numUsers === 1) {
message += "there's 1 participant";
@@ -36,7 +36,7 @@ $(function() {
}

// Sets the client's username
function setUsername () {
const setUsername = () => {
username = cleanInput($usernameInput.val().trim());

// If the username is valid
@@ -52,7 +52,7 @@ $(function() {
}

// Sends a chat message
function sendMessage () {
const sendMessage = () => {
var message = $inputMessage.val();
// Prevent markup from being injected into the message
message = cleanInput(message);
@@ -69,13 +69,13 @@ $(function() {
}

// Log a message
function log (message, options) {
const log = (message, options) => {
var $el = $('<li>').addClass('log').text(message);
addMessageElement($el, options);
}

// Adds the visual chat message to the message list
function addChatMessage (data, options) {
const addChatMessage = (data, options) => {
// Don't fade the message in if there is an 'X was typing'
var $typingMessages = getTypingMessages(data);
options = options || {};
@@ -100,14 +100,14 @@ $(function() {
}

// Adds the visual chat typing message
function addChatTyping (data) {
const addChatTyping = (data) => {
data.typing = true;
data.message = 'is typing';
addChatMessage(data);
}

// Removes the visual chat typing message
function removeChatTyping (data) {
const removeChatTyping = (data) => {
getTypingMessages(data).fadeOut(function () {
$(this).remove();
});
@@ -118,7 +118,7 @@ $(function() {
// 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) {
const addMessageElement = (el, options) => {
var $el = $(el);

// Setup default options
@@ -145,20 +145,20 @@ $(function() {
}

// Prevents input from having injected markup
function cleanInput (input) {
return $('<div/>').text(input).text();
const cleanInput = (input) => {
return $('<div/>').text(input).html();
}

// Updates the typing event
function updateTyping () {
const updateTyping = () => {
if (connected) {
if (!typing) {
typing = true;
socket.emit('typing');
}
lastTypingTime = (new Date()).getTime();

setTimeout(function () {
setTimeout(() => {
var typingTimer = (new Date()).getTime();
var timeDiff = typingTimer - lastTypingTime;
if (timeDiff >= TYPING_TIMER_LENGTH && typing) {
@@ -170,14 +170,14 @@ $(function() {
}

// Gets the 'X is typing' messages of a user
function getTypingMessages (data) {
const 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) {
const getUsernameColor = (username) => {
// Compute hash code
var hash = 7;
for (var i = 0; i < username.length; i++) {
@@ -190,7 +190,7 @@ $(function() {

// Keyboard events

$window.keydown(function (event) {
$window.keydown(event => {
// Auto-focus the current input when a key is typed
if (!(event.ctrlKey || event.metaKey || event.altKey)) {
$currentInput.focus();
@@ -207,26 +207,26 @@ $(function() {
}
});

$inputMessage.on('input', function() {
$inputMessage.on('input', () => {
updateTyping();
});

// Click events

// Focus input when clicking anywhere on login page
$loginPage.click(function () {
$loginPage.click(() => {
$currentInput.focus();
});

// Focus input when clicking on the message input's border
$inputMessage.click(function () {
$inputMessage.click(() => {
$inputMessage.focus();
});

// Socket events

// Whenever the server emits 'login', log the login message
socket.on('login', function (data) {
socket.on('login', (data) => {
connected = true;
// Display the welcome message
var message = "Welcome to Socket.IO Chat – ";
@@ -237,45 +237,45 @@ $(function() {
});

// Whenever the server emits 'new message', update the chat body
socket.on('new message', function (data) {
socket.on('new message', (data) => {
addChatMessage(data);
});

// Whenever the server emits 'user joined', log it in the chat body
socket.on('user joined', function (data) {
socket.on('user joined', (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) {
socket.on('user left', (data) => {
log(data.username + ' left');
addParticipantsMessage(data);
removeChatTyping(data);
});

// Whenever the server emits 'typing', show the typing message
socket.on('typing', function (data) {
socket.on('typing', (data) => {
addChatTyping(data);
});

// Whenever the server emits 'stop typing', kill the typing message
socket.on('stop typing', function (data) {
socket.on('stop typing', (data) => {
removeChatTyping(data);
});

socket.on('disconnect', function () {
socket.on('disconnect', () => {
log('you have been disconnected');
});

socket.on('reconnect', function () {
socket.on('reconnect', () => {
log('you have been reconnected');
if (username) {
socket.emit('add user', username);
}
});

socket.on('reconnect_error', function () {
socket.on('reconnect_error', () => {
log('attempt to reconnect has failed');
});

31 changes: 31 additions & 0 deletions examples/cluster-haproxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

# Socket.IO Chat with haproxy & 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 haproxy instance which will loadbalance the requests (using a cookie for sticky sessions, see [cookie](https://cbonte.github.io/haproxy-dconv/1.7/configuration.html#4.2-cookie)).

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.
51 changes: 51 additions & 0 deletions examples/cluster-haproxy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

haproxy:
build: ./haproxy
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"
2 changes: 2 additions & 0 deletions examples/cluster-haproxy/haproxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM haproxy:1.7-alpine
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
31 changes: 31 additions & 0 deletions examples/cluster-haproxy/haproxy/haproxy.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Reference: http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/

global
daemon
maxconn 4096
nbproc 2

defaults
mode http
balance roundrobin
option http-server-close
timeout connect 5s
timeout client 30s
timeout client-fin 30s
timeout server 30s
timeout tunnel 1h
default-server inter 1s rise 2 fall 1 on-marked-down shutdown-sessions
option forwardfor

listen chat
bind *:80
default_backend nodes

backend nodes
option httpchk HEAD /health
http-check expect status 200
cookie serverid insert
server john server-john:3000 cookie john check
server paul server-paul:3000 cookie paul check
server george server-george:3000 cookie george check
server ringo server-ringo:3000 cookie ringo check
15 changes: 15 additions & 0 deletions examples/cluster-haproxy/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -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" ]
87 changes: 87 additions & 0 deletions examples/cluster-haproxy/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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'));

// Health check
app.head('/health', function (req, res) {
res.sendStatus(200);
});

// 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
});
}
});
});
17 changes: 17 additions & 0 deletions examples/cluster-haproxy/server/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
28 changes: 28 additions & 0 deletions examples/cluster-haproxy/server/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO Chat Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="pages">
<li class="chat page">
<div class="chatArea">
<ul class="messages"></ul>
</div>
<input class="inputMessage" placeholder="Type here..."/>
</li>
<li class="login page">
<div class="form">
<h3 class="title">What's your nickname?</h3>
<input class="usernameInput" type="text" maxlength="14" />
</div>
</li>
</ul>

<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/main.js"></script>
</body>
</html>
286 changes: 286 additions & 0 deletions examples/cluster-haproxy/server/public/main.js
Original file line number Diff line number Diff line change
@@ -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 = $('<li>').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 = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);

var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message"/>')
.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 $('<div/>').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);
})

});
149 changes: 149 additions & 0 deletions examples/cluster-haproxy/server/public/style.css
Original file line number Diff line number Diff line change
@@ -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%;
}
31 changes: 31 additions & 0 deletions examples/cluster-httpd/README.md
Original file line number Diff line number Diff line change
@@ -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.
51 changes: 51 additions & 0 deletions examples/cluster-httpd/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions examples/cluster-httpd/httpd/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FROM httpd:2.4-alpine
COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf
52 changes: 52 additions & 0 deletions examples/cluster-httpd/httpd/httpd.conf
Original file line number Diff line number Diff line change
@@ -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

<Proxy "balancer://nodes_polling">
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
</Proxy>

<Proxy "balancer://nodes_ws">
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
</Proxy>

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
15 changes: 15 additions & 0 deletions examples/cluster-httpd/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -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" ]
82 changes: 82 additions & 0 deletions examples/cluster-httpd/server/index.js
Original file line number Diff line number Diff line change
@@ -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
});
}
});
});
17 changes: 17 additions & 0 deletions examples/cluster-httpd/server/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
28 changes: 28 additions & 0 deletions examples/cluster-httpd/server/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO Chat Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="pages">
<li class="chat page">
<div class="chatArea">
<ul class="messages"></ul>
</div>
<input class="inputMessage" placeholder="Type here..."/>
</li>
<li class="login page">
<div class="form">
<h3 class="title">What's your nickname?</h3>
<input class="usernameInput" type="text" maxlength="14" />
</div>
</li>
</ul>

<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/main.js"></script>
</body>
</html>
286 changes: 286 additions & 0 deletions examples/cluster-httpd/server/public/main.js
Original file line number Diff line number Diff line change
@@ -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 = $('<li>').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 = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);

var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message"/>')
.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 $('<div/>').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);
})

});
149 changes: 149 additions & 0 deletions examples/cluster-httpd/server/public/style.css
Original file line number Diff line number Diff line change
@@ -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%;
}
31 changes: 31 additions & 0 deletions examples/cluster-nginx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

# Socket.IO Chat with nginx & 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 nginx proxy which will loadbalance the requests (using the IP of the client, see [ip_hash](http://nginx.org/en/docs/http/ngx_http_upstream_module.html#ip_hash)).

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.
51 changes: 51 additions & 0 deletions examples/cluster-nginx/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

nginx:
build: ./nginx
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"
3 changes: 3 additions & 0 deletions examples/cluster-nginx/nginx/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
35 changes: 35 additions & 0 deletions examples/cluster-nginx/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Reference: https://www.nginx.com/resources/wiki/start/topics/examples/full/

worker_processes 4;

events {
worker_connections 1024;
}

http {
server {
listen 80;

location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;

proxy_pass http://nodes;

# enable WebSockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

upstream nodes {
# enable sticky session
ip_hash;

server server-john:3000;
server server-paul:3000;
server server-george:3000;
server server-ringo:3000;
}
}
15 changes: 15 additions & 0 deletions examples/cluster-nginx/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -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" ]
82 changes: 82 additions & 0 deletions examples/cluster-nginx/server/index.js
Original file line number Diff line number Diff line change
@@ -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
});
}
});
});
17 changes: 17 additions & 0 deletions examples/cluster-nginx/server/package.json
Original file line number Diff line number Diff line change
@@ -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": "MIT",
"dependencies": {
"express": "4.13.4",
"socket.io": "^1.7.2",
"socket.io-redis": "^3.0.0"
},
"scripts": {
"start": "node index.js"
}
}
28 changes: 28 additions & 0 deletions examples/cluster-nginx/server/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO Chat Example</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul class="pages">
<li class="chat page">
<div class="chatArea">
<ul class="messages"></ul>
</div>
<input class="inputMessage" placeholder="Type here..."/>
</li>
<li class="login page">
<div class="form">
<h3 class="title">What's your nickname?</h3>
<input class="usernameInput" type="text" maxlength="14" />
</div>
</li>
</ul>

<script src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="/main.js"></script>
</body>
</html>
286 changes: 286 additions & 0 deletions examples/cluster-nginx/server/public/main.js
Original file line number Diff line number Diff line change
@@ -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 = $('<li>').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 = $('<span class="username"/>')
.text(data.username)
.css('color', getUsernameColor(data.username));
var $messageBodyDiv = $('<span class="messageBody">')
.text(data.message);

var typingClass = data.typing ? 'typing' : '';
var $messageDiv = $('<li class="message"/>')
.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 $('<div/>').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);
})

});
149 changes: 149 additions & 0 deletions examples/cluster-nginx/server/public/style.css
Original file line number Diff line number Diff line change
@@ -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%;
}
50 changes: 50 additions & 0 deletions examples/custom-parsers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

# Socket.IO custom parsers

Since Socket.IO version 2.0.0, you can provide your custom parser, according to the needs of your application.

Several parsers are showcased here:

- the default one: [socket.io-parser](https://github.com/socketio/socket.io-parser)
- one based on msgpack: [socket.io-msgpack-parser](https://github.com/darrachequesne/socket.io-msgpack-parser)
- one based on native JSON: [socket.io-json-parser](https://github.com/darrachequesne/socket.io-json-parser)
- a custom one based on [schemapack](https://github.com/phretaddin/schemapack)

They are tested with various payloads:

- string: `['1', '2', ... '1000']`
- numeric: `[1, 2, ... 1000]`
- binary: `Buffer.allocUnsafe(1000), where buf[i] = i`

## How to use

```
$ npm i && npm start
```

## Results

| bytes / packet | CONNECT packet | string | numeric | binary |
|----------------|----------------|--------|---------|-----------|
| default | 1 | 5903 | 3904 | 43 + 1000 |
| msgpack | 20 | 3919 | 2646 | 1029 |
| JSON | 20 | 5930 | 3931 | 3625 |
| schemapack | 20 | 3895 | 2005 | 1005 |

## Comparison

`default parser`
- supports any serializable datastructure, including Blob and File
- **but** binary payload is encoded as 2 packets

`msgpack`
- the size of payloads containing mostly numeric values will be greatly reduced
- **but** rely on [ArrayBuffer](https://caniuse.com/#feat=typedarrays) in the browser (IE > 9)

`JSON`
- optimized
- **but** does not support binary payloads

`schemapack`
- the most efficient in both speed and size
- **but** you have to provide a schema for each packet
21 changes: 21 additions & 0 deletions examples/custom-parsers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "parsers",
"version": "1.0.0",
"description": "Various socket.io parsers",
"scripts": {
"build": "webpack --config ./support/webpack.config.js",
"start": "npm run build && node ./src/server.js"
},
"author": "Damien Arrachequesne",
"license": "MIT",
"dependencies": {
"component-emitter": "^1.2.1",
"express": "^4.15.2",
"schemapack": "^1.4.2",
"socket.io": "socketio/socket.io",
"socket.io-client": "socketio/socket.io-client",
"socket.io-json-parser": "^1.0.0",
"socket.io-msgpack-parser": "^1.0.0",
"webpack": "^2.4.1"
}
}
13 changes: 13 additions & 0 deletions examples/custom-parsers/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO custom parsers</title>
</head>
<body>
<script src="client1.bundle.js"></script>
<script src="client2.bundle.js"></script>
<script src="client3.bundle.js"></script>
<script src="client4.bundle.js"></script>
</body>
</html>
8 changes: 8 additions & 0 deletions examples/custom-parsers/src/client1.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

const socket = require('socket.io-client')('localhost:3001', {});

socket.io.engine.on('data', (data) => console.log('[default]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength)));

socket.on('string', (data) => console.log('[default] [string]', data));
socket.on('numeric', (data) => console.log('[default] [numeric]', data));
socket.on('binary', (data) => console.log('[default] [binary]', data));
11 changes: 11 additions & 0 deletions examples/custom-parsers/src/client2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

const customParser = require('socket.io-msgpack-parser');
const socket = require('socket.io-client')('http://localhost:3002', {
parser: customParser
});

socket.io.engine.on('data', (data) => console.log('[msgpack]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength)));

socket.on('string', (data) => console.log('[msgpack] [string]', data));
socket.on('numeric', (data) => console.log('[msgpack] [numeric]', data));
socket.on('binary', (data) => console.log('[msgpack] [binary]', data));
11 changes: 11 additions & 0 deletions examples/custom-parsers/src/client3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

const customParser = require('socket.io-json-parser');
const socket = require('socket.io-client')('localhost:3003', {
parser: customParser
});

socket.io.engine.on('data', (data) => console.log('[json]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength)));

socket.on('string', (data) => console.log('[json] [string]', data));
socket.on('numeric', (data) => console.log('[json] [numeric]', data));
socket.on('binary', (data) => console.log('[json] [binary]', data));
11 changes: 11 additions & 0 deletions examples/custom-parsers/src/client4.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

const customParser = require('./custom-parser');
const socket = require('socket.io-client')('localhost:3004', {
parser: customParser
});

socket.io.engine.on('data', (data) => console.log('[custom]' + ' size= ' + (typeof data === 'string' ? data.length : data.byteLength)));

socket.on('string', (data) => console.log('[custom] [string]', data));
socket.on('numeric', (data) => console.log('[custom] [numeric]', data));
socket.on('binary', (data) => console.log('[custom] [binary]', data));
125 changes: 125 additions & 0 deletions examples/custom-parsers/src/custom-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@

const Emitter = require('component-emitter');
const schemapack = require('schemapack');

/**
* Packet types (see https://github.com/socketio/socket.io-protocol)
*/

const TYPES = {
CONNECT: 0,
DISCONNECT: 1,
EVENT: 2,
ACK: 3,
ERROR: 4,
BINARY_EVENT: 5,
BINARY_ACK: 6
};

const stringSchema = schemapack.build({
_id: 'uint8',
data: [ 'string' ],
nsp: 'string'
});

const numericSchema = schemapack.build({
_id: 'uint8',
data: [ 'uint16' ],
nsp: 'string'
});

const binarySchema = schemapack.build({
_id: 'uint8',
data: 'buffer',
nsp: 'string'
});

const errorPacket = {
type: TYPES.ERROR,
data: 'parser error'
};

class Encoder {
encode (packet, callback) {
switch (packet.type) {
case TYPES.EVENT:
return callback([ this.pack(packet) ]);
default:
return callback([ JSON.stringify(packet) ]);
}
}
pack (packet) {
let eventName = packet.data[0];
let flatPacket = {
data: packet.data[1],
nsp: packet.nsp
};
switch (eventName) {
case 'string':
flatPacket._id = 1;
return stringSchema.encode(flatPacket);
case 'numeric':
flatPacket._id = 2;
return numericSchema.encode(flatPacket);
case 'binary':
flatPacket._id = 3;
return binarySchema.encode(flatPacket);
default:
throw new Error('unknown event name: ' + eventName);
}
}
}

class Decoder extends Emitter {
add (obj) {
if (typeof obj === 'string') {
this.parseJSON(obj);
} else {
this.parseBinary(obj);
}
}
parseJSON (obj) {
try {
let decoded = JSON.parse(obj);
this.emit('decoded', decoded);
} catch (e) {
this.emit('decoded', errorPacket);
}
}
parseBinary (obj) {
let view = new Uint8Array(obj);
let packetId = view[0];
try {
let packet = {
type: TYPES.EVENT
};
let decoded;
switch (packetId) {
case 1:
decoded = stringSchema.decode(obj);
packet.data = [ 'string', decoded.data ];
packet.nsp = decoded.nsp;
break;
case 2:
decoded = numericSchema.decode(obj);
packet.data = [ 'numeric', decoded.data ];
packet.nsp = decoded.nsp;
break;
case 3:
decoded = binarySchema.decode(obj);
packet.data = [ 'binary', decoded.data.buffer ];
packet.nsp = decoded.nsp;
break;
default:
throw new Error('unknown type');
}
this.emit('decoded', packet);
} catch (e) {
this.emit('decoded', errorPacket);
}
}
destroy () {}
}

exports.Encoder = Encoder;
exports.Decoder = Decoder;
55 changes: 55 additions & 0 deletions examples/custom-parsers/src/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@

const express = require('express');
const app = express();
const server = require('http').createServer(app);
const path = require('path');
const port = process.env.PORT || 3000;

app.use(express.static(path.join(__dirname, '../public')));

server.listen(port, () => console.log('>>> http://localhost:' + port));

const io = require('socket.io');
const msgpackParser = require('socket.io-msgpack-parser');
const jsonParser = require('socket.io-json-parser');
const customParser = require('./custom-parser');

let server1 = io(3001, {});
let server2 = io(3002, {
parser: msgpackParser
});
let server3 = io(3003, {
parser: jsonParser
});
let server4 = io(3004, {
parser: customParser
});

let string = [];
let numeric = [];
let binary = Buffer.allocUnsafe(1e3);
for (var i = 0; i < 1e3; i++) {
string.push('' + i);
numeric.push(i);
binary[i] = i;
}

server1.on('connect', onConnect(1000));
server2.on('connect', onConnect(2000));
server3.on('connect', onConnect(3000));
server4.on('connect', onConnect(4000));

function onConnect (delay) {
return function (socket) {
console.log('connect ' + socket.id);

setTimeout(() => {
socket.emit('string', string);
socket.emit('numeric', numeric);
socket.emit('binary', binary);
}, delay);

socket.on('disconnect', () => console.log('disconnect ' + socket.id));
};
}

15 changes: 15 additions & 0 deletions examples/custom-parsers/support/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

const path = require('path');

module.exports = {
entry: {
client1: './src/client1.js',
client2: './src/client2.js',
client3: './src/client3.js',
client4: './src/client4.js'
},
output: {
path: path.resolve(__dirname, '../public'),
filename: '[name].bundle.js'
}
};
18 changes: 18 additions & 0 deletions examples/webpack-build-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

# Socket.IO WebPack build

A sample Webpack build for the server.

## How to use

```
$ npm i
$ npm run build
$ npm start
```

**Note:**

- the `bufferutil` and `utf-8-validate` are optional dependencies from `ws`, compiled from native code, which are meant to improve performance ([ref](https://github.com/websockets/ws#opt-in-for-performance)). You can also omit them, as they have their JS fallback, and ignore the WebPack warning.

- the server is initiated with `serveClient` set to `false`, so it will not serve the client file.
16 changes: 16 additions & 0 deletions examples/webpack-build-server/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

const server = require('http').createServer();
const io = require('socket.io')(server, {
serveClient: false,
wsEngine: 'ws' // uws is not supported since it is a native module
});
const port = process.env.PORT || 3000;

io.on('connect', onConnect);
server.listen(port, () => console.log('server listening on port ' + port));

function onConnect(socket){
console.log('connect ' + socket.id);

socket.on('disconnect', () => console.log('disconnect ' + socket.id));
}
15 changes: 15 additions & 0 deletions examples/webpack-build-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "webpack-build-server",
"version": "1.0.0",
"description": "A sample Webpack build (for the server)",
"scripts": {
"start": "node dist/server.js",
"build": "webpack --config ./support/webpack.config.js"
},
"author": "Damien Arrachequesne",
"license": "MIT",
"devDependencies": {
"socket.io": "^2.0.3",
"webpack": "^2.6.1"
}
}
9 changes: 9 additions & 0 deletions examples/webpack-build-server/support/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

module.exports = {
entry: './lib/index.js',
target: 'node',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'server.js'
}
};
20 changes: 20 additions & 0 deletions examples/webpack-build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

# Socket.IO WebPack build

A sample Webpack build for the browser.

## How to use

```
$ npm i
$ npm run build-all
```

There are two WebPack configuration:

- the minimal configuration, just bundling the application and its dependencies. The `app.js` file in the `dist` folder is the result of that build.

- a slimmer one, where:
- the JSON polyfill needed for IE6/IE7 support has been removed.
- the `debug` calls and import have been removed (the [debug](https://github.com/visionmedia/debug) library is included in the build by default).
- the source has been uglified (dropping IE8 support), and an associated SourceMap has been generated.
13 changes: 13 additions & 0 deletions examples/webpack-build/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO WebPack Example</title>
</head>
<body>

<!-- <script src="dist/app.js"></script> -->
<script src="dist/app.slim.js"></script>

</body>
</html>
12 changes: 12 additions & 0 deletions examples/webpack-build/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

import io from 'socket.io-client';

const socket = io('http://localhost:3000');

console.log('init');

socket.on('connect', onConnect);

function onConnect(){
console.log('connect ' + socket.id);
}
21 changes: 21 additions & 0 deletions examples/webpack-build/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "webpack-build",
"version": "1.0.0",
"description": "A sample Webpack build",
"scripts": {
"build": "webpack --config ./support/webpack.config.js",
"build-slim": "webpack --config ./support/webpack.config.slim.js",
"build-json-parser": "webpack --config ./support/webpack.config.json-parser.js",
"build-all": "npm run build && npm run build-slim && npm run build-json-parser"
},
"author": "Damien Arrachequesne",
"license": "MIT",
"dependencies": {
"socket.io-client": "^2.0.2",
"socket.io-json-parser": "^2.1.0"
},
"devDependencies": {
"strip-loader": "^0.1.2",
"webpack": "^2.6.1"
}
}
2 changes: 2 additions & 0 deletions examples/webpack-build/support/noop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

module.exports = function () { return function () {}; };
8 changes: 8 additions & 0 deletions examples/webpack-build/support/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.js'
}
};
Loading