【Deno1.13】高速化されたHTTPサーバー＆WebSocketに移行する

DenoのHTTPサーバー+WebSocketサーバーはv1.13から高速化されました。
これに伴いAPIが変更されており、移行にはコードの変更が必要です。

## 時系列

deno deployを「deploy」、ダウンロードして使うdenoを「CLI」と表記します。

- 元々deno CLIには標準ライブラリを使用したサーバーが、deno deployには`fetch event`を使用したサーバーがあった
- パフォーマンス向上＆http2対応のためdeno CLIに`Deno.serveHttp`と`Deno.upgradeWebSocket`が導入された
- その後、CLIとの互換性のためにdeployにも両者が導入された
- 従来のサーバー実装であった標準ライブラリの`http`は、内部で`Deno.serveHttp`を使うように変更され、deployからも利用可能になった
- 従来のwebsocketサーバー実装であった標準ライブラリの`ws`は非推奨化
- `http`モジュールに、deploy向けにポート番号をオプションにした`serve`関数が登場。`listenAndServe`が非推奨化
- `http`モジュールの`serve`関数のポート指定方法が変更される

|日付|変更点|
|--|--|
|2021.04.13 (Deno 1.9) | `Deno.serveHttp`が`--unstable`付きで導入される|
|2021.07.13 (Deno 1.12) | `Deno.upgradeWebSocket`が`--unstable`付きで導入される|
|2021.08.10 (Deno 1.13) | `Deno.serveHttp`が安定する|
|2021.09.01 (Deno Deploy Beta 2) | `Deno.serveHttp`が導入される<br>`Deno.upgradeWebSocket`が導入される|
|2021.09.14 (Deno 1.14) | `Deno.upgradeWebSocket`が安定する|
|2021.09.14 (Deno std 0.107.0) | <ul><li>`http/server`モジュールが`Deno.serveHttp`を使うように変更<ul><li>→CLI, deployの両方で`http/server`モジュールが利用可能に</li></ul></li><li>従来の`http/server`モジュールは`http/server_legacy`に移動し非推奨化</li></ul>|
|2021.09.28 (Deno std 0.109.0) | `ws`モジュールの非推奨化|
|2021.11.09（Deno std 0.114.0）|`http/server`モジュールの`listenAndServe`が非推奨化し、`serve`に変更される|
|2021.12.16（Deno std 0.118.0）|`http/server`モジュールの`serve`のポート指定方法が変更される|

## 変更後の書き方

#### HTTPサーバー

##### 変更前

```ts
import { serve } from "https://deno.land/std@0.106.0/http/server.ts";
const server = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of server) {
  req.respond({ body: "Hello World\n" });
}
```

##### 変更後

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

// 引数にRequestオブジェクトを取ってResponseオブジェクトを返す関数
serve((request) => new Response("Hello World"), {
  port: 80, // ポート番号80を使用（デフォルトは8000）
  // hostname: "0.0.0.0", // ホスト名を指定（デフォルトは0.0.0.0）
});

console.log("http://localhost:80/");
```
※従来の`serve`関数は`server_legacy.ts`からimportすることで、v0.107.0以降でも利用可能。

標準ライブラリの新API対応が1ヶ月遅かった関係で、ドキュメントでは生の`Deno.serveHttp`が使われている例がありますが、エラーハンドリング等を正しく実装している標準ライブラリを使ったほうがいいと思います。

また、2021.09.14の`v0.107.0`から2021.11.09の`v0.114.0`まで`listenAndServe`関数が存在しました。これは現在では`serve`関数に置き換えられており、`listenAndServe`関数は非推奨になっています。
`v0.117.0`以前は、第2引数に文字列で`addr`を渡してポート指定していましたが、`v0.117.0`以降は数値を`port`オプションに渡す形に変更されました。

#### WebSocketサーバー

##### 変更前

※公式ドキュメントから拝借したecho serverの例

```ts
import { serve } from "https://deno.land/std@0.106.0/http/server.ts";
import {
  acceptWebSocket,
  isWebSocketCloseEvent,
  isWebSocketPingEvent,
  WebSocket,
} from "https://deno.land/std@0.106.0/ws/mod.ts";

async function handleWs(sock: WebSocket) {
  console.log("socket connected!");
  try {
    for await (const ev of sock) {
      if (typeof ev === "string") {
        // text message.
        console.log("ws:Text", ev);
        await sock.send(ev);
      } else if (ev instanceof Uint8Array) {
        // binary message.
        console.log("ws:Binary", ev);
      } else if (isWebSocketPingEvent(ev)) {
        const [, body] = ev;
        // ping.
        console.log("ws:Ping", body);
      } else if (isWebSocketCloseEvent(ev)) {
        // close.
        const { code, reason } = ev;
        console.log("ws:Close", code, reason);
      }
    }
  } catch (err) {
    console.error(`failed to receive frame: ${err}`);

    if (!sock.isClosed) {
      await sock.close(1000).catch(console.error);
    }
  }
}

