前のページへ

サーバ編

次のページ





セッション管理 (Session mgt.)

セッション管理とはクライアントとのトランザクションにおいてサーバー側のクライアント/サーバー通信を管理し、共有オブジェクトを管理するものである。その為の手段の一つとして古くからクッキーを使う方式が使われてきている。その概要は筆者の「改訂サーブレット・チュートリアル」の第9章を見て頂きたい。

当初HttpServerはセッション管理に関するAPIを用意していなかった。しかしながら筆者からの要請を受け、DartチームのIO担当グループは20121022日にr13870としてIOライブラリに追加した。これはHttpSession.dataという単なるオブジェクトがバインドできるというシンプルなものだったが、それでも我々が簡単なラッパをかぶせることでJava Servlet並みの機能を提供することが出来た。これはシンプル性を追求するというDartの目標に沿ったものであろう。その後2013211日にdart:ioライブラリにHttpSessionという抽象クラスが追加され、単なるオブジェクトではなくてMapを実装させ、サーブレットと同じように名前と値のペアでオブジェクトをバインド出来るようにした。これに加え同時にisNewも付加されたこともあり、ラッパを用意しなくても基本的なアプリケーションには十分な機能が得られるようになっている。



HttpSessionクラス

Dartが用意しているAPIdart.io.HttpSessiondart.io.HttpServer.sessionTimeout()及びdart.io.HttpRequest.sessionである。

  • HttpSessionオブジェクトはHttpRequestオブジェクトの属性として取得する

  • セッションのタイムアウト管理はHttpServerの属性としてセットできる

set sessionTimeout(int timeout)

従ってセッションごとのタイムアウト設定はできないことに注意されたい。

  • またセッションに対するオブジェクトのバインドはHttpSessionそのものがMapであるので簡単である

void operator []=(K key, V value)

どのような型でも値としてバインド可能である。



セッション管理のメカニズム

HttpServerにおけるセッション管理はクッキー・ベースで行われる。つまり当該クライアントとのセッションが維持されるということは、サーバが渡したこのセッションに対応したクッキー(そのセッション固有のID文字列)をそのクライアントがHTTP要求ヘッダの中に含めて送信しているということになる。HttpSession session([init(HttpSession session)])というメソッド呼び出しにより、サーバは該要求に対するセッション・オブジェクトが存在するかどうかを調べる。存在しない場合は新規のセッションIDを持ったセッション・オブジェクトを渡し、これを返す。このオブジェクトはHttpServerが一括管理する。つまり:

  • クライアントに返すHTTP応答メッセージのなかにそのIDを含めたクッキー・ヘッダ(set-cookieヘッダ行)を含める。例えば:

set-cookie: DARTSESSID=6d2afc1b0bdadb50eae546f568ec0c90 HttpOnly

  • アプリケーションからのrequest.sessionゲッタ呼び出しにより、当該要求に対するセッション・オブジェクトを返される

  • このオブジェクトには当該セッション固有のデータがキーと値のペアとしてセットできる。具体的にはショッピング・カートのようなこのセッション(クライアント)固有のオブジェクトのセット/ゲットである

  • 管理しているセッション・オブジェクトたちの各々にたいし、当該オブジェクトに対応したHTTP要求が次に到来するまでの期間がタイムアウト時間を経過したかどうかを調べる。タイムアウトしたものはこれを必要があればアプリケーションに通知する(void set onTimeout(void callback())セッタを使用する)。これはServletには無い機能であるが、この関数を使わなくてもそのアプリケーションの通常のセッション処理(画面遷移処理など)あるいは例外発生処理で済ませても構わない。あるクライアントがそのアプリケーションをアクセス中に何らかの理由でタイムアウト時間以上長く席を外してしまい、その後引き続きそのアプリケーションにアクセスするということは頻繁であり、アプリケーションは画面遷移シーケンス・チェック等で必ずそれに対応できるように作成される。この関数はその為のひとつの手段を提供している

  • HttpServerは唯一つのタイムアウト時間しかもてない。Servletのようにセッションごとにタイムアウト時間を設定できないことに注意しなければならない

  • destroy()メソッド呼び出しで当該セッションを破棄できる。たとえばショッピング・カートのアプリケーションでクライアントが商品発注の手続きを完了したときに当該セッションを破棄する。廃棄してもその要求処理の期間はrequest.session()メソッドは現在の当該セッションを返す。次の要求からは新規のセッションが割り当てられることに注意

