目次


ツイートの件数を地図上で視覚化する WebSocket アプリケーションを作成する

Comments

このチュートリアルでは、WebSocket、Twitter 検索、および Google Maps API を使用してツイートの発信元をリアルタイムで地図上に表示する、Node.js Web アプリケーションを作成する方法を紹介します。また、アプリケーションを IBM Bluemix に (そして一般には、Cloud Foundry ベースのあらゆる PaaS に) デプロイする方法も説明します。

このアプリケーションを作成した理由は、特定のトピックを話題にしているツイートがどの地域から発信されているのかをひと目で把握できるビューが欲しかったからです。例えば、イノベーションに関してツイートしている人々が世界のどこにいるかがわかるようなビューです。

この TOTEM (Tweets On ThE Map) という名前のアプリケーションには単純な UI があり、この UI に (Twitter での場合と同じように) クエリーを入力すると、地図上にはそのクエリーにマッチするツイートの発信元が上下に跳ねるツイート・アイコンで示されます。 :

ツイートの場所が示された地図のスクリーンショット
ツイートの場所が示された地図のスクリーンショット

私が興味を持っているのは、メッセージの内容ではなく、各地で発信されたツイートの件数です。そのため、TOTEM アプリケーションでは、発信場所を示す円のサイズが、その場所から発信されるツイートの件数が増えるに従って大きくなるようになっています。円をクリックすると、その場所の名前、検索を開始してから投稿されたツイートの件数、そしてその場所から発信された最新のツイートが表示されます。

このアプリケーションでは、HTTP を使用した複雑なポーリング・ロジックは使用しておらず、代わりに単一の全二重 WebSocket チャネルを開きます。このチャネルを介して、クライアントとサーバーはそれぞれ独自のプロトコルに従ってメッセージを交換できるようになっています。

アプリケーションを作成するために必要となるもの

  • Node.js、Node.js 開発環境、そして Node で Express を使用して Web アプリケーションを作成する方法についての十分な知識。(このアプリケーションは、Nodeclipse プラグインをインストールした Eclipse を使用して開発しました。)
  • Twitter アカウント。
  • Google Maps JavaScript API の十分な知識。
  • Bluemix 上にデプロイする場合:

このチュートリアルを手引きに、皆さんも同様のアプリケーションを作成することができます。ステップ 1 でコードを入手した後、アプリケーションを試してみたいと思ったら、ローカルでコードの変更をテストしてから (ステップ 5 で概説しているように) アプリケーションを Bluemix にデプロイして試すことができます。アプリケーションをローカルで実行する方法については、私の DevOps Services プロジェクトにある README.md ファイルを参照してください。

アプリを実行するコードを入手する

このアプリケーションでは、HTTP を使用した複雑なポーリング・ロジックは使用しておらず、代わりに単一の全二重 WebSocket チャネルを開きます。このチャネルを介して、クライアントとサーバーはそれぞれ独自のプロトコルに従ってメッセージを交換できるようになっています。

ステップ 1. ソース・コードを入手して環境を構築する

  1. コードを入手するために、お好みの Git クライアントを使用して TOTEM プロジェクトの Git リポジトリーを複製します。
    git clone https://hub.jazz.net/git/mcrudele/totem
  2. ルート・フォルダーから npm install を実行します。このコマンドにより、package.json に記載されている依存関係が node_modules ディレクトリーにインストールされます。依存関係には、Express と Jade の他に、ws という WebSocket ライブラリーと、ntwitter という非同期 Twitter クライアント API があります。
  3. ローカル・プロジェクトの node_modules/ntwitter フォルダーにカレント・ディレクトリーを変更して、ntwitter パッケージに必要な修正を適用します。
    • lib/keys.js で、search_base: 'http://search.twitter.com'search_base: 'https://api.twitter.com/1.1/search' で置き換えます。
    • lib/twitter.js で、var url = this.options.search_base + '/search.json';var url = this.options.search_base + '/tweets.json'; で置き換えます。
  4. Twitter アクセス・トークンと API キーを生成します。(Twitter API の使用法がよくわからない場合は、「How to get my api key」を参照するか、直接 Twitter の Application Management にアクセスしてください)。

ステップ 2. WebSocket エンドポイントを Node.js サーバーに追加する

私は Nodeclipse 環境で Node パースペクティブに切り替えてから「New (新規)」 > 「Node.js Express」の順にクリックするという方法で、この Web アプリケーションのスケルトンを作成しました。皆さんは、express という名前の NPM パッケージに提供されている express コマンドを実行することで、同じスケルトンを取得することができます。

