Intelligent Technology's Technical Blog

株式会社インテリジェントテクノロジーの技術情報ブログです。

Socket.IOでリアルタイムWebアプリ

はじめまして、出石です。
ふとwebアプリを作ってみようかと以前から気になっていたSocket.IOを使ってみましたのでご紹介します。
目新しい技術ではありませんが、簡単なリアルタイムチャットを作ってみたいと思います。

Socket.IO

はじめにSocket.IOとはリアルタイムwebを実現するAPIです。

リアルタイムwebを実現するにはサーバ・クライアントの双方向通信が必要です。実現する為の技術としては「Comet」などの「long polling」がありますが、HTTPコネクションの維持によるオーバーヘッドがあるなどリアルタイム性のある処理には最適とは言えません。

リアルタイムな双方向通信の手段としてはwebsokcetがあります。
websocketとはブラウザ間の双方向通信を行う通信規格で、持続的接続(ソケット)を持つ為、同じく双方向通信を実現する「Comet」等でネックだったHTTP通信のオーバーヘッドが少ないというメリットがあります。

Socket.IOはpollingも通信方法として利用できますが、今回はリアルタイムチャットの為「websocket」を利用したいと思います。

準備

Socket.IOはnpmのモジュールとして利用できますので、expressを使ってアプリケーションを作成します。
node.js・expressのインストールは、当ブログの記事「サーバサイドJavaScriptのツール:Node.js Express.js MongoDB mongooseを使ってみる -インストール編-」を参考にしてください。
なお、expressはver.4からexpress-generatorパッケージでインストールできます。コマンドは以下となります。

$ npm install -g express-generator

サンプルプログラムはこちらから取得できます。


今回は下記環境にて作成しました。

 OS : windows7
 node.js : 0.10.5
 express : 4.0.0

まずは、expressで雛形を作成します。
viewはejsとします。

$ express -e socketio-sample

続いてsocketio-sample/package.jsonのdependenciesに以下を追加します。

  "dependencies": {
    "socket.io": "0.9.16",
    "dateformat": "*",
    ・・・
   }

Socket.IOのgithubでは1.0.0-pre4(2014/5/19)がありますが、公式ドキュメントは0.9.xとなっているので、ここでは0.9.16を利用します。

githubのHistoryでは0.9.14(2013/03/29)が0.9系最終verのように見えますが
npmでは0.9.16(2013/06/06)が取得できます。

dateformatは日付フォーマット変換のモジュールです。


追加したpackage.jsonを保存しインストールします。

$ cd socketio-sample
$ npm install

socket.ioのインストール時にsocket.io-clientのビルドでエラーとなる場合.NET FrameworkやVisualStudioのインストール後、下記設定を行う事で解消できます。

$ npm config set msvs_version 2011

2011の部分はインストールしてあるVisualStudio等によって適宜変えて下さい。
設定後、再度インストールできれば完了です。

実装

画面・仕様は単純に

ログイン画面を表示→
ユーザー名を入力してログイン→
チャット画面へ遷移→
メッセージを入力して送信

とします。

またファイル構成は以下のようにしました。
f:id:IntelligentTechnology:20140522115418p:plain
なお、express4.xからエントリーポイントがapp.jsからbin/wwwとなっています。

追加したファイル

models/sio.js : サーバ側Socket.IO実装
public/javascripts/client.js : クライアント側Socket.IO実装
routes/chat.js : チャット画面コントローラー
routes/login.js : ログイン画面コントローラー
views/chat.ejs : チャット画面
views/login.ejs : ログイン画面


express雛形ファイルでの変更は
app.jsでログイン画面とチャット画面のルーティングを行います。

var login = require('./routes/login');
var chat = require('./routes/chat');
・・・
app.use('/', login);
app.use('/chat', chat);

さらにbin/www(エントリーポイント)でmodels/sio.jsを読み込んでサーバ側のSocket.IOを初期化します。

var app = require('../app');
var sio = require('../models/sio');

app.set('port', process.env.PORT || 3000);

var server = app.listen(app.get('port'), function() {
  debug('Express server listening on port ' + server.address().port);
});

sio(server);

基本的な通信

Socket.IOの通信は

socket.emit(event,data)

でイベントを送信し、

socket.on(event,callback)

でイベントを受信します。

ブロードキャスト送信は

socket.broadcast.emit(event,data)

で行えます。
もちろんサーバとクライアント双方でsocketの実装を行います。

サーバ実装

Socket.IOのサーバ実装はmodels/sio.jsです。

var socketio = require('socket.io');
var dateformat = require('dateformat');

module.exports = sio;

function sio(server) {

  // Socket.IO
  var sio = socketio.listen(server);
  sio.set('transports', [ 'websocket' ]);

  // 接続
  sio.sockets.on('connection', function(socket) {

    // 通知受信
    socket.on('notice', function(data) {
      // すべてのクライアントへ通知を送信
      // ブロードキャスト
      socket.broadcast.emit('recieve', {
        type : data.type,
        user : data.user,
        value : data.value,
        time : dateformat(new Date(), 'yyyy-mm-dd HH:MM:ss'),
      });
    });

    // 切断
    socket.on("disconnect", function() {
    });
  });
}

