BunとWebSocket
BunでWebSocket Serverを立てる場合、wsモジュールは動かない。
代わりに Bun.serve()のwebsocket (ServerWebSocket)を使う。
// bun websocket server
// for bun v0.2.1-
const PORT=8000;
Bun.serve({
port: PORT,
websocket: {
open(ws) {
console.log('--open--');
ws.send('connected!');
},
async message(ws, message) {
console.log('received: %s', message);
ws.send('Echoback:' + message);
const text = '' + message;
if (text === 'QUIT') {
console.log('QUIT Server');
process.exit(0);
//ws.close(); <-- bun ERROR
}
},
close(ws) {
console.log('--close--');
},
perMessageDeflate: false,
},
fetch(req, server) {
// Upgrade to a ServerWebSocket if we can
// This automatically checks for the `Sec-WebSocket-Key` header
// meaning you don't have to check headers, you can just call `upgrade()`
if (server.upgrade(req))
// When upgrading, we return undefined since we don't want to send a Response
return;
return new Response("Regular HTTP response");
},
});
console.log('Server start on port:' + PORT);
WebSocket クライアントでは wsモジュールが使える
//
// simple client with ws
//
const PORT = 8000;
const URL = 'ws://localhost:' + PORT; // OK for Node.js, NG for Bun
const Client = require('ws').WebSocket;
const ws = new Client(URL);
ws.on('open', function open() {
ws.send('Hello');
});
ws.on('message', function message(data) {
console.log('received: %s', data);
const text = '' + data;
if (text === 'Echoback:Hello') {
console.log('got hello');
ws.send('QUIT');
}
});
実行してみると、接続できない
$ bun bun_server.js
$ bun client.js
クライアントを Node.js で実行すると、接続できる
$ node client.js
received: connected!
received: Echoback:Hello
got hello
received: Echoback:QUIT
bunでクライアントを実行する際に、 「localhost」がIPv6のアドレス 「::1」に解決されている様子。
※環境は ubuntu 22.04 LTS (ARM64)。
※その後の調査で、ubuntu 20.04 LTSでは問題無いいことが判明。macOS 12 でも問題なし。
明示的に 127.0.0.1 を指定すると一歩前進
//
// simple client with ws
//
const PORT = 8000;
//const URL = 'ws://localhost:' + PORT; OK for Node.js, NG for Bun
const URL = 'ws://127.0.0.1:' + PORT; // OK
const Client = require('ws').WebSocket;
const ws = new Client(URL);
ws.on('open', function open() {
ws.send('Hello');
});
ws.on('message', function message(data) {
console.log('received: %s', data);
const text = '' + data;
if (text === 'Echoback:Hello') {
console.log('got hello');
ws.send('QUIT');
}
});
実行してみると接続されるが、メッセージ交換が行われない(切断されてしまう様子)
$ bun client.js
received: connected!
ベンチマークとの違いを調べる
どうやら、サーバー側のオープン時の処理でメッセージを直ちに送信しているが影響しているらしい。
websocket: {
open(ws) {
console.log('--open--');
ws.send('connected!'); // <-- これが悪影響
},
}
ベンチマークでは、setTimeout()を使って遅らせている。
同様に、しばらく待ってからメッセージを送ればOK。
- setTimeout( , 10) ... OK
- setTimeout(, 0) ... NG
- setImmediate() ... NG
- nextTick() ... NG
※その後の調査で、Bun Server - Bun Clientの組み合わせでだけで発生することが判明。
- Bun Server - Bun Client ... 切断される
- Bun Server - Node.js Client ... 問題なし
- Bun Server - ブラウザ ... 問題なし
// bun websocket server
// for bun v0.2.1-
const PORT=8000;
async function sleep(milisec) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('OK'), milisec);
});
}
Bun.serve({
port: PORT,
websocket: {
open(ws) {
console.log('--open--');
console.log('ws:', toString.call(ws));
//ws.send('connected!'); // ... NG
/* --- await --
await sleep(0); // 100:OK, 0:NG
ws.send('connected!');
console.log('-sent connected-');
---*/
// /*-- NEED timeout for Bun ws client ※タイムアウトを入れて、送信を遅らせるとOK
setTimeout(() => {
//ws.publishText("room", `connected!`);
ws.send('connected!');
console.log('-sent connected-');
}, 1); // 100...OK, 10...OK, 0...NG (close)
// -- */
/* --- NG ---
setImmediate(() => {
ws.send('connected!');
});
// NG : close after receive Hello (same as setTimeout 0);
---*/
/* --- NG ---
process.nextTick(() => {
ws.send('connected!')i;
// NG : close after receive Hello (same as setTimeout 0);
});
---*/
},
async message(ws, message) {
console.log('received: %s', message);
ws.send('Echoback:' + message);
const text = '' + message;
if (text === 'QUIT') {
console.log('QUIT Server');
await sleep(10); // wait for ws.send() finish
process.exit(0);
//ws.close(); <-- bun ERROR
}
},
close(ws) {
console.log('--close--');
},
perMessageDeflate: false,
},
fetch(req, server) {
// Upgrade to a ServerWebSocket if we can
// This automatically checks for the `Sec-WebSocket-Key` header
// meaning you don't have to check headers, you can just call `upgrade()`
if (server.upgrade(req))
// When upgrading, we return undefined since we don't want to send a Response
return;
return new Response("Regular HTTP response");
},
});
console.log('Server start on port:' + PORT);
実行結果
$ bun bun_websocket_server.js
$ bun client.js
received: connected!
received: Echoback:Hello
got hello
received: Echoback:QUIT
メッセージ交換まで成功。
Bun で ws モジュールを読み込んだ場合、npm モジュールではなく、Bun内部のwsで上書きされる様子。
npm モジュールをインストールしていない状態でも、エラーなく動く。
$ bun client.js
Node.js では当然エラーになる。
$ node client.js
node:internal/modules/cjs/loader:988
throw err;
^
Error: Cannot find module 'ws'
Node.js と Bun では ws の実体(型)が異なる
ws.WebSocket | ws.WebSocketServer | |
---|---|---|
Node.js | [class WebSocket extends EventEmitter] | [class WebSocketServer extends EventEmitter] |
Bun | [Function: BunWebSocket] | [Function: WebSocketServer] インスタンスを作ろうとすると「Not implemented yet!」とエラーが発生する |
const ws = require('ws');
console.log('type ws:', toString.call(ws));
console.log('ws:', ws);
console.log('---------');
console.log('type ws.WebSocket:', toString.call(ws.WebSocket));
console.log('ws.WebSocket:', ws.WebSocket);
console.log('---------');
console.log('ws.WebSocketServer:', ws.WebSocketServer);
console.log('type ws.WebSocketServer:', toString.call(ws.WebSocketServer));
type ws: [object Function]
ws: <ref *1> [class WebSocket extends EventEmitter] {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
createWebSocketStream: [Function: createWebSocketStream],
Server: [class WebSocketServer extends EventEmitter],
Receiver: [class Receiver extends Writable],
Sender: [class Sender],
WebSocket: [Circular *1],
WebSocketServer: [class WebSocketServer extends EventEmitter]
}
---------
type ws.WebSocket: [object Function]
ws.WebSocket: <ref *1> [class WebSocket extends EventEmitter] {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
createWebSocketStream: [Function: createWebSocketStream],
Server: [class WebSocketServer extends EventEmitter],
Receiver: [class Receiver extends Writable],
Sender: [class Sender],
WebSocket: [Circular *1],
WebSocketServer: [class WebSocketServer extends EventEmitter]
}
---------
ws.WebSocketServer: [class WebSocketServer extends EventEmitter]
type ws.WebSocketServer: [object Function]
$ bun checkWs.js
type ws: [object Module]
ws: Module { Receiver: [Function: Receiver], Sender: [Function: Sender], WebSocket: [Function: BunWebSocket], WebSocketServer: [Function: WebSocketServer], createWebSocketStream: [Function], default: [Function: BunWebSocket] }
---------
type ws.WebSocket: [object Function]
ws.WebSocket: [Function: BunWebSocket]
---------
ws.WebSocketServer: [Function: WebSocketServer]
type ws.WebSocketServer: [object Function]
Bun でWebサーバー/WebSocketサーバーを立てるときに、ホスト名を明示的に localhost に指定してみる。
Bun.serve({
host: 'localhost',
port: PORT,
websocket: {
open(ws) {
},
}
})
すると、クライアントで(127.0.0.1でなく) localhostに対して接続できるようになる。
const PORT = 8000;
const URL = 'ws://localhost:' + PORT; // OK for Node.js, OK for Bun
//const URL = 'ws://127.0.0.1:' + PORT; // OK
const Client = require('ws').WebSocket;
const ws = new Client(URL);