このスケルトンは、Node.js サーバーである app.js ファイルの他に、画像ファイル、CSS ファイル、HTML ファイルなどの静的リソースが含まれるディレクトリー (public など)、Jade テンプレート・ファイルが含まれるディレクトリー (views)、REST ハンドラー・ファイルが含まれるディレクトリー (routes) で構成されています。

TOTEM アプリケーションが定義する WebSocket エンドポイントは、message イベントのクエリー・ストリングを受け取って、そのクエリーとマッチするツイートの定期的な検索を開始し、マッチしたツイートをクライアントに送信します。

定期的な検索を扱うために、ntwitter Node.js モジュールを使用して (tfinder.js モジュール内に) TwitterFinder クラスを作成しました。TwitterFinder は Node.js の EventEmitter であり、クエリーにマッチする新規ツイートを受信すると、data イベントを発生させます。

以下のコードが、WebSocket /search エンドポイントを作成します。

var WebSocket = require('ws');
var WebSocketServer = WebSocket.Server;

var app = express();

var server = http.createServer(app).listen(app.get('port'), function() {
  console.log('TOTEM server listening on port ' + app.get('port'));
});

// Create the WebSocket endpoint that does the job
//
var searchServer = new WebSocketServer( {server: server, path: '/search'});

WebSocketServer は、Node.js の WebSocket モジュール ws で定義されています。WebSocketServer は、WebSocket プロトコルのサーバー・サイドを実装する Node.js の EventEmitter です。したがって、クライアントとの新規 WebSocket 接続が確立された時点でトリガーされる connection イベントのハンドラーを追加しました。接続が確立されている間は、このイベントがパラメーターとして指定している WebSocket クライアントを使用しなければならず、WebSocket 上で message イベントを処理してクエリー・ストリングを受け取らなければなりません。

// One connection manages one twitter search.
// To stop current search simply close this connection.
//
searchServer.on('connection', function(ws) {

  // Create a twitter finder for the duration of the connection
  //
  var finder = new TwitterFinder(TwitterKeys);

  ws.on('message', function(searchstring, flags) {

    // Ignore the message if the search is already running
    // The client must close the connection to stop searching
    //
    if ( finder.isRunning() ) {
      console.log('Finder already running.');
      return;
    }

    // Set the data handler on the twitter finder
    //
    finder.on('data', function(tweets) {

      // This is the format of the object that should be sent:
      //
      // {
      //   address: "user.location attribute of the tweet",
      //   text: "text attribute of the tweet formatted as html"
      // }
      //
      // Get tweets from the bottom of the array to start
      // from the oldest.
      //
      for (var i=tweets.length-1; i>=0; i--) {
        var data = { address: tweets[i].user.location,
                     text:     tweetToText(tweets[i]) };
        ws.send(JSON.stringify(data));
      }

    });

    console.log('Start a new search for: ', searchstring);
    finder.start(searchstring);
  });

});

ステップ 3. HTML ビューを作成する

次に、ユーザーと対話するための HTML ファイル (public/index.html) を作成しました。Web レイアウトは、このチュートリアルの冒頭に記載したページで構成されていて、そこに、以下の統計を示すパネルが追加されています。

  • 地図に送信されたツイートの合計数。
  • エラーが発生したツイートの数。これらのツイートは、user.location 属性が空になっているか、Google Geocoder サービスが解決できない値 (なかには、自分のプロファイルに架空の場所を指定する Twitter ユーザーもいます) に設定されているために、地図に送信できなかったツイートを指します。
  • Google Geocoder サービスが繰り返し query limit エラーを返しているため (クライアントが大量のツイートを受信すると、このエラーが発生する場合があります)、地図に送信できなかったツイートの数。

以下に、TOTEM Web レイアウトのコードを記載します。

  <body>
    <div id="panel">
      <input id="search" type="search" title="Write a search, click the Search button, 
      and see what's going on on Twitter! Tips: use OR, AND logical operators for multiple-words query">
      <input type="button" value="Search" onclick="startSearch()" title="Click to start searching">
    </div>
    <div id="map-canvas"></div>
    <div id="panel-monitor">
      <div>Tweets on the Map</div>
      <input id="num-tweets" type="text" readonly title="Number of tweets posted to the map"><br>
      <div>Tweets in error</div>
      <input id="num-tweets-in-error" type="text" readonly 
      title="Number of tweets not posted to the map because of invalid location provided"><br>
      <div>Tweets discarded</div>
      <input id="num-tweets-discarded" type="text" readonly title="Number of tweets discarded 
      because of errors with the Geocoder service (query limit)"><br>
      <div>Tweets/secs</div>
      <input id="tweets-per-sec" type="text" readonly title="Tweets per seconds posted to the map">
    </div>
  </body>

