連載
» 2010年10月05日 00時00分 公開

WebSocketで目指せ! リアルタイムWeb(1):node.jsの衝撃とWebSocketが拓く未来 (2/2)

[井上真,New Bamboo]
前のページへ 1|2       

WebSocketと最初のnode.jsアプリ「Activity Monitor」

 「いや〜、node.jsってすごいよね」。カンファレンス参加後、しばらくの間はNew Bamboo社内ではnode.jsの話題が何度も上がってきたのですが、実際にnode.jsを使って何をすれば良いかはちょっと考えあぐねていました。その頃は、node.jsを使用したWebフレームワークなどが雨後のタケノコのように出てきていたのですが、「今までのWebサーバで出来ることをただ置き換えるだけっていうのはあんまり面白くないよね」というのが正直な気持ちだったと思います。

 それから1カ月ほど経った2009年の12月、Webの世界に新たなニュースがありました。Googleが開発するブラウザのオープンソース版であるChromiumにWebSocketという新たな機能が加わるというのです

 WebSocketを一言で言うと「WebのためのTCP」です。今までのWebはHTTPプロトコルを基本とした一方向のものでした。クライアント(ブラウザ)はサーバにリクエストを渡し、サーバはクラインとにレスポンスを返すというスタイルです。

 TCPはHTTPより下層に位置するネットワークプロトコルで、クライアントとサーバ間の双方向通信を可能とするものです。

 TCPサーバをnode.jsで書くと以下のようになります。

var net = require('net');
var sys = require('sys');
var server = net.createServer(function (stream) {
  var array = [];
  stream.setEncoding('utf8');
  stream.on('connect', function () {
    stream.write('connected\r\n');
  });
  stream.on('data', function (data) {
    array.push(data.slice(0,-2));
    stream.write('res: ' + array.join(',') + '\n');
  });
  stream.on('end', function () {
    sys.log('disconnected')
    stream.end();
  });
});
server.listen(8127, 'localhost');

 この状態でtelnetすると、以下のようにTCPサーバとやりとりすることができます。

