データがサーバー上で更新されたら即時にブラウザで反映される。そんなWEBアプリケーションを作りたいと思ったことはありませんか。
たとえば、野球速報みたいなものです。Twitterのタイムラインでもいいかもしれません。
こういうアプリケーションを実現するときの常とう手段としては、ブラウザ側が「更新情報のリクエスト」を一定間隔でサーバーに送って、「データが更新された」というレスポンスが返ってきたときだけ表示を変更する方法が考えられます。これを、Polling パターンと呼びます。ブラウザが一定間隔でサーバーへ問い合わせることをPollingというのです。
しかし、Pollingでは、サーバーの更新がない場合は不必要なリクエストを送り続けることになりますし、逆にデータの更新が早い場合はすべての更新を受け取れない場合があり、効率の悪い方法です。
ほかにも、最近だとWebSocketやCometなどのHTTP以外のプロトコルを使って通信する方法もあります。(FacebookのメッセージがCometを使って実装されていたという話を聞いたことがあります。)
しかしながら、これらのプロトコルはサーバーによっては実現が困難だったりします。一例として、対応するポート番号のポートがセキュリティの都合で空いていなかったりします。ほかにも、実装しようとしても、たとえばレンタルサーバーではPHPしか使えなかったりして、WebSocketの実装が困難だったりする場合もあります。
そこで本記事では、更新情報をサーバー側のタイミングでプッシュし、ブラウザ上にリアルタイムで反映させる方法を紹介します。通信プロトコルはHTTPしか使いません。レンタルサーバーでも動きます。それがタイトルのHTTP Streamingパターンです。
※2015/03/09 修正:
「"Comet" は "HTTP" とは異なるプロトコルである」という旨の記述をしておりましたが,誤りであるとのご指摘を頂きました。詳しくは,ページ下部のコメントをご確認ください。
完成品イメージ
図の通りですが、ポイントは以下の2点です。
- クライアントからサーバーへの通信は、最初の接続のときのみであること
- サーバーの情報が更新されたタイミングで、サーバーから情報がプッシュされること
サーバーサイド
一定間隔でブラウザにデータを掃きだし続けるサーバーサイド(PHP)のスクリプトです。
<?php /** * push.php - 一秒に一回 json データを chunked data として掃き出す */ function output_chunk($chunk) { echo sprintf("%x\r\n", strlen($chunk)); echo $chunk . "\r\n"; } header("Content-type: application/octet-stream"); header("Transfer-encoding: chunked"); ob_flush(); flush(); $i = 0; while ( !connection_aborted() ) { // クライアントからの接続が切断されない限り繰り返す // JSON に入れるデータ $id = $i; $message = "hoge".$i; $createdAt = date("Y-m-d H:i:s"); // JSON データを作る $json = json_encode( array( "id" => $id, "message" => $message, "createdAt" => $createdAt ) ); // データを掃き出す output_chunk( $json . str_repeat(' ', 8000) . "\n" ); ob_flush(); flush(); // 一秒停止 sleep(1); // 一秒ごとに生成 $i++; } echo "0\r\n\r\n";
ポイントはヘッダを次のようにすること。
header("Content-type: application/octet-stream"); header("Transfer-encoding: chunked");
「chunked」とは「塊」の意味で、レスポンステキストを一気に送るのではなく、ある程度のサイズの塊にしてちょっとずつ送っていく方式の通信が実現できます。
塊を送るたびに、毎回 flush() することもポイントの1つです。
この例では、push.phpから掃き出されるデータはoctet-stream形式なので、ちゃんとサーバーからデータが送られているかブラウザ上で確認することができません。そこで、次のようにヘッダ情報を設定することで、テキスト形式でブラウザ上に掃きださせることができます。
header("Content-type: text/plain"); header("Transfer-encoding: chunked");
http://{your.server.com}/http-streaming/push.php
にアクセスしてみてください。
次のようなデータが表示されるはずです。
{"id":0,"message":"hoge0","createdAt":"2013-12-06 00:49:29"}
{"id":1,"message":"hoge1","createdAt":"2013-12-06 00:49:30"}
{"id":2,"message":"hoge2","createdAt":"2013-12-06 00:49:31"}
{"id":3,"message":"hoge3","createdAt":"2013-12-06 00:49:32"}
{"id":4,"message":"hoge4","createdAt":"2013-12-06 00:49:33"}
{"id":5,"message":"hoge5","createdAt":"2013-12-06 00:49:34"}
このデータをクライアントで受け取って加工すればいいですね。
注意したいのは、サーバーが一定間隔でデータを送らない場合です。データがある程度の時間送られないと、HTTP通信のセッションタイムアウトとなってしまい、接続が切れてしまいます。一定間隔で空の文字列を送るようにしましょう。
クライアントサイド
クライアントサイドのJavaScriptコードを置いておきます。上のデモとは若干書き方が違うのですが、基本は同じです。
$(function(){ /** * タイマー */ var mytimer = null; /** * データ受信先のURL */ var url = './push.php'; /** * 引数のテキストが JSON かどうか 判別する内部関数 */ var isJSON = function(arg) { arg = (typeof arg === "function") ? arg() : arg; if (typeof arg !== "string") { return false; } try { arg = (!JSON) ? eval("(" + arg + ")") : JSON.parse(arg); return true; } catch (e) { return false; } }; // サーバーに通信のコネクションを張る $.ajax({ type: 'get', url: url, cache: false, // ブラウザにキャッシュさせない xhrFields: { onloadstart: function() { var xhr = this; // 前回取得したデータの文字数 var textlength = 0; // データが来るまで待つ (100ms間隔でレスポンステキストを確認する) mytimer = setInterval(function() { // 受信済みテキストを保存 var text = xhr.responseText; // 前回の取得からの差分を取得 var newText = text.substring(textlength); // JSONデータを取得 var lines = newText.split("\n"); if( text.length > textlength ) { // 長さを更新 textlength = text.length; lines.forEach(function(line){ if( isJSON(line) ){ // 正常な JSON データの時 var json = JSON.parse(line); /** * ここに更新処理を書く */ console.log('updated.'); } }); } }, 100); } }, success: function() { console.log('successed.'); // 一秒後にタイマー停止 setTimeout(function(){ clearInterval(mytimer); }, 1000); }, error: function(XMLHttpRequest, textStatus, errorThrown) { console.log('error occured.'); // 即座にタイマー停止 clearInterval(mytimer); } }); });
ここで、setIntervalを使って繰り返し処理を行っていますが、レスポンステキストを確認しているだけで、サーバーへの通信は最初の一回しか行っていません。
ChromeではGETアクセスに対して、キャッシュを作ります。それが原因で同じブラウザから同時にアクセスしようとすると片方の受信が止まってしまうという問題がありました。ここでは、Ajaxリクエストのパラメータとして、
cache: false,
を付けることでこれを解決するできました。
GitHub
今回の記事の内容をソースコードとしてまとめました。
Cloneしてサーバーに置けばデモと同様に動くはずです。
MIT Licenseなので自由に使ってくださって結構です。
参考文献
PHPとJavaScriptでHTTPストリーミングする話(Transfer-Encoding: chunked編) - id:anatooのブログ
jQueryの$.ajaxで通信途中のresponseTextを取得する - あらびき日記
jquery - How to split a long array into smaller arrays, with JavaScript - Stack Overflow
Chunked transfer encoding - Wikipedia, the free encyclopediaクライアントからの接続が切れたかどうか判定
PHP: connection_aborted - ManualAjax リクエストのみ受け付ける
PHPのイタい入門書を読んでAjaxのXSSについて検討した(3)〜JSON等の想定外読み出しによる攻撃〜 - ockeghem(徳丸浩)の日記
SE奮闘記: 【PHP】Ajaxによる通信(リクエスト)かどうか判定する方法
↓Ajaxのデザインパターンに関する分厚い本です。値段はお高いですが、HTTP Streamingパターンについても、かなり詳しく乗っています。
Ajaxデザインパターン ―ユーザビリティと開発効率の向上のために (THEORY/IN/PRACTICE)
- 作者: Michael Mahemoff,牧野聡
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/05/15
- メディア: 大型本
- 購入: 1人 クリック: 45回
- この商品を含むブログ (16件) を見る