前のページへ

サーバ編

次のページ





Websocket通信 (Websocket)

DartWebSocket対応のAPIを備えているので、この章ではWebSocketベースのサーバを簡単に紹介する。

WebSocketは単一のTCP接続上での完全双方向全二重通信を行う為の技術である。TCP接続が常にクライアントとの間で保持されるので、リアルタイムの送受信が必要なアプリケーションに適している。クライアント(即ちブラウザ)でのWebSocket APIW3Cが標準化を行っており、通信プロトコルはIETFRFC 6455として標準化されている。RFC 6455は既に日本語に翻訳されたものもあるので、それを参照していただきたい。

WebSocketはブラウザとウェブ・サーバが実装するよう設計されているが、そのようないわゆるウェブ・アプリだけでなく他のクライアントとサーバのアプリケーションでも使用可能である。ブラウザとサーバ間の通信がこれまでのHTTPの要求/応答のパラダイムに制約されず、より密に双方が関わりあえる為、チャットなどのライブのコンテントやリアル・タイムのゲームなどへの適用が容易になる。

WebSocketは接続開始の手段としてHTTPに類似した手順を使うので、通常のHTTPTCPポート番号80(あるいはTLS接続の場合443)が使われており、ファイアウォール通過の制約が少ない。

WebSocketのもう一つの利点として、クライアントとのTCP接続が維持されることがある。TCPでは、サーバから見ると自分のポート番号、相手のポート番号、そして相手のIPアドレスのセットで接続が管理されている。従ってその接続をクライアント識別に使えるので、HTTPサーバのようなクッキーなどによるセッション管理は不要である。即ちWebScket接続がセッションそのものだと考えることができる。

WebSocketはセキュア・バージョンも含めて現在殆どのブラウザが多少の差はあるものの実装している。



webSocketプロトコルの概要

接続の確立

WebSocket接続開始に当たっては、クライアントが先ず接続要求を行い、ブラウザがこれに答える形式になっている。

クライアント要求例:

GET /mychat HTTP/1.1

Host: server.example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==

Sec-WebSocket-Protocol: chat

Sec-WebSocket-Version: 13

Origin: http://example.com

サーバ応答例:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

Sec-WebSocket-Protocol: chat

これら、特に要求行とステータス行はHTTPのメッセージと似ている。

クライアントはbase64エンコードされたSec-WebSocket-Key(キー)を渡す。応答に際しては、258EAFA5-E914-47DA-95CA-C5AB0DC85B11というマジック文字列(GUI: Globally Unique Identifier)がこのキーに追加される。追加された文字列は次にSHA-1でハッシュ化(160ビット)され、base64エンコードされる。その結果がSec-WebSocket-Acceptヘッダの値になっている。

一旦WebSocket接続が確立されたら、WebSocketデータ・フレームはクライアントとサーバ間で全二重で送信される。すなわち双方とも相手を待つことなく勝手に同時に相手に送信できる。

接続のクローズ

クローズはクライアントとサーバのどちらからでもその手続きを開始できる。クローズする側は先ず特定の制御シーケンスを含んだデータ・フレームを送信する。そのフレームを受けとった側はその応答としてCloseフレームを送信する。その制御フレームを受け取った最初の側がその接続(TCP接続を含めて)をクローズする。

これらの手順はAPIの中で行われるので、プログラマはその為のAPIのみを理解しておれば良い。

WebSocketフレーム

WebSocketフレームは次のような簡単な構成をとる:

0 1 2 3

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1

+-+-+-+-+-------+-+-------------+-------------------------------+

|F|R|R|R| opcode|M| Payload len | Extended payload length |

|I|S|S|S| (4) |A| (7) | (16/64) |

|N|V|V|V| |S| | (if payload len==126/127) |

| |1|2|3| |K| | |

+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

| Extended payload length continued, if payload len == 127 |

+ - - - - - - - - - - - - - - - +-------------------------------+

| |Masking-key, if MASK set to 1 |

+-------------------------------+-------------------------------+

| Masking-key (continued) | Payload Data |

+-------------------------------- - - - - - - - - - - - - - - - +

: Payload Data continued ... :

+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

| Payload Data continued ... |

+---------------------------------------------------------------+

  • FIN: 1ビットの情報で、これはあるメッセージの最後の断片であることを示す

  • opcode: 4ビットの情報で、ペイロードのデータの解釈を指定する:

    • %x0: 継続フレームである

    • %x1: テキスト(UTF-8エンコード)のフレームである

    • %x2: バイナリ・データのフレームである

    • %x3-7: 予約

    • %x8: 接続のクローズ

    • %x9: ping

    • %xA: pong

    • %xB-F: 予約

  • Mask: 1ビットの情報で、クライアント側からのペイロードのデータがMasking-keyで指定した32ビット長のキーでマスク解除できるようマスクされていることを示す。クライアントからサーバへ送信される総てのフレームでこのビットは1にセットされる。Masking-keyはクライアントがランダムに選択した値である

  • Payload length: 77+16、または7+64ビット長の情報で、ペイロードのバイト長を示す。

  • Payload Data: これは拡張データ(Extension data)とアプリケーション・データ(Application data)をあわせたものである。

Masking-keyPayload data長に影響を与えない。送信されるデータのi番目のバイトは、オリジナルのデータのi番目のバイトとMasking-keyi modulo 4番目のバイトとの排他的論理和をとったものである。Masking-keyを毎回変化させることで、悪意を持ったハッカたちがネットワーク上のデータを予測できないようにしている。

なおW3CWebSocket API仕様は日本語に翻訳したものがあるので参考にされたい。



基本的なWebSocketサーバ