なおクッキーに対しては、20125月にdart:ioの中にCookieというクラスが追加されている。これは筆者が指摘した時点で既にDart IO担当のGjesseが追加作業を行っていた。

ブラウザのクッキー保持

クッキーはブラウザのインスタンスが保持している。一般的にはExpires属性による期限指定がないクッキーに対しては、最近のブラウザたちはそれをセッション・クッキーだとみなして、そのブラウザのセッションの終了時点(そのブラウザのインスタンスが閉じる時点)でそれらのクッキーを削除している。従って、PC上に同じブラウザの複数のタブやウィンドウを立ち上げても、クッキーの値は共有されることに注意のこと。つまり同じPC上では例えば複数のChromeのタブやウィンドウを開いても、それらは同じクッキー、つまり同じセッションが適用される。

Internet ExplorerOperaSafari、及びChromeの総てがそのような動作になっている。しかしながら、Firefox3.0.9時点)ではそのような規則に準じておらず、そのブラウザを閉じたときだけでなくOSを再スタートしたときでもそのクッキーは存続するという報告がある。これはFirefoxの設計によるもので、Firefoxを閉じるときにタブを保存するかどうかを聞いてくるが、「保存して終了」を選択すると再立ち上げしたときにこれまでの状態に復旧する。これを「セッション復旧(session restore)」と呼んでいる。もしそれを望まないときは、タブの総てを閉じてからブラウザを閉じれば良い。

簡単なテスト・サーバによる確認

このサーバGithubにあるGooSushiのリポジトリの中にある。




  1. この画面のClone or downloadをクリックし、Download ZIPを選択してこのリポジトリをダウンロードする。ダウンロードしたファイルを解凍・展開しGooSushi-masterのフォルダをGooSushiにする。masterというのはブランチの識別のために付されている。

  2. 自分のIDE上でFile > Open ..を選択し、このダウンロードにあるフォルダをこのウィンドウで開く。

  3. このアプリケーションのpubspec.yamlを開くあるいは右クリックして、Pub : Get Dependenciesを実行して、必要なライブラリを取り込む。

  4. しばらくすると自分のIDE上のファイル・ビューには次のようなファイル構成が表示される:




  5. この時点でDart support is not enabled...が表示されるので、Enable Dart supportをクリックして、Dartのプラグインを取り込む。

  6. このアプリケーションのpubspec.yamlを開く、あるいは右クリックして、Get dependenciesを実行して、必要なライブラリを取り込む。

この結果フォルダの中身は次のようになる

.packages : これはPubpubspec.yamlを見て追加したフォルダ

.gitignore: Githubが無視すべきファイルとフォルダを指定したファイル

pubspec.lock : これもPubpubspec.yamlを見て追加した管理用のファイル

pubspec.yaml : このアプリケーションの仕様

bin : サーバの実行に必要なファイルを収容する

http_session_test.dart : これがHttpSessionを調べる為のサーバ・プログラム

shopping_cart_server.dart : これがHttpSessionを使ったショッピング・カートサーバ・プログラム

README.md : Githubには必要なマークダウン形式のファイルで、このリポジトリの内容を説明したもの

  1. ファイル・ビュー上でそのホルダにあるhttp_session_test.dartを選択する

  2. run → Run 'HttpSessionTest.dart'でこのサーバ・アプリケーションの実行を指定する。

  3. サーバが起動するとServing the SessionTest on http://127.0.0.1:8080.とコンソールに表示する。

  4. Chromeまたはその他のブラウザからhttp://localhost:8080/SessionTestをアクセスする。

最初に次のような初期画面が表示されよう:




この画面では:

  • クライアントからのHTTP要求に対応したオブジェクトから得られるデータ

  • その要求から得られるセッション関連データ

  • クライアントに応答を返す時点でのセッション関連データ

