Open8

BunとWebSocket

mganekomganeko

BunでWebSocket Serverを立てる場合、wsモジュールは動かない。
代わりに Bun.serve()のwebsocket (ServerWebSocket)を使う。

bun_server.js
// 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);
mganekomganeko

WebSocket クライアントでは wsモジュールが使える

client.js
//
// 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');
  }
});
mganekomganeko

実行してみると、接続できない

$ bun bun_server.js
$ bun client.js

クライアントを Node.js で実行すると、接続できる

$ node client.js 
received: connected!
received: Echoback:Hello
got hello
received: Echoback:QUIT
mganekomganeko

bunでクライアントを実行する際に、 「localhost」がIPv6のアドレス 「::1」に解決されている様子。

※環境は ubuntu 22.04 LTS (ARM64)。
※その後の調査で、ubuntu 20.04 LTSでは問題無いいことが判明。macOS 12 でも問題なし。

明示的に 127.0.0.1 を指定すると一歩前進

client.js
//
// 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!
mganekomganeko

ベンチマークとの違いを調べる

どうやら、サーバー側のオープン時の処理でメッセージを直ちに送信しているが影響しているらしい。

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.js
// 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

メッセージ交換まで成功。

mganekomganeko

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'
mganekomganeko

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!」とエラーが発生する
checkWs.js
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]
mganekomganeko

Bun でWebサーバー/WebSocketサーバーを立てるときに、ホスト名を明示的に localhost に指定してみる。

bun_websocket_server.js
Bun.serve({
  host: 'localhost',
  port: PORT,
  websocket: {
    open(ws) {
    },
  }
 })

すると、クライアントで(127.0.0.1でなく) localhostに対して接続できるようになる。

client.js

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);