次のコードは、DartチームのメンバであるSeth Laddがそのブログに掲載した記事に示されている簡単なエコー・サーバを新しいAPIに対応するよう変更したものである(IDE上ではC:\dart2_code_samples\websocket\echo\bin\simple_echo_server.dart

websocket\echo\bin\simple_echo_server.dart

// Sample Dart WebSocket echo server
// Source : http://blog.sethladd.com/2012/04/dart-server-supports-web-sockets.html#disqus_thread
// you can connect to ws://localhost:8080/ws
// Feb. 2013, revised to incorporate redesigned dart:io v2 library
// April 2013, WebSocket.send -> add API change fixed

import 'dart:io';

void main() {
  HttpServer.bind('127.0.0.1', 8080).then((HttpServer server) {
    server
        .where((request) => request.uri.path == '/ws')
        .transform(new WebSocketTransformer())
        .listen((WebSocket ws) {
      wsHandler(ws);
    });
    print("Echo server started");
  });
}

wsHandler(WebSocket ws) {
  print('new connection : ${ws.hashCode}');
  ws.listen((message) {
    print('message is ${message}');
    ws.add('Echo: ${message}');
  }, onDone: (() {
    print(
        'connection ${ws.hashCode} closed with ${ws.closeCode} for ${ws.closeReason}');
  }));
}

このコードが前記APIの基本的な使い方を良く説明している。

  1. HttpServer.bindIPアドレスとTCPポート番号を指定してHTTPサーバを用意する。

  2. whereメソッドで要求パスが'/ws'である要求のみを取り込むストリームを用意する。本来ならrequest.uri.scheme == 'ws'で判断すべきであるが、localhostに対しては機能しない

  3. このストリームに対しtransformメソッドでウェブ・ソケットに対応する変換機を付加したストリームにする。

  4. このストリームに対しlistenメソッドでその要素をイベントとして取り込むハンドラwsHandlerを用意する。

  5. 従ってクライアントからのウェブ・ソケット接続に対応したWebSocketのオブジェクト毎にこのハンドラが呼ばれる。つまりこのハンドラは複数のWebSocketオブジェクトが共有する。

  6. WebSocketオブジェクト、即ちクライアントを識別するひとつの手段はこのオブジェクトのハッシュ・コードを使うことである。

  7. ws.listenメソッドでWebSocketオブジェクトからのメッセージ・イベントを受け付けるハンドラを用意する。

  8. ws.addメソッドはクライアントにデータを送り返す。ここではエコーバックなので、受信したメッセージに'Echo : 'という文字列を付加して送り返している。

  9. onDoneで完了のイベントが到来したときは、ウェブ・ソケット接続が切れたことを意味する。



基本的なWebSocketクライアント

以下はGoogledart:io担当のSøren Gjesse氏があるバグ報告の中で示しているサンプルである。このこのプログラムは当初はwebsocket.orgのエコー・サーバにWebSocket接続をし、次に'echo!'というメッセージを送信し、エコーバックされてきたメッセージをコンソールに表示するものだったこれを筆者は折角作ったsimple_websocket_servarに接続するように変更したものである(IDE上ではC:\dart2_code_samples\websocket\echo\bin\simple_echo_client.dart)。このままだとサーバに'echo!'というメッセージを送信したのちは動作を継続したままだが、実際には不要になった時にどちらかから接続を終了させる必要がある。

websocket\echo\bin\simple_echo_client.dart

// Sample Dart WebSocket echo server
// Source : https://github.com/dart-lang/sdk/issues/24278
import 'dart:io';

main() async {

  // Connect to a web socket.
  WebSocket socket = await WebSocket.connect('ws://127.0.0.1:8080/ws');

  // Setup listening.
  socket.listen((message) {
    print('message: $message');
  }, onError: (error) {
    print('error: $error');
  }, onDone: () {
    print('socket closed.');
  }, cancelOnError: true);

  // Add message
  socket.add('echo!');

  // Wait for the socket to close.
  try {
    await socket.done;
    print('WebSocket done');
  } catch (error) {
    print('WebScoket done with error $error');
  }
}



プロキシによるネットワーク・レベルでの確認

プロキシの使用法は「HTTPサーバ」の章で解説してあるのでここでは省略する。simple_echo_server.dartsimple_echo_client.dartの間にsimple_proxy.dartIDE上ではC:\dart2_code_samples\websocket\echo\bin\simple_proxy.dartを介在させ、simple_echo_client.dartのサーバ呼び出しを

WebSocket socket = await WebSocket.connect('ws://127.0.0.1:12345/ws');

と変更してサーバ→プロキシ→クライアントの順に実行させると、プロキシの画面に以下のような記録が残される:

21:12:07.500860 : Proxy is waiting for client's request on InternetAddress('127.0.0.1', IPv4):12345

21:12:22.445637 : New client connection 543583548

21:12:22.460638 : Proxy establised a connection for incomig socket 543583548 with server socket 613259729

21:12:22.465638 : Current connections (client side socket, server side socket) :

(543583548, 613259729)


21:12:22.498640 : ** connection (543583548, 613259729) inbound traffic **

GET /ws HTTP/1.1

user-agent: Dart/2.2 (dart:io)

connection: Upgrade

cache-control: no-cache

accept-encoding: gzip

content-length: 0

sec-websocket-version: 13

host: 127.0.0.1:12345

sec-websocket-extensions: permessage-deflate; client_max_window_bits

sec-websocket-key: iPZEkB4GAQK81HcuQG+sLA==

upgrade: websocket



21:12:22.557643 : ** connection (543583548, 613259729) outbound traffic **

HTTP/1.1 101 Switching Protocols

connection: Upgrade

content-length: 0

sec-websocket-extensions: permessage-deflate; client_max_window_bits=15

x-frame-options: SAMEORIGIN

content-type: text/plain; charset=utf-8

x-xss-protection: 1; mode=block

x-content-type-options: nosniff

upgrade: websocket

sec-websocket-accept: mxhffRZTgRCdM+GQRybhSLW8xZs=



21:12:22.605646 : ** connection (543583548, 613259729) inbound traffic **

????~^

21:12:22.605646 : ** connection (543583548, 613259729) inbound traffic **

??????~

21:12:22.629648 : ** connection (543583548, 613259729) outbound traffic **

??

21:12:22.629648 : ** connection (543583548, 613259729) outbound traffic **

rM???RH?????

これを「プロトコル」の節の「接続の確立」の項で示したヘッダと比較すると面白い。特にsecure-websocketが使われていることに注目されたい。Dart 2WebSocketChromeWebSocketともにすでにsecure-websocketに対応している。その後2つのTCPパケットで情報が交換されているが、暗号化されていて読むことは不可能である。



チャット・サーバ

Echoサーバのアプリケーションを参考にすれば、チャット・サーバは容易に開発できよう。ここではGithubに置いてあるwebsocket_chat_serverでその簡単な見本を示すことにする。

このアプリケーションはサーバのWebSocketChatServer.dartDartで書かれたWebSocketChatClient.dartとそれと組になるWebSocketChatClient.html、及びJavaScriptを使ったクライアントの為のWebSocketChat.html(これはテスト用)で構成されている。




  1. この画面のZIPと表示されたダウンロードの個所をクリックするとこのアプリケーション全体がZIP圧縮された形式のwebsocket_chat_server-master.zipというファイルが取得できる。これを適当な解凍ツールで展開する。

  2. 自分のIDEFile -> Open、そしてこのwebsocket_chat_server-masterを選択し、”This window”を選択して展開する。




  3. ここでpubspec.yamlを選択して、Get dependenciesを実行する。sarani

  4. 更にBuildコマンドを実行してクライアントのDartコードをJavaScriptに変換し、webアプリとして配布に必要なbuildという名前のフォルダを作る。




  5. これでこのアプリが必要とする以下のようなファイルが得られる:

    • bin\WebSocketChatServer.dart:サーバのコード

    • build\WebSocketChatClient.html:クライアントがアクセスするページ

    • build\WebSocketChatClient.dart.js:上記のページが使うJavaScriptに変換したDartコード

    • build\WebSocketChat.html:これは参考の為のJavaScriptで書いた上のページと等価なもの

  1. WebSocketChatServer.dartを実行させる。コンソールには次のようなメッセージが表示される

    20:17:38.540634 - Serving Chat on 127.0.0.1:8080.

  2. 次にブラウザからこのサーバをアクセスする。これには5つの手段がある:

    • ブラウザのアドレス・バーからhttp://localhost/chatを入力する(これが本来の使用法になる)

    • IDE上の\build\WebSocketChatClient.htmlを選択し、Open in browser→chromeでこのファイルを開く

    • IDE上の\build\WebSocketChatClient.htmlを選択し,ファイルパスをコピーし、アドレスバーに貼り付けファイル読み出しを実行する

    • IDE上の\build\WebSocketChat.htmlを選択し、Open in browser→chromeでこのファイルを開く(これは参考の為)

    • IDE上の\build\WebSocketChat.htmlを選択し,ファイルパスをコピーし、アドレスバーに貼り付けファイル読み出しを実行する(これは参考の為)

      注意:なおもし最初の方法でブラウザが接続できないと表示したときはブラウザの指示に従いプロキシ設定を変更する

  1. WebSocket接続を開始するには、ユーザ名を入力してJoinボタンをクリックする。

  2. メッセージを送信するには、メッセージを入力してSendボタンをクリックする。

  3. 接続を解放するには、Leaveボタンをクリックする。

2つのブラウザのインスタンスからのアクセス例

下図は、スクリーン上に2つのブラウザを立ち上げ、左が習近平国家主席、右がマクロン大統領に見立てたものである。この画面で判るように、同じサーバに対し2つのWebSocket接続がともに確立され、またそれぞれが独立してサーバと交信している。







そのシナリオは:

  1. 最初に習近平国家主席、次にマクロン大統領がこのチャットに参加している

  2. 習近平国家主席は「古代のシルクロードを再生させたい。中国は双方向の貿易と投資の流れを望んでいる」と切り出す

  3. マクロン大統領は「中国と欧州は相互に利益を尊重し、バランスのとれた関係であるべきだ」

  4. 習近平国家主席は「中国は自国の発展が諸外国に恩恵を与えることを望んでおり、一帯一路についても同様の考えだ」と述べる

  5. 結局すれ違いになり、習近平国家主席は1032分に席を立っている

IDE上のコンソールには次のような記録が残される:

10:30:36.972593 : New connection 625205500 (active connections : 1)

10:30:37.011487 : Received message on connection 625205500: userName=習近平

10:30:46.296077 : New connection 104511982 (active connections : 2)

10:30:46.309078 : Received message on connection 104511982: userName=マクロン

10:31:20.453094 : Received message on connection 625205500: 古代のシルクロードを再生させたい。中国は双方向の貿易と投資の流れを望んでいる

10:31:55.954156 : Received message on connection 104511982: 中国と欧州は相互に利益を尊重し、バランスのとれた関係であるべきだ

10:32:28.378081 : Received message on connection 625205500: 中国は自国の発展が諸外国に恩恵を与えることを望んでおり、一帯一路についても同様の考えだ

10:32:53.311222 : Closed connection 625205500 with 1005 for (active connections : 1)

WebSocketConnectionオブジェクトは習近平が参加した時点で1個、次にマクロンが参加した時点で2個になっている。

チャット・サーバのプログラムのポイント

まずこのサーバのmain関数のコードを以下に示す。main()ではHTTPWebSocket2つのプロトコルに対応する必要がある。何故ならチャットの為の画面(HTMLテキストとDartiumにはdartコード)をクライアントにHTTP応答として送り返さねばならないからである。HTTPサービスとWebSocketサービスをIPアドレスは同じにしてポート番号を違わせて区別することも可能だが、ここでは同じアドレス(実験の為にここではループバック・アドレス)とポート(8080)を使う方式を紹介する。最初はHTTP要求とWebSocket接続要求の双方を受け付けるmain()関数を示す

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void main() {
  WebSocketHandler webSocketHandler = WebSocketHandler();
  HttpRequestHandler httpRequestHandler = HttpRequestHandler();
  print(
      '${DateTime.now().toString().substring(11)} - Serving Chat on ${HOST}:${PORT}.');

  HttpServer.bind(HOST, PORT).then((HttpServer server) {
    server.listen((request) {
      if (request.uri.path == WEB_SOCKET_REQUEST_PATH) {
        WebSocketTransformer.upgrade(request).then((ws) {
          webSocketHandler.wsHandler(ws);
        });
      } else if (request.uri.path.startsWith(HTTP_REQUEST_PATH)) {
        httpRequestHandler.requestHandler(request);
      }
    });
  });
}
  • 7、8行目:最初にHttpServerのオブジェクトを作り、HTTPWebSocket接続の双方の要求を受け付ける

  • 9、10行目:その要求パスがWebSocketのパスだったら、WebSocketTransformer.upgrade(request)WebSocket接続のプロトコルを交わし、結果としてのwebsocketオブジェクトをそのハンドラwsHandler(ws)に渡す

  • 通常のHTTP要求は1314行でrequestHandler(request)に渡している

WebSocket接続での処理はwebSocketHandlerクラスが行っている。以下はその一部である:

 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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// handle WebSocket events
class WebSocketHandler {
  Map<String, WebSocket> users = {}; // Map of current users

  wsHandler(WebSocket ws) {
    log('New connection ${ws.hashCode} '
        '(active connections : ${users.length + 1})');
    ws.listen((message) {
      processMessage(ws, message);
    }, onDone: () {
      processClosed(ws);
    });
  }

  processMessage(WebSocket ws, String receivedMessage) {
    try {
      String sendMessage = '';
      String userName;
      userName = getUserName(ws);
      log('Received message on connection'
          ' ${ws.hashCode}: $receivedMessage');
      if (userName != null) {
        sendMessage = '${timeStamp()} $userName >> $receivedMessage';
      } else if (receivedMessage.startsWith("userName=")) {
        userName = receivedMessage.substring(9);
        if (users[userName] != null) {
          sendMessage = 'Note : $userName already exists in this chat room. '
              'Previous connection was deleted.\n';
          log('Duplicated name, closed previous '
              'connection ${users[userName].hashCode} (active connections : ${users.length})');
          users[userName]
              .add(preFormat('$userName has joind using another connection!'));
          users[userName].close(); //  close the previous connection
        }
        users[userName] = ws;
        sendMessage = '${sendMessage}${timeStamp()} * $userName joined.';
      }
      sendAll(sendMessage);
    } catch (err, st) {
      print('${new DateTime.now().toString()} - Exception - ${err.toString()}');
      print(st);
    }
  }
  processClosed(WebSocket ws) {
    try {
      String userName = getUserName(ws);
      if (userName != null) {
        String sendMessage = '${timeStamp()} * $userName left.';
        users.remove(userName);
        sendAll(sendMessage);
        log('Closed connection '
            '${ws.hashCode} with ${ws.closeCode} for ${ws.closeReason}'
            '(active connections : ${users.length})');
      }
    } catch (err, st) {
      print(
          '${new DateTime.now().toString().substring(11)} - Exception - ${err.toString()}');
      print(st);
    }
  }
  • wsHandler(WebSocket ws)メソッドではWebSocketのオブジェクトwsStreamを実装しているので、このストリームからのmessageclosedのイベントを受け取りそれらのイベントの処理メソッドprocessMessage及びprocessClosedに渡している。

  • サーバは例外が発生してもサービスを停止させてはいけない。従ってこれらのメソッド内ではtry文を使って例外をきちんと捕捉することが必須である。

  • チャット・サーバの場合は、現在参加しているユーザを何時も管理しなければならない。ここではユーザ名をキー、WebSocketオブジェクトを値としたMapであるusersというオブジェクトでこれを管理している。

  • クライアントは接続を開始すると同時に、ユーザ名をサーバに送る。サーバはこのユーザ名と接続のペアをusersに登録する。

  • sendAllという関数は、現在登録されている総てのユーザに指定したテキストを送信する。その際、送信するテキストをpreFormatという関数に通している。これは例えば<br>というテキストをブラウザが改行と解釈したり、逆に改行を無視したり、連続したスペースを1個のみしか表示したりしないようにするものである。この作業はクライアント側で行っても良いが、ここではサーバ側で実施している。これにより、改行入りのテキストを送信しても、クライアント側では正しく表示される。

  • 最初にあるユーザが接続を開始したときには、025行目に示すようにuserName=という形式でユーザ名が送られてくるので、これを識別する必要がある。間違って既に参加済みのユーザがuserName=というテキストを送信しても、それは新たな参加とは見做されない。

  • 26行目にあるように、既に参加済みのユーザ名と同じユーザ名で接続してきた場合(例えばクライアントがソケット切断しないまま別の画面に移り、再びこのチャットに参加してきた場合)は、警告を出してその接続削除し、新しいWebSocketオブジェクトとuserNameを登録する。

  • logという関数は、プログラマが使っているロガーに適合させる。ここではコンソールに出力している。

JavaScriptベースのクライアント側のコード

ここではサーバのテスト、及びJavaScriptコードとDartコードとの比較の為に、次のようなJavaScriptベースのHTMLテキストをクライアントように置いてある

websocket_chat_server\web\WebSocketChat.html

<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<title>WebSocket Chat</title>
<script language="javascript" type="text/javascript">
  var wsUri = "ws://localhost:8080/Chat";
  var mode = "DISCONNECTED";
  window.addEventListener("load", init, false);
  function init() {
    var consoleLog = document.getElementById("consoleLog");
    var clearLogBut = document.getElementById("clearLogButton");
    clearLogBut.onclick = clearLog;
    var connectBut = document.getElementById("joinButton");
    connectBut.onclick = doConnect;
    var disconnectBut = document.getElementById("leaveButton");
    disconnectBut.onclick = doDisconnect;
    var sendBut = document.getElementById("sendButton");
    sendBut.onclick = doSend;
    var userName = document.getElementById("userName");
    var sendMessage = document.getElementById("sendMessage");
  }
  function onOpen(evt) {
    logToConsole("CONNECTED");
    mode = "CONNECTED";
    websocket.send("userName=" + userName.value);
  }
  function onClose(evt) {
    logToConsole("DISCONNECTED");
    mode = "DISCONNECTED";
  }
  function onMessage(evt) {
    logToConsole('<span style="color: blue;">' + evt.data+'</span>');
  }
  function onError(evt) {
    logToConsole('<span style="color: red;">ERROR:</span> ' + evt.data);
    websocket.close();
  }
  function doConnect() {
    if (mode == "CONNECTED") {
      return;
    }
    if (window.MozWebSocket) {
      logToConsole('<span style="color: red;"><strong>Info:</strong> This browser supports WebSocket using the MozWebSocket constructor</span>');
      window.WebSocket = window.MozWebSocket;
    }
    else if (!window.WebSocket) {
      logToConsole('<span style="color: red;"><strong>Error:</strong> This browser does not have support for WebSocket</span>');
      return;
    }
    if (!userName.value) {
      logToConsole('<span style="color: red;"><strong>Enter your name!</strong></span>');
      return;
    }
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }
  function doDisconnect() {
    if (mode == "CONNECTED") {
    }
    websocket.close();
  }
  function doSend() {
    if (sendMessage.value != "" && mode == "CONNECTED") {
      websocket.send(sendMessage.value);
      sendMessage.value = "";
    }
  }
  function clearLog() {
    while (consoleLog.childNodes.length > 0) {
      consoleLog.removeChild(consoleLog.lastChild);
    }
  }
  function logToConsole(message) {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    consoleLog.appendChild(pre);
    while (consoleLog.childNodes.length > 50) {
      consoleLog.removeChild(consoleLog.firstChild);
    }
    consoleLog.scrollTop = consoleLog.scrollHeight;
  }
</script>
<h2>WebSocket Chat Sample</h2>
<div id="chat">
  <div id="chat-access">
    <strong>Your Name:</strong><br>
    <input id="userName" cols="40">
    <br>
    <button id="joinButton">Join</button>
    <button id="leaveButton">Leave</button>
    <br>
    <br>
    <strong>Message:</strong><br>
    <textarea rows="5" id="sendMessage" style="font-size:small; width:265px"></textarea>
    <br>
    <button id="sendButton">Send</button>
    <br>
    <br>
  </div>
  <div id="chat-log"> <strong>Chat:</strong>
    <div id="consoleLog" style="font-size:small; width:270px; border:solid;
         border-width:1px; height:172px; overflow-y:scroll"></div>
    <button id="clearLogButton" style="position: relative; top: 3px;">Clear log</button>
  </div>
</div>
</html>
  • JavaScriptWebSocket接続を行うには、URIを引数にして新しいWebSocketのインスタンスを生成する。

  • その後接続完了、接続開放、メッセージ受信、及びエラー発生のイベント処理の為のハンドラをこのオブジェクトにセットする。

  • チャットのログの為の"consoleLog”というIDの要素に対しては、logToConsoleという関数を使用する。このテキスト領域は最大50メッセージを収容させ、それを超えたら最初のほうから削除してゆく。

  • </script>以降のHTMLテキストは、画面を見れば特に説明する必要はなかろう。

Dartベースのクライアント側のコード

VavaScriptに変換して実行させる為のDartベースのクライアントのDart部を以下に示す。HTML部はJavaScriptのそれと同じである。またその動作もJavaScriptベースのクライアントとまったく同じである。双方のコードを比較すると、DOMアクセスに対する両者の相違が判って参考になろう。

websocket_chat_server\web\WebSocketChatClient.dart

import 'dart:html';
var wsUri = 'ws://localhost:8080/Chat';
var mode = 'DISCONNECTED';
WebSocket webSocket;
var userName;
var sendMessage;
var consoleLog;
void main() {
  show('Dart WebSocket Chat Sample');
  userName = document.querySelector('#userName');
  sendMessage = document.querySelector('#sendMessage');
  consoleLog = document.querySelector('#consoleLog');
  document.querySelector('#clearLogButton').onClick.listen((e) {clearLog();});
  document.querySelector('#joinButton').onClick.listen((e) {doConnect();});
  document.querySelector('#leaveButton').onClick.listen((e) {doDisconnect();});
  document.querySelector('#sendButton').onClick.listen((e) {doSend();});
}
doConnect() {
  if (mode == 'CONNECTED') {
    return;
  }
  if (userName.value == '') {
    logToConsole('<span style="color: red;"><strong>Enter your name!</strong></span>');
    return;
  }
  webSocket = new WebSocket(wsUri);
  webSocket.onOpen.listen(onOpen);
  webSocket.onClose.listen(onClose);
  webSocket.onMessage.listen(onMessage);
  webSocket.onError.listen(onError);
}
doDisconnect() {
  if (mode == 'CONNECTED') {
  }
  webSocket.close();
}
doSend() {
  if (sendMessage.value != '' && mode == 'CONNECTED') {
    webSocket.send(sendMessage.value);
    sendMessage.value = '';
  }
}
clearLog() {
  while (consoleLog.nodes.length > 0) {
    consoleLog.nodes.removeLast();
  }
}
onOpen(open) {
  logToConsole('CONNECTED');
  mode = 'CONNECTED';
  webSocket.send('userName=${userName.value}');
}
onClose(close) {
  logToConsole('DISCONNECTED');
  mode = 'DISCONNECTED';
}
onMessage(message) {
  logToConsole('<span style="color: blue;">${message.data}</span>');
}
onError(error) {
  logToConsole('<span style="color: red;">ERROR:</span> ${error}');
  webSocket.close();
}
logToConsole(message) {
  Element pre = new Element.tag('p');
  pre.style.wordWrap = 'break-word';
  pre.innerHtml = message;
  consoleLog.nodes.add(pre);
  while (consoleLog.nodes.length > 50) {
    consoleLog.$dom_removeChild(consoleLog.nodes[0]);
  }
  pre.scrollIntoView();
}
show(String message) {
  document.querySelector('#status').text = message;
}

WebSocket接続の記述は:

webSocket = new WebSocket(wsUri);

webSocket.onOpen.listen(onOpen);

webSocket.onClose.listen(onClose);

webSocket.onMessage.listen(onMessage);

webSocket.onError.listen(onError);

と、JavaScriptに比べるとより短い記述で済む。

またログ表示の関数は:

logToConsole(message) {

var pre = document. $dom_createElement('p');

pre.style.wordWrap = 'break-word';

pre.innerHtml = message;

consoleLog.nodes.add(pre);

while (consoleLog.nodes.length > 50) {

consoleLog.$dom_removeChild(consoleLog.nodes[0]);

}

pre.scrollIntoView();

}

と、DOMアクセスの書き方がやや異なってくる。



WebSocketの為のAPI

20132月のM3版でdart:ioに更なる変更がなされた。これは非同期処理の為のFutureStreamの積極的な導入である。

WebSocketに関してはインターフェイスの簡素化がおこなわれるとともに、サーバ側とクライアント側がソケット接続に対し同じクラスが使われるようになった。サーバ側ではWebSocketの処理はストリーム・トランスフォーマとして組み入れられた。このトランスフォーマはHttpRequestのストリームをWebSocketのストリームに変換するものである。

更に20133月のAPI変更ではWebSocketからのイベントがメッセージのストリームとなった(List<int> 型またはString型)。クローズにイベントは onDoneとなり、クローズ・コードと理由句はWebSocketオブジェクトの属性となった。

更に20161月にRFC 7692対応の圧縮に対応させた。これはデフォルト実装となった。メソッドWebSocket.connect, WebSocket.fromUpgradedSocket, 及び WebSocketTransformer.upgrade、そしてWebSocketTransformerにおいて、名前付きパラメタcompressionCompressionOptionsクラスを使ってmodifyまたはdisableに出来る。

dart:io.WebSocketTransformer

Class WebSocketTransformer


WebSocketTransformerHTTPまたはHTTPSからの各HttpRequestWebSocket接続にアップグレードする機能を持つ。これは単一のHttpRequest及びHttpRequestのストリームをアップグレードできる。

単一のHttpRequestをアップグレードするには、staticupgradeメソッドを使用する:

HttpServer server; server.listen((request) { if (...) { WebSocketTransformer.upgrade(request).then((websocket) { ... }); } else { // Do normal HTTP request processing. } });

HttpRequestのイベントたちを変換するには、HTTPまたはHTTPSサーバからの各HttpRequestをアップグレードしてWebSocketプロトコルにアップグレードすることにより、HttpRequestのストリームを WebSocketsのストリーム変換する stream transformerを実装しているので、以下のように記述できる:

server.transform(new WebSocketTransformer()).listen((webSocket) => ...);

この変換器はRFC6455で規定されたウェブ・ソケットを実装している。

実装

StreamTransformer<HttpRequest, WebSocket>


コンストラクタ

factory WebSocketTransformer({dynamic protocolSelector(List<String> protocols), CompressionOptions compression: CompressionOptions.compressionDefault })

Create a new WebSocketTransformer. [...]


Static メソッド

isUpgradeRequest(HttpRequest request) → bool

該要求が有効なWebSocketアップグレード要求かどうかをチェックする

upgrade(HttpRequest request, { dynamic protocolSelector(List<String> protocols), CompressionOptions compression: CompressionOptions.compressionDefault }) → Future<WebSocket>

HttpRequestWebSocket接続にアップグレードする。もし該要求が有効なWebSocketアップグレード要求でないときは、ステータス・コード500HTTP応答が返される。それ以外の場合は返されるfutureはそのアップグレードのプロセスが完了したらそのWebSocketのオブジェクトで完了する。

dart:io.WebSocket

Abstract Class WebSocketConnection

代替的なウェブ・ソケットのクライアント・インターフェイス。このインターフェイスはhttp://dev.w3.org/html5/websockets/で規定されているウェブ・ソケットの為のW3CブラウザAPIに対応している。

実装

Stream<Event>


static属性

const int CLOSED


const int CLOSING


const int CONNECTING


const int OPEN


staticメソッド

Future<WebSocket> connect(String url, [protocols])

新しいウェブ・ソケット接続を作る。uriで指定するURIwsまたはwssのスキームを使用しなければならない。protocols引数は該クライアントが使いたいサブプロトコルを指定するもので、StringまたはList<String>でなければならない。

属性

final int bufferedAmount

送信の為に現在バッファリングされているバイト数を返す。

final int closeCode

WebSocket接続がクローズしたときにセットされるクローズ・コード。クローズ・コードが得られないときはこの属性はnullとなる。

final String closeReason

WebSocket接続がクローズしたときにセットされるクローズ理由句。クローズ理由が得られないときはこの属性はnullとなる。

final String extensions

このextensions属性は初期には空のStringである。ウェブ・ソケット接続が確立された後はこの文字列はこのサーバが使っている拡張(extension)たちを反映する。

final Future<T> first

Streamから継承)

最初の要素を返す。

もし空のときはStateErrorをスローする。そうでないときはこのメソッドはthis.elementAt(0)と等価である。

final bool isBroadcast

Streamから継承)

このストリームが放送ストリームかどうかを返す。

final Future<bool> isEmpty

Streamから継承)

このストリームが何ら要素を含んでいないかどうかを報告する。

final Future<T> last

Streamから継承)

最後の要素を返す。

もし空のときはStateErrorをスローする。

final Future<int> length

Streamから継承)

