generated at
denoのhttp serverが新しくなりました
先々週末にdeno_std http モジュールの再設計を行ってたkeroxp2019/2/19
deno_stdのhttpモジュールはstdの中でも最古(2ヶ月前くらい)に書かれたコードベース
自分がdenoを触り始めた昨年末、その最たるきっかけになったのがこの http モジュール
「deno? どうせまだ動か……なんだコイツ!」となったのが、httpのREADMEにあったsampleだった
その後二ヶ月の間にこのhttpモジュールを使ったライブラリが生まれて盛り上がり見せた
個人的にdenoのhttpサーバで新しさを感じたのは AsyncIterableIterator でリクエストを処理するところ
ts
for await (const req of serve("0.0.0.0:8080")) { req.respond({ body: new TextEncoder().encode("OK") }) }
こういうコードになっていて興味を惹かれた
Node.js時代、httpリクエストは server.on("request", req => {...}) というイベント駆動だった
DenoではEventEmitterは推奨されていない
async/await が基本
でも実際、非同期IOを行う場合はイベントっぽい書き方が必要になる
歴史を振り返ると、どうやらstdのhttpモジュールはRyan自身が書いたコードだったのだが、
知らなかった…keroxp
こういう場面で AsyncIterableIterator (Promiseを返すGenerator)と for await 構文が使われていてへ〜となった
のだけど、自分はこのasync iterationがベストなスタイルだとは思わなくなった
なぜかというと、iteratorを await しているときそのasync関数はブロックしてしまうからだ
また、AsyncIteratorはそのままだとgeneratorをawaitしているときそれをcancelする手段がない
一度iterationを始めたasync iteratorを止めたいという場面はあった
例えばテスト
serveは最終的にlistenから Listener を作り、listener.acceptで新規接続をwhile loopの中でawaitし続ける
このacceptRoutineを外部から止める手段が今までなかった
新しいhttpモジュールの serve はこうなります
ts
import {serve} from "https://deno.land/x/std/http/server.ts" import {defer} from "https://deno.land/x/std/util/deferred.ts" import {encode} from "https://deno.land/x/std/strings/strings.ts" // #1 const cancel = defer() setTimeout(cancel.resolve, 10 * 1000) // 10秒後にserveを止める (async function () { // #2 for await (const {req, res} of serve("0.0.0.0:8080", cancel.promise) { await res.resond({ status: 200, headers: new Headers({ "Content-Type": req.headers.get("Content-Type") }), body: req.body }) } })()
いくつかのポイントが有るので解説
1: Deferred APIの追加
defer()でcancellerを作ります
deferは、手動でresolve/reject可能なpromiseとresolver/rejectorを返してくれます
ts
export type Deferred<T = any, R = Error> = { promise: Promise<T>; resolve: (t?: T) => void; reject: (r?: R) => void; readonly handled: boolean; };
serve の第二引数がそのcancellerのpromiseを受け取れるようになりました
serveが返すasync iteratorは、毎回cancellerと Promise.race で争われ、cancellerがresolveされた場合このfor awaitを安全にbreakします
このcancellerはオプショナルで、なにも渡さないと常に listener.accept() が勝つようになります
テストなどではsetupでサーバーを立てて、teardownでcancellerを解決するなどができるようになりました
2: serveのイテレーション型の変更、ServerRequest, ServerResponderの分離
serve のイテレータの型が変わりました
もともと ServerRequest のasync iteratorでしたが、こうなりました
ts
export async function* serve( addr: string, cancel: Deferred = defer() ): AsyncIterableIterator<{ req: ServerRequest; res: ServerResponder }>
どこかで見慣れたこの2つの組み合わせになりました
ts
for await (const {req, res} of serve("0.0.0.0:8080")) { }
こうやってデストラクチャするのがいい感じ
またいままで ServerRequest オブジェクトがリクエストとレスポンダを兼任していましたが、この変更で以下の2つに分離されました
ts
export type ServerRequest = { url: string; method: string; proto: string; headers: Headers; match: RegExpMatchArray; body: Reader; }; export interface ServerResponder { respond(response: ServerResponse): Promise<void>; respondJson(obj: any, headers?: Headers): Promise<void>; respondText(text: string, headers?: Headers): Promise<void>; readonly isResponded: boolean; }
reqはimmutableなリクエストオブジェクト
resはレスポンスを返すためのオブジェクトという感じ
この変更の一番の理由は ServerRequest を非class化したかったから
ServerRequestがclassのままだとポリモーフィックにかけない部分が増えてくるのでた
ついでに respondJson() respondText() も追加した
No More new TextEncoder()
今はこんな感じだけど、将来的にはExpressのResponseみたいになると思う
また、ユーザがrespondしなかった場合もクライアントに500を返すようになりました
今までの仕様だと永遠にレスポンス返ってこなかった
HttpServerの新設
これが一番の大きな変更です
上記のような理由もあり、 serve はhttp serverの最下層のAPIという感じだった
のだがもう少し中間層のServer APIがあってもいいかなと思っていた
ので HttpServer というAPIを追加した
見たほうが早い
http_server.ts
import { createServer } from "https://deno.land/x/std/http/server.ts"; // サーバー作成 const server = createServer(); // ルート登録(文字列) server.handle("/", async (req, res) => { await res.respondText("Hello Deno!"); }); // ルート登録(正規表現) server.handle(new RegExp("/foo/(?<id>.+)"), async (req, res) => { // 名前付きキャプチャからparmsを取得(後述) const { id } = req.match.groups; await res.respondJson({ id }); }); // 起動 server.listen("127.0.0.1:8080");
1: createServer
HttpServer はInterfaceで、 createServer はそのファクトリです。実態はクラスですが。
ts
export interface HttpServer { handle(pattern: string | RegExp, handler: HttpHandler); listen(addr: string, cancel?: Deferred): Promise<void>; }
これはGoのServerMuxに影響を受けています
http.handleFunc とか
ServerMux.Handle()は、文字列しか引数に取れずprefix matchしかしてくれないのだが、自分は RegExp も渡せていいんじゃないかと思いこうデザインした
なおこれについてはRyanと少し議論があった
ry曰く、
> I would very happily have a ServeMux class here that implemented the above routing logic.
> そういう(prefix matchだけ)ルーティングロジックを実装したServerMuxがあるといいんだけどなー
keroxp曰く、
>I don't think that it is much complicated...routing with string or regex can be used for general purpose. serve() should be considered as the lowest http server api.
> これそんなに複雑じゃないと思うんだけど…文字列と正規表現でルーティングするの、一般的用途に使えると思う。 serve() こそ最下層のAPIとして使われるべきでは?
これについては少しRyanに妥協してもらう形になったかもしれない😅
引数を文字列か正規表現にしたのは、正規表現の名前付きキャプチャが実装された - JS.next というのを知ったからだ
最初はExpressスタイルのpath-to-regexpをそのまま入れようとしたのだが、ES2018の正規表現を使うとPure ESでこういうのができる
ts
server.handle(new RegExp("/users/(?<id>\d+)", (req, res) => { const {id} = req.match.groups; res.respondJson({id}) })
Expressでは慣れた操作だけど、RegExpだけでできるのはちょっと面白い
ServerRequest match プロパティは req.url.match(pattern) (意訳)の返り値になっている
ので、名前付きキャプチャをしなくても正規表現的な抜き出しが可能
これ地味にシンプルで強力な実装だと思う
これでDenoがhttpサーバーを立てるのに一番簡単な選択肢に躍り出てきたんじゃないかと密かに思ってます
JS/TSで書ける
標準ライブラリに最低限+αのHTTPサーバーAPIがある
パッケージマネージャいらない
babelいらない。最新ES仕様に対応
Node.jsユーザなら見慣れたサーブスタイル
ただまだDeno本体は全然開発途上で、やっぱりいまDenoで何かを作るのはオススメしません
今回の変更も結構なbreak cahngeなので…
今回の変更で serve() の内部ロジックも変えたんだけど、検証の結果遅くなってしまったのでrevertされたり…
Deno側の最適化、ts側の最適化がまだまだ不十分だし、Httpサーバーとしての機能もまだまだ不完全です
Http2未対応
TLS未対応
Keep-Alive未対応(?)
gzip未対応
Cookie未対応
Trailerヘッダ未対応
その他HTTP/1.1の隠し仕様を誰も把握してない可能性あり
でもこれからどんどん対応してくと思う
Denoのこれからにご期待下さい
あ、技術書展6でDeno本を出すのでオナシャス!