前のページへ

サーバ編

次のページ





ファイル・サーバ

本章はいわゆるWebサーバといった本格的な静的ファイルのサーバを対象としたものではない。一般的なサーバ・アプリの中でファイルをクライアントに渡すための基本的な事項を簡単なサンプルをもとに解説している。



file_server

file_server.dartは簡単なファイル・サーバである。このサーバはあくまでもDartコードのサンプルであり、実際のアプリケーションに使ってはいけない。何故なら、このコードはセキュリティに配慮されていないからである。一般にはこの種のアプリケーションではHTTPSなどのセキュアな接続が必要である。

このアプリケーションのダウンロードと配備

  1. Githubfile_serverのリポジトリをブラウザで開くと次のような画面が表示される:




  2. この画面でClone or downloadのボタンをクリックするとOpen in DesktopまたはDownload ZIPの選択メニューが表示される。Download ZIPと表示した個所をクリックしてfile_server-master.zipという圧縮ファイルをダウンロードする。masterというのはブランチの識別のために付されている。

  3. このファイルを適当な解凍ツールを使って解凍するとfile_server-masterというフォルダが作成される。その中も同じ名前のフォルダがある筈である。このフォルダの名前をfile_serverと変更する。

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

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

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




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

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

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

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

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

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

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

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

  • file_server.dart : これがファイル・サーバのプログラム

  • resources : サーバがクライアントに開放するファイルを置くフォルダ

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

file_server.dartを使ってみる

  1. FileServer.dartを実行させる。IDE上でこのファイルを選択し、Run 'FileServer.dart'を実行する。コンソールには

    Serving /fserver on http://127.0.0.1:8080と表示される。

  2. 次に自分のブラウザからこのサーバを次のようにアクセスする:

    http://localhost:8080/fserver

    サーバは次のような初期画面を生成してブラウザ側に返す:




これはIEの例であるが、アドレスバーとタブにはDartのアイコン(favicon.ico)が正しく表示されている。Chromeからだとアドレスバーにはアイコンは表示されない。

このアプリの動作は次のようである:

  1. このサーバはresources/のディレクトリに置かれたファイルのみしかダウンロードを許していない。ここではまずこのディレクトリ内のファイルの一覧を表示してその中からユーザが選択できるようにしている。各ファイルの表示にはそのファイルを呼び出すリンクが張られている。例えばresources/Client.htmlの表示にはhttp://localhost:8080/fserver/resources/Client.htmlというリンクが張られている。

  1. 従ってHTMLページを介さなくてもブラウザのアドレス・バーに直接http://localhost:8080/fserver/resources/Client.htmlを入力しても同じ結果が得られる。

  2. もうひとつはテキスト・エリア経由でファイルの名前を入力してSubmitボタンでこれをサーバにクエリ文字列としてサーバに送信できるようにしてある。

    つまりサーバは直接http://localhost:8080/fserver/resources/Client.htmlといった要求パスによる要求とhttp://localhost:8080/fserver?fileName=resources%2FClient.htmlといったクエリによる要求の双方に対応している。

  3. テキスト・エリアからファイルを指定するときは、相対指定でも絶対指定でも可能である。前記のようにresources/readme.textと入力する代わりにC:/dart_apps_server/file_seerver/resources/readme.textといったように絶対パスを入力することも可能である(ここではこのアプリケーションがC:/dart_apps_serverというフォルダに含まれているとしている)。絶対パスはIDE上でそのファイルを選択し、右クリック > Copy Pathでそのパスをコピーし、テキスト・エリアに張り付ければ良い。各自試して見られたい。

FileServer.dartのポイント

最初にクライアントからの要求の振り分け部分(即ち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}.");
  });
}
  1. 2行目でアドレスが128.0.0.1 localhost即ちIPv4ループバック・アドレス)でポートが8080のサーバを作る

  1. 3行目でそのサーバで到来要求を受信する

  2. 4-9行はその要求の処理が終了したときのループバック関数である

  3. 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);
    }
  });
  1. この処理の本体02行目はクライアントからのHTTP要求を完全に受理したfuture.thenメソッドの関数リテラルとして記述されている

  2. 08行目でfileNameという名前のクエリが存在するかどうか、即ちテキスト・ボックスにファイル名が入力されSubmitボタンが押された結果の要求かどうかを調べている。その場合は直ちにFileHandleronRequestメソッドを呼び出している

  3. 14行目以降は正しい直接指定かどうかを調べている。該要求の要求パスの/server以降にresources/という文字列があるかどうかを調べ、存在するときは18行目でFileHandleronRequestメソッドを呼び出している

  4. そうでないときは20行目で正しくない要求だとしてyou can access files in resouces/ only!という表示を付けて初期画面をクライアントに返す

  5. そうでないときは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);
    }
  }
}
  1. クライアントから要求されたファイル名は08行目に示されるように"fileName”という名前でクエリ文字列から取得される

  2. 18行目に示すようにFileインターフェイスのオブジェクトはその名前を引数にして取得される

  3. 20行目に示すように、当該ファイルが存在するかどうかを知るのにexists()またはexistsSync()メソッドを使用する。両者の相違は、そのメソッドが実行完了まで留まる(ブロックされる)か、または非ブロッキングでFutureのイベントを使うかである

  4. ファイル名の拡張子(extension)によって"Content-Type"ヘッダでクライアントに知らせるMIME形式が異なるので、21で示されているようにMIMEタイプもライブラリのメソッドを使用する。mime_typeというライブラリは筆者が用意しpub.dartlangパブリッシュしたものであるが、これらの形式以外のファイル形式の場合はそれを追加する必要がある。IANAのサイトなどで確認して追加されたい。mime_typeライブラリのmime(String FileName)というメソッドは、該当するファイル・タイプが存在しないときはnullを返すので、そのときは22行目のようにブラウザのデフォルトのMIMEタイプを設定する

  5. コメントアウトされている"Content-Disposition", "attachment; filename=\"${fileName}\""というヘッダを追加するとブラウザの対応が異なってくるので、試されると良い。 その値である”inline”はエンティティがユーザにすぐに表示されるべきであるのに対し、”attachment”はユーザがエンティティを閲覧するためには追加的行動をとる必要があることを示す。ここでは”attachment”と指定しているので、Chromeではブラウザに表示するのではなく、ダウンロード物として取り扱う。IEでは次のような問合せをしてくる:




  6. ファイルを開くには26行目に示すように同期して開くopenSyncと非同期(非ブロック)で開くopenのふたつのメソッドがある。スループットが問題になる場合を除いて、通常はコーディングが楽なopenSyncが使われそうである。次の行のlengthSyncも同様である。

  7. 応答ヘッダ部分の設定が終わったので、30行目で該ファイルの中身を出力ストリームに書き込む。この際file.openRead()pipeメソッドを使うと、非常に簡単な記述で済んでしまう。デフォルトでは入力ストリームからのデータを総て書き込むと、出力ストリームは自動的に閉じるので、closeを実行する必要がない。