このストリームの中の要素の数を数える。

final String protocol

このprotocol属性は初期は空の文字列である。ウェブ・ソケット接続が確立された後はこの値はサーバが選択したサブプロトコルがこの値となる。サブプロトコルのネゴシエーションがされていなければこの値はnullのままである。

final int readyState

現在の接続の状態を返す。

final Future<T> single

Streamから継承)

単一の要素を返す。

空のときまたはひとつ以上の要素を持っているときはStateErrorをスローする。

メソッド

abstract void add(data)

このウェブ・ソケット接続上でデータを送信する。このデータはStringまたはバイト列であるList<int>でなければならない。

abstract void addError(errorEvent)

EventSinkから継承)

asyncエラーを生成する

abstract Future addStream(Stream stream)

あるストリームからデータをウェブ・ソケット接続上で送信する。ストリームからの各データ・イベントは単一のWebSocketフレームとして送信される。streamからのデータはStringsまたはバイトを保持するList<int>sでなければならない。

Future<bool> any(bool test(T element))

Streamから継承)

このストリームが用意している要素のどれかをtestが受け付けるかどうかをチェックする。

その答えが判ったときにFutureを完了させる。もしこのストリームがエラーを報告したときは、このFutureはエラーを報告する。

Stream<T> asBroadcastStream()

