websocker-server

This commit is contained in:
Jarred Sumner
2022-10-18 21:59:47 -07:00
parent b0fe167910
commit 9c7eb75a9a
9 changed files with 578 additions and 0 deletions

View File

@@ -2118,6 +2118,13 @@ Features:
- HTTPS
- Pubsub / broadcast support with MQTT-like topics
It's also fast. For [a chatroom](./bench/websocket-server/) on Linux x64:
| Messages sent per second | Runtime |
| ------------------------ | ------------------------------ |
| ~700,000 | (`Bun.serve`) Bun v0.2.1 (x64) |
| ~100,000 | (`ws`) Node v18.10.0 (x64) |
Here is an example that echoes back any message it receives:
```ts

169
bench/websocket-server/.gitignore vendored Normal file
View File

@@ -0,0 +1,169 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*

View File

@@ -0,0 +1,37 @@
# websocket-server
This benchmarks a websocket server intended as a simple but very active chat room.
First, start the server. By default, it will wait for 16 clients which the client script will handle.
Run in Bun (`Bun.serve`):
```bash
bun ./chat-server.bun.js
```
Run in Node (`"ws"` package):
```bash
node ./chat-server.node.mjs
```
Run in Deno (`Deno.serve`):
```bash
deno run -A --unstable ./chat-server.deno.mjs
```
Then, run the client script. By default, it will connect 16 clients. This client script can run in Bun, Node, or Deno
```bash
node ./chat-client.mjs
```
The client script loops through a list of messages for each connected client and sends a message.
For example, when the client sends `"foo"`, the server sends back `"John: foo"` so that all members of the chatroom receive the message.
The client script waits until it receives all the messages for each client before sending the next batch of messages.
This project was created using `bun init` in bun v0.2.1. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,182 @@
const env =
"process" in globalThis
? process.env
: "Deno" in globalThis
? Deno.env.toObject()
: {};
const SERVER = env.SERVER || "ws://0.0.0.0:4001";
const WebSocket = globalThis.WebSocket || (await import("ws")).WebSocket;
const LOG_MESSAGES = env.LOG_MESSAGES === "1";
const CLIENTS_TO_WAIT_FOR = parseInt(env.CLIENTS_COUNT || "", 10) || 16;
const DELAY = 64;
const MESSAGES_TO_SEND = Array.from({ length: 32 }, () => [
"Hello World!",
"Hello World! 1",
"Hello World! 2",
"Hello World! 3",
"Hello World! 4",
"Hello World! 5",
"Hello World! 6",
"Hello World! 7",
"Hello World! 8",
"Hello World! 9",
"What is the meaning of life?",
"where is the bathroom?",
"zoo",
"kangaroo",
"erlang",
"elixir",
"bun",
"mochi",
"typescript",
"javascript",
"Hello World! 7",
"Hello World! 8",
"Hello World! 9",
"What is the meaning of life?",
"where is the bathroom?",
"zoo",
"kangaroo",
"erlang",
"elixir",
"bun",
"mochi",
"typescript",
"javascript",
"Hello World! 7",
"Hello World! 8",
"Hello World! 9",
"What is the meaning of life?",
"Hello World! 7",
"Hello World! 8",
"Hello World! 9",
"What is the meaning of life?",
"where is the bathroom?",
"zoo",
"kangaroo",
"erlang",
"elixir",
"bun",
"mochi",
"typescript",
"javascript",
]).flat();
const NAMES = Array.from({ length: 50 }, (a, i) => [
"Alice" + i,
"Bob" + i,
"Charlie" + i,
"David" + i,
"Eve" + i,
"Frank" + i,
"Grace" + i,
"Heidi" + i,
"Ivan" + i,
"Judy" + i,
"Karl" + i,
"Linda" + i,
"Mike" + i,
"Nancy" + i,
"Oscar" + i,
"Peggy" + i,
"Quentin" + i,
"Ruth" + i,
"Steve" + i,
"Trudy" + i,
"Ursula" + i,
"Victor" + i,
"Wendy" + i,
"Xavier" + i,
"Yvonne" + i,
"Zach" + i,
])
.flat()
.slice(0, CLIENTS_TO_WAIT_FOR);
console.log(`Connecting ${CLIENTS_TO_WAIT_FOR} WebSocket clients...`);
console.time(`All ${CLIENTS_TO_WAIT_FOR} clients connected`);
var remainingClients = CLIENTS_TO_WAIT_FOR;
var promises = [];
const clients = new Array(CLIENTS_TO_WAIT_FOR);
for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) {
clients[i] = new WebSocket(`${SERVER}?name=${NAMES[i]}`);
promises.push(
new Promise((resolve, reject) => {
clients[i].onmessage = (event) => {
resolve();
};
})
);
}
await Promise.all(promises);
console.timeEnd(`All ${clients.length} clients connected`);
var received = 0;
var total = 0;
var more = false;
var remaining;
for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) {
clients[i].onmessage = (event) => {
if (LOG_MESSAGES) console.log(event.data);
received++;
remaining--;
if (remaining === 0) {
more = true;
remaining = total;
}
};
}
// each message is supposed to be received
// by each client
// so its an extra loop
for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) {
for (let j = 0; j < MESSAGES_TO_SEND.length; j++) {
for (let k = 0; k < CLIENTS_TO_WAIT_FOR; k++) {
total++;
}
}
}
remaining = total;
function restart() {
for (let i = 0; i < CLIENTS_TO_WAIT_FOR; i++) {
for (let j = 0; j < MESSAGES_TO_SEND.length; j++) {
clients[i].send(MESSAGES_TO_SEND[j]);
}
}
}
var runs = [];
setInterval(() => {
const last = received;
runs.push(last);
received = 0;
console.log(
last,
`messages per second (${CLIENTS_TO_WAIT_FOR} clients x ${MESSAGES_TO_SEND.length} msg, min delay: ${DELAY}ms)`
);
if (runs.length >= 10) {
console.log("10 runs");
console.log(JSON.stringify(runs, null, 2));
if ("process" in globalThis) process.exit(0);
runs.length = 0;
}
}, 1000);
var isRestarting = false;
setInterval(() => {
if (more && !isRestarting) {
more = false;
isRestarting = true;
restart();
isRestarting = false;
}
}, DELAY);
restart();

View File

@@ -0,0 +1,56 @@
// See ./README.md for instructions on how to run this benchmark.
const CLIENTS_TO_WAIT_FOR = parseInt(process.env.CLIENTS_COUNT || "", 10) || 16;
var remainingClients = CLIENTS_TO_WAIT_FOR;
const COMPRESS = process.env.COMPRESS === "1";
const port = process.PORT || 4001;
const server = Bun.serve({
port: port,
websocket: {
open(ws) {
ws.subscribe("room");
remainingClients--;
console.log(`${ws.data.name} connected (${remainingClients} remain)`);
if (remainingClients === 0) {
console.log("All clients connected");
setTimeout(() => {
console.log('Starting benchmark by sending "ready" message');
ws.publishText("room", `ready`);
}, 100);
}
},
message(ws, msg) {
const out = `${ws.data.name}: ${msg}`;
if (ws.publishText("room", out) !== out.length) {
throw new Error("Failed to publish message");
}
},
close(ws) {
remainingClients++;
},
perMessageDeflate: false,
},
fetch(req, server) {
if (
server.upgrade(req, {
data: {
name:
new URL(req.url).searchParams.get("name") ||
"Client #" + (CLIENTS_TO_WAIT_FOR - remainingClients),
},
})
)
return;
return new Response("Error");
},
});
console.log(
`Waiting for ${remainingClients} clients to connect...\n`,
` http://${server.hostname}:${port}/`
);

