サーバ編 |
*** Dart 2 の解説:サーバ・サイドのアプリケーション ***
ここではDart 2ベースのサーバ・アプリケーション開発を目指す読者の皆さんを対象に、必要な基本的な事柄を解説する。このサイトでは従来「プログラミング言語Dartの基礎」の後半でサーバ関連の事項を扱っていた。このサーバ編ではこの内容をベースにDart 2対応させ、またその後パブリッシュされたサーバ関連のライブラリも紹介するように配慮した。
注意:この編では読者はこの解説書の言語編、ライブラリ編、及び開発編をある程度理解されていることを前提としている。
注意:読者はGoogleのアカウントとgmailアドレスを持っている(Android端末の所有者は通常そうなっている)ことが好ましい。これはDart開発のディスカッションに参加したり、自分の作ったライブラリをPubにパブリッシュしたりする際に必要である。またDartPadやGithubで自分のコードを共有できるようにするときなどでもgmailを介すとなにかと有用である。
注意:Dartでは内部ではUnicodeを、ファイルにはUTF-8が使われている。従ってテキスト・エディタ等の自分のツールはUnicodeでの表示(コピーと貼り付け)とUTF-8でのファイル読み書きができるようにしておいたほうが好ましい。
HTTPサーバ開発にはHTTPプロトコルを理解しておく必要がある。筆者の改訂サーブレット・チュートリアルの第2章の一読をお勧めする。
また読者のIDE上には、「IDE」の章の「dart2_code_samplesのダウンロード」の節で示した手順に示した手順に基づき、この章に記載されているコードが既にC:\dart2_code_samples\http_serversのフォルダの中に収容されている筈であるので、各自これらのサンプルを試してみることをお勧めする。
サーバ・サイドでDartを使うには、dart:ioライブラリを使用する。特にHTTPサーバが良く使用されると思われるので、この章ではこのライブラリに含まれているHttpServerクラスについて手短に説明する。
HttpServerクラスはサーブレット・コンテナのDart版ともいえる。従ってクライアントからの要求が到来するごとにHttpRequestとHttpResponse(HttpRequestの属性として)のオブジェクトが渡される。サーブレットと違う点はその要求に対する処理と応答がイベント・ベースになっていることである。
DartのHttpServerクラスは、HTTPプロトコルによるサーバの為の基本的な手段を提供しているだけで、いろんなツールはいずれ誰かが用意してくれるという考え方であるので、Dartの基本思想に準じシンプルである。セッション管理は筆者の指摘で追加されたが、その他の機能はユーザがを自分で用意するかPubを探す必要がある。例えばPubには以下のものが登録されている:
http_server : HttpServerと組み合わせてより高レベルのクラスたちが用意されており、静的コンテント提供(いわゆるウェブ・サーバ)に最適
shelf : ミドルウエアで、ウェブ・サーバの開発をより簡単化する
http : HTTP要求の為のFutureベースのライブラリ
path : いろんなプラットホーム上でファイルパスを取得できる
HTTPプロトコルにおいて、HTTPサーバ側及びブラウザ等のクライアント側(能動的にHTTPサーバに要求を開始する場合)で各クラスは以下の機能を持つ:
HttpRequestとHttpClientResponseのオブジェクトはStream<List<int>>を実装する。つまりバイト列のデータ・イベントとしてボディ・データを渡す。
HttpClientRequestとHttpResponseオブジェクトはIOSinkを実装する。つまりボディ・データを受け取りネットワークに送信する。
クライアントからのHTTP要求を受け付けるHttpServerはstaticなメソッドのbindで生成される。
HttpServerはHttpRequestのストリームとして機能する。つまりHttpRequestのオブジェクトをデータ・イベント・ベースで渡す。
HTTPサーバにおける各クラスの役割は下図のようである:
|
HttpServerはその要素がHttpRequestであるストリームでもある。HttpServerのあるオブジェクトに対しlistenメソッドによりその要素(即ちHTTP要求)を受理する受信(StreamSubscription)を付加する。そうするとその受信に対し到来したHTTP要求に対応したオブジェクトがイベントとして渡される。
HttpRequestもまたその要素がHTTP要求のボディ部のバイト列であるストリームでもある。どうしてボディ・データがストリームとして供給されるのか不思議に思われるかも知れないが、長いボディ・データはチャンクとして送信されてくるからである。このストリームに対しlistenメソッドによりバイト列を取り込む。全部のデータが受理されたかどうかはlistenメソッドのonDoneで知ることができる。必要ならStringに変換するコンバータをtransformメソッドで付加することもできる。HTTP要求のヘッダ部やセッション、そしてHttpResponseなどはHttpRequestオブジェクトの属性として取得できる。
HTTP応答を返す為のHttpResponseはHttpRequestオブジェクトの属性として取得される。これは(request, response)のパラダイムのJava Servletに馴染んだ読者には新鮮に映るかもしれない。
HttpResponseはIOSinkを実装しているが、そのIOSinkはバイト列を取り込むStreamConsumer<List<int>, T>を実装している。従ってaddやaddStringメソッドによりバイト列としてHTTP応答のボディ部を受け付ける。ヘッダ部は別途HttpResponseオブジェクトの属性として設定できる。
HttpSessionもまたHttpRequestオブジェクトの属性として取得できる。HTTP要求処理はこのオブジェクトを使ってクライアント間のセッションを管理できる。
基本的なHttpServerの作り方は次のようである:
async関数を使った場合
\bin\simple_server_1.dart
|
|
Future APIを使った場合:
\bin\simple_server_2.dart
|
|
IDE上でこれらのサーバのうちのどれかを選択して起動し、ブラウザからhttp://localhost:8080でアクセスすれば下図のように'Hello, world!'のテキストが表示される筈である。
|
サーバは外部から停止させるか何らかの障害が起きない限り走り続ける。
Staticメソッドのbindは指定されたIPアドレスとポート番号でのクライアント側からのHTTP要求の到来待ちを開始する。条件を満たす要求が到来したら、該要求オブジェクトのストリームとしてそのfutureの受理側に渡す。その引数は以下のようである:
Future<HttpServer> bind (dynamic address, int port, {int backlog: 0, bool v6Only: false, bool shared: false})
オプショナルなパラメタの詳細はAPIを見て頂きたい。最初のパラメタはホスト名を指定する。特定のホスト名またはIPアドレスをStringで指定する。代わりに InternetAddressクラスが持っているあらかじめ定義された値を使うこともできる。
値 |
使用法 |
loopbackIPv4 または loopbackIPv6 |
サーバはクライアントからのHTTP要求をループバックアドレスでリスンする:これは実行的にlocalhostである。IP V4またはV6のどちらかを使う。これらは通常テスト用である。 |
anyIPv4 または anyIPv6 |
サーバはクライアントからのHTTP要求を任意のIPアドレスの指定されたポート上でリスンする。IP V4またはV6のどちらかを使う。 |
デフォルトでは、V6のインターネット・アドレスを使っていると、V4のリスナも同様に使われる。
このサーバは await forを使って到来HTTP要求のストリームをリスン開始する。各要求が到来するごとに、このコードは“Hello, world!”という文字列の応答をクライアントに送信する。HttpResponseのオブジェクトはHttpRequestのオブジェクトの属性として取得される。
HttpRequestクラスは到来したHTTP要求を表現している。従ってHTTP要求メッセージの概要をまず知っておく必要がある。
HTTPプロトコルではクライアントからのHTTP要求とそれに対するサーバからのHTTP応答のメッセージは似たような構造になっている:
|
HttpRequestクラスではその要求メッセージをもとに以下のようにその情報を取り出している:
|
具体的にHttpRequestを使ってどのような情報が取り出されるかをdump_http_requestというアプリ(IDE上ではC:\dart2_code_samples\http_servers\dump_http_request)で確認することとする。
\dump_http_request\bin\dump_http_request.dart
|
|
\dump_http_request\web\send_form_data.html
|
<!-- Send HTTP test request to the DumpHttpRequest server --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>send_form_data</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> </head> <body> <H1>Send text area value to the server</H1> <form method="post" action="http://localhost:8080/DumpHttpRequest" enctype="text/plain"> <!-- replace upper line with : <form method="post" action="http://localhost:8080/DumpHttpRequest"> and compare the result (default enctype is "application/x-www-form-urlencoded") --> <textarea rows="5" cols="80" name="submitPost"></textarea><br> <input type="submit" value="Submit using POST"> </form> <br> <form method="get" action="http://localhost:8080/DumpHttpRequest"> <textarea rows="5" cols="80" name="submitGet"></textarea><br> <input type="submit" value="Submit using GET"> </form> <br> <H3>Send button clicked event using GET :</H3> <form method="get" action="http://localhost:8080/DumpHttpRequest"> <input type="submit" value="Submit using GET" name="clicked"> </form> </body> </html> |
IDE上でまずdump_http_request.dartを開始させる。次にsend_form_data.htmlのコードを右クリックし、Copy Filepathでコピーしたパスをブラウザのアドレス・バーに貼り付けアクセスすると下図のような画面が得られる:
|
二つのテキストエリアには筆者が入力した記事が入っている。このページは3つのフォームの為のボタンがある:
最初は上のテキストをPOST要求の形式でサーバに送信する
2番目のボタンはそのテキストをGET要求の形でサーバに送信する
最後のボタンはボタンが押されたことをGET要求でサーバに送信する。
一番上のボタンをクリックするとサーバは次のような画面を返してくる:
|
各ボタンでどのような画面が返されてくるかを確認して頂きたい。3番目のボタンを押すと、GET要求なのでその要求パラメタはクエリ文字列としてアドレス・バーに表示される:
|
入力ストリームはクライアントのテキスト・エリアからの比較的長いテキストやファイル・アップロードなどのデータを受理する為に使われる。転送形式はテキストであればURLエンコードされた各種エンコーディングの文字列データ、あるいは単純なUTF-8形式のバイト列であるが、画像などの場合はバイナリが使われる。
それらのデータはHTTPメッセージのボディ部に置かれ、その転送形式や長さはヘッダ行で知らされる。
HttpRequestはStream<List<int>>を継承している。つまりこのオブジェクトからボディ部のデータをバイト列として受理できるようになっている。このオブジェクトに対し:
Stream transform(StreamTransformer<T, dynamic> streamTransformer)でバイト列をユニコードのリストに変換する。StreamTransformerのサブクラスにはStringDecoderがあり、これのコンストラクタはデフォルトでUTF-8だとしてデコードしてくれる。UTF-8は最も一般的なエンコーディングであるが、それ以外にもASCII、ISO_8859_1(これはサーブレットのデフォルト)、及びSYSTEMが現在用意されている。しかしながら日本でよく使われるWindows-31J(Shift-JIS)は含まれていない。
このコンバータが付加されたStreamに対し、listenメソッドでsubscription(受信受信)が用意され、データが読みとられる。受信ハンドラは:
abstract StreamSubscription<T> listen(void onData(T event), {void onError(AsyncError error), void onDone(), bool unsubscribeOnError})
となっている。
HttpServerはこの入力ストリームを介して次のようなイベントを渡す:
入力ストリームに受信デーが存在し、読み出し可能になった。 (onData)
エラーが発生した。 (onError)
データを総て受信し、データの総てが読みだされた。 (onDone)
従ってこれらの3つのハンドラを使って正しくデータを読みだす必要がある。
なお、データが総て受信したことは、次の状態でこのサーバが知ることができる:
所定の長さのバイトを読みだした。
チャンク形式の場合は長さ0のチャンクを受信した。
TCP接続が切れた。
一般的な使い方は\dump_http_request\bin\dump_http_request.dartを見て頂ければ良い。
001 void requestReceivedHandler(HttpRequest request) { 002 HttpResponse response = request.response; 003 String bodyString = ""; // request body byte data 004 var completer = new Completer(); 005 if (request.method == "GET") { completer.complete("query string data received"); 006 } else if (request.method == "POST") { 007 request 008 .transform(new StringDecoder()) 009 .listen( 010 (String str){bodyString = bodyString.concat(str);}, 011 onDone: (){ 012 completer.complete("body data received");}, 013 onError: (e){ 014 print('exeption occured : ${e.toString()}');} 015 ); 016 } 017 else { 018 response.statusCode = HttpStatus.methodNotAllowed; 019 response.close(); 020 return; 021 } |
003行目: bodyStringは読みだした総ての文字列である。
004行目: completerはボディ部からのデータ読み出しが完了したことを、次の処理のトリガとする為のCompleterオブジェクトである。
005行目: 該要求がGETのときは入力ストリームは使わないので、直ちにcompleter.completeを呼ぶ。
007-021行目: この部分がPOST要求でのボディ部読み出し部分である。
008行目: transform(new StringDecoder())でストリームにコード・コンバータをバインドし、UTF-8エンコーディングで文字列を受け付けるStringの形式の入力ストリームを生成する。
010行目: onDataのハンドラである。読みだしたデータ(この場合は文字列)をこれまでに読みだされたデータと連結する。
011-012行目: onDoneのハンドラである。この場合は読み出しが完了したことになるので、completer.completeでこれで待機している処理を起動させる。
013-014行目: onErrorのハンドラである。このときは例外を出力する。必要ならクライアントに500 (Internal Server Error)応答を返すようにもできよう。
017-021行目: GETまたはPOST以外の要求が来たときは中身が空の応答を返しているが、これも何らかの応答(例えば405 Method Not Allowedというメッセージ)を返すようにも出来よう。
localhostはいわゆる「ループバック・アドレス」であり、単一のコンピュータ上でクライアント(ブラウザ)とサーバを構成してテストするには便利なアドレスである。しかしながら'localhost'という名前はIPv4でもIPv6でも使われている。通常はOSに応じてどちらかが選択されるので、このことはあまり気にする必要はないが、間にプロキシをかませたり、IPv4またはIPv6で確実に動作させたいときには、注意が必要である。
明示的に確実にIPv4またはIPv6で動作させたいときは次のような記述を使う必要がある:
|
IPv4 |
IPv6 |
サーバのアドレス |
InternetAddress.loopbackIPv4 または"127.0.0.1" |
InternetAddress.loopbackIPv6 または"[::1]" |
ブラウザからのアクセス例 |
http://127.0.0.1:8080/.... |
http:[::1]:8080/.... |
HttpResponseクラスでは以下のように与えられた情報をもとに応答メッセージ組立て、それをネットワークに送信している:
|
HttpResponseクラスはクライアントに返すHTTP応答を抽象化したものである。HTTP応答のヘッダ部はこのクラスのフィールドたちを使ってヘッダ部とボディ部を書き込む。ボディ部はこのオブジェクトがIOSinkでもあるので書き込みが便利になった。このオブジェクトは該応答のHTTPヘッダ設定の為の一連の属性を持っている。該ヘッダが設定されたら、HTTP応答の実際のボディ部に書き込むのにIOSinkのメソッドたちが使えるようになる。このIOSinkのメソッドのどれかが初めて使われると、応答ヘッダ部はネットワークに送信される。それが送信された後でのヘッダ部を変更するメソッド呼び出しには例外がスローされる。IOSinkを介して文字列データを書き込む際は、Java Servletと同じように、エンコーディングは"Content-Type"ヘッダの"charset"パラメタによって決まる。
応答オブジェクトを使ってクライアントにHTTP応答を送信する基本的な使い方は、dump_http_request.dartのコードを読めば理解できよう。
必要に応じHttpResponse.headersを使ってHttpHeadersオブジェにクトに値をセットすることで、HTTP応答のヘッダ部をセットする。
HttpResponse.addStringボディ部にデータを書き込みネットワークに送出させる。この際ヘッダ部分が直ちにネットワーク側に送信され、その後のヘッダ部分の変更は出来なくなるので注意しなけらばならない。
一般的にはHttpResponse.writeを使って文字列を所定の文字セットでデータを書き込む。writeのデフォルトのエンコーディングはJava Servletと同じくISO-8859-1 (Latin 1) である。必要なら"Content-Typeヘッダでこれを変更して別のエンコーディングを適用することもできる。
総てのデータの書き込みが終了したら、その出力ストリームを閉じることで、HTTP応答の送信処理が終了する。
ネットワークへのヘッダ部とボディ部の送信が終了したことは、OutputStream.doneで知ることができる。
Dart v1.6 ではDartで開発されたHTTPサーバがよりセキュアのものとするためにヘッダおよびクッキー設定にデフォルトを用意し、ベスト・プラクティスに従うようにした。
HttpServerにdefaultResponseHeadersという新しいフィールドが追加された。これは推奨ヘッダたちを集めたものである。各要求に対応したHttpResponseはdefaultResponseHeadersからのヘッダが使われる。この値を変更したければHttpResponse.headersでこれらの値を追加または変更する。
またHttpHeadersに明確なメソッドたちが付加された。これによりdefaultResponseHeadersのすべての値をクリアしたり、このヘッダのオブジェクトの他のインスタンスをクリアしたりできる。
極力デフォルト値を常に使い、必要に応じ特定の応答だけに個々のヘッダを変更・追加するようにすることが好ましい。
応答ヘッダのステータス行は、HttpHeadresで特に指定しない限り"200 OK”がセットされる。それではそれ以外のステータスを設定したら、HttpServerインターフェイスはどのような応答をするのだろうか?
次のようなサーバ(status_line_test.dart、IDE上ではC:\dart2_code_samples\http_servers\status_line_test)を実行させてみよう。このサーバはいろんなステータスをセットして応答を返すと、ブラウザがどのような応答が返されるかを調べるためのものである。すなわちブラウザ側のHTTPステータス選択ラジオボタンのひとつを選択してサブミット・ボタンをクリックすることで、いろんなステータス行のHTTP応答を返させることができる。
\status_line_test\bin\status_line_test.dart
|
|
\status_line_test\web\status_line_test.html
|
<!-- Request StatusLineTest.dart server to sent a status line --> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>StatusLineTest</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> </head> <body> <H1>Request to send a status line</H1> <br>For details, refer to < a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10">RFC-2616 Sec. 10</a>.<br><br> <form method="get" action="http://localhost:8080/StatusLineTest"> <input type="radio" name="raioButton" value="100" /> 100 Continue<br> <input type="radio" name="raioButton" value="101" /> 101 Switching Protocols<br> <input type="radio" name="raioButton" value="200" /> 200 OK<br> <input type="radio" name="raioButton" value="201" /> 201 Created<br> <input type="radio" name="raioButton" value="202" /> 202 Accepted<br> <input type="radio" name="raioButton" value="203" /> 203 Non-Authoritative Information<br> <input type="radio" name="raioButton" value="204" /> 204 No Content<br> <input type="radio" name="raioButton" value="205" /> 205 Reset Content<br> <input type="radio" name="raioButton" value="206" /> 206 Partial Content<br> <input type="radio" name="raioButton" value="300" /> 300 Multiple Choices<br> <input type="radio" name="raioButton" value="301" /> 301 Moved Permanently<br> <input type="radio" name="raioButton" value="302" /> 302 Found<br> <input type="radio" name="raioButton" value="303" /> 303 See Other<br> <input type="radio" name="raioButton" value="304" /> 304 Not Modified<br> <input type="radio" name="raioButton" value="305" /> 305 Use Proxy<br> <input type="radio" name="raioButton" value="307" /> 307 Temporary Redirect<br> <input type="radio" name="raioButton" value="400" /> 400 Bad Request<br> <input type="radio" name="raioButton" value="401" /> 401 Unauthorized<br> <input type="radio" name="raioButton" value="403" /> 403 Forbidden<br> <input type="radio" name="raioButton" value="404" /> 404 Not Found<br> <input type="radio" name="raioButton" value="405" /> 405 Method Not Allowed<br> <input type="radio" name="raioButton" value="406" /> 406 Not Acceptable<br> <input type="radio" name="raioButton" value="407" /> 407 Proxy Authentication Required<br> <input type="radio" name="raioButton" value="408" /> 408 Request Timeout<br> <input type="radio" name="raioButton" value="409" /> 409 Conflict<br> <input type="radio" name="raioButton" value="410" /> 410 Gone<br> <input type="radio" name="raioButton" value="411" /> 411 Length Required<br> <input type="radio" name="raioButton" value="412" /> 412 Precondition Failed<br> <input type="radio" name="raioButton" value="413" /> 413 Request Entity Too Large<br> <input type="radio" name="raioButton" value="414" /> 414 Request-URI Too Long<br> <input type="radio" name="raioButton" value="415" /> 415 Unsupported Media Type<br> <input type="radio" name="raioButton" value="416" /> 416 Requested Range Not Satisfiable<br> <input type="radio" name="raioButton" value="417" /> 417 Expectation Failed<br> <input type="radio" name="raioButton" value="500" /> 500 Internal Server Error<br> <input type="radio" name="raioButton" value="501" /> 501 Not Implemented<br> <input type="radio" name="raioButton" value="502" /> 502 Bad Gateway<br> <input type="radio" name="raioButton" value="503" /> 503 Service Unavailable<br> <input type="radio" name="raioButton" value="504" /> 504 Gateway Timeout<br> <input type="radio" name="raioButton" value="505" /> 505 HTTP Version Not Supported<br> <br> <input type="submit" value="Submit using GET" /> </form> </body> |
IDE上でまずstatus_line_test.dartを開始させる。次にstatus_line_test.htmllのコードを右クリックし、Copy Filepathでコピーしたパスをブラウザのアドレス・バーに貼り付けアクセスすると下図のような画面が得られる:
|
選択ラジオボタンのひとつを選択してサブミット・ボタンをクリックすることで、いろんなステータス行のHTTP応答をサーバから返させ、ブラウザの反応を調べる。ブラウザに依って応答が多少異なる場合がある。
HTTP応答メッセージのボディ部へのデータの書き込みの為にHttpResponseはIOSink<HttpResponse>を実装しており、そのIOSinkはStreamConsumer<List<int>, T>を実装している。即ちバイト列としてボディ部をネットワークに送信できる。またなお2013年3月にIOSinkインターフェイスがStringSinkを実装するよう変更された。従ってwrite、writeln、writeAll及びwriteCharCodeメソッドを使って文字列をIOSinkに書き込むにはencoding属性を使ってエンコーディングを指定する。HttpResponse及びHttpClientRequestのIOSinkでは、Java Servletと同じようにContent-Typeヘッダのcharsetパラメタをもとにエンコーディングが選択される。デフォルトのエンコーディングはこれもJava Servletと同じで ISO_8859_1となった。
送信するデータには画像のようなバイナリ・ファイルとHTMLテキストのような文字列の形式がある。文字列データの送信にはHttpResponseのwriteを、バイト・データの送信には通常pipeメソッドを使用する。どちらも送信形式はバイト数指定、即ちcontent-length:ヘッダで長さを相手に知らせる方式と、HTTP/1.1で導入されたtransfer-encoding : chunkedヘッダでそれを相手に知らせるチャンク形式が利用できる。当初の実装はチャンクの為のバッファもない状態だったが、筆者の指摘でその1年後にしっかりしたものとなっている。
テキストの送信は、読者は既にこれまでのサンプルを見て理解されている筈である。バイナリ・データの送信については追って「ファイル・サーバ」の章で示すことにする。
HTTP応答にはデフォルトの応答ヘッダが通常セットされる。それ以外の場合は必要に応じて応答ヘッダを変更する。
よく使われるのが応答ボディ部をGZIP圧縮してクライアントに渡す手段である。2014年8月のDart v1.6からはHttpServer.autoCompressをtrueにすると、該応答のボディ部はGZIP圧縮され、それに応じて応答ヘッダ部のcontentLengthヘッダもセットされるようになった(それ以前はすべての応答がGZIP圧縮されていた)。但し条件としては:
クライアントがGZIP圧縮を受けるという要求ヘッダで要求してきた(通常殆どのブラウザがそうなっている)
クライアントがHTTP/1.1で要求してきた(これも通常殆どのブラウザがそうなっている)
HttpHeaders.chunkedTransferEncodingがtrueにセットされていること(クライアントがHTTP/1.1で要求してきたときはそうなっている)
例えば以下のサーバ(IDE上ではC:\dart2_code_samples\http_servers\bin\simple_gzip_server.dart)を実行させてみよう:
\bin\simple_gzip_server.dart
|
|
これは単にsimple_server_1に1行を追加しただけである。これを走らせてもsimple_server_1と全く相違なく動作する。しかし実際はネットワーク上はボディ・データは圧縮されたバイト列になっている。これは次節のプロキシ・サーバを使って確認することとする。
HttpServerを使ってクライアントと交信しているときには一体ネットワーク上ではどのようなデータが流れているのか知る必要が出る場合もある。そのようなときはプロキシが役に立つ。プロキシ・サーバをサーバとクライアントの間に置くと上りと下りの双方のデータをプリントしてくれる。
|
以下はそのコードである(IDE上ではC:\dart2_code_samples\http_servers\bin\simple_proxy.dart):
\bin\simple_proxy.dart
|
|
simple_proxy.dartのプログラムはまた、ソケット・レベルでの通信のプログラム作成の良いサンプルにもなるので、興味のある読者はこのプログラムのウオークスルーをお勧めする。一般にブラウザは複数のTCP接続を使ってウェブ・サーバとやり取りしている。したがって接続ごとにデータ送受信のためのコールバック関数を用意してやらねばならない。また現在ハンドルしている接続たちを管理することが必須である。
まず以下の2つのサーバを立ち上げる:
\http_servers\bin\simple_proxy.dart
\http_servers\bin\simple_server_1.dart
次にブラウザから次のようにアドレスバーに入力する:
http://localhost:12345/
そうするとIDE上のプロキシ画面には下図のように上りと下りの双方向のデータが表示される:
|
サーバからの応答は次の2つのチャンクで送信されていることがわかる:
15:45:44.104880 : ** connection (199350695, 337576809) outbound traffic ** HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 x-frame-options: SAMEORIGIN x-xss-protection: 1; mode=block transfer-encoding: chunked x-content-type-options: nosniff |
|
最初のチャンクはヘッダ部である。次のチャンクはボディ部である。Dはバイト数を意味し、最後の0はデータの終わりを意味する。
それではサーバをGZIPでボディ部を送信するsimple_gzip_serverに切り替えてみるとどうなるだろうか。
16:03:37.057844 : ** connection (268248386, 196862498) outbound traffic ** HTTP/1.1 200 OK content-type: text/plain; charset=utf-8 x-frame-options: SAMEORIGIN x-xss-protection: 1; mode=block transfer-encoding: chunked x-content-type-options: nosniff content-encoding: gzip |
|
GZIPされたデータの最初は10バイトのヘッダがつき、その後がデータになっている。このような短いテキストではGZIP変換する意味はない。サイズの大きなHTMLテキストなどでは大きな効果を発揮する。
Dartでは発生した例外は、そのコードを呼び出した側に伝搬する。最終的にどの呼び出し側でもその例外を捕捉しないと、サーバは停止する。サーバ・アプリケーションでは、サーバの停止は極力避けねばならない。従って、しかるべき場所で例外を捕捉し、エラー・ページでそれをクライアントに通知するのが一般的な対処法である。エラー・ページはJSPではデフォルトのページが用意されているが、Dartでは自分で用意することになる。
一般的なサーバの記述は次のようなものになろう(IDE上ではC:\dart2_code_samples\http_servers\bin\non_stop_server_1.dart):
\bin\non_stop_server_1.dart
|
|
到来した要求オブジェクトに対する処理は独立した関数(ここではrequestHandler)とし、その中で発生するエラーはtry-catchで捕捉し、その処理は呼び出し側には伝搬させないようにする。エラーが発生したらここでは単にテキストを返しているが、エラー・ページを使ってその内容をクライアントに知らせるのが親切である。
なお、アプリケーションによってはこのrequestHandlerのなかでFutureやStreamのオブジェクトを使用する場合もあろう。この場合はそれらのオブジェクトで発生したエラーはonErrorやcatchErrorで捕捉し、これをExceptionのオブジェクトとしてスローする必要がある:
|
そうすればこれはrequestHandlerのtryブロックのcatch文で捕捉される。
それではサーブレット・コンテナに相当する部分であるHttpServerではエラーはどう処理されているのだろうか?Googleの技術者のAnders Johnsenは次のように説明している:
HTTP要求のリスン中はエラーは発生しない。不完全なHTTPヘッダがあればそれは無視され、当該ソケットはクローズされる。
完全なHTTP要求が受信されたら、HttpRequestオブジェクトが生成される。ボディ部分のリスン中(例えばrequest.listen(...)とり出し中)に、もしそのボディ全部が受信される前に当該ソケットが閉じてしまったときはエラーが生成される。ここではソケットが閉じられるが、このエラーはユーザに対してこの要求データが終了していないことを警告するために生成されている。GET要求のようなボディ部を持たない要求の場合はエラーは発生し得ない。
HttpResponseにデータを送信中はソケット関連のエラーは発生しない。応答にデータを送信中にエラーが発生するただ2つの例は:
HTTP規約の違反。ひとつのシナリオはContent-Lengthで設定した値と実際のコンテント長が異なった場合。このときはHttpExceptionが投げられる。
応答にエラーを送信した場合。例えば:
var response = ...;
|
このコードは HttpResponseにFileSystemExceptionを送信し、pipe'から返されるfutureはerrorで完了する。
HTTP応答にデータを送信中に当該ソケットがクローズされていると、そのデータは単に無視される。
したがって上記の1と2を除いて、ユーザはサーブレットの場合と同様にきちんとtry-catchで処理している限り気にする必要はない。
それではAnders Johnsenが挙げている二つに関して次のコードで調べてみよう(IDE上ではC:\dart2_code_samples\http_servers\bin\non_stop_server_2.dart)。
\bin\non_stop_server_2.dartのrequestHandler |
|
ここでは3つのエラーまたは例外を発生できる。
通常の例外
Anders Johnsenが挙げている最初のエラー
Anders Johnsenが挙げている2番目のエラー
最初の例外は意図せぬ例外が発生した場合で、これは単純にcatch句で処理され、クライアントに'500 Internal Server Error'を返しているが、HTMLできちんとエラー・ページとして伝えたほうが好ましい。試しにコメントアウトされている以下の行を活かす:
|
これでこのサーバを開始させ、ブラウザからhttp://localhost:8080でアクセスするとブラウザには'500 Internal Server Error'というテキストが表示されよう。コンソールには次のメッセージが表示される:
|
サーバは停止せず、クライアントからのfavicon要求にも空の応答を返している。
2番目の例はHTTP規約の違反である。応答ヘッダに指定したcontentLengthと実際にボディに書き込まれたデータの長さが一致しないHttpExceptionが生じる場合である。コメントアウトされている以下の行を生かす:
|
これでこのサーバを開始させ、ブラウザからhttp://localhost:8080でアクセスするとブラウザにはサーバから空の応答が来たという表示がされる。コンソールには次のメッセージが表示される:
|
サーバは停止しない。
3番目の例は応答にエラーを送信した場合である。応答に送り出すファイルが存在しなかった事象である。コメントアウトされている以下の行を活かし、次の行をコメントアウトする:
|
これでこのサーバを開始させ、ブラウザからhttp://localhost:8080でアクセスするとブラウザにはサーバから空の応答が来たという表示がされる。コンソールには次のメッセージが表示される:
|
この場合もサーバは停止しない。
従ってこのこのサーバは2つのエラーが発生しても停止しないことが確認されよう。
なおAnders JohnsenはZoneを使ってエラーを閉じ込める方法も示している:
|
しかしながらこの方法は積極的に推奨する手段ではなく、エラーが発生した箇所で対処すべきことも述べている。
前節で例外とエラーに対する対処を説明したが、サーバ動作をより堅牢化するために2014年からdart:asyncに抽象クラスのZoneとstaticメソッドのrunZonedが追加された。この節はFlorian Loitsch及びKathy Walrathの両名によるZone解説書の前半に準拠しているので、残りの後半はこの解説書を見ていただきたい。
ZoneというのはLispで言う動的エクステント(dynamic extent)の非同期版ともいえる。非同期コールバック関数たちはそれが待ち行列に入っていたと同じゾーンで実行される。例えばあるfuture.thenコールバックはそれらが呼び出されたと同じゾーン内で実行される。
Zonesの最も一般的な使いみちは非同期で実行されるコードのなかで生起されたエラーの取り扱いである。例えばシンプルなHTTPサーバでの使い方は以下のようなコードとなる:
runZoned(() { HttpServer.bind('0.0.0.0', port).then((server) { server.listen(staticFiles.serveRequest); }); }, |
このHTTPサーバをあるゾーン内で走らせることで、このサーバの非同期コード内で捕捉されないエラー(uncaught errors:但し致命的でないエラー)が起きてもこのアプリケーションを停止させないようにできる。
注意:この使用例は必ずしもzoneを必要とするものではない。dart:isolateが将来捕捉されないエラーに対するAPIを有するようになると考えられる。
Zonesを使うと以下のタスクが可能になる:
前例で示したように非同期コード内でスローされた処理されない例外の為にアプリケーションが止まってしまうのを防ぐ。
データ(ゾーン・レベルの値たちとして知られる)を個々のゾーンたちに結びつける。
そのコード内の一部またはすべてのなかでのprint()あるいはscheduleTask()といった限定されたメソッドのセットのオーバライド。
そのコードがあるゾーンに入るあるいは出る度にある操作(タイマの開始や停止、あるいはスタック・トレースの保管など)を実行する。
読者は他の言語でzoneに似たものに出くわしたことがあるかもしれない。Node.jsにおけるDomainがDartのZoneのもとになっている。Javaのthread-localストレージも似たものである。最も近いのはDartのzonesをBrian Ford氏がJavaScriptにポートしたzone.jsで、彼のビデオが参考になる。
Zoneはある呼び出しの非同期動的エクステントを表現している。これはそのコードによって登録されている呼び出し及び(推移的なものとしての)非同期コールバックたちの一部として実行される計算である。前述のHTTPサーバの例では、bind()、then()、およびthen()のコールバックの総てが同じゾーン、即ちrunZoned()で生成されたzone内で実行される。
次の例を考えてみよう。このコードはzone #1(ルート・ゾーン)、zone #2、およびzone #3の3つのゾーンが走る。
import 'dart:async'; main() { foo(); var future; runZoned(() { // 新規の子供のゾーンを開始させる(zone #2). future = new Future(bar).then(baz); }); future.then(qux); } foo() => ...foo-body... // 2回実行 (2つのzone内で各々). bar() => ...bar-body... baz(x) => runZoned(() => foo()); // 新規の子供のzone(zone #3). |
下図はこのコードの実行順とこのコードがどのゾーン内で走るかを示している:
|
runZoned()が呼び出される度に新しいzoneが生成され、そのzone内でコードが実行される。そのコードがあるタスク(例えばbaz()呼び出し)のスケジューリングするときは、そのタスクはそれがスケジュールされたzone内で走る。例えば、qux()(main()の最後の行)呼び出しは、例えそれ自身が zone #2内で走るfutureが付加されていても、zone #1(ルート・ゾーン)内で走る。
子のzoneは親のzoneに完全に置き換わる訳ではない。そうではなくて、新しいzoneは自分たちを取り巻いているzoneの内部にネストされる。例えば、zone #2は zone #3を含んでおり、zone #1(ルート・ゾーン)はzone #2とzone #3の双方を含んでいる。
総てのDartコードはルート・ゾーン内で走る。コードは他のネストした子供のzone内で走る場合もあるが、少なくとも常にルート・ゾーン内で走る。
最も使われるZonesの機能のひとつは、非同期で実行しているコード内での捕捉されないエラーが取り扱えられることである。このコンセプトは同期コードにおけるtry-catchと似ている。
「処理されないエラー」はしばしばthrowを使ってcatch文で捕捉されない例外を生起するコードが原因となる。これはhttp_serverのpubライブラリを使用したときなどで発生することがある。捕捉されないエラーを起こすもうひとつの手段はnew Future.error()あるいはCompleterのcompleteError()メソッドを呼び出すことである。
Zone化したエラー・ハンドラ(そのzone内の捕捉されないエラー発生ごとに呼び出される非同期エラー・ハンドラ)をインストールするには、runZoned()のonError 引数を使う。例えば:
runZoned(() {
Timer.run(() { throw 'Would normally kill the program'; });
}, onError: (error, stackTrace) {
print('Uncaught error: $error');
|
このコードでは例外を発生する非同期コールバック(Timer.run()を介した)がある。通常この例外は処理されないエラーになり、トップ・レベルまで伝搬してしまう(即ちスタンド・アロンのDart実行コードではその実行プロセスを止めてしまう)。然しこのゾーン化されたエラー・ハンドラではこのエラーはエラー・ハンドラに渡され、このプログラムを停止させない。
try-catchとゾーン化されたエラー・ハンドラの顕著な相違点は捕捉されないエラーが発生したとしてもそのゾーンは実行を継続することである。そのゾーン内で他の非同期コールバックがスケジュールされていたときは、それらのコールバックも実行を継続する。その結果ゾーン化されたエラー・ハンドラは複数回呼び出される可能性がある。
またあるエラー・ゾーン(エラー・ハンドラを持ったゾーン)はそのゾーンの子供(あるいは孫など)内で発生したエラーも取り扱う。future変換(then()またはcatchError()を使った)のシーケンスのなかでエラーたちはどこで取り扱われるかはシンプルなルールに基づく:
Futureチェイン上のエラーはエラー・ゾーンのバウンダリを決して跨がない
あるエラーがエラー・ゾーンのバウンダリに達したら、その時点でそれは処理されないエラーとして取り扱われる。
次の例では、最初の行で生起されたエラーはエラー・ゾーンに入れない。
import 'dart:async';
main() {
var f = new Future.error(499); //次のイベント・ループでエラーを発生させるFuture
f = f.whenComplete(() { print('Outside runZoned'); });
runZoned(() {
f = f.whenComplete(() { print('Inside non-error zone'); });
});
runZoned(() {
f = f.whenComplete(() { print('Inside error zone (not called)'); });
}, onError: print);
|
この例を実行させると次のような出力となる:
Outside runZoned
Inside non-error zone
Uncaught Error: 499
Unhandled exception:
499
|
runZoned()呼び出しを削除するまたはonError引数を削除すれば、次のような出力となる:
Outside runZoned
Inside non-error zone
Inside error zone (not called)
Uncaught Error: 499
Unhandled exception:
499
|
ゾーンたちのどれか、またはエラー・ゾーンを削除するとそのエラーはさらに伝搬することに注意のこと。
このエラーはあるエラー・ゾーンの外部で発生しているので、スタック・トレースが出力される。このコード全体を囲むエラー・ゾーンを付加すれば、このスタック・トレースを発生させなくできる。
前の例で示されているように、エラーはゾーンを跨げない。同じように、エラーはエラー・ゾーンの外に伝搬できない。次の例を考えてみよう:
import 'dart:async';
main() {
var completer = new Completer();
var future = completer.future.then((x) => x + 1);
var zoneFuture;
runZoned(() {
zoneFuture = future.then((y) => throw 'Inside zone');
}, onError: (error) {
print('Caught: $error');
});
zoneFuture.catchError((e) { print('Never reached'); });
completer.complete(499);
|
例えfutureチェインがcatchError()で終了していたとしても、この非同期エラーはこのエラー・ゾーンから出られない。このエラーはゾーンのエラー・ハンドラ(onErrorで指定されている)が取り扱う。その結果、zoneFutureは決して値やエラーで完了することはない。
ストリームとゾーンのルールはfutureよりもシンプルである:
変換およびその他のコールバック関数たちはそのストリームがリスンされているゾーン内で実行される
このルールはストリームはリスンされるまでは何の副作用も持ってはいけないという指針に沿ったものである。同期コードにおける似たような状況はIterableたちの振る舞いで、この場合は値たちを取りに行くまでは計算されない。
例:runZoned()でStreamを使う
runZoned()でStreamを使った例を以下に示す:
var stream = new File('stream.dart').openRead() .map((x) => throw 'Callback throws'); runZoned(() { stream.listen(print); }, |
このコールバックでスローされた例外はrunZoned()のエラー・ハンドラで捕捉される。出力は次のようになる:
|
この出力が示すように、このコールバックはリスンしているゾーンに結び付けられていて、map() が呼び出されているゾーンではない。