Streamから継承)

これと同じイベントたちを作り出す複数受信のストリームを返す。もしこのストリームが単一受信のときは、複数の受信者が許される新しいストリームを返す。その最初の受信者が付加されたときにこれはこのストリームに受信し、最後の受信者がキャンセルされたときに再度受信解除される。

もしこのストリームが既に放送ストリームであるときは、これは手を加えられないで返される。

abstract void close([int code, String reason])

このウェブ・ソケット接続を閉じる。

Future<bool> contains(T match)

Streamから継承)

このストリームが用意した要素たちのなかでmatchが生じるかどうかをチェックする。

この答えが判ったときにこのFutureを完了させる。このストリームがエラーを報告したときは、該Futureはそのエラーを報告する。

Stream<T> distinct([bool equals(T previous, T next)])

Streamから継承)

もし以前のデータ・イベントと同じのときはこれらのデータ・イベントをスキップする。

返されるストリームは、ふたつの連続したデータが決して同じで無いということを除いて、このストリームと同じイベントを作る。

用意されるequalsメソッドによって等しいかどうかが判断される。もしこれがオミットされているときは、最後に用意されたデータ要素には'=='演算子が使われる。

Future<T> elementAt(int index)

Streamから継承)

このストリームのindex番目のデータ・イベントの値を返す。