View File

@@ -0,0 +1,48 @@
// See ./README.md for instructions on how to run this benchmark.
const port = Deno.env.get("PORT") || 4001;
const CLIENTS_TO_WAIT_FOR =
parseInt(Deno.env.get("CLIENTS_COUNT") || "", 10) || 16;
var clients = [];
async function reqHandler(req) {
if (req.headers.get("upgrade") != "websocket") {
return new Response(null, { status: 501 });
}
const { socket: client, response } = Deno.upgradeWebSocket(req);
clients.push(client);
const name = new URL(req.url).searchParams.get("name");
console.log(
`${name} connected (${CLIENTS_TO_WAIT_FOR - clients.length} remain)`
);
client.onmessage = (event) => {
const msg = `${name}: ${event.data}`;
for (let client of clients) {
client.send(msg);
}
};
client.onclose = () => {
clients.splice(clients.indexOf(client), 1);
};
if (clients.length === CLIENTS_TO_WAIT_FOR) {
sendReadyMessage();
}
return response;
}
function sendReadyMessage() {
console.log("All clients connected");
setTimeout(() => {
console.log("Starting benchmark");
for (let client of clients) {
client.send(`ready`);
}
}, 100);
}
console.log(`Waiting for ${CLIENTS_TO_WAIT_FOR} clients to connect..`);
Deno.serve(reqHandler, { port });

View File

@@ -0,0 +1,52 @@
// See ./README.md for instructions on how to run this benchmark.
const port = process.env.PORT || 4001;
const CLIENTS_TO_WAIT_FOR = parseInt(process.env.CLIENTS_COUNT || "", 10) || 16;
import { createRequire } from "module";
const require = createRequire(import.meta.url);
var WebSocketServer = require("ws").Server,
config = {
host: "0.0.0.0",
port,
},
wss = new WebSocketServer(config, function () {
console.log(`Waiting for ${CLIENTS_TO_WAIT_FOR} clients to connect..`);
});
var clients = [];
wss.on("connection", function (ws, { url }) {
const name = new URL(new URL(url, "http://localhost:3000")).searchParams.get(
"name"
);
console.log(
`${name} connected (${CLIENTS_TO_WAIT_FOR - clients.length} remain)`
);
clients.push(ws);
ws.on("message", function (message) {
const out = `${name}: ${message}`;
for (let client of clients) {
client.send(out);
}
});
// when a connection is closed
ws.on("close", function (ws) {
clients.splice(clients.indexOf(ws), 1);
});
if (clients.length === CLIENTS_TO_WAIT_FOR) {
sendReadyMessage();
}
});
function sendReadyMessage() {
console.log("All clients connected");
setTimeout(() => {
console.log("Starting benchmark");
for (let client of clients) {
client.send(`ready`);
}
}, 100);
}

View File

@@ -0,0 +1,13 @@
{
"name": "websocket-server",
"module": "index.ts",
"type": "module",
"devDependencies": {
"bun-types": "^0.2.0"
},
"dependencies": {
"bufferutil": "^4.0.7",
"utf-8-validate": "^5.0.10",
"ws": "^8.9.0"
}
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
// so that if your project isn't using TypeScript, it still has autocomplete
"allowJs": true,
// "bun-types" is the important part
"types": ["bun-types"]
}
}