ステップ 4. JavaScript クライアント・コードを作成する

ユーザー・クエリーを受け入れて WebSocket エンドポイントに接続し、受信ツイートを処理するためのクライアント・ロジックを作成しました。

すべては、検索ボタンの onclick ハンドラーである startSearch() から始まります。

function startSearch() {
  var search = document.getElementById('search').value;
  if ( search.trim().length===0 ) {
    alert('Cannot submit an empty search.');
    return;
  }

  // Create the connection for the new search
  // Use secure WebSocket (wss) when running on Bluemix
  //
  if ( window.document.location.host.match(/localhost/) ) {
    var wsUri = 'ws://' + window.document.location.host + '/search';
  } else {
    var wsUri = 'wss://' + window.document.location.host + '/search';
  }
  wssearch = new WebSocket(wsUri);
  wssearch.onopen = function(evt) { onOpen(evt) };
  wssearch.onclose = function(evt) { onClose(evt) };
  wssearch.onmessage = function(evt) { onMessage(evt) };
  wssearch.onerror = function(evt) { onError(evt) };
}

function onOpen(e) {
  // Submit the search to the server
  //
  var search = document.getElementById('search').value.trim();
  wssearch.send(search);
}

startSearch() 関数は新規 WebSocket オブジェクトを作成し、openclosemessage、および error の各イベントのハンドラーを設定します。WebSocket が開始したハンドシェークが完了すると同時に、onOpen() ハンドラーが呼び出されます。すると、このハンドラーがユーザー・クエリーを取得して、検索を開始するためにそのクエリーを送信します。

最後に、onMessage() ハンドラーがデータを解析して地図に書き込むという楽しい処理が行われます。この処理では msg.address の座標を取得するために、Google Geocoder サービスを利用しました。しかし、このサービスを頻繁に呼び出すと OVER_QUERY_LIMIT エラーが返されるので、この状況を緩和するために次の 2 つのことを行いました。1 つは、解決されたアドレスをメモリーに保管することで、呼び出しの回数を減らすようにしました。そしてもう 1 つは、メッセージを可変の時間間隔で処理するようにしました。具体的には、OVER_QUERY_LIMIT に達した時点で、処理の間隔を長くするようにしました。

プロジェクト・コードに含まれる public/index.html を表示して、onMessage() ハンドラーと、地図に結果を書き込むロジックを実装するその他の関数を確認してください。

ステップ 5. アプリケーションを Bluemix にデプロイする

  1. manifest.yml.change_me ファイル (ルート・ディレクトリーにあります) の名前を manifest.yml に変更します。
  2. manifest.yml で、host 値をアプリケーションに固有の名前に設定し、TWITTER_KEYS 値を入力します。
     ---
      applications:
      - name: totem
        memory: 512M
        domain: mybluemix.net
        instances: 1
        host: CHANGE_ME
        path: .
        env:
          TWITTER_KEYS: '{"consumer_key":"CHANGE_ME","consumer_secret":
          "CHANGE_ME","access_token_key":"CHANGE_ME","access_token_secret":"CHANGE_ME"}'
    consumer_keyconsumer_secret の値は、Twitter Application Settings ページから取得した API KeyAPI Secret の値です。
  3. ルート・ディレクトリーにある package.json ファイルを編集して、依存関係から ntwitter モジュールを削除します。こうすることで、Bluemix にプッシュするときに、このモジュールがローカル・ディレクトリーからアップロードされるようになります (このステップは、ntwitter パッケージに対して行った修正を維持するために必要です。このステップを行わなければ、Bluemix はアプリケーションをステージングするときに、修正されたモジュールを上書きしてしまいます)。
  4. アプリケーションのルート・ディレクトリーから cf push コマンドを実行した後、ブラウザーで https://yourappname.mybluemix.net/ に接続してアプリケーションの動作を確認します。

まとめ

TOTEM アプリケーションは、リアルタイムでの通信が必要とされる Web アプリケーションを作成する場合の WebSocket の強力さを実証しています。HTTP は、こうしたことが要求されるアプリケーションで使用するには、重要な部分が不足していて不向きですが、WebSocket はこれらの部分に対処します。HTTP でポーリングする場合と比べ、WebSocket はパフォーマンスを向上させて、単純さを高めることができるため、HTML5 接続仕様の標準となっています。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development, Cloud computing
ArticleID=994108
ArticleTitle=ツイートの件数を地図上で視覚化する WebSocket アプリケーションを作成する
publish-date=01152015