もしエラーが起きれば、このfutureはエラーで終了する。

もしこのストリームが閉じる前にindex要素たちより少ない数しか用意していないときは、エラーが報告される。

Future<bool> every(bool test(T element))

Streamから継承)

このストリームが用意している総ての要素をtestが受け付けるかどうかをチェックする。

この答えが判った時点でこのFutureを完了させる。もしこのストリームがエラーを報告するときは、このFutureはそのエラーを報告する。

Stream expand(Iterable convert(T value))

Streamから継承)

各要素をゼロまたはそれ以上のイベントたちに変換する新しいストリームをこのストリームから生成する。

各到来イベントは新しいイベントたちのIterableに変換され、これらの新しいイベントたちの各々が次に順番に返されたストリームによって送信される。

Future<T> firstMatching(bool test(T value), {T defaultValue()})

Streamから継承)

testにマッチするこのストリームの最初の要素を見つける。

testtrueを返すこのストリームの最初の要素で満たされたfutureを返す。

このストリームが完了する前にそのような要素が見つからず、またdefaultValue関数が用意されているときは、defaultValue呼び出しの結果がそのfutureの値となる。

エラーが発生したとき、あるいはこのストリームが一致する要素が見つからないで終了し、またdefaultValue関数が用意されていないときは、このfutureはエラーを受信する。