が表示されている。要求オブジェクトに関するデータは既に解説済みであるので省略する。セッションに関する情報はHttpSessionクラスのものではなく、それをラップしたSessionという筆者が用意したクラスからの情報である。このようなラッパを使用したのは、Java Servletに馴染んだユーザには(名前、値)ペアによる属性のバインドがより使い勝手が良いだろうとの判断による。このクラスは後で説明する。力のある読者は是非ラッパを使わないコードに挑戦して頂きたい。

ここでは

  • isNewはこのセッションが新規に生成されたものかどうか

  • sessionIdHttpServletが用意したこのセッションの為のID256ビットのハッシュ値)

  • getAttributesはこのセッションにバインドされた属性の一覧(Map

  • getAttributeNamesはこのセッションにバインドされた属性たちの名前のリスト(List

が表示されている。

この初期画面でstartボタンをクリックすると次のような表示が出よう:

Page 1

 Session will be expired after 20 seconds.

 Available data from the request : request.headers.host : localhost
request.headers.port : 8080
request.connectionInfo.localPort : 8080
request.connectionInfo.remoteHost : 127.0.0.1
request.connectionInfo.remotePort : 49398
request.method : GET
request.persistentConnection : true
request.protocolVersion : 1.1
request.contentLength : -1
request.uri : /SessionTest?command=Start
request.uri.path : /SessionTest
request.uri.query : command=Start
request.uri.queryParameters :
  command : Start
request.cookies :
  testname=testvalue_%e2%88%9a2%3d1.41
  dartsessid=6d5367c8aec1a487ab52ec02ba5f0648
request.headers.expires : null
request.headers :
  user-agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)
  connection: Keep-Alive
  accept: text/html, application/xhtml+xml, */*
  accept-language: ja
  accept-encoding: gzip, deflate
  cookie: testName=TestValue_%E2%88%9A2%3D1.41; DARTSESSID=6d5367c8aec1a487ab52ec02ba5f0648
  host: localhost:8080
  referer: http://localhost:8080/SessionTest
request.session.id : 979b1fd290234e91493eee54050f7cab
requset.session.isNew : true

 Session data obtained from the request :   session.isNew : true
  session.id : 979b1fd290234e91493eee54050f7cab
  session.getAttributeNames : []
  session.getAttributes : {}
 Session data for the response :   session.isNew : true
  session.id : 979b1fd290234e91493eee54050f7cab
  session.getAttributeNames : [pageNumber]
  session.getAttributes : {pageNumber: 1}

このデータから次のことが理解されよう:

  • 初期画面からStartボタンを押したことでクライアント(即ちブラウザ)から送信されるHTTP要求には名前がDARTSESSIDで値が6d5367c8aec1a487ab52ec02ba5f0648であるクッキーが含まれている。これは以前の使われていたセッションIDをサーバに送り返した為である。しかしながらサーバではこのIDは無効化されているのでこの要求が到来した時点で新しいセッションを用意する必要があると判断している。即ちrequset.session.isNew : trueとなっている。

  • 実はこの画面をクライアントに返した時点でサーバがHTTP応答の中にset-cookie: DARTSESSID=979b1fd290234e91493eee54050f7cabというヘッダ行を含めている。クライアントはそのクッキーを保管し、以後このサーバに送信する要求にはこのクッキーを返すことで、サーバはこの要求がどのセッションに対応したものかを知ることが出来る。

  • このセッションにはこの要求が来た時点では属性がバインドされていない。つまりsession.attributesは空の状態である。しかしながらこの画面をクライアントにHTTP応答として渡す時点では、名前がpageNumber、値が1というデータが属性としてセットされている。これはこのクライアントには1ページめの画面を送ったという情報である。次回同じセッションIDを持った要求が到来したときには、サーバはそのクライアントは1ページめの画面から要求を送ってきたということが判る。この属性は画面遷移に良く使用される。

クッキーはブラウザ単位で保持される。従って同じPC上で複数のタブからこのサーバをアクセスしてもそれは同じクライアントと見做される。同じPC上でChromeIEを立ち上げ各々からこのサーバをアクセスすれば、別のクライアントと見做されるので、各自確認されたい。

オブジェクトのセッションへのバインド

オブジェクトのセッションへのバインドは、上記のような画面遷移決定の目的だけではなく、いわゆるショッピング・カートで良く使用される。クライアントがあるショッピングのアプリケーションにアクセスしているとき、そのセッション(即ちそのクライアント)に対する固有の買物情報をショッピング・カートのオブジェクトとしてバインドする。後の節ではその具体的なプログラムを説明する。

Dartは動的な型づけの言語であるので、Javaと違ってセッションに値としてバインドしたオブジェクトを取得するのにキャストする必要は無い。また単一スレッドであるのでスレッドに対する配慮の必要がない。

セッションのタイムアウト

この状態で20秒間以上放置すると、IDEのコンソールには次のようなonTimeout(void callback())でセットしたコールバック関数の出力が表示される:

2013-02-23 20:40:38 : timeout occurred for session 979b1fd290234e91493eee54050f7cab

これは、このHttpSessionオブジェクトがタイムアウトを起こしたことを通知するものである。

HTTPプロトコルにはそのクライアント間がアクティブでなくなった(このサイトから別のサイトに移ってしまった)ことを示す手段は存在しないので、サーバがそうだと判断する為の手段としてタイムアウトが使われる。つまり当該セッション・オブジェクトが所定時間を経過してもアクセスされないことでタイムアウトだと判断している。DartHttpServerはセッションごとにタイムアウトを持つことができず。総てのセッションに対して適用される。HttpServerの持っているデフォルトのタイムアウト時間は20分間である。set sessionTimeout(int timeout)というセッタでこのタイムアウト時間を秒単位で指定できる。このHttpSessionTest.dartアプリケーションでは20秒がセットされている。

HttpServerがあるHttpSessionオブジェクトがタイムアウトを起こしたことを検出したら:

  • onTimeout(void callback())でセットしたコールバック関数が実行される。

  • 当該HttpSessionオブジェクトを廃棄する。即ちdestroy()メソッドが呼ばれる。

  • 次回のそのクライアント(当該セッションID)からのHTTP要求のセッション・クッキーは無視され当該HttpSessionオブジェクトは削除される。またHttpRequest.session呼び出しに対しては新規のHttpSessionオブジェクトが返される。

onTimeoutコールバック関数は、当該HttpSessionオブジェクトが存続していれば呼び出される。この関数を使えば、顧客がそのサイトでのセッションの途中で昼食等で席を離れてしまったりしたことが判り、その時点で何らかの対策(例えばその顧客とのセッションの放棄、DBセッションの開放、携帯メール経由での顧客への通知など)をとることができる。このコールバックはサーブレットには無かったもので、顧客管理のひとつの情報源となり得る。

セッションの廃棄

セッションの廃棄はdestroyとかdiscardとかという言葉が良く使われる。Servletではinvalidateと呼んでいる。

例えばオンライン・ショッピングのアプリケーションでは、あるクライアントが幾つかの画面を遷移しながら在庫確認から発注、最終確認までの一連の手順が終了した時点で、そのセッションが終了したことになる。そのクライアントが次回このアプリケーションをアクセスしたときは、このクライアントは再度このアプリケーションの最初からショッピングの手順を踏めば良い。そのような場合には当該セッションの廃棄が良く使用される。

アプリケーションが明示的にセッションを廃棄しなくても:

  • アプリケーションで再度そのクライアントがアクセスしたときでも、これまでのショッピングの手順が完了していることを知り、そのクライアントには最初の手順を踏ませることができる。

  • タイムアウトのメカニズムが使われなくなったセッション・オブジェクトを整理してくれるので、そのようなオブジェクトが蓄積されてしまい、サーバの負荷となることが防止される。

DartHttpSessionクラスでは、destroy()というメソッドが用意されている。このメソッドは:

  • このメソッドが呼ばれても、このオブジェクトそのものが消えるわけではない。従って、このメソッドを呼んだあとでもこのオブジェクトにはアクセスが可能である。

  • 次回のそのクライアント(当該セッションID)からのHTTP要求のセッション・クッキーは無視され当該HttpSessionオブジェクトは削除される。またHttpRequest.session呼び出しに対しては新規のHttpSessionオブジェクトが返される。

例えばあるページからNew Sessionというボタンをクリックして初期画面に戻ったときの画面には次のようなセッション・データが表示される:

Session data obtained from the request :

session.isNew : false

session.sessionId : 97df64cbfad26bcbeca5dfb9381298ab

session.attributes : {pageNumber: 4}

session.getAttributeNames : [pageNumber]



Session data for the response :

session.isNew : false

session.sessionId : 97df64cbfad26bcbeca5dfb9381298ab

session.attributes : {pageNumber: 4}

session.getAttributeNames : [pageNumber]

New SessionというボタンをクリックしたときのHTTP要求に対しては、このサーバは実は当該HttpSessionオブジェクトのdestroy()メソッドを呼び出している。それにもかかわらずHTTP応答を返す時点ではこのオブジェクトへのアクセスが可能であり、その内容も変化していない。

Http only クッキー・パラメタ

通常クライアントに送るセッションIDの為のクッキー・ヘッダ行にはHttpOnlyが付加されているが、Dartでは当初これが付加されていなかった。しかし筆者からの指摘が即座に受け入れられ、これが付加された。Java ServletではHttpOnlyの付加がデフォルトとなっている。Java Servlet仕様書の新しい3.0版に書かれているように、HttpOnlyクッキーはクライアントに対しこれらのクッキーがクライアント・サイドのJavascriptのコードからは見えなくするべきであることを示している(これはそのクライアントがこの属性を探すことを知っていないかぎりフィルタされない)。HttpOnlyクッキー使用はある種のサイトをまたぐスクリプティング攻撃を最小化するのにも寄与する。



http_session_test.dartの概要

まずこのコードで使われているSessionというクラスを説明する。

Sessionクラス

SessionクラスはHttpSessionをラップして、これまでの(名前、値)ペアで属性をバインドすることに馴染んでいるServletユーザのために用意したものである。出来ればこのクラスはライブラリにしたほうが好ましい。このクラスにより、Javaで書かれたアプリケーションをDartに移植するのが容易になる。このクラスをHttpRequestのオブジェクトを引数にしてインスタンス化することで、HttpSessionHttpRequest.session()メソッドが呼び出される。ある要求処理プロセスの中で何回もこのクラスのオブジェクトを生成してもSessionのオブジェクトの内容はisNewを除いて変化せず、問題は起きない。

このクラスのフィールドは:

String id

当該セッションのID

bool isNew

このセッションが新規のもの、つまり当該要求に該当するセッションが存在せず、このオブジェクト生成時に初めてこれに対するセッションがHttpServerの中に作成されたことを示す

HttpSession session

HttpSessionのオブジェクトで、アプリケーションが直接扱うことは無い

メソッドは:

Session(HttpRequest request)

コンストラクタで、要求オブジェクトを引数とする

getAttribute(String name)

ある名前を持った値(オブジェクト)を取得

void setAttribute(String name, dynamic value)

(名前、値)のペアであるオブジェクトを属性としてこのセッションにバインドする

List getAttributeNames()

バインドされている属性たちの名前のリストを取得

Map getAttributes()

総ての属性を含むMapを取得

void removeAttribute(String name)

指定した名前の属性を削除

void invalidate()

このセッションを無効化する。これは次の同じセッションIDを持った要求から効果を持つ

以下はそのコードである。

  Session(HttpRequest request) {
    _session = request.session;
    _id = request.session.id;
    _isNew = request.session.isNew;
    request.session.onTimeout = () {
      print("${new DateTime.now().toString().substring(0, 19)} : "
          "timeout occurred for session ${_id}");
    };
  }
  // getters
  HttpSession get session => _session;
  String get id => _id;
  bool get isNew => _isNew;
  // getAttribute(String name)
  dynamic getAttribute(String name) => _session[name];
  // setAttribute(String name, dynamic value)
  setAttribute(String name, dynamic value) {
    _session[name] = value;
  }
  // getAttributes()
  Map getAttributes() {
    Map attributes = {};
    for (String x in _session.keys) attributes[x] = _session[x];
    return attributes;
  }
  // getAttributeNames()
  List getAttributeNames() {
    List names = [];
    for (String x in _session.keys) names.add(x);
    return names;
  }
  // removeAttribute()
  removeAttribute(String name) {
    _session.remove(name);
  }
  // invalidate()
  invalidate() {
    _session.destroy();
  }
}

タイムアウト処理

このコードではコンストラクタの中でHttpSessionオブジェクトを取得したあとで次のようなタイムアウト処理を付加している:

request.session.onTimeout = () {
  print("${new DateTime.now().toString().substring(0, 19)} : "
      "timeout occurred for session ${_id}");
};

ここではこのオブジェクトがタイムアウトを起こしたときにそれをタイムスタンプつきでコンソールに出力するだけである。タイムアウトは既に説明したようにアプリケーションの中でいろんなやり方で処理されるが、このコールバック関数はその為のひとつの手段になり得る。

要求処理ハンドラ

要求処理ハンドラは到来要求を処理し応答をクライアントに返すベースとなる関数である。この関数の中で004行から031行の範囲でこの間に生じた何らかの例外をトラップし、そのことをエラー・パージとしてクライアントに返す。これによりサーバはサービスを継続できる。

014行目ではNew Sessionというボタンのクリックで到来した要求に対してはsession.invalidate()つまりHttpSessiondestroyメソッドを呼んでいる。これは既に述べたように現在の処理中のsessionオブジェクトには影響を与えないので、017行目の新たなSessionオブジェクトの取得は意味がない。この行は読者の確認実験の為に置かれている。

037行目にクッキー設定の例を示している。このクッキーはセッション・クッキーの為のset-cookieヘッダとは別のset-cookieヘッダで送信される。最近の仕様書(RFC 6265)の第3章では「set-cookieフォールディング」は使用すべきではないと書かれているので、これに準拠している。クッキーを含めたHTTPヘッダ行で使われる文字コードはASCIIに限定されるので、URLエンコードしなければならない。setCookieParameterという関数はその為にあり、引数の名前と値は多バイトのユニコードであっても構わないよう配慮されている。ここではtestName=TestValue_√2=1.41testName=TestValue_%E2%88%9A2%3D1.4と安全な文字列に変換されクッキーとして保持される。

http_session_test.dartの一部

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void handleRequest(HttpRequest request) {
  HttpResponse response = request.response;
  String responseBody;
  try {
    reqLog = createLogMessage(request);
    if (LOG_REQUESTS) print(reqLog.toString());

    Session session = new Session(request); // get session for the request
    sesLog = createSessionLog(session);
    if (LOG_REQUESTS) {
      print(sesLog.toString());
    }

    if (request.uri.queryParameters["command"] == "New Session") {
      session
          .invalidate(); // note: HttpSession.destroy() is effective from the next request
      session = new Session(request); // get the new session
    }

    if (request.uri.queryParameters["command"] == "New Session" ||
        request.uri.queryParameters["command"] == null ||
        session.isNew) {
      responseBody = createInitialPage(session);
    } else {
      int iPage = 1;
      if (!(session.isNew) &&
          !(request.uri.queryParameters["command"] == "Start"))
        iPage = session.getAttribute("pageNumber") + 1;
      responseBody = createNextPage(session, iPage);
    }
  } catch (err, st) {
    responseBody = createErrorPage('$err : $st');
  }
  response.headers.add("Content-Type", "text/html; charset=UTF-8");

  // cookie setting example (accepts multi-byte characters)
  setCookieParameter(
      response, "testName", "TestValue_√2=1.41", request.uri.path);
  response.write(responseBody);
  response.close(); // flush
}

画面遷移

画面遷移はcreateHtmlResponse(request, session)のなかで行われている。画面遷移の処理は遷移表を使う、セッションにバインドされたデータ(どのページにある筈だなどの)をもとに判断する、あるいはHTMLformで指定されたデータ(hiddenやボタンの値)を使うなどの手段があろう。ここでは簡単にボタン・クリックの値で判断している。



ショッピング・カートのアプリケーション・サーバ

オブジェクトのセッションへのバインドは、いわゆるショッピング・カートで良く使用される。クライアントがあるショッピングのアプリケーションにアクセスしているとき、そのセッション(即ちそのクライアント)に対する固有の買物情報をショッピング・カートのオブジェクトとしてバインドする。以下の節ではその具体的なサーバ・プログラムを説明する。これらのプログラムで使っているSessionというラッパ・クラスあるいはHttpSessionライブラリを用いることで、従来のJavaで書かれたサーバ・アプリケーションを比較的容易にDartサーバに移植できる。しかしながらラッパをわざわざ使わなくても同じアプリが開発できるので、力のある読者はasync関数ベースでラッパなしのコードに挑戦してみて頂きたい。

このサーバ・アプリケーションはGooSushiという寿司屋の注文から、注文の確認、代金清算までを含むサービスを表現したものである。このサーバのコードのGooSushi\bin\shopping_cart_server.dartGooSushi\bin\http_session_test.dartと同じフォルダに入っているので、IDE上ですぐに走らせることができる。

サーバが起動するとコンソールには次のような表示がされる:

today's menu
itemCode : 150, itemName : Tobiko (Flying Fish Roe), perItemCost : 520.0
itemCode : 160, itemName : Ebi (Shrimp), perItemCost : 240.0
itemCode : 170, itemName : Unagi (Eel), perItemCost : 520.0
itemCode : 180, itemName : Anago (Conger Eal), perItemCost : 360.0
itemCode : 190, itemName : Ika (Squid), perItemCost : 200.0
itemCode : 260, itemName : Kanpachi (Great amberjack), perItemCost : 360.0
itemCode : 270, itemName : Hamachi (Yellowtail), perItemCost : 360.0
itemCode : 280, itemName : Sake (Salmon), perItemCost : 360.0
itemCode : 290, itemName : Maguro (Tuna), perItemCost : 360.0
itemCode : 300, itemName : Tai (Japanese red sea bream), perItemCost : 360.0
Serving /GooSushi on http://127.0.0.1:8080.

最初に用意されている本日のメニューをアイテム・コード(整数)の若い順にアイテムの名前、及び単価のリストを表示している。その後このサーバはIPアドレスがローカル・アドレス(ループバック・アドレス)でTCPポート番号が8080でクライアントからの要求の受付開始をしたことを表示している。

実行例

このプログラムを以下のように実行する:

  1. 自分のブラウザのアドレス・バーに、http://localhost:8080/GooSushiと入力してこのサーバをアクセスする。そうすると下図のようなメニュー画面が表示される。




  2. ここで適当な数量を選択メニューから選択し、orderボタンをクリックすると、確認画面に遷移する。




  3. Confirmedボタンをクリックすると最終画面に遷移する。




  4. 確認画面で注文しなおしの為にno, re-orderボタンをクリックすると、最初のメニュー画面に遷移するが、既に選択したアイテムの個数が赤で表示されているところが相違している。




ショッピング・カート

ショッピング・カートはそのカートに入れる為の商品アイテムのMap(_items)がその主たる構成要素である。各アイテムは商品コード(int _itemCode)をキーとしてアクセスできる。それ以外のこのカートに関する情報、例えばこの例では総額_amount_orderedAtがフィールドとして存在し、これらの要素に対するセッタとゲッタが用意されている。

class ShoppingCart {
  Map<int, ShoppingCartItem> _items;
  double _grandTotal;
  DateTime _orderedAt;
  // constructor
  ShoppingCart() {
    _items = new TodaysMenu().items;
    _grandTotal = 0.0;
  }
....

それ以外にこのカート内の各アイテムの追加と削除などのメソッドも存在する。

各アイテムは商品コード(_itemCode)、商品名(_itemName)、購入数量(_qty)、単価(_perItemCost)、小計(_subtotal)などがその要素になり、これらの各要素はセッタ及びゲッタでアクセスする。これはJavaBeansと似た構成である。

class ShoppingCartItem {
  int _itemCode;
  String _itemName;
  int _qty;
  double _perItemCost;
  double _subTotal;
  //update items in the shopping cart
  void update(int itemCode, int iQty, double perItemCost) {
    this.itemCode = itemCode;
    this.iQty = iQty;
    this.perItemCost = perItemCost;
  }
.....

HTTPサーバにおける例外とエラーの捕捉とエラー・ページ

Dartでは発生した例外は、そのコードを呼び出した側に伝搬する。最終的にどの呼び出し側でもその例外を捕捉しないと、サーバは停止する。一般にサーバ・アプリケーションでは、サーバの停止は極力避けねばならない。従って、しかるべき場所で例外を捕捉し、エラー・ページでそれをクライアントに通知するのが一般的な対処法である。エラー・ページはJSPではデフォルトのページが用意されているが、Dartでは自分で用意することになる。

このプログラムでは、基となる要求処理のための関数requestReceivedHandlerで発生した例外を捕捉している。この関数の役割は、到来した要求に対しセッション・オブジェクトを取得し、その要求とセッションをHTML応答を作成する関数に渡し、得られたHTMLテキストを出力ストリームに書き込み、クライアントにその応答を送信することである。加えて、このアプリケーションのパラメタであるLOG_REQUESTStrueになっていれば、到来要求及びそれに対するセッションの情報をコンソールに出力する。

この関数の中で発生した総ての例外をtry { ~ } catchで捕捉し、エラー・ページとしてクライアントに送信している

// handle request //
void handleRequest(HttpRequest request) {
  final HttpResponse response = request.response;
  String htmlResponse;
  try {
    if (LOG_REQUESTS) print("\n" + createLogMessage(request).toString());
    Session session = new Session(request);
    if (LOG_REQUESTS) print(createSessionLog(session));
    htmlResponse = createHtmlResponse(request, session).toString();
  } catch (err, st) {
    htmlResponse = createErrorPage(err.toString() + st.toString()).toString();
  }
  response.headers.add("Content-Type", "text/html; charset=UTF-8");
  response.write(htmlResponse);
  response.close();
}

なお、アプリケーションによってはrequestReceivedHandlerのなかでFutureStreamのオブジェクトを使用する場合もあろう。この場合はそれらのオブジェクトで発生したエラーはonErrorcatchErrorで捕捉し、これをExceptionのオブジェクトとしてスローする必要がある:

throw new Exception('exception raised');

そうすればこれはrequestReceivedHandlertryブロックのcatch文で捕捉される。

HttpServerのエラー

それではサーブレット・コンテナに相当する部分であるHttpServerではエラーはどう処理されているのだろうか?Googleの技術者のAnders Johnsen は次のように説明している:

  • HTTP要求のリスン中はエラーは発生しない。不完全なHTTPヘッダがあればそれは無視され、当該ソケットはクローズされる。

  • 完全なHTTP要求が受信されたら、HttpRequestオブジェクトが生成される。ボディ部分のリスン中(例えばrequest.listen(...)とり出し中)に、もしそのボディ全部が受信される前に当該ソケットが閉じてしまったときはエラーが生成される。ここではソケットが閉じられるが、このエラーはユーザに対してこの要求データが終了していないことを警告するために生成されている。GET要求のようなボディ部を持たない要求の場合はエラーは発生し得ない。

  • HttpResponseにデータを送信中はソケット関連のエラーは発生しない。応答にデータを送信中にエラーが発生するただ2つの例は:

  1. HTTP規約の違反。ひとつのシナリオは Content-Lengthで設定した値と実際のコンテント長が異なった場合。このときは HttpExceptionが投げられる。

  2. 応答にエラーを送信した場合。例えば:

var response = ...;

var future = new File("non_existing_file").openRead().pipe(response);

このコードは HttpResponseFileSystemExceptionを送信し、pipe'から返されるfutureerrorで完了する。

  • HTTP応答にデータを送信中に当該ソケットがクローズされていると、そのデータは単に無視される。

したがって上記の12を除いて、HttpServerクラスで発生する例外を除いて、ユーザはサーブレットの場合と同様に気にする必要はない。

1と2の件はHTTPサーバ」の章の終わりで解説してある。





前のページへ

次のページ