$ telnet 127.0.0.1 8127
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
connected
a
res: a
b
res: a,b
c
res: a,b,c
d
res: a,b,c,d
^]  # Ctrl + ] でtelnetに終了シグナルを送っています
telnet> quit
Connection closed.
$ 

 ここでTCPサーバは各セッションごとにarrayという配列を用意しておき、入力リクエストが来ると、そのデータを配列に代入し、全配列の結果をレスポンスとして戻しています。

 HTTPとの重要な違いですが、HTTPは各リクエスト、レスポンスはまったく別個のものであり、各リクエスト間で状態(ステート)を共有することはありません。逆に、TCPの方は一度クライアントとサーバとの間で接続が確立されると、その後のやり取りで状態を共有することができます。HTTPのことをステートレスと呼ぶのに対し、TCPのことをステートフルと呼びます。

 今までHTTPで擬似的にステートを共有するには、以下の方法が取られていました。

  • クッキーに情報を保存する
  • リクエストのURIパラメータにすべての値を指定する(例:http://example.com/?val=[a,b,c])

 もともとHTTPはHTML文書を表示するために規定されたものなので、「リクエスト/リスポンス」の単純なモデルで十分だったのですが、多くのWebサイトが「アプリ化」してきている現在では、上のような方法で擬似的にダイナミックなアプリを作るには限界があります。WebSocketはそんな限界を打ち破る可能性を秘めた新たなプロトコルといって良いでしょう。

 そしてWebSocketのステートフルな性質を利用することで「サーバサイドプッシュ」を実現することができます。必要なときにサーバサイドから情報を送ることができれば、ムダなトラフィックを減らせます。

 例えば、雪が降ったり、災害のときなどに交通機関のサイトやニュースサイトがアクセス超過でサイトがダウンすることがあります。これは、情報が更新されるタイミングが分からないために、みんなが何度も「リフレッシュボタンを」押すからでしょう。これはリクエスト/レスポンスというモデルの弊害と言ってよいでしょう。

 もしこういったサイトをWebブラウザで開いておいて(TCPでいう接続の確立)、ニュースがあったときのみサーバからクライアントの方に情報をプッシュしてくれれば、余計なトラフィックを抑えられるはずです。

 前置きが長くなってしまいましたが、WebSocketをブラウザから使うためのJavaScript APIは以下です。

var ws = new WebSocket("ws://example.com/service");
ws.onopen = function() {
  // Web Socket is connected. You can send data by send() method.
  ws.send("message to send"); ....
};
ws.onmessage = function (evt) { var received_msg = evt.data; ... };
ws.onclose = function() { // websocket is closed. };

[注]WebSocket はW3Cによって規定されるAPIIETFによって規定されてい通信プロトコルによってなりたちます。以前はAPIの方を「The Web Sockets API」(複数形)、プロトコルの方を「The Web Socket protocol」(単数形)としていましたが、現在では両者とも「The WebSocket 」に統一されたようです。


 APIは非常に単純ではないでしょうか。

 まず、1行目でWebSocketオブジェクトを作成した後、「onopen,onmessage,onclose」という3つのコールバックファンクションを定義するだけです。サーバの方にメッセージを送りたい場合は「send()」というファンクションを呼び出すだけです。

 このWebSocketのAPI、先ほど例として挙げたnode.jsのTCPサーバサイドのコードに非常に似ていると思いませんか? 私はこの例を見たときに「WebSocketとnode.jsって合うのでは」と思いました。早速node.jsのWebSocketサーバを探してみたところ、プロトタイプ版のようなライブラリが2つほど見つかりました。そこで、これを使い、週末を利用してMacのActivity MonitorのようなもののWeb版を作ってみることにしました。

サンプルとして作成したリアルタイム性の高いモニターの動画(クリックで再生) サンプルとして作成したリアルタイム性の高いモニターの動画(クリックで再生)

 ソースの全文はgithub上にあります。当時は「websocket-server-node.js」というライブラリを使っていたのですが、今はすでにメンテされていないようなので「node-websocket-server」というライブラリに変更して書き直してみました。

 なお、今回のサンプルのiostat-client.htmlですが、わざわざWebサーバ上に置かなくとも、ファイルをローカル上で開くだけで実行可能なはずです。

var sys = require('sys')
  , http = require("http")
  , ws = require('./node-websocket-server/lib/ws');
var iostat = require('child_process').spawn("iostat", ["-w 1"]);
var httpServer = http.createServer();
var server = ws.createServer({}, httpServer);
function format (data) {
// JSON形式にOutputを変換するコード
}
// Handle WebSocket Requests
server.addListener("connection", function(conn){
  server.send(conn.id, "Connected as: "+conn.id);
  iostat.stdout.on('data', function (data) {
    server.send(conn.id, format(data));
  });
});
server.addListener("close", function(conn){
  sys.log("closed connection: "+conn.id);
});
server.listen(8000);

 「connection」と「close」という2つのコールバック関数を指定している点は、TCPサーバの例と大変似ていますね。少し違う点としては、ブラウザからの入力を取らず、

var iostat = require('child_process').spawn("iostat", ["-w 1"]);

のところで子プロセスを作り、iostatというコマンドが1秒おきにCPUやIO情報を出力するようにしている点です。そして、

  iostat.stdout.on('data', function (data) {
    server.send(conn.id, format(data));
  });

のところでチャイルドプロセスの出力結果を1秒ごとに出力するようにしています。

 ブラウザ側のコードもいたってシンプルです。

webSocket = new WebSocket('ws://localhost:8000/iostat');
webSocket.onopen = function() {
    out.html('Connection opened.<br>');
};
webSocket.onmessage = function(event) {
  stats = event.data;
  data = JSON.parse(stats);
  drawCharts(total_array.reverse());
};

 「onmessage」コールバックにデータが送られてくるたびに、JSONデータを解析し、その結果をグラフとして再描画しています。

AjaxやCometと何が違うの?

 ここまで読んだ読者の皆さんは思うかもしれません。「AjaxやCometでも同じことができるんじゃないの?」

 まず以下の図を見てみてください。

Ajax vs Comet(Long Polling) vs WebSocket Ajax vs Comet(Long Polling) vs WebSocket

 最初の例はAjaxです。毎秒ごとにHTTPリクエストを送っています。 HTTPのリクエストとレスポンスにはヘッダー部分に以下のようにいろいろな付帯情報を付けなければいけません。

GET /index.html HTTP/1.1
Host: www.example.com
HTTP/1.1 200 OK
Date: Mon, 23 May 2005 22:38:34 GMT
Server: Apache/1.3.3.7 (Unix)  (Red-Hat/Linux)
Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
Etag: "3f80f-1b6-3e1cb03b"
Accept-Ranges: bytes
Content-Length: 438
Connection: close
Content-Type: text/html; charset=UTF-8
※http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocolからの例

 1リクエスト/レスポンスヘッダー自体は数KBと、そう大きくありませんが、リクエストの更新頻度が密になってくると、毎回のリクエスト量は馬鹿になりません。更新頻度を遅くすると、欲しい情報がすぐに手に入らないし、更新頻度を上げるとサーバへのリソース要求が高くなる、というジレンマを抱えることになります。

 また疑似サーバプッシュ技術を総称する用語として「Comet」というものがあります。これはいろいろな実装方法が混在しており、なかなか分かりづらい用語なのですが、一般的なものに「Long Polling」と呼ばれるものがあります。これは次のように流れになります。

 まず、サーバがクライアントからリクエストを受けた際、すぐにレスポンスを返すのではなく、 コネクションをつなぎっぱなしにしておきます。そして何か情報を更新するときになってからレスポンスを返します。これは私が最初に述べた「災害情報の更新」やチャットなど数秒以上に一度しかレスポンスを返さないようなケースでは有効ですが、更新頻度が上がってくるにつれ、AJAXと同じような問題を抱えることになります。さらにCometで定期的に疑似プッシュしようとした場合、「どの時点までのデータをクライアントに送信したか」というステート情報も別に管理しなければいけません。

 私のActivity Monitorの例を見た方から「node.jsとLong Polling方式で実装しました」と教えていただいたので、彼のコードの一部を見てみましょう。

var rb = new lpb.LongPollingBuffer(200);
var iostat = process.createChildProcess("iostat", ["-w 1"])
//Setup the listener to handle the flow of data from iostat 
iostat.addListener("output", function (data) {
    sys.puts(data);
    if(data.search(/cpu/i) == -1){ //suppress the column header from iostat
        rb.push(data.trim().split(/\s+/).join(" "));
    }
});
//Setup the updater page for long polling  
fu.get("/update", function (req, res) {
      res.sendHeader(200,{"Content-Type": "text/html"});
      var thesince;
      if(url.parse(req.url,true).hasOwnProperty('query') && url.parse(req.url,true).query.hasOwnProperty('since')){
          thesince = parseInt(url.parse(req.url,true)['query']['since']);
      }
      else {thesince = -1;}
      rb.addListenerForUpdateSince(thesince, function(data){
           var body = '['+_.map(data,JSON.stringify).join(',\n')+']';
           res.sendBody( body );
           res.finish();
      });
});

 「/update」のURIにクライアントが最後にデータを受け取った時間を毎回送付して、サーバの現在時刻と比較。差分のデータを送るようにしています。毎リクエストごとのステートを管理するコードを別途書かないといけない上に、今回のように時間を扱う場合、「クライアントマシンの時間設定がちゃんと設定されていない場合はどうなるの」といったケースにも対応する必要があるでしょう。

 一方のWebSocketですが最初に接続を確立する時に以下のようなリクエストをサーバの方に送ります。

GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2: 12998 5 Y3 1  .P00
Sec-WebSocket-Protocol: sample
Upgrade: WebSocket
Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
Origin: http://example.com
^n:ds[4U

 サーバサイドはリクエストを受け取った後、以下のようなレスポンスを返します。

HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: http://example.com
Sec-WebSocket-Location: ws://example.com/demo
Sec-WebSocket-Protocol: sample
8jKS'y:G*Co,Wxa-

 基本的には通常のHTTPリクエスト/レスポンスとほぼ同じです。

 ここまでだとAJAXやCometと同じように見えるかもしれませんが、このやりとりを行ってクライアントとサーバの間で接続が確立された後は、以下のようにデータの前後に1バイトずつ付けた「データフレーミング」という形式でデータのやりとりを行うことになります。

0x00[Data]0xFF

【注】WebSocketのデータフレーム形式(0x00[Data]0xFF)は、この夏ごろから仕様の変更が議論されています。今後、連載中にも仕様が変わる可能性があります。


 接続時のHTTPリクエスト/レスポンスに比べて明らかにデータ転送のオーバヘッドが少なくなるのが見て取れると思います。

 こういったパフォーマンスの劇的な向上をもたらす可能性を秘めたWebSocketですが、ドラフト起草者であるGoogleのIan Hicksonさんはメーリングリストの中で以下のように述べています。

Reducing kilobytes of data to 2 bytes…and reducing latency from 150ms to 50ms is far more than marginal. In fact, these two factors alone are enough to make WebSocket seriously interesting to Google.

「数キロバイトのデータ転送量を(最低)2バイトへ、150msの遅延を50msにできるとしたら、それはわずかな向上といったような生やさしいものではない。実際この2つの事実だけをもっても、WebSocketをGoogleにとって本気にさせるには十分なものだ」


 またWebSocketのうれしい点としてクロスドメインが可能な点も挙げられます。

 AJAXやCometの場合、原則クロスドメインスクリプティングができないため、JSONPなどのテクニックと併用する必要がありますが、WebSocketは原則可能です(最初のリクエストヘッダーの部分でSec-WebSocket-Locationを指定しているので、それを利用して既知のドメインのみアクセスを許すといった制限をかけることも可能です)。

 さて、今回はここまでです。リアルタイムWebとは何かということと、WebSocketの実例を示し、AjaxやCometとの違いについて述べました。次回は、WebSocketのブラウザの対応状況などを交えつつ、技術的課題やサーバサイドの実装例などについてご紹介したいと思います。


前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。