mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
websocker-server
This commit is contained in:
@@ -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
169
bench/websocket-server/.gitignore
vendored
Normal 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.\*
|
||||
37
bench/websocket-server/README.md
Normal file
37
bench/websocket-server/README.md
Normal 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.
|
||||
182
bench/websocket-server/chat-client.mjs
Normal file
182
bench/websocket-server/chat-client.mjs
Normal 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();
|
||||
56
bench/websocket-server/chat-server.bun.js
Normal file
56
bench/websocket-server/chat-server.bun.js
Normal 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}/`
|
||||
);
|
||||
48
bench/websocket-server/chat-server.deno.mjs
Normal file
48
bench/websocket-server/chat-server.deno.mjs
Normal 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 });
|
||||
52
bench/websocket-server/chat-server.node.mjs
Normal file
52
bench/websocket-server/chat-server.node.mjs
Normal 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);
|
||||
}
|
||||
13
bench/websocket-server/package.json
Normal file
13
bench/websocket-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
14
bench/websocket-server/tsconfig.json
Normal file
14
bench/websocket-server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user