for await (const req of serve(":8080")) {
  const { conn, r: bufReader, w: bufWriter, headers } = req;
  try {
    const socket = await acceptWebSocket({
      conn,
      bufReader,
      bufWriter,
      headers,
    });
    await handleWs(socket);
  } catch (err) {
    console.error(`failed to accept websocket: ${err}`);
    await req.respond({ status: 400 });
  }
}
```

##### 変更後

※pingやエラーハンドリングなどの処理は省略

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

serve((request) => {
  console.log("socket connected!");

  // この`socket`変数はクライアント側のWebSocketオブジェクトと同様に扱える
  const { socket, response } = Deno.upgradeWebSocket(request);

  // 受け取ったデータをそのまま送り返す例
  socket.onmessage = (event) => socket.send(event.data);
  socket.onclose = (event) =>
    console.log(
      `websocket was closed because ${event.reason} (code: ${event.code})`,
    );

  return response;
}, {
  port: 80, // ポート番号80を使用（デフォルトは8000）
});
```

サーバー側でもWebSocket API（従来クライアント側で使用）が使えるようになり、書きやすくなりました。

## ベンチマーク

どれくらい速くなったのか計測してみます。

##### `Deno.serveHttp`導入前

```
Time taken for tests:   25.596 seconds
Requests per second:    390.69 [#/sec] (mean)
Time per request:       25595.659 [ms] (mean)
```

<details>
<summary>詳細</summary>
<div>

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

for await (const req of serve({ port: 80 })) {
  req.respond({
    body: "Hello World\n",
    headers: new Headers({ "Connection": "keep-alive" }),
  });
}
```

```
Server Software:
Server Hostname:        localhost
Server Port:            80

Document Path:          /
Document Length:        12 bytes

Concurrency Level:      10000
Time taken for tests:   25.596 seconds
Complete requests:      10000
Failed requests:        0
Keep-Alive requests:    10000
Total transferred:      750000 bytes
HTML transferred:       120000 bytes
Requests per second:    390.69 [#/sec] (mean)
Time per request:       25595.659 [ms] (mean)
Time per request:       2.560 [ms] (mean, across all concurrent requests)
Transfer rate:          28.62 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2  32.9      0     521
Processing:   310 11426 6672.4  11390   23455
Waiting:        9 11424 6672.3  11390   23455
Total:        310 11429 6672.5  11390   23455

Percentage of the requests served within a certain time (ms)
  50%  11390
  66%  14706
  75%  16887
  80%  18496
  90%  20714
  95%  22350
  98%  22905
  99%  23442
 100%  23455 (longest request)
```
</div>
</details>



##### `Deno.serveHttp`導入後

```
Time taken for tests:   16.297 seconds
Requests per second:    613.60 [#/sec] (mean)
Time per request:       16297.147 [ms] (mean)
```

<details>
<summary>詳細</summary>
<div>

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

listenAndServe(":80", () => {
  return new Response("Hello world\n", {
    headers: new Headers({ "Connection": "keep-alive" }),
  });
});
```

```
Server Software:
Server Hostname:        localhost
Server Port:            80

Document Path:          /
Document Length:        12 bytes

Concurrency Level:      10000
Time taken for tests:   16.297 seconds
Complete requests:      10000
Failed requests:        0
Keep-Alive requests:    10000
Total transferred:      1520000 bytes
HTML transferred:       120000 bytes
Requests per second:    613.60 [#/sec] (mean)
Time per request:       16297.147 [ms] (mean)
Time per request:       1.630 [ms] (mean, across all concurrent requests)
Transfer rate:          91.08 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1  25.0      0     527
Processing:   277 6925 3898.0   6694   14180
Waiting:        0 6924 3898.0   6694   14180
Total:        277 6926 3898.1   6694   14180

Percentage of the requests served within a certain time (ms)
  50%   6694
  66%   9038
  75%  10213
  80%  10799
  90%  12499
  95%  13597
  98%  14158
  99%  14164
 100%  14180 (longest request)
```
</div>
</details>

計測にはabコマンドを使用しました。
古いほうのサーバーは[keep-aliveの処理で失敗する](https://github.com/denoland/deno_std/issues/1112)ようなので、`-k`オプションを使って計測しました。

なので[公式のベンチマーク](https://deno.land/benchmarks#http-server-throughput)とは条件が異なるものの、このベンチマークでは**従来の約1.6倍高速化しています**。

なお、公式ベンチマークでは**従来の1.03倍、Node.jsの1.14倍高速**ということになっています。（これは標準ライブラリを使用しないコードで計測されている）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678625/206638ed-5f5d-8a24-6f9d-adbebccebe51.png)


## まとめ

- 2021年春～夏にかけてHTTPサーバー実装にrustを使うようになり、高速化されました。
- 移行にはdenoのバージョンアップとコードの書き換えが必要です。
- `Deno.serveHttp`はdeno deployでも利用できます。

高速化と同時にHTTP2対応が入っているので、新APIへの移行をおすすめします。

また従来のサーバーは`for-await-of`形式で記述するものでしたが、標準ライブラリの内部でこのループが呼ばれるようになったので、ユーザーが`for-await-of`を呼ぶ必要はなくなりました。
従来形式はtry-catchを付けるとネストが深くなってしまったり、不適切な場所に`await`を付けると遅くなることがあったりしたので、コールバック形式になったことでより簡単に記述できるようになりました。