なおファイルまたはディレクトリの存在のチェックは次のような記述となる:

var path = "myPath";
var fileExists =
    new File(path).existsSync() ||
        new Directory(path).existsSync();

ここでは指定したパス(ファイル・パス)を持つファイルが存在するかまたは指定したパス(ディレクトリ・パス)を持つディレクトリが存在するかを調べている。



MIMEライブラリ

ブラウザが受け付けるContent-TypeContent-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パッケージ

http_serverパッケージGoogleDartチームが作成したライブラリで、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_serverpathのパッケージをインポートしている。

  • 7行目のpathToBuildは現在のディレクトリを今使っているプラットホームに合わせて作ってくれる。joinpathのメソッドである。

  • 9行目のstaticFilesはそのpathToBuildを引数にしたVirtualDirectoryのオブジェクトである。ここでvirtualと言っているのは「実質的な」という意味である。VirtualDirectoryHTTP要求とファイル・システム間のリンクを安全なものとし、また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\httpserverIDE上で展開し、Get dependenciesを実行すると下図のようになる:






static_file_server.dartを使ってみる

ここでstatic_file_server.dartを実行させると次のようにコンソール表示される:

Listening on port 4048

次にブラウザからhttp://localhost:4048でこのサーバをアクセスすると次のように表示される:




またnotes.txtフォイルを呼び出すと次のようにテキスト表示される:




更に存在しなファイル指定すると:




404応答が返される。



APIの概要

http_serverAPIの概要を以下に示す。詳細はこのライブラリのAPIを見て頂きたい。

このライブラリの核となっているのがVirtualDirectoryVirtualHostという二つのクラスである:

  • VirtualDirectoryクラスは該ファイル・システムからの静的コンテントのサービスを書きやすくしている:

    • レンジ・ベースの要求

    • If-Modified-Sinceベースの要求対応

    • 自動的GZip圧縮

    • システム全域またはjailedルート内のいずれかでsymlink対応

    • ディレクトリ・リスティング

  • VirtualHostクラスは到来要求のHostフィールドを使って同じアドレスを持った複数のホストでサービスできるようにしている。サブ・ドメインにはワイルドカードが使える:

var virtualHost = new VirtualHost(server);
// 特定のホスト名に限定
var stream1 = virtualServer.addHost('static.myserver.com');
// ワイルドカードでどんなドメインでも対応させる
var stream2 = virtualServer.addHost('*.myserver.com');
// マッチしない要求に対しHttpStatus.forbidden応答を返す
var stream3 = virtualServer.unhandled;

クラス一覧

HttpBody

HttpRequestまたはHttpClientResponse用にHttpBodyHandlerによって作られたHTTPコンテント・ボディ

HttpBodyFileUpload

ファイル・アップロードをラップしたHttpBodyFileUploadオブジェクトで、アップロードされたファイルのファイル名、contentType及びデータを取り出す手段を提供している

HttpBodyHandler

HttpBodyHandlerは使い勝手の良いHttpBodyオブジェクトのかたちでHTTPメッセージ・データを処理及び収集するためのヘルパ・クラスである。コンテント・ボディは Content-Typeヘッダ・フィールドに基づいて解析される。ボディ全体が読み込まれ解析されたら該ボディ・コンテントは利用可能となる。このクラスはサーバでの要求とクライアントでの応答の双方での処理に使える

HttpClientResponseBody

HttpClientResponseHttpBodyHttpClientResponseBodyの型となる。これはヘッダたちのアクセスの為に HttpClientResponseオブジェクトが含まれている

HttpMultipartFormData

MimeMultipart'multipart/form-data'部として解析することでMimeMultipartをアップグレードするのに使われるクラス

HttpRequestBody

HttpRequestHttpBodyHttpRequestBodyの型となる。総ての要求ヘッダ情報を読み出し、クライアントに応答する為の該要求へのアクセスを提供している

VirtualDirectory

ルート・パスからディレクトリに亘るファイルへのアクセスを HttpRequestsに与える

VirtualHost

名前ベースのアプローチを使って複数のホストで処理するためのユーティリティ・クラス





前のページへ

次のページ