Stream<T> handleError(void handle(AsyncError error), {bool test(error)})

Streamから継承)

このストリームからの何らかのエラーを横取りするラッパーのストリームを生成する。

もしこのラッパ・ストリームがtestにマッチするエラーを送信するときは、それはhandle関数により横取りされる。

test(e)trueを返すと[AsyncError] [:e:]test関数によりマッチがとられる。testがオミットされているときは各エラーはマッチしていると見做される。

もしそのエラーが横取りされたときは、handle関数はそれに対してどうするかを判断できる。この関数は新しい(あるいは同じ)エラーを生起させたいときはスローできるし、あるいはこのストリームにこのエラーを忘れさせる為に単に戻ることができる。

あるエラーをデータ・イベントに変換したいときは、より一般的なStream.transformEvenを使って出力sinkへのデータ・イベントを書いて、このイベントを処理させる。

Future<T> lastMatching(bool test(T value), {T defaultValue()})

Streamから継承)

このストリームの中でtestにマッチする最後の要素を探す。

最後のマッチする要素を見つけることを除いてfirstMatchingとおなじ。このことはこのストリームが完了するまでこの結果は得られないことを意味する。

abstract StreamSubscription<T> listen(void onData(T event), {void onError(AsyncError error), void onDone(), bool unsubscribeOnError})

