LoginSignup
3
5

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-09-30

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

時系列

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

  • 元々deno CLIには標準ライブラリを使用したサーバーが、deno deployにはfetch eventを使用したサーバーがあった
  • パフォーマンス向上&http2対応のためdeno CLIにDeno.serveHttpDeno.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が導入される
Deno.upgradeWebSocketが導入される
2021.09.14 (Deno 1.14) Deno.upgradeWebSocketが安定する
2021.09.14 (Deno std 0.107.0)
  • http/serverモジュールがDeno.serveHttpを使うように変更
    • →CLI, deployの両方でhttp/serverモジュールが利用可能に
  • 従来のhttp/serverモジュールはhttp/server_legacyに移動し非推奨化
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サーバー

変更前
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" });
}
変更後
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の例

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やエラーハンドリングなどの処理は省略

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)


詳細
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)


Deno.serveHttp導入後
Time taken for tests:   16.297 seconds
Requests per second:    613.60 [#/sec] (mean)
Time per request:       16297.147 [ms] (mean)


詳細
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)


計測にはabコマンドを使用しました。
古いほうのサーバーはkeep-aliveの処理で失敗するようなので、-kオプションを使って計測しました。

なので公式のベンチマークとは条件が異なるものの、このベンチマークでは従来の約1.6倍高速化しています

なお、公式ベンチマークでは従来の1.03倍、Node.jsの1.14倍高速ということになっています。(これは標準ライブラリを使用しないコードで計測されている)
image.png

まとめ

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

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

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

3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5