├── .gitignore ├── README.md ├── assets └── example.gif ├── load-test ├── advanced-test.yaml ├── custom.js └── simple-test.yaml ├── package-lock.json ├── package.json ├── public ├── index.html ├── main.js └── style.css └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## socketio-loadtesting 🚀 2 | 3 | This repository contains the tutorial files which companion my blog post on how to loadtest socket.io WebSocket applications. A live version of this application can be previewed at https://socketio-loadtest.herokuapp.com, and artillery.io loadtesting configuration files are found in the `load-test` directory. Usage instructions are in my blog post, [https://medium.com/@kyle_martin/load-testing-socket-io-web-applications-and-infrastructure-3e96011898e0](https://medium.com/@kyle_martin/load-testing-socket-io-web-applications-and-infrastructure-3e96011898e0) 4 | 5 | To run the application: 6 | 7 | ``` 8 | $ npm install 9 | $ npm start 10 | ``` 11 | 12 | The example socket.io application will be available on the configured `PORT`, otherwise it will default to port `8080`. 13 | 14 | ![loadtesting example](assets/example.gif "Loadtest example") 15 | 16 | -------------------------------------------------------------------------------- /assets/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/js-kyle/socketio-loadtesting/c2ea883ea150a51be39f58a99251e2343144d679/assets/example.gif -------------------------------------------------------------------------------- /load-test/advanced-test.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "ws://localhost:8080" 3 | ensure: 4 | max: 500 # fail if max response time exceeds 500ms 5 | maxErrorRate: 1 # fail if error rate exceeds 1% 6 | socketio: 7 | transports: ["websocket"] 8 | processor: "./custom.js" # set a custom processor to load dynamic scenario data 9 | phases: 10 | - duration: 120 11 | arrivalRate: 10 12 | rampTo: 50 13 | name: "Warm up the application" 14 | - duration: 600 15 | arrivalRate: 50 16 | name: "Sustained max load" 17 | scenarios: 18 | - engine: "socketio" 19 | flow: 20 | - function: "getChatData" # load variables 21 | - emit: 22 | channel: "add user" 23 | data: "{{ name }}" 24 | - emit: 25 | channel: "new message" 26 | data: "{{ greeting }}" 27 | - think: 10 # stay connected for 10 seconds 28 | - emit: 29 | channel: "new message" 30 | data: "{{ goodbye }}" 31 | -------------------------------------------------------------------------------- /load-test/custom.js: -------------------------------------------------------------------------------- 1 | const {faker} = require('@faker-js/faker'); 2 | 3 | module.exports.getChatData = (userContext, events, done) => { 4 | userContext.vars.name = faker.name.findName(); 5 | userContext.vars.greeting = `Hello! I'm from ${faker.address.city()}`; 6 | userContext.vars.goodbye = `Goodbye, I will be back on ${faker.date.weekday()}`; 7 | done(); 8 | }; 9 | -------------------------------------------------------------------------------- /load-test/simple-test.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | target: "wss://socketio-loadtest.herokuapp.com" 3 | socketio: 4 | transports: ["websocket"] 5 | phases: 6 | - duration: 10 # Run scenario for 10 seconds 7 | arrivalCount: 20 # Create 20 virtual users per scenario 8 | scenarios: 9 | - engine: "socketio" 10 | flow: 11 | - emit: 12 | channel: "add user" 13 | data: "John Doe" 14 | - emit: 15 | channel: "new message" 16 | data: "Hello! {{ $randomString() }}" 17 | - think: 5 # do nothing for 5 seconds, then disconnect -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-loadtesting", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "express": { 8 | "version": "4.13.4", 9 | "resolved": "https://registry.npmjs.org/express/-/express-4.13.4.tgz", 10 | "integrity": "sha1-PAt288d1kMg0VzkGHsC9O6Bn7CQ=", 11 | "requires": { 12 | "accepts": "~1.2.12", 13 | "array-flatten": "1.1.1", 14 | "content-disposition": "0.5.1", 15 | "content-type": "~1.0.1", 16 | "cookie": "0.1.5", 17 | "cookie-signature": "1.0.6", 18 | "debug": "~2.2.0", 19 | "depd": "~1.1.0", 20 | "escape-html": "~1.0.3", 21 | "etag": "~1.7.0", 22 | "finalhandler": "0.4.1", 23 | "fresh": "0.3.0", 24 | "merge-descriptors": "1.0.1", 25 | "methods": "~1.1.2", 26 | "on-finished": "~2.3.0", 27 | "parseurl": "~1.3.1", 28 | "path-to-regexp": "0.1.7", 29 | "proxy-addr": "~1.0.10", 30 | "qs": "4.0.0", 31 | "range-parser": "~1.0.3", 32 | "send": "0.13.1", 33 | "serve-static": "~1.10.2", 34 | "type-is": "~1.6.6", 35 | "utils-merge": "1.0.0", 36 | "vary": "~1.0.1" 37 | }, 38 | "dependencies": { 39 | "accepts": { 40 | "version": "1.2.13", 41 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", 42 | "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", 43 | "requires": { 44 | "mime-types": "~2.1.6", 45 | "negotiator": "0.5.3" 46 | }, 47 | "dependencies": { 48 | "mime-types": { 49 | "version": "2.1.24", 50 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 51 | "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", 52 | "requires": { 53 | "mime-db": "1.40.0" 54 | }, 55 | "dependencies": { 56 | "mime-db": { 57 | "version": "1.40.0", 58 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 59 | "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" 60 | } 61 | } 62 | }, 63 | "negotiator": { 64 | "version": "0.5.3", 65 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", 66 | "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=" 67 | } 68 | } 69 | }, 70 | "array-flatten": { 71 | "version": "1.1.1", 72 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 73 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 74 | }, 75 | "content-disposition": { 76 | "version": "0.5.1", 77 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.1.tgz", 78 | "integrity": "sha1-h0dsamfI2qh+Muh2Ft+IO6f7Bxs=" 79 | }, 80 | "content-type": { 81 | "version": "1.0.4", 82 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 83 | "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=" 84 | }, 85 | "cookie": { 86 | "version": "0.1.5", 87 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.5.tgz", 88 | "integrity": "sha1-armUiksa4hlSzSWIUwpHItQETXw=" 89 | }, 90 | "cookie-signature": { 91 | "version": "1.0.6", 92 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 93 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 94 | }, 95 | "debug": { 96 | "version": "2.2.0", 97 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 98 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 99 | "requires": { 100 | "ms": "0.7.1" 101 | }, 102 | "dependencies": { 103 | "ms": { 104 | "version": "0.7.1", 105 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 106 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" 107 | } 108 | } 109 | }, 110 | "depd": { 111 | "version": "1.1.2", 112 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 113 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 114 | }, 115 | "escape-html": { 116 | "version": "1.0.3", 117 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 118 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 119 | }, 120 | "etag": { 121 | "version": "1.7.0", 122 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz", 123 | "integrity": "sha1-A9MLX2fdbmMtKUXTDWZScxo01dg=" 124 | }, 125 | "finalhandler": { 126 | "version": "0.4.1", 127 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz", 128 | "integrity": "sha1-haF8bFmpRxfSYtYSMNSw6+PUoU0=", 129 | "requires": { 130 | "debug": "~2.2.0", 131 | "escape-html": "~1.0.3", 132 | "on-finished": "~2.3.0", 133 | "unpipe": "~1.0.0" 134 | }, 135 | "dependencies": { 136 | "unpipe": { 137 | "version": "1.0.0", 138 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 139 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 140 | } 141 | } 142 | }, 143 | "fresh": { 144 | "version": "0.3.0", 145 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz", 146 | "integrity": "sha1-ZR+DjiJCTnVm3hYdg1jKoZn4PU8=" 147 | }, 148 | "merge-descriptors": { 149 | "version": "1.0.1", 150 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 151 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 152 | }, 153 | "methods": { 154 | "version": "1.1.2", 155 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 156 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 157 | }, 158 | "on-finished": { 159 | "version": "2.3.0", 160 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 161 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 162 | "requires": { 163 | "ee-first": "1.1.1" 164 | }, 165 | "dependencies": { 166 | "ee-first": { 167 | "version": "1.1.1", 168 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 169 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 170 | } 171 | } 172 | }, 173 | "parseurl": { 174 | "version": "1.3.3", 175 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 176 | "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=" 177 | }, 178 | "path-to-regexp": { 179 | "version": "0.1.7", 180 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 181 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 182 | }, 183 | "proxy-addr": { 184 | "version": "1.0.10", 185 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", 186 | "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", 187 | "requires": { 188 | "forwarded": "~0.1.0", 189 | "ipaddr.js": "1.0.5" 190 | }, 191 | "dependencies": { 192 | "forwarded": { 193 | "version": "0.1.2", 194 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 195 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 196 | }, 197 | "ipaddr.js": { 198 | "version": "1.0.5", 199 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", 200 | "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" 201 | } 202 | } 203 | }, 204 | "qs": { 205 | "version": "4.0.0", 206 | "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", 207 | "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=" 208 | }, 209 | "range-parser": { 210 | "version": "1.0.3", 211 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", 212 | "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" 213 | }, 214 | "send": { 215 | "version": "0.13.1", 216 | "resolved": "https://registry.npmjs.org/send/-/send-0.13.1.tgz", 217 | "integrity": "sha1-ow1fTILIqbrprQCh2bG9vm8Zntc=", 218 | "requires": { 219 | "debug": "~2.2.0", 220 | "depd": "~1.1.0", 221 | "destroy": "~1.0.4", 222 | "escape-html": "~1.0.3", 223 | "etag": "~1.7.0", 224 | "fresh": "0.3.0", 225 | "http-errors": "~1.3.1", 226 | "mime": "1.3.4", 227 | "ms": "0.7.1", 228 | "on-finished": "~2.3.0", 229 | "range-parser": "~1.0.3", 230 | "statuses": "~1.2.1" 231 | }, 232 | "dependencies": { 233 | "destroy": { 234 | "version": "1.0.4", 235 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 236 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 237 | }, 238 | "http-errors": { 239 | "version": "1.3.1", 240 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", 241 | "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", 242 | "requires": { 243 | "inherits": "~2.0.1", 244 | "statuses": "1" 245 | }, 246 | "dependencies": { 247 | "inherits": { 248 | "version": "2.0.4", 249 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 250 | "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" 251 | } 252 | } 253 | }, 254 | "mime": { 255 | "version": "1.3.4", 256 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", 257 | "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" 258 | }, 259 | "ms": { 260 | "version": "0.7.1", 261 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 262 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" 263 | }, 264 | "statuses": { 265 | "version": "1.2.1", 266 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", 267 | "integrity": "sha1-3e1FzBglbVHtQK7BQkidXGECbSg=" 268 | } 269 | } 270 | }, 271 | "serve-static": { 272 | "version": "1.10.3", 273 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz", 274 | "integrity": "sha1-zlpuzTEB/tXsCYJ9rCKpwpv7BTU=", 275 | "requires": { 276 | "escape-html": "~1.0.3", 277 | "parseurl": "~1.3.1", 278 | "send": "0.13.2" 279 | }, 280 | "dependencies": { 281 | "send": { 282 | "version": "0.13.2", 283 | "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz", 284 | "integrity": "sha1-dl52B8gFVFK7pvCwUllTUJhgNt4=", 285 | "requires": { 286 | "debug": "~2.2.0", 287 | "depd": "~1.1.0", 288 | "destroy": "~1.0.4", 289 | "escape-html": "~1.0.3", 290 | "etag": "~1.7.0", 291 | "fresh": "0.3.0", 292 | "http-errors": "~1.3.1", 293 | "mime": "1.3.4", 294 | "ms": "0.7.1", 295 | "on-finished": "~2.3.0", 296 | "range-parser": "~1.0.3", 297 | "statuses": "~1.2.1" 298 | }, 299 | "dependencies": { 300 | "destroy": { 301 | "version": "1.0.4", 302 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 303 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 304 | }, 305 | "http-errors": { 306 | "version": "1.3.1", 307 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", 308 | "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", 309 | "requires": { 310 | "inherits": "~2.0.1", 311 | "statuses": "1" 312 | }, 313 | "dependencies": { 314 | "inherits": { 315 | "version": "2.0.4", 316 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 317 | "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=" 318 | } 319 | } 320 | }, 321 | "mime": { 322 | "version": "1.3.4", 323 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", 324 | "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" 325 | }, 326 | "ms": { 327 | "version": "0.7.1", 328 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 329 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" 330 | }, 331 | "statuses": { 332 | "version": "1.2.1", 333 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", 334 | "integrity": "sha1-3e1FzBglbVHtQK7BQkidXGECbSg=" 335 | } 336 | } 337 | } 338 | } 339 | }, 340 | "type-is": { 341 | "version": "1.6.18", 342 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 343 | "integrity": "sha1-TlUs0F3wlGfcvE73Od6J8s83wTE=", 344 | "requires": { 345 | "media-typer": "0.3.0", 346 | "mime-types": "~2.1.24" 347 | }, 348 | "dependencies": { 349 | "media-typer": { 350 | "version": "0.3.0", 351 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 352 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 353 | }, 354 | "mime-types": { 355 | "version": "2.1.24", 356 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 357 | "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", 358 | "requires": { 359 | "mime-db": "1.40.0" 360 | }, 361 | "dependencies": { 362 | "mime-db": { 363 | "version": "1.40.0", 364 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 365 | "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" 366 | } 367 | } 368 | } 369 | } 370 | }, 371 | "utils-merge": { 372 | "version": "1.0.0", 373 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", 374 | "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" 375 | }, 376 | "vary": { 377 | "version": "1.0.1", 378 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", 379 | "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" 380 | } 381 | } 382 | }, 383 | "faker": { 384 | "version": "4.1.0", 385 | "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", 386 | "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", 387 | "dev": true 388 | }, 389 | "socket.io": { 390 | "version": "2.2.0", 391 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.2.0.tgz", 392 | "integrity": "sha1-8PYzFh72cSyXKzB1mOzQjJsbTVs=", 393 | "requires": { 394 | "debug": "~4.1.0", 395 | "engine.io": "~3.3.1", 396 | "has-binary2": "~1.0.2", 397 | "socket.io-adapter": "~1.1.0", 398 | "socket.io-client": "2.2.0", 399 | "socket.io-parser": "~3.3.0" 400 | }, 401 | "dependencies": { 402 | "debug": { 403 | "version": "4.1.1", 404 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 405 | "integrity": "sha1-O3ImAlUQnGtYnO4FDx1RYTlmR5E=", 406 | "requires": { 407 | "ms": "^2.1.1" 408 | }, 409 | "dependencies": { 410 | "ms": { 411 | "version": "2.1.2", 412 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 413 | "integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk=" 414 | } 415 | } 416 | }, 417 | "engine.io": { 418 | "version": "3.3.2", 419 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.3.2.tgz", 420 | "integrity": "sha1-GMvItvNulGHFwPgd8rgw3hYFilk=", 421 | "requires": { 422 | "accepts": "~1.3.4", 423 | "base64id": "1.0.0", 424 | "cookie": "0.3.1", 425 | "debug": "~3.1.0", 426 | "engine.io-parser": "~2.1.0", 427 | "ws": "~6.1.0" 428 | }, 429 | "dependencies": { 430 | "accepts": { 431 | "version": "1.3.7", 432 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 433 | "integrity": "sha1-UxvHJlF6OytB+FACHGzBXqq1B80=", 434 | "requires": { 435 | "mime-types": "~2.1.24", 436 | "negotiator": "0.6.2" 437 | }, 438 | "dependencies": { 439 | "mime-types": { 440 | "version": "2.1.24", 441 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", 442 | "integrity": "sha1-tvjQs+lR77d97eyhlM/20W9nb4E=", 443 | "requires": { 444 | "mime-db": "1.40.0" 445 | }, 446 | "dependencies": { 447 | "mime-db": { 448 | "version": "1.40.0", 449 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", 450 | "integrity": "sha1-plBX6ZjbCQ9zKmj2wnbTh9QSbDI=" 451 | } 452 | } 453 | }, 454 | "negotiator": { 455 | "version": "0.6.2", 456 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 457 | "integrity": "sha1-/qz3zPUlp3rpY0Q2pkiD/+yjRvs=" 458 | } 459 | } 460 | }, 461 | "base64id": { 462 | "version": "1.0.0", 463 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", 464 | "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" 465 | }, 466 | "cookie": { 467 | "version": "0.3.1", 468 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 469 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 470 | }, 471 | "debug": { 472 | "version": "3.1.0", 473 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 474 | "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", 475 | "requires": { 476 | "ms": "2.0.0" 477 | }, 478 | "dependencies": { 479 | "ms": { 480 | "version": "2.0.0", 481 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 482 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 483 | } 484 | } 485 | }, 486 | "engine.io-parser": { 487 | "version": "2.1.3", 488 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", 489 | "integrity": "sha1-dXq5cPvy37Mse3SwMyFtVznveaY=", 490 | "requires": { 491 | "after": "0.8.2", 492 | "arraybuffer.slice": "~0.0.7", 493 | "base64-arraybuffer": "0.1.5", 494 | "blob": "0.0.5", 495 | "has-binary2": "~1.0.2" 496 | }, 497 | "dependencies": { 498 | "after": { 499 | "version": "0.8.2", 500 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 501 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 502 | }, 503 | "arraybuffer.slice": { 504 | "version": "0.0.7", 505 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 506 | "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=" 507 | }, 508 | "base64-arraybuffer": { 509 | "version": "0.1.5", 510 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 511 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 512 | }, 513 | "blob": { 514 | "version": "0.0.5", 515 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 516 | "integrity": "sha1-1oDu7yX4zZGtUz9bAe7UjmTK9oM=" 517 | } 518 | } 519 | }, 520 | "ws": { 521 | "version": "6.1.4", 522 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", 523 | "integrity": "sha1-W1yIAK+rkl6UzLKdFTyNAsF3bvk=", 524 | "requires": { 525 | "async-limiter": "~1.0.0" 526 | }, 527 | "dependencies": { 528 | "async-limiter": { 529 | "version": "1.0.1", 530 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 531 | "integrity": "sha1-3TeelPDbgxCwgpH51kwyCXZmF/0=" 532 | } 533 | } 534 | } 535 | } 536 | }, 537 | "has-binary2": { 538 | "version": "1.0.3", 539 | "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", 540 | "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", 541 | "requires": { 542 | "isarray": "2.0.1" 543 | }, 544 | "dependencies": { 545 | "isarray": { 546 | "version": "2.0.1", 547 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 548 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 549 | } 550 | } 551 | }, 552 | "socket.io-adapter": { 553 | "version": "1.1.1", 554 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz", 555 | "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=" 556 | }, 557 | "socket.io-client": { 558 | "version": "2.2.0", 559 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.2.0.tgz", 560 | "integrity": "sha1-hOc+48Q9UCDMwaJY+u65rsJyOvc=", 561 | "requires": { 562 | "backo2": "1.0.2", 563 | "base64-arraybuffer": "0.1.5", 564 | "component-bind": "1.0.0", 565 | "component-emitter": "1.2.1", 566 | "debug": "~3.1.0", 567 | "engine.io-client": "~3.3.1", 568 | "has-binary2": "~1.0.2", 569 | "has-cors": "1.1.0", 570 | "indexof": "0.0.1", 571 | "object-component": "0.0.3", 572 | "parseqs": "0.0.5", 573 | "parseuri": "0.0.5", 574 | "socket.io-parser": "~3.3.0", 575 | "to-array": "0.1.4" 576 | }, 577 | "dependencies": { 578 | "backo2": { 579 | "version": "1.0.2", 580 | "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", 581 | "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" 582 | }, 583 | "base64-arraybuffer": { 584 | "version": "0.1.5", 585 | "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", 586 | "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" 587 | }, 588 | "component-bind": { 589 | "version": "1.0.0", 590 | "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", 591 | "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" 592 | }, 593 | "component-emitter": { 594 | "version": "1.2.1", 595 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 596 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 597 | }, 598 | "debug": { 599 | "version": "3.1.0", 600 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 601 | "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", 602 | "requires": { 603 | "ms": "2.0.0" 604 | }, 605 | "dependencies": { 606 | "ms": { 607 | "version": "2.0.0", 608 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 609 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 610 | } 611 | } 612 | }, 613 | "engine.io-client": { 614 | "version": "3.3.2", 615 | "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.3.2.tgz", 616 | "integrity": "sha1-BOBoeY11vtoUN1omS7PXQte8M6o=", 617 | "requires": { 618 | "component-emitter": "1.2.1", 619 | "component-inherit": "0.0.3", 620 | "debug": "~3.1.0", 621 | "engine.io-parser": "~2.1.1", 622 | "has-cors": "1.1.0", 623 | "indexof": "0.0.1", 624 | "parseqs": "0.0.5", 625 | "parseuri": "0.0.5", 626 | "ws": "~6.1.0", 627 | "xmlhttprequest-ssl": "~1.5.4", 628 | "yeast": "0.1.2" 629 | }, 630 | "dependencies": { 631 | "component-inherit": { 632 | "version": "0.0.3", 633 | "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", 634 | "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" 635 | }, 636 | "engine.io-parser": { 637 | "version": "2.1.3", 638 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.3.tgz", 639 | "integrity": "sha1-dXq5cPvy37Mse3SwMyFtVznveaY=", 640 | "requires": { 641 | "after": "0.8.2", 642 | "arraybuffer.slice": "~0.0.7", 643 | "base64-arraybuffer": "0.1.5", 644 | "blob": "0.0.5", 645 | "has-binary2": "~1.0.2" 646 | }, 647 | "dependencies": { 648 | "after": { 649 | "version": "0.8.2", 650 | "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", 651 | "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" 652 | }, 653 | "arraybuffer.slice": { 654 | "version": "0.0.7", 655 | "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", 656 | "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=" 657 | }, 658 | "blob": { 659 | "version": "0.0.5", 660 | "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", 661 | "integrity": "sha1-1oDu7yX4zZGtUz9bAe7UjmTK9oM=" 662 | } 663 | } 664 | }, 665 | "ws": { 666 | "version": "6.1.4", 667 | "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", 668 | "integrity": "sha1-W1yIAK+rkl6UzLKdFTyNAsF3bvk=", 669 | "requires": { 670 | "async-limiter": "~1.0.0" 671 | }, 672 | "dependencies": { 673 | "async-limiter": { 674 | "version": "1.0.1", 675 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 676 | "integrity": "sha1-3TeelPDbgxCwgpH51kwyCXZmF/0=" 677 | } 678 | } 679 | }, 680 | "xmlhttprequest-ssl": { 681 | "version": "1.5.5", 682 | "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", 683 | "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" 684 | }, 685 | "yeast": { 686 | "version": "0.1.2", 687 | "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", 688 | "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" 689 | } 690 | } 691 | }, 692 | "has-cors": { 693 | "version": "1.1.0", 694 | "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", 695 | "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" 696 | }, 697 | "indexof": { 698 | "version": "0.0.1", 699 | "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", 700 | "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" 701 | }, 702 | "object-component": { 703 | "version": "0.0.3", 704 | "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", 705 | "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" 706 | }, 707 | "parseqs": { 708 | "version": "0.0.5", 709 | "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", 710 | "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", 711 | "requires": { 712 | "better-assert": "~1.0.0" 713 | }, 714 | "dependencies": { 715 | "better-assert": { 716 | "version": "1.0.2", 717 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 718 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 719 | "requires": { 720 | "callsite": "1.0.0" 721 | }, 722 | "dependencies": { 723 | "callsite": { 724 | "version": "1.0.0", 725 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 726 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 727 | } 728 | } 729 | } 730 | } 731 | }, 732 | "parseuri": { 733 | "version": "0.0.5", 734 | "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", 735 | "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", 736 | "requires": { 737 | "better-assert": "~1.0.0" 738 | }, 739 | "dependencies": { 740 | "better-assert": { 741 | "version": "1.0.2", 742 | "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", 743 | "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", 744 | "requires": { 745 | "callsite": "1.0.0" 746 | }, 747 | "dependencies": { 748 | "callsite": { 749 | "version": "1.0.0", 750 | "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", 751 | "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" 752 | } 753 | } 754 | } 755 | } 756 | }, 757 | "to-array": { 758 | "version": "0.1.4", 759 | "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", 760 | "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" 761 | } 762 | } 763 | }, 764 | "socket.io-parser": { 765 | "version": "3.3.0", 766 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", 767 | "integrity": "sha1-K1KpalCf3zFEC6QP7WCUx9TxJi8=", 768 | "requires": { 769 | "component-emitter": "1.2.1", 770 | "debug": "~3.1.0", 771 | "isarray": "2.0.1" 772 | }, 773 | "dependencies": { 774 | "component-emitter": { 775 | "version": "1.2.1", 776 | "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", 777 | "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" 778 | }, 779 | "debug": { 780 | "version": "3.1.0", 781 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 782 | "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", 783 | "requires": { 784 | "ms": "2.0.0" 785 | }, 786 | "dependencies": { 787 | "ms": { 788 | "version": "2.0.0", 789 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 790 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 791 | } 792 | } 793 | }, 794 | "isarray": { 795 | "version": "2.0.1", 796 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", 797 | "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" 798 | } 799 | } 800 | } 801 | } 802 | } 803 | } 804 | } 805 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-loadtesting", 3 | "version": "1.0.0", 4 | "description": "socket.io loadtesting examples", 5 | "main": "server.js", 6 | "author": "Kyle Martin", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@faker-js/faker": "^6.2.0", 10 | "express": "4.17.3", 11 | "socket.io": "^4.5.0" 12 | }, 13 | "scripts": { 14 | "start": "node server.js" 15 | }, 16 | "devDependencies": { 17 | "faker": "^6.6.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Chat Example 6 | 7 | 8 | 9 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var FADE_TIME = 150; // ms 3 | var TYPING_TIMER_LENGTH = 400; // ms 4 | var COLORS = [ 5 | '#e21400', '#91580f', '#f8a700', '#f78b00', 6 | '#58dc00', '#287b00', '#a8f07a', '#4ae8c4', 7 | '#3b88eb', '#3824aa', '#a700ff', '#d300e7' 8 | ]; 9 | 10 | // Initialize variables 11 | var $window = $(window); 12 | var $usernameInput = $('.usernameInput'); // Input for username 13 | var $messages = $('.messages'); // Messages area 14 | var $inputMessage = $('.inputMessage'); // Input message input box 15 | 16 | var $loginPage = $('.login.page'); // The login page 17 | var $chatPage = $('.chat.page'); // The chatroom page 18 | 19 | // Prompt for setting a username 20 | var username; 21 | var connected = false; 22 | var typing = false; 23 | var lastTypingTime; 24 | var $currentInput = $usernameInput.focus(); 25 | 26 | var socket = io(); 27 | 28 | const addParticipantsMessage = (data) => { 29 | var message = ''; 30 | if (data.numUsers === 1) { 31 | message += "there's 1 participant"; 32 | } else { 33 | message += "there are " + data.numUsers + " participants"; 34 | } 35 | log(message); 36 | } 37 | 38 | // Sets the client's username 39 | const setUsername = () => { 40 | username = cleanInput($usernameInput.val().trim()); 41 | 42 | // If the username is valid 43 | if (username) { 44 | $loginPage.fadeOut(); 45 | $chatPage.show(); 46 | $loginPage.off('click'); 47 | $currentInput = $inputMessage.focus(); 48 | 49 | // Tell the server your username 50 | socket.emit('add user', username); 51 | } 52 | } 53 | 54 | // Sends a chat message 55 | const sendMessage = () => { 56 | var message = $inputMessage.val(); 57 | // Prevent markup from being injected into the message 58 | message = cleanInput(message); 59 | // if there is a non-empty message and a socket connection 60 | if (message && connected) { 61 | $inputMessage.val(''); 62 | addChatMessage({ 63 | username: username, 64 | message: message 65 | }); 66 | // tell server to execute 'new message' and send along one parameter 67 | socket.emit('new message', message); 68 | } 69 | } 70 | 71 | // Log a message 72 | const log = (message, options) => { 73 | var $el = $('
  • ').addClass('log').text(message); 74 | addMessageElement($el, options); 75 | } 76 | 77 | // Adds the visual chat message to the message list 78 | const addChatMessage = (data, options) => { 79 | // Don't fade the message in if there is an 'X was typing' 80 | var $typingMessages = getTypingMessages(data); 81 | options = options || {}; 82 | if ($typingMessages.length !== 0) { 83 | options.fade = false; 84 | $typingMessages.remove(); 85 | } 86 | 87 | var $usernameDiv = $('') 88 | .text(data.username) 89 | .css('color', getUsernameColor(data.username)); 90 | var $messageBodyDiv = $('') 91 | .text(data.message); 92 | 93 | var typingClass = data.typing ? 'typing' : ''; 94 | var $messageDiv = $('
  • ') 95 | .data('username', data.username) 96 | .addClass(typingClass) 97 | .append($usernameDiv, $messageBodyDiv); 98 | 99 | addMessageElement($messageDiv, options); 100 | } 101 | 102 | // Adds the visual chat typing message 103 | const addChatTyping = (data) => { 104 | data.typing = true; 105 | data.message = 'is typing'; 106 | addChatMessage(data); 107 | } 108 | 109 | // Removes the visual chat typing message 110 | const removeChatTyping = (data) => { 111 | getTypingMessages(data).fadeOut(function () { 112 | $(this).remove(); 113 | }); 114 | } 115 | 116 | // Adds a message element to the messages and scrolls to the bottom 117 | // el - The element to add as a message 118 | // options.fade - If the element should fade-in (default = true) 119 | // options.prepend - If the element should prepend 120 | // all other messages (default = false) 121 | const addMessageElement = (el, options) => { 122 | var $el = $(el); 123 | 124 | // Setup default options 125 | if (!options) { 126 | options = {}; 127 | } 128 | if (typeof options.fade === 'undefined') { 129 | options.fade = true; 130 | } 131 | if (typeof options.prepend === 'undefined') { 132 | options.prepend = false; 133 | } 134 | 135 | // Apply options 136 | if (options.fade) { 137 | $el.hide().fadeIn(FADE_TIME); 138 | } 139 | if (options.prepend) { 140 | $messages.prepend($el); 141 | } else { 142 | $messages.append($el); 143 | } 144 | $messages[0].scrollTop = $messages[0].scrollHeight; 145 | } 146 | 147 | // Prevents input from having injected markup 148 | const cleanInput = (input) => { 149 | return $('
    ').text(input).html(); 150 | } 151 | 152 | // Updates the typing event 153 | const updateTyping = () => { 154 | if (connected) { 155 | if (!typing) { 156 | typing = true; 157 | socket.emit('typing'); 158 | } 159 | lastTypingTime = (new Date()).getTime(); 160 | 161 | setTimeout(() => { 162 | var typingTimer = (new Date()).getTime(); 163 | var timeDiff = typingTimer - lastTypingTime; 164 | if (timeDiff >= TYPING_TIMER_LENGTH && typing) { 165 | socket.emit('stop typing'); 166 | typing = false; 167 | } 168 | }, TYPING_TIMER_LENGTH); 169 | } 170 | } 171 | 172 | // Gets the 'X is typing' messages of a user 173 | const getTypingMessages = (data) => { 174 | return $('.typing.message').filter(function (i) { 175 | return $(this).data('username') === data.username; 176 | }); 177 | } 178 | 179 | // Gets the color of a username through our hash function 180 | const getUsernameColor = (username) => { 181 | // Compute hash code 182 | var hash = 7; 183 | for (var i = 0; i < username.length; i++) { 184 | hash = username.charCodeAt(i) + (hash << 5) - hash; 185 | } 186 | // Calculate color 187 | var index = Math.abs(hash % COLORS.length); 188 | return COLORS[index]; 189 | } 190 | 191 | // Keyboard events 192 | 193 | $window.keydown(event => { 194 | // Auto-focus the current input when a key is typed 195 | if (!(event.ctrlKey || event.metaKey || event.altKey)) { 196 | $currentInput.focus(); 197 | } 198 | // When the client hits ENTER on their keyboard 199 | if (event.which === 13) { 200 | if (username) { 201 | sendMessage(); 202 | socket.emit('stop typing'); 203 | typing = false; 204 | } else { 205 | setUsername(); 206 | } 207 | } 208 | }); 209 | 210 | $inputMessage.on('input', () => { 211 | updateTyping(); 212 | }); 213 | 214 | // Click events 215 | 216 | // Focus input when clicking anywhere on login page 217 | $loginPage.click(() => { 218 | $currentInput.focus(); 219 | }); 220 | 221 | // Focus input when clicking on the message input's border 222 | $inputMessage.click(() => { 223 | $inputMessage.focus(); 224 | }); 225 | 226 | // Socket events 227 | 228 | // Whenever the server emits 'login', log the login message 229 | socket.on('login', (data) => { 230 | connected = true; 231 | // Display the welcome message 232 | var message = "Welcome to Socket.IO Chat – "; 233 | log(message, { 234 | prepend: true 235 | }); 236 | addParticipantsMessage(data); 237 | }); 238 | 239 | // Whenever the server emits 'new message', update the chat body 240 | socket.on('new message', (data) => { 241 | addChatMessage(data); 242 | }); 243 | 244 | // Whenever the server emits 'user joined', log it in the chat body 245 | socket.on('user joined', (data) => { 246 | log(data.username + ' joined'); 247 | addParticipantsMessage(data); 248 | }); 249 | 250 | // Whenever the server emits 'user left', log it in the chat body 251 | socket.on('user left', (data) => { 252 | log(data.username + ' left'); 253 | addParticipantsMessage(data); 254 | removeChatTyping(data); 255 | }); 256 | 257 | // Whenever the server emits 'typing', show the typing message 258 | socket.on('typing', (data) => { 259 | addChatTyping(data); 260 | }); 261 | 262 | // Whenever the server emits 'stop typing', kill the typing message 263 | socket.on('stop typing', (data) => { 264 | removeChatTyping(data); 265 | }); 266 | 267 | socket.on('disconnect', () => { 268 | log('you have been disconnected'); 269 | }); 270 | 271 | socket.on('reconnect', () => { 272 | log('you have been reconnected'); 273 | if (username) { 274 | socket.emit('add user', username); 275 | } 276 | }); 277 | 278 | socket.on('reconnect_error', () => { 279 | log('attempt to reconnect has failed'); 280 | }); 281 | 282 | }); 283 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* Fix user-agent */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-weight: 300; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | 12 | html, input { 13 | font-family: 14 | "HelveticaNeue-Light", 15 | "Helvetica Neue Light", 16 | "Helvetica Neue", 17 | Helvetica, 18 | Arial, 19 | "Lucida Grande", 20 | sans-serif; 21 | } 22 | 23 | html, body { 24 | height: 100%; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | ul { 30 | list-style: none; 31 | word-wrap: break-word; 32 | } 33 | 34 | /* Pages */ 35 | 36 | .pages { 37 | height: 100%; 38 | margin: 0; 39 | padding: 0; 40 | width: 100%; 41 | } 42 | 43 | .page { 44 | height: 100%; 45 | position: absolute; 46 | width: 100%; 47 | } 48 | 49 | /* Login Page */ 50 | 51 | .login.page { 52 | background-color: #000; 53 | } 54 | 55 | .login.page .form { 56 | height: 100px; 57 | margin-top: -100px; 58 | position: absolute; 59 | 60 | text-align: center; 61 | top: 50%; 62 | width: 100%; 63 | } 64 | 65 | .login.page .form .usernameInput { 66 | background-color: transparent; 67 | border: none; 68 | border-bottom: 2px solid #fff; 69 | outline: none; 70 | padding-bottom: 15px; 71 | text-align: center; 72 | width: 400px; 73 | } 74 | 75 | .login.page .title { 76 | font-size: 200%; 77 | } 78 | 79 | .login.page .usernameInput { 80 | font-size: 200%; 81 | letter-spacing: 3px; 82 | } 83 | 84 | .login.page .title, .login.page .usernameInput { 85 | color: #fff; 86 | font-weight: 100; 87 | } 88 | 89 | /* Chat page */ 90 | 91 | .chat.page { 92 | display: none; 93 | } 94 | 95 | /* Font */ 96 | 97 | .messages { 98 | font-size: 150%; 99 | } 100 | 101 | .inputMessage { 102 | font-size: 100%; 103 | } 104 | 105 | .log { 106 | color: gray; 107 | font-size: 70%; 108 | margin: 5px; 109 | text-align: center; 110 | } 111 | 112 | /* Messages */ 113 | 114 | .chatArea { 115 | height: 100%; 116 | padding-bottom: 60px; 117 | } 118 | 119 | .messages { 120 | height: 100%; 121 | margin: 0; 122 | overflow-y: scroll; 123 | padding: 10px 20px 10px 20px; 124 | } 125 | 126 | .message.typing .messageBody { 127 | color: gray; 128 | } 129 | 130 | .username { 131 | font-weight: 700; 132 | overflow: hidden; 133 | padding-right: 15px; 134 | text-align: right; 135 | } 136 | 137 | /* Input */ 138 | 139 | .inputMessage { 140 | border: 10px solid #000; 141 | bottom: 0; 142 | height: 60px; 143 | left: 0; 144 | outline: none; 145 | padding-left: 10px; 146 | position: absolute; 147 | right: 0; 148 | width: 100%; 149 | } 150 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Setup basic express server 2 | const express = require('express'); 3 | const app = express(); 4 | const path = require('path'); 5 | const server = require('http').createServer(app); 6 | const io = require('socket.io')(server); 7 | const port = process.env.PORT || 8080; 8 | 9 | server.listen(port, () => { 10 | console.log('Server listening at port %d', port); 11 | }); 12 | 13 | // load static front end site 14 | app.use(express.static(path.join(__dirname, 'public'))); 15 | 16 | // socket.io-server 17 | 18 | var numUsers = 0; 19 | 20 | io.on('connection', (socket) => { 21 | var addedUser = false; 22 | 23 | // when the client emits 'new message', this listens and executes 24 | socket.on('new message', (data) => { 25 | // we tell the client to execute 'new message' 26 | socket.broadcast.emit('new message', { 27 | username: socket.username, 28 | message: data 29 | }); 30 | }); 31 | 32 | // when the client emits 'add user', this listens and executes 33 | socket.on('add user', (username) => { 34 | if (addedUser) return; 35 | 36 | // we store the username in the socket session for this client 37 | socket.username = username; 38 | ++numUsers; 39 | addedUser = true; 40 | socket.emit('login', { 41 | numUsers: numUsers 42 | }); 43 | // echo globally (all clients) that a person has connected 44 | socket.broadcast.emit('user joined', { 45 | username: socket.username, 46 | numUsers: numUsers 47 | }); 48 | }); 49 | 50 | // when the client emits 'typing', we broadcast it to others 51 | socket.on('typing', () => { 52 | socket.broadcast.emit('typing', { 53 | username: socket.username 54 | }); 55 | }); 56 | 57 | // when the client emits 'stop typing', we broadcast it to others 58 | socket.on('stop typing', () => { 59 | socket.broadcast.emit('stop typing', { 60 | username: socket.username 61 | }); 62 | }); 63 | 64 | // when the user disconnects.. perform this 65 | socket.on('disconnect', () => { 66 | if (addedUser) { 67 | --numUsers; 68 | 69 | // echo globally that this client has left 70 | socket.broadcast.emit('user left', { 71 | username: socket.username, 72 | numUsers: numUsers 73 | }); 74 | } 75 | }); 76 | }); 77 | --------------------------------------------------------------------------------