Streamから継承)

このストリームにひとつの受信を付加する。

このストリームからの各テータ・イベントにたいし、受信者たちのonDataハンドラが呼び出される。もしonDatanullのときは何も起きない。

このストリームからのエラーにたいし、onErrorハンドラにはそのエラーを記述した AsyncErrorが与えられる。

このストリームがクローズしたときは、onDone ハンドラが呼び出される。

unsubscribeOnErrortrueのときは、最初のエラーが報告されたときにこの受信は終了する。デフォルト値はfalseである。

Stream map(convert(T event))

Streamから継承)

このストリームの各要素をconvert関数を使って新しい値に変換する新しいストリームを生成する。

Future<T> max([int compare(T a, T b)])

Streamから継承)

このストリームのなかの最大の要素を探す。

もしこのストリームが空のときはその結果はnullである。そうでないときは、その結果はこのストリームからの他のどの値よりも小さくないこのストリームからの値となる(Comparatorでなければならないcompareに従って)。

もしcompareがオミットされているときは、そのデフォルトはComparable.compareである。

Future<T> min([int compare(T a, T b)])

Streamから継承)

このストリームのなかの最小の要素を探す。

もしこのストリームが空のときはその結果はnullである。そうでないときは、その結果はこのストリームからの他のどの値よりも大さくないこのストリームからの値となる(Comparatorでなければならないcompareに従って)。

もしcompareがオミットされているときは、そのデフォルトはComparable.compareである。

Future pipe(StreamConsumer<T, dynamic> streamConsumer)

Streamから継承)

このストリームを用意されているStreamConsumerの入力に結び付ける。

Future pipeInto(StreamSink<T> sink, {void onError(AsyncError error), bool unsubscribeOnError})

Streamから継承)

Future reduce(initialValue, combine(previous, T element))

Streamから継承)

