WebSocketをクロスブラウザで使う
Socket.IO(MITライセンス)は、WebSocket(注1)と同様の機能をクロスブラウザで提供する便利なライブラリです。サーバから任意のタイミングでブラウザにデータをプッシュしたい場合、WebSocketを使えば簡単に実現できますが、WebSocketをサポートしていないブラウザに対して代替手段を提供するのは非常に面倒な作業となります。Socket.IOは、そのようなブラウザごとの違いを吸収して共通のAPIを提供し、ほぼすべてのブラウザ上でWebSocketと同様の機能を提供してくれます。Socket.IOはサーバ側をNode.jsのWebサーバに組み込んで使います。
Webブラウザとサーバ間で1つのTCPソケットを使って双方向通信を行うための技術。通常80番ポートを利用するためHTTPとの親和性が高く、既存のWebサーバに組み込む形で利用できる。フォームの送信などとは異なり、ソケットをつなぎっぱなしにしてページ内でデータのやり取りを何回も行うことを意図して設計されている。
従来はFlashやCometなどを使って実装しなければならなかったことが簡単なAPIで実現でき、またAPIやプロトコルの仕様も標準化されているため、誰でも広く利用できる(少し古いブラウザではサポートされていない)。
Socket.IOのインストール(サーバ側)
サーバ側のプロジェクトのディレクトリにモジュールをインストールします。
$ npm install socket.io
Socket.IOの使い方(サーバ側)
Node.jsのWebサーバにSocket.IOを組み込みます。連載第4回で紹介したExpress 3.0と組み合わせる場合はリスト1のように記述します。
express = require 'express'
app = express()
app.configure ->
  # 静的ファイルをpublicディレクトリで提供
  app.use express.static "#{__dirname}/public"
# ここでExpressのルーティングなどを設定する
# app.get '/path', (req, res) ->
#   ...
# 3000番ポートで待ち受ける
server = app.listen 3000
io = require('socket.io').listen server
io.sockets.on 'connection', (socket) ->
  # クライアントが接続した時に実行される
  console.log "connected: #{socket.id}"
  socket.on 'data', (data) ->
    # クライアントからdataイベントが来た時に実行される
    console.log "data: #{data}"
    # 文字列を逆さまにして送り返す
    socket.emit 'data', data.split('').reverse().join('')
  Socket.IOの使い方(クライアント側)
 リスト1を実行すると、サーバ上の/socket.io/socket.io.jsというパスでSocket.IOのJavaScriptライブラリが提供されるようになります。クライアント側のHTMLではこのJavaScriptを次のように<script>タグで読み込みます。HTMLはひとまず静的ファイルとして作り、前述のサーバ側のプロジェクト内にpublicディレクトリを作成してその中に配置します。
<script src="/socket.io/socket.io.js"></script>
クライアント側のコードはリスト2のように記述します。
sock = io.connect 'http://localhost:3000' sock.on 'data', (data) -> # サーバからdataイベントが来た時に実行される console.log data # サーバにdataイベントを送信 sock.emit 'data', 'hello!'
 これをブラウザで実行すると「hello!」という文字列がサーバに送信され、サーバは受け取った文字列を逆さまにしてクライアントに送り返し、ブラウザは受け取った文字列「!olleh」をコンソールに出力します。上記の例ではイベント名としてdataを使っていますが、connect、message、disconnect以外の任意の名前を使うことができます。
APIサーバの実装例
リスト3、4は、Socket.IOを利用して簡単なAPIサーバを実装したものです。
express = require 'express'
# 接続中のソケット一覧
sockets = {}
app = express()
app.configure ->
  app.use express.static "#{__dirname}/public"
#app.get '/path', (req, res) ->
#    ...
server = app.listen 3000
io = require('socket.io').listen server
io.sockets.on 'connection', (socket) ->
  sockets[socket.id] = socket
  console.log "connected: #{socket.id}"
  socket.on 'request', (message) ->
    # リクエストを元にデータを送り返す
    switch message
      when 'weather'
        socket.emit 'weather', '快晴'
      when 'temperature'
        socket.emit 'temperature', 14
      else
        socket.emit 'request-error', "Unknown message: #{message}"
  socket.on 'disconnect', ->
    # クライアントとの接続が切れたらsocketsから削除する
    console.log "disconnected: #{socket.id}"
    delete sockets[socket.id]
# 接続中の全クライアントに1秒おきにサーバの時刻を送信
setInterval ->
  for id, socket of sockets
    socket.emit 'time', new Date() + ''
, 1000
  
sock = io.connect 'http://localhost:3000'
sock.on 'weather', (weather) ->  # weatherイベント
  console.log "天気: #{weather}"
sock.on 'temperature', (temp) ->  # temperatureイベント
  console.log "気温: #{temp}"
sock.on 'time', (time) ->  # timeイベント
  console.log "時刻: #{time}"
sock.on 'disconnect', ->
  # サーバとの接続が切れた(自動的に再接続される)
  console.log 'disconnected from server'
# リクエストを送信
sock.emit 'request', 'weather'
# 1秒後にリクエストを送信
setTimeout ->
  sock.emit 'request', 'temperature'
, 1000
  この例のように、WebSocketを使うと自由に双方向の通信を行うことができます。
再接続パラメータの設定
Socket.IOのクライアントは、サーバとの接続が切れた時に自動的に再接続する機能を持っており、デフォルトで有効になっています。自動再接続を無効にしたい場合は、クライアント側で次のようにreconnectオプションを指定します。
sock = io.connect 'http://127.0.0.1:3000', reconnect: false # 再接続しない
また、再接続に関するパラメータを変更する場合はクライアント側で次のように指定します。
sock = io.connect 'http://127.0.0.1:3000', # 接続に失敗した後、再接続を試みるまでの待ち時間(ミリ秒)。 # 失敗し続けるとだんだん長くなる。デフォルトは500。 'reconnection delay': 100 # 再接続に失敗し続けると待ち時間が長くなっていくが、その最大値。 # デフォルトはInfinity、つまり制限なし。 'reconnection limit': 100 # 接続が切れてから何回再接続を試みるか。 # この回数連続で失敗すると再接続が行われなくなる。デフォルトは10。 'max reconnection attempts': 100
その他、指定できるオプションの一覧はこちらのWebページ(英語)に掲載されています。
本番環境での推奨設定
本番環境で動かす際には、サーバ側で以下のオプションを設定することが推奨されています。
io.configure ->
  io.enable 'browser client minification'  # JSを圧縮する
  io.enable 'browser client etag'  # etagによるキャッシュを有効にする
  io.enable 'browser client gzip'  # gzipして転送する
  io.set 'log level', 1   # ログ出力を減らす
  io.set 'transports', [  # すべての通信手段を有効にする
    'websocket'
    'flashsocket'
    'htmlfile'
    'xhr-polling'
    'jsonp-polling'
  ]
  