DenoのWebSocketサーバーの書き方 2022冬編

DenoのWebSocketサーバーの書き方の方法をまとめます。

2022年10月現在、WebSocketサーバーの書き方としてメジャーなものは以下の2つです。

- 標準ライブラリを使用する方法
- oakやhonoなどのミドルウェアを使う方法

この記事ではそれぞれ使い方をまとめたいと思います。

:::note warn
標準ライブラリの`ws`モジュールは廃止されました。
:::

# 標準ライブラリを使う場合

かつては標準ライブラリの`ws`モジュールを使うやり方が主流だったのですが、現在はこれは廃止され、`http`モジュールと[Deno.upgradeWebSocket()](https://doc.deno.land/deno/stable/~/Deno.upgradeWebSocket)という関数を使う書き方が推奨されています。

この書き方の特徴は、サードパーティ製のライブラリは必要なく完全にDenoの組み込みAPIだけで書けるという点です。

標準ライブラリを使ってWebSocketサーバーを作成するには、以下のようなコードになります。

```ts
import { serve } from "https://deno.land/std@0.156.0/http/server.ts";
serve((req) => {
  // `Deno.upgradeWebSocket(req)`でHTTP接続をWebSocket用に切り替え
  const { response, socket } = Deno.upgradeWebSocket(req);

  // ↓socketという変数に対してaddEventListenerなどを設定し、WebScoketをハンドリング
  socket.addEventListener("message", (event) => console.log(event.data));

  // レスポンスを返してWebSocket通信スタート
  return response;
});
```

コード中の変数`socket`の中にはWebSocketを処理する変数が入っています。例えば

```ts
// 接続を開いた時の処理
socket.addEventListener("open", () => console.log("open!"));
// メッセージを受信した時の処理
socket.addEventListener("message", () => console.log("message!"));
// エラー発生時の処理
socket.addEventListener("error", () => console.log("error!"));
// 接続を閉じた時の処理
socket.addEventListener("close", () => console.log("close!"));
// メッセージを送信する
socket.send("message");
// 接続を閉じる
socket.close();
```

という風に書くことができます。勘が言い方はもうお分かりだと思いますが、ブラウザで`new WebSocket()`した時と同じように使うことができます。

#### WebSocket以外もハンドリングする方法

一般的なサーバーではWebSocket以外のリクエストも捌く必要があると思います。
その場合は以下のように、`url.protocol`や`url.pathname`を見てif文で条件分岐させます。

```ts
import { serve } from "https://deno.land/std@0.156.0/http/server.ts";

serve((req) => {
  const url = new URL(req.url);

  // if (url.pathname==="/foo") {...などで更に条件分岐させることもできる
  if (url.protocol === "wss:" || url.protocol === "ws:") {
    // websocketのハンドリング
    const { response, socket } = Deno.upgradeWebSocket(req);
    socket.addEventListener("message", (event) => console.log(event.data));
    return response;
  } else {
    // websocket以外のハンドリング
    return new Response("ok!");
  }
});
```

# oakを使う場合

[oak](https://oakserver.github.io/oak/)はexpressライクなDeno向けミドルウェアです。現時点（2022年）では一番普及しており、安定して使うことができます。

oakの場合は、`ctx.upgrade()`というメソッドを呼ぶことでWebSocket通信をすることができます。

```ts
import { Application, Router } from "https://deno.land/x/oak@v11.1.0/mod.ts";
const app = new Application();
const router = new Router();
router.get("/socket", (ctx) => {
  const socket = ctx.upgrade(); // ここでwebsocket接続を開始
  // ここでwebsocketのハンドリング
  socket.addEventListener("message", (e) => console.log(e.data));
});
app.use(router.routes());
app.use(router.allowedMethods());

app.listen({ port: 8080 });
```

# Honoを使う場合

[Hono](https://honojs.dev/)もexpressのようなミドルウェアですが、他のミドルウェアより高速らしいです。Deno以外のCloudflare WorkersやBunといったランタイムでも動作することが特徴です。
 
HonoはWeb標準の`Response`/`Request`を使用しているため、`Deno.upgradeWebSocket()`を使ってWebSocket通信するのがよさそうです。

```ts
import { serve } from "https://deno.land/std@0.158.0/http/server.ts";
import { Hono } from "https://deno.land/x/hono@v2.2.2/mod.ts";

const app = new Hono();

app.get("/", (c) => c.text("Hello! Hono!"));
app.get("/ws/", (c) => {
  const { response, socket } = Deno.upgradeWebSocket(c.req);

  // websocketのハンドリング
  socket.addEventListener("message", (e) => console.log(e));

  return response;
});

serve(app.fetch);
```

# まとめ

- 標準ライブラリやHonoなど、Web標準`Request`/`Response`を使用するサーバーにおいては、`Deno.upgradeWebSocket()`を使用します。
- oakなどの、ライブラリ固有の方法が用意されている場合はそちらを使用します。

WebSocketを使おうとしたときに`Deno.upgradeWebSocket`がなかなか検索に引っかからず使い方が分からないという場面もありそうなので、今回まとめてみました。