サーバ編 |
本章はいわゆるWebサーバといった本格的な静的ファイルのサーバを対象としたものではない。一般的なサーバ・アプリの中でファイルをクライアントに渡すための基本的な事項を簡単なサンプルをもとに解説している。
file_server.dartは簡単なファイル・サーバである。このサーバはあくまでもDartコードのサンプルであり、実際のアプリケーションに使ってはいけない。何故なら、このコードはセキュリティに配慮されていないからである。一般にはこの種のアプリケーションではHTTPSなどのセキュアな接続が必要である。
Githubのfile_serverのリポジトリをブラウザで開くと次のような画面が表示される:
|
この画面でClone or downloadのボタンをクリックするとOpen in DesktopまたはDownload ZIPの選択メニューが表示される。Download ZIPと表示した個所をクリックしてfile_server-master.zipという圧縮ファイルをダウンロードする。masterというのはブランチの識別のために付されている。
このファイルを適当な解凍ツールを使って解凍するとfile_server-masterというフォルダが作成される。その中も同じ名前のフォルダがある筈である。このフォルダの名前をfile_serverと変更する。
自分のIDE上でFile > Open ..を選択し、このダウンロードにあるフォルダをこのウィンドウで開く。
このアプリケーションのpubspec.yamlを開くあるいは右クリックして、Pub : Get Dependenciesを実行して、必要なライブラリを取り込む。
しばらくすると自分のIDE上のファイル・ビューには次のようなファイル構成が表示される:
|
この時点でDart support is not enabled...が表示されるので、Enable Dart supportをクリックして、Dartのプラグインを取り込む。
このアプリケーションのpubspec.yamlを開く、あるいは右クリックして、Get dependenciesを実行して、必要なライブラリを取り込む。
この結果フォルダの中身は次のようになる:
.packages : これはPubがpubspec.yamlを見て追加したフォルダ
.gitignore: Githubが無視すべきファイルとフォルダを指定したファイル
pubspec.lock : これもPubがpubspec.yamlを見て追加した管理用のファイル
pubspec.yaml : このアプリケーションの仕様
bin : サーバの実行に必要なファイルを収容する
file_server.dart : これがファイル・サーバのプログラム
resources : サーバがクライアントに開放するファイルを置くフォルダ
README.md : Githubには必要なマークダウン形式のファイルで、このリポジトリの内容を説明したもの
FileServer.dartを実行させる。IDE上でこのファイルを選択し、Run 'FileServer.dart'を実行する。コンソールには
Serving /fserver on http://127.0.0.1:8080と表示される。
次に自分のブラウザからこのサーバを次のようにアクセスする:
サーバは次のような初期画面を生成してブラウザ側に返す:
|
これはIEの例であるが、アドレスバーとタブにはDartのアイコン(favicon.ico)が正しく表示されている。Chromeからだとアドレスバーにはアイコンは表示されない。
このアプリの動作は次のようである:
このサーバはresources/のディレクトリに置かれたファイルのみしかダウンロードを許していない。ここではまずこのディレクトリ内のファイルの一覧を表示してその中からユーザが選択できるようにしている。各ファイルの表示にはそのファイルを呼び出すリンクが張られている。例えばresources/Client.htmlの表示にはhttp://localhost:8080/fserver/resources/Client.htmlというリンクが張られている。
従ってHTMLページを介さなくてもブラウザのアドレス・バーに直接http://localhost:8080/fserver/resources/Client.htmlを入力しても同じ結果が得られる。
もうひとつはテキスト・エリア経由でファイルの名前を入力してSubmitボタンでこれをサーバにクエリ文字列としてサーバに送信できるようにしてある。
つまりサーバは直接http://localhost:8080/fserver/resources/Client.htmlといった要求パスによる要求とhttp://localhost:8080/fserver?fileName=resources%2FClient.htmlといったクエリによる要求の双方に対応している。
テキスト・エリアからファイルを指定するときは、相対指定でも絶対指定でも可能である。前記のようにresources/readme.textと入力する代わりにC:/dart_apps_server/file_seerver/resources/readme.textといったように絶対パスを入力することも可能である(ここではこのアプリケーションがC:/dart_apps_serverというフォルダに含まれているとしている)。絶対パスはIDE上でそのファイルを選択し、右クリック > Copy Pathでそのパスをコピーし、テキスト・エリアに張り付ければ良い。各自試して見られたい。
最初にクライアントからの要求の振り分け部分(即ちmain()関数)を示す。
file_server.dartの一部 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void main() { HttpServer.bind(HOST, PORT).then((HttpServer server) { server.listen((HttpRequest request) { request.response.done.then((d) { if (LOG_REQUESTS) print("sent response to the client for request : ${request.uri}"); }).catchError((e) { print("new DateTime.now()} : Error occured while sending response: $e"); }); if (request.uri.path.contains(REQUEST_PATH)) { requestReceivedHandler(request); } else if (request.uri.path.contains(FAVICON_PATH)) { FileHandler().onRequest(request, "resources/images/favicon.ico"); } else { BadRequestHandler().onRequest(request); } }); print( "${DateTime.now()} : Serving $REQUEST_PATH on http://${HOST}:${PORT}."); }); } |
2行目でアドレスが128.0.0.1 (localhost即ちIPv4ループバック・アドレス)でポートが8080のサーバを作る
3行目でそのサーバで到来要求を受信する
4-9行はその要求の処理が終了したときのループバック関数である
10-17行はその要求処理である
要求の振り分けは:
/fserverというパスを持った要求はrequestReceivedHandler(request)へ
/favicon.icoというパスで来た要求はブラウザからのfavicon要求なので、favicon.icoのファイルを返すために FileHandler().onRequestメソッドを呼ぶ
その他のパスの要求は400 Bad Requestという応答をクライアントに返す為 BadRequestHandler().onRequestメソッドを呼ぶ
次にクライアントからの要求が到来した時点でその要求パスとクエリを調べ、どのように処置するかを判断している。
requestReceivedHandler(HttpRequest request)の一部 |
|
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 |
// process the request completer.future.then((data) { try { if (LOG_REQUESTS) { print(createLogMessage(request, bodyString)); } // select requests with 'fileName' query if (request.uri.queryParameters['fileName'] != null) { FileHandler().onRequest(request); } else // select request without 'fileName' query { // select direct designation of the file String fName = request.uri.path.replaceFirst(REQUEST_PATH, ''); if (fName.length > 2) { fName = fName.substring(1); if (fName.contains('resources/')) { FileHandler().onRequest(request, fName); // fixed for API change } else InitialPageHandler() .onRequest(request, 'you can access files in resouces/ only!'); } // new client, send initial page else { // FileHandler().onRequest(request, 'resources/Client.html'); InitialPageHandler().onRequest(request); } } } catch (err, st) { print('File Handler error : $err.toString()'); print(st); } }); |
この処理の本体の02行目はクライアントからのHTTP要求を完全に受理したfuture.thenメソッドの関数リテラルとして記述されている
08行目でfileNameという名前のクエリが存在するかどうか、即ちテキスト・ボックスにファイル名が入力されSubmitボタンが押された結果の要求かどうかを調べている。その場合は直ちにFileHandlerのonRequestメソッドを呼び出している
14行目以降は正しい直接指定かどうかを調べている。該要求の要求パスの/server以降にresources/という文字列があるかどうかを調べ、存在するときは18行目でFileHandlerのonRequestメソッドを呼び出している
そうでないときは20行目で正しくない要求だとしてyou can access files in resouces/ only!という表示を付けて初期画面をクライアントに返す
そうでないときは26行目で初期画面をクライアントに返している
このサーバの中心になっているのがFileHandlerというクラスであるので、次にこのクラスを説明する。
FileHandler |
|
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 42 |
// return requested file to the client class FileHandler { void onRequest(HttpRequest request, [String fileName = null]) { File file; try { final HttpResponse response = request.response; if (fileName == null) { fileName = request.uri.queryParameters['fileName']; // check for absolute path file = new File(fileName); if (!file.existsSync()) { fileName = '../' + request.uri.queryParameters['fileName']; } } if (LOG_REQUESTS) { print('Requested file name : $fileName'); } file = new File(fileName); String mimeType; if (file.existsSync()) { mimeType = mime.mime(fileName); if (mimeType == null) mimeType = 'text/plain; charset=UTF-8'; // default response.headers.set('Content-Type', mimeType); // response.headers.set('Content-Disposition', 'attachment; filename=\'${fileName}\''); // Get length of the file for Content-Length header. RandomAccessFile openedFile = file.openSync(); response.contentLength = openedFile.lengthSync(); openedFile.closeSync(); // Pipe the file content into the response. file.openRead().pipe(response); } else { if (LOG_REQUESTS) { print('File not found: $fileName'); } new NotFoundHandler().onRequest(request); } } catch (err, st) { print('File Handler error : $err.toString()'); print(st); } } } |
クライアントから要求されたファイル名は08行目に示されるように"fileName”という名前でクエリ文字列から取得される
18行目に示すようにFileインターフェイスのオブジェクトはその名前を引数にして取得される
20行目に示すように、当該ファイルが存在するかどうかを知るのにexists()またはexistsSync()メソッドを使用する。両者の相違は、そのメソッドが実行完了まで留まる(ブロックされる)か、または非ブロッキングでFutureのイベントを使うかである
ファイル名の拡張子(extension)によって"Content-Type"ヘッダでクライアントに知らせるMIME形式が異なるので、21行目で示されているようにMIMEタイプもライブラリのメソッドを使用する。mime_typeというライブラリは筆者が用意しpub.dartlangにパブリッシュしたものであるが、これらの形式以外のファイル形式の場合はそれを追加する必要がある。IANAのサイトなどで確認して追加されたい。mime_typeライブラリのmime(String FileName)というメソッドは、該当するファイル・タイプが存在しないときはnullを返すので、そのときは22行目のようにブラウザのデフォルトのMIMEタイプを設定する
コメントアウトされている"Content-Disposition", "attachment; filename=\"${fileName}\""というヘッダを追加するとブラウザの対応が異なってくるので、試されると良い。 その値である”inline”はエンティティがユーザにすぐに表示されるべきであるのに対し、”attachment”はユーザがエンティティを閲覧するためには追加的行動をとる必要があることを示す。ここでは”attachment”と指定しているので、Chromeではブラウザに表示するのではなく、ダウンロード物として取り扱う。IEでは次のような問合せをしてくる:
|
ファイルを開くには26行目に示すように同期して開くopenSyncと非同期(非ブロック)で開くopenのふたつのメソッドがある。スループットが問題になる場合を除いて、通常はコーディングが楽なopenSyncが使われそうである。次の行のlengthSyncも同様である。
応答ヘッダ部分の設定が終わったので、30行目で該ファイルの中身を出力ストリームに書き込む。この際file.openRead()のpipeメソッドを使うと、非常に簡単な記述で済んでしまう。デフォルトでは入力ストリームからのデータを総て書き込むと、出力ストリームは自動的に閉じるので、closeを実行する必要がない。
なおファイルまたはディレクトリの存在のチェックは次のような記述となる:
|
ここでは指定したパス(ファイル・パス)を持つファイルが存在するかまたは指定したパス(ディレクトリ・パス)を持つディレクトリが存在するかを調べている。
ブラウザが受け付けるContent-TypeをContent-Typeヘッダにセットしてやらないと、たとえWordの文書であってもこれを正しく表示しないことがあるので注意が必要である。ブラウザが自分のウィンドウ内で開けないドキュメントはダウンロード扱いとなる。
mime_typeはその為のライブラリである。このライブラリのメソッドは:
String mime(String fileName)
String mimeFromExtension(String extension)
の2つである。mime(String fileName)はファイル名(即ち拡張子を含む)を含むパスを指定するとそれに対応したMIMEタイプを戻し、mimeFromExtension(String extension)は、拡張子を指定するとそれに対応したMIMEタイプを戻す。該当するMIMEタイプが無いときはともにnullを返す。
http_serverパッケージはGoogleのDartチームが作成したライブラリで、HttpServerと組み合わせてウェブ・コンテントのサービスのアプリ作成を容易にしようというものである。
このパッケージのサンプルはDartのコードサンプルのリポジトリに含まれている。従ってこれを各自ダウンロードして試してみるとこのパッケージの使い方が理解されよう。
例えばdart-tutorials-samples/httpserver/bin/の中にあるstatic_file_server.dartを見てみよう:
static_file_server.dart |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import 'dart:async'; import 'dart:io'; import 'package:http_server/http_server.dart'; import 'package:path/path.dart'; Future main() async { var pathToBuild = join(dirname(Platform.script.toFilePath())); var staticFiles = VirtualDirectory(pathToBuild); staticFiles.allowDirectoryListing = true; /*1*/ staticFiles.directoryHandler = (dir, request) /*2*/ { var indexUri = Uri.file(dir.path).resolve('index.html'); staticFiles.serveFile(File(indexUri.toFilePath()), request); /*3*/ }; var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 4048); print('Listening on port 4048'); await server.forEach(staticFiles.serveRequest); /*4*/ } |
3、4行目でhttp_serverとpathのパッケージをインポートしている。
7行目のpathToBuildは現在のディレクトリを今使っているプラットホームに合わせて作ってくれる。joinはpathのメソッドである。
9行目のstaticFilesはそのpathToBuildを引数にしたVirtualDirectoryのオブジェクトである。ここでvirtualと言っているのは「実質的な」という意味である。VirtualDirectoryはHTTP要求とファイル・システム間のリンクを安全なものとし、またmimeタイプの修正、及びエラー・ページの付加等をやってくれる。
10行目はクライアントたちがこのサーバのディレクトリ内のファイルを要求するのを許すよう設定している。
11行目からの匿名関数はディレクトリ自身へのアクセスにたいし、指定したファイル名(ここではindex.html)を返すよう設定する。index.htmlは書かなくても勝手にそう判断するが、そうでない名前のファイルを指定できる。この関数はここではこれらの要求をindex.htmlにリダイレクトする。
13行目のserveFileメソッドでは指定したindex.htmlをディレクトリ要求の応答としている。
18行目のserveRequestメソッドはVirtualDirectoryのメソッドで、ファイルを指定しているHTTP要求を処理している。
「ファイルサーバのダウンロードと配備」で示したと同様の手順でdart-lang/dart-tutorials-samplesをダウンロードし、そのなかのdart-tutorials-samples-master\dart-tutorials-samples\httpserverをIDE上で展開し、Get dependenciesを実行すると下図のようになる:
|
ここでstatic_file_server.dartを実行させると次のようにコンソール表示される:
|
次にブラウザからhttp://localhost:4048でこのサーバをアクセスすると次のように表示される:
|
またnotes.txtフォイルを呼び出すと次のようにテキスト表示される:
|
更に存在しなファイル指定すると:
|
と404応答が返される。
http_serverのAPIの概要を以下に示す。詳細はこのライブラリのAPIを見て頂きたい。
このライブラリの核となっているのがVirtualDirectoryとVirtualHostという二つのクラスである:
VirtualDirectoryクラスは該ファイル・システムからの静的コンテントのサービスを書きやすくしている:
レンジ・ベースの要求
If-Modified-Sinceベースの要求対応
自動的GZip圧縮
システム全域またはjailedルート内のいずれかでsymlink対応
ディレクトリ・リスティング
VirtualHostクラスは到来要求のHostフィールドを使って同じアドレスを持った複数のホストでサービスできるようにしている。サブ・ドメインにはワイルドカードが使える:
|
クラス一覧
HttpBody |
HttpRequestまたはHttpClientResponse用にHttpBodyHandlerによって作られたHTTPコンテント・ボディ |
HttpBodyFileUpload |
ファイル・アップロードをラップしたHttpBodyFileUploadオブジェクトで、アップロードされたファイルのファイル名、contentType及びデータを取り出す手段を提供している |
HttpBodyHandler |
HttpBodyHandlerは使い勝手の良いHttpBodyオブジェクトのかたちでHTTPメッセージ・データを処理及び収集するためのヘルパ・クラスである。コンテント・ボディは Content-Typeヘッダ・フィールドに基づいて解析される。ボディ全体が読み込まれ解析されたら該ボディ・コンテントは利用可能となる。このクラスはサーバでの要求とクライアントでの応答の双方での処理に使える |
HttpClientResponseBody |
HttpClientResponseのHttpBodyが HttpClientResponseBodyの型となる。これはヘッダたちのアクセスの為に HttpClientResponseオブジェクトが含まれている |
HttpMultipartFormData |
MimeMultipartを'multipart/form-data'部として解析することでMimeMultipartをアップグレードするのに使われるクラス |
HttpRequestBody |
HttpRequestの HttpBodyが HttpRequestBodyの型となる。総ての要求ヘッダ情報を読み出し、クライアントに応答する為の該要求へのアクセスを提供している |
VirtualDirectory |
ルート・パスからディレクトリに亘るファイルへのアクセスを HttpRequestsに与える |
VirtualHost |
名前ベースのアプローチを使って複数のホストで処理するためのユーティリティ・クラス |