combineを繰り返し適用することで値たちの並びを減らす。

Future<T> singleMatching(bool test(T value))

Streamから継承)

testにマッチするこのストリームのなかの最初の要素を探す。

このストリームの中でひとつ以上のマッチする要素が生じないときにエラーとなることを除いてlastMatchと似ている。

Stream<T> skip(int count)

Streamから継承)

このストリームからの最初のcount個数のデータ・イベントをスキップする。

Stream<T> skipWhile(bool test(T value))

Streamから継承)

testにマッチする間はこのストリームからのデータ・イベントをスキップする。

返されたストリームはエラーと完了のイベントを加工しないで渡す。

そのイベント・データに対しtesttrueを返す最初のデータ・イベントから始まり、返されるこのストリームはこのストリームと同じイベントを持つことになる。

Stream<T> take(int count)

Streamから継承)

このストリームの最大n個の値を渡す。

このストリームの最初のn個のデータ・イベント、及び総てのエラー・イベントを返されるストリームに転送し、完了イベントで終了する。

このストリームがその完了前にcount値より少ない場合は、返されるストリームもそうする。

Stream<T> takeWhile(bool test(T value))

Streamから継承)

testが成功している間はデータ・イベントを転送する。

返されたストリームはそのイベントデータに対しtesttrueを返す限りこのストリームと同じイベントを渡す。このストリームはthisストリームが完了した、あるいはthisストリームが最初にtestを受け付けない値を最初に渡したときに完了する。

Future<List<T>> toList()

Streamから継承)

このストリームのデータをListとして収集する。

Future<Set<T>> toSet()

Streamから継承)

このストリームのデータをSetとして収集する。

CodeStream transform(StreamTransformer<T, dynamic> streamTransformer)

Streamから継承)

このストリームを指定されたStreamTransformerの入力として連結する。

streamTransformer.bind自身の結果を返す。

Stream<T> where(bool test(T event))

Streamから継承)

何らかのデータ・イベントたちを破棄する新しいストリームをこのストリームから生成する。

新しいストリームはこのストリームと同じエラーと完了のイベントを送信するが、testを満足させるデータ・イベントのみを送信する。

dart:html.WebSocket

これはブラウザ内で使われるウェブ・ソケットのインターフェイスである。

Class WebSocket

WebSocketに接続し、そのWebSocket上でデータを送受信するのにこのWebSocketインターフェイスを使用する。

自分のアプリ内でWebSocketを使用するには最初に WebSocket URLを該コンストラクタに渡してWebSocketオブジェクトを生成する。

var webSocket = new WebSocket('ws://127.0.0.1:1337/ws');

WebSocket上でデータを送信するには、sendメソッドを使用する:

if (webSocket != null && webSocket.readyState == WebSocket.OPEN) {

webSocket.send(data);

} else {

print('WebSocket not connected, message $data not sent');

}

WebSocket上でデータを受信するには、メッセージ・イベントたちのリスナを登録する:

webSocket.onMessage.listen((MessageEvent e) {

receivedData(e.data);

});

メッセージ・イベント・ハンドラはその単一の引数としての MessageEventオブジェクトを受理する。WebSocketEvents で規定されているようにopenclose、及びerrorのハンドラたちも定義できる。


継承

Object→EventTarget→WebSocket

アノテーション

@SupportedBrowser(SupportedBrowser.CHROME)

@SupportedBrowser(SupportedBrowser.FIREFOX)

@SupportedBrowser(SupportedBrowser.IE, '10')

@SupportedBrowser(SupportedBrowser.SAFARI)

@Unstable()

@Native("WebSocket")

コンストラクタ

WebSocket(

String url, [

Object protocols

])


プロパティ

binaryType ↔ String

read / write


bufferedAmount → int

final


extensions → String

final


onClose → Stream<CloseEvent>

read-only

このWebSocketで扱われたcloseイベントのストリーム

onError → Stream<Event>

read-only

このWebSocketで扱われたerrorイベントのストリーム

onMessage → Stream<MessageEvent>

read-only

このWebSocketで扱われたmessageイベントのストリーム

onOpen → Stream<Event>

read-only

このWebSocketで扱われたopenイベントのストリーム

protocol → Stringfinal


readyState → int

final


url → String

final


メソッド

close([int code, String reason ]) → void


send(dynamic data) → void

この接続上でサーバにデータを送信する

sendBlob(Blob data) → void

@JSName('send')

この接続上でサーバにデータを送信する

sendByteBuffer(ByteBuffer data) → void

@JSName('send')

この接続上でサーバにデータを送信する

sendString(String data) → void

@JSName('send')

この接続上でサーバにデータを送信する

sendTypedData(TypedData data) → void

@JSName('send')

この接続上でサーバにデータを送信する

addEventListener(String type, EventListener listener, [ bool useCapture ]) → void

inherited


dispatchEvent(Event event) → bool

inherited


staticプロパティ

supported → bool

read-only

現在のプラットホーム上でこのタイプが対応しているかをチェックする

常数

CLOSED → const int

3


closeEvent → const EventStreamProvider<CloseEvent>

必ずしもWebSocketのインスタンスではないcloseイベントをイベント・ハンドラにエクスポーズするために設計されたstaticなファクトリ

const EventStreamProvider<CloseEvent>('close')

CLOSING → const int

2


CONNECTING → const int

0


errorEvent → const EventStreamProvider<Event>

必ずしもWebSocketのインスタンスではないerrorイベントをイベント・ハンドラにエクスポーズするために設計されたstaticなファクトリ

const EventStreamProvider<Event>('error')

messageEvent → const EventStreamProvider<MessageEvent>

必ずしもWebSocketのインスタンスではないmessageイベントをイベント・ハンドラにエクスポーズするために設計されたstaticなファクトリ

const EventStreamProvider<MessageEvent>('message')

OPEN → const int

1

必ずしもWebSocketのインスタンスではないopenイベントをイベント・ハンドラにエクスポーズするために設計されたstaticなファクトリ

const EventStreamProvider<Event>('open')

openEvent → const EventStreamProvider<Event>





前のページへ

次のページ