socketio.listen()でソケットを生成・待機し

  sio.set('transports', [ 'websocket' ]);

で通信方法を「websocket」としています。

続いて

sio.sockets.on('connection', function(socket) {
・・・
}

では接続イベントの登録を行っています。
接続イベントの中では受信と切断のイベントを登録しています。

connection,disconnectは予約されたイベントです。
また、イベントは自由に使う事ができます。
今回はサーバの通知イベントをnotice、クライアントへの受信イベントをrecieveとして使用しています。

サーバで受信した通知は

socket.broadcast.emit('recieve', {
  type : data.type,
  user : data.user,
  value : data.value,
  time : dateformat(new Date(), 'yyyy-mm-dd HH:MM:ss'),
});

で接続している全クライアントへブロードキャストしています。
クライアントでは受信イベントにてデータをtypeで判断し処理するI/Fとしました。

クライアント実装

続いてSocket.IOのクライアント実装です。ソースはpublic/javascripts/client.jsです。

// socket.io接続
var socket = io.connect();

// 接続時
socket.on('connect', function() {
  // ログイン通知
  emit('login');
});

// 切断時
socket.on('disconnect', function(client) {
});

// 受信時
socket.on('recieve', function(data) {
  var item = $('<li>').append($('<small>').append(data.time));

  // data.typeを解釈し、要素を生成する
  if (data.type === 'login') {
    item.addClass('alert alert-success').append($('<div>').append(data.user + 'がログインしました'));
  } else if (data.type === 'logout') {
    item.addClass('alert alert-danger').append($('<div>').append(data.user + 'がログアウトしました'));
  } else if (data.type === 'chat') {
    var msg = data.value.replace(/[!@$%<>'"&|]/g, '');
    item.addClass('well well-lg').append($('<div>').text(msg)).children('small').prepend(data.user + ':');
  } else {
    item.addClass('alert alert-danger').append($('<div>').append('不正なメッセージを受信しました'));
  }

  $('#chat-area').prepend(item).hide().fadeIn(800);
});

// イベント発信
function emit(type, msg) {
  socket.emit('notice', {
    type : type,
    user : $('#username').val(),
    value : msg,
  });
}

// クライアントからメッセージ送信
function sendMessage() {
  // メッセージ取得
  var msg = $('#message').val();
  // 空白にする
  $('#message').val("");
  // メッセージ通知
  emit('chat', msg);
}

// イベントの登録
$(document).ready(function() {
  $(window).on('beforeunload', function(e) {
    // ログアウト通知
    emit('logout');
  });

  // 送信ボタンのコールバック設定
  $('#send').click(sendMessage);
});


まずはio.connect()でソケット接続を行い

socket.on('connect', function() {
・・・
}

でサーバと同じく接続イベントの登録を行っています。
さらに接続イベントでは他のクライアントへログインを知らせる為の通知を送っています。

  // ログイン通知
  emit('login');

ログアウト通知はブラウザ(タブ)のクローズイベントで通知します。

$(window).on('beforeunload', function(e) {
  // ログアウト通知
  emit('logout');
});

送信ボタン押下時のsendMessage()ではクライアントからのメッセージ送信を行います。

  // メッセージ通知
  emit('chat', msg);

サーバへのイベント発信はemit()に纏めています。

function emit(type, msg) {
  socket.emit('notice', {
    type : type,
    user : $('#username').val(),
    value : msg,
  });
}


受信処理ではrecieveを受け受信データのtypeによってログイン通知[login]、ログアウト通知[logout]、メッセージ通知[chat]を判断し画面表示を追加しています。

if (data.type === 'login') {
  item.addClass('alert alert-success').append($('<div>').append(data.user + 'がログインしました'));

} else if (data.type === 'logout') {
  item.addClass('alert alert-danger').append($('<div>').append(data.user + 'がログアウトしました'));

} else if (data.type === 'chat') {
  var msg = data.value.replace(/[!@$%<>'"&|]/g, '');
  item.addClass('well well-lg').append($('<div>').text(msg)).children('small').prepend(data.user + ':');

} else {
  item.addClass('alert alert-danger').append($('<div>').append('不正なメッセージを受信しました'));
}

動作確認

サーバの起動はpackage.jsonにエントリーポイントが設定されていますので

npm start

で行えます。

サーバを起動したらブラウザで確認してみます。
http://localhost:3000/にアクセスすればログイン画面が表示されます。
動作確認はローカルにて行います。
f:id:IntelligentTechnology:20140522115452p:plain

今回確認したブラウザは以下になります。

 GoogleChrome : 35
 firefox : 29
 IE : 11
 Opera : 21

古いIEではwebsocketが動かなかった記憶があるのですが、最近の主要ブラウザは一通り動作するようでした。
f:id:IntelligentTechnology:20140522115518p:plain

参考までですが、HTML5対応状況がわかるテストサイトでブラウザのwebsocket対応状況もありました。

所感

今回はローカル環境のみでしたが、サーバは公開サーバにすると面白そうです。
その際はユーザー管理やセキュリティをきちんと設計する必要があると思います。
また、今回紹介できなかったSocket.IOの機能で「ルーム機能」があるのですが、ユーザー管理とあわせると共有ツールとしても面白そうな物ができるのではないかと思いました。

興味がある方は是非試してみてください。