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 が導入されるDeno.upgradeWebSocket が導入される |
2021.09.14 (Deno 1.14) |
Deno.upgradeWebSocket が安定する |
2021.09.14 (Deno std 0.107.0) |
|
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倍高速ということになっています。(これは標準ライブラリを使用しないコードで計測されている)
まとめ
- 2021年春~夏にかけてHTTPサーバー実装にrustを使うようになり、高速化されました。
- 移行にはdenoのバージョンアップとコードの書き換えが必要です。
-
Deno.serveHttp
はdeno deployでも利用できます。
高速化と同時にHTTP2対応が入っているので、新APIへの移行をおすすめします。
また従来のサーバーはfor-await-of
形式で記述するものでしたが、標準ライブラリの内部でこのループが呼ばれるようになったので、ユーザーがfor-await-of
を呼ぶ必要はなくなりました。
従来形式はtry-catchを付けるとネストが深くなってしまったり、不適切な場所にawait
を付けると遅くなることがあったりしたので、コールバック形式になったことでより簡単に記述できるようになりました。