ExpressとSocket.ioを使ったチャットサンプル

このエントリはリアルタイムWebハッカソンのハンズオン資料その4です。

前回の続きです。それでは次に簡単なチャットアプリのコードを見てみましょう。かなりの部分(特にデザイン面)をSocket.ioのチャットサンプルをパクって参考にしています。

サーバ側であるapp.jsはこんな感じです。

var express = require('express'),
    io = require('socket.io'),
    json = JSON.stringify;

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.use(express.bodyDecoder());
  app.use(express.methodOverride());
  app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] }));
  app.use(app.router);
  app.use(express.staticProvider(__dirname + '/public'));
  app.use(express.logger());
});

app.configure('development', function(){
  express.logger("development mode");
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function(){
  express.logger("production mode");
  app.use(express.errorHandler());
});

// Routes

app.get('/', function(req, res){
  res.render('index.jade', {
    locals: {
        title: 'リアルタイムWebハッカソン Chat Room'
    }
  });
});

// Only listen on $ node app.js

if (!module.parent) {
  app.listen(3000);
  console.log("Express server listening on port %d", app.address().port)
}

var socket = io.listen(app);
var count = 0;
socket.on('connection', function(client) {
  count++;
  client.broadcast(json({count: count}));
  client.send(json({count: count}));

  client.on('message', function(message) {
    // message
    client.broadcast(message);
    client.send(message);
  });
  client.on('disconnect', function() {
    // disconnect
    count--;
    client.broadcast(json({count: count}));
  });
});

やりとりする内容をJSON形式に変えてます。人数のカウント情報だけでなく、チャット内容もやりとりするため、ただの文字列ではどちらが来てるのかを判断しづらいからです。
messageを受け取るところの処理では受け取った内容をそのまま配信してます。

次にクライアント側を見ましょう。views/layout.jadeはこんな感じです。

!!! 5
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js')
    script(type='text/javascript', src='http://cdn.socket.io/stable/socket.io.js')
    script(type='text/javascript', src='/javascripts/client.js')
  body!= body

先頭に5が加わってHTML5の宣言にした事以外前回から変わりありません。HTML5にしてるのは下にあるindex.jadeのテキストボックスにplaceholder属性の設定をしているからです。
views/index.jadeは次のようになっています。

h1= title
p 現在接続している人は
  span#count
  人います
#chat
form#form(onsubmit='send(); return false;')
  input#name(type='text', placeholder='Twitter ID')
  input#text(type='text', autocomplete='off')
  input(type='submit', value='送信')

チャット表示欄#chatと入力フォーム#formが追加されています。#chatのように要素を省略すると自動的にdivだと判断されます。
public/stylesheets/style.lessは次のようになっています。

body {
  padding: 10px 50px 5px 50px;
  font: 14px "Lucida Grande", "Helvetica Nueue", Arial, sans-serif;
}
#chat {
  height: 500px;
  overflow: auto;
  width: 800px;
  border: 1px solid #eee;
  p {
    padding: 0px;
    margin: 0px;
  }
  div {
    padding: 5px;
    margin: 0;
  }
  div:nth-child(odd) {
    background: #F6F6F6
  }
  .permalink {
    font: 3px;
    color: #AEAEAE;
  }
}
#form {
  width: 782px;
  background: #333;
  padding: 5px 10px;
  input[type=text] {
    padding: 5px;
    background: #fff;
    border: 1px solid #eee;
  }
  #name {
    width: 85px;
  }
  #text {
    width: 620px;
  }
  input[type=checkbox] {
  }
  input[type=submit] {
    cursor: pointer;
    background: #999;
    border: none;
    padding: 6px 8px;
    -moz-border-radius: 8px;
    -webkit-border-radius: 8px;
    margin-left: 5px;
    text-shadow: 0 1px 0 #fff;
  }
  input[type=submit]:hover {
      background: #A2A2A2;
  }
  input[type=submit]:active {
      position: relative;
      top: 2px;
  }
}

最後にclient.jsを見てみましょう。

var socket = new io.Socket('localhost'),
    json = JSON.stringify;
socket.connect();
socket.on('message', function(message) {
  message = JSON.parse(message);
  if (message.count) {
    $('#count').text(message.count);
  }
  if (message.message) {
    var data = message.message;
    var date = new Date();
    date.setTime(data.time);
    $('#chat').append('<div class="chatlog"><p><a name=' + data.time + '></a><a href="http://twitter.com/' + data.name + '"><img src="http://api.dan.co.jp/twicon/' + data.name + '/mini" /></a> ' + data.text + '</p><a class="permalink" href="#' + data.time + '">' + date.toString() + '</a></div>')
    $('#chat').scrollTop(1000000);
  }
});

function send() {
  var name = $('#name').val();
  var text = $('#text').val();
  if (text && name && name != "Twitter ID") {
    var time = new Date().getTime();
    socket.send(json({message: {name: name, text: text, time: time}}));
    $('#text').val('');
  }
}

特に難しいところはないと思います!

ExpressとSocket.ioを使ったWebSocketのサンプルを作る

このエントリはリアルタイムWebハッカソンのハンズオン資料その3です。

前回の続きです。まずはSocket.ioを使えるようにしましょう。

app.jsの先頭を

var express = require('express');

から

var express = require('express'),
    io = require('socket.io');

にする。
また、ファイルの最後に

var socket = io.listen(app);
socket.on('connection', function(client) {
  // connect
  client.on('message', function(message) {
    // message
  });
  client.on('disconnect', function() {
    // disconnect
  });
});

を追加する。

クライアントから接続がきたらconnectに書かれた処理が、メッセージがきたらmessageに書かれた処理が、切断されたらdisconnectにかかれた処理が実行される。

次にクライアント側の処理。
sample/views/layout.jadeを開いて

!!!
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript', src='http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js')
    script(type='text/javascript', src='http://cdn.socket.io/stable/socket.io.js')
    script(type='text/javascript', src='/javascripts/client.js')
  body!= body

と記述。jQueryを使い、socket.ioのクライアントライブラリは公式サイトCDNを使い、自分で書くクライアント側のJavaScriptファイルはclient.jsという名前にする。

sample/public/javascripts/client.jsを以下の内容で作る。

var socket = new io.Socket('localhost');
socket.connect();
socket.on('message', function(message) {
  //message
});

見てのとおり2行目で接続し、サーバからメッセージがきたらmessageに書かれた処理が実行される。

んじゃとりあえず、現在接続している人数をリアルタイムに表示するプログラムでも書いてみましょう。
まずはapp.js。最後のsocket.ioのところを以下のようにする

var socket = io.listen(app);
var count = 0;
socket.on('connection', function(client) {
  count++;
  client.broadcast(count);
  client.send(count);
  client.on('message', function(message) {
    // message
  });
  client.on('disconnect', function() {
    // disconnect
    count--;
    client.broadcast(count);
  });
});
  1. countの初期値を0にしておく
  2. 接続がきたらcountを+1して、その値をbroadcast(現在のクライアント以外にメッセージを送信)とsend(現在のクライアントにメッセージを送信)
  3. 切断したらcountを-1して、その値をbroadcast

わかりやすいですね。

次にsample/views/index.jadeに以下のように追記します。

h1= title
p Welcome to #{title}
p 現在接続している人は
  span#count
  人います

そしてsample/public/javascripts/client.jsを以下のように修正します。

var socket = new io.Socket('localhost');
socket.connect();
socket.on('message', function(message) {
  $('#count').text(message);
});

以上で終了です。
それでは

$ node app.js

で起動してみましょう。
localhost:3000にアクセスすると「現在接続している人は1人います」と出るはずです。コンソールには

Express server listening on port 3000
18 Oct 18:44:52 - socket.io ready - accepting connections
xxx.xxx.xxx.xxx - - [Mon, 18 Oct 2010 09:44:56 GMT] "GET /favicon.ico HTTP/1.1" 404 - "" "Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3"
18 Oct 18:44:56 - Initializing client with transport "websocket"
18 Oct 18:44:56 - Client 14417221560142934 connected

こんな感じでログが出てるはずです。ちなみにfaviconがないよっていう404はウザいので適当にfavicon.icoを作ってpublic直下に置いとけばいいと思います。
では、現在のタブを開いたまま新しいタブでもlocalhost:3000にアクセスしてみましょう。「現在接続している人は2人います」と表示され、コンソールにも

18 Oct 18:50:14 - Initializing client with transport "websocket"
18 Oct 18:50:14 - Client 2343847642187029 connected

みたいに接続が増えたログが出てるはずです。
さらに、最初の「1人」と表示されていたタブを見てください。リロードしなくてもすでに「2人」になっているはずです!
タブを増やすたびに人数が増えていき、また閉じるたびに減っていくことを確認してください。
これでnode.jsを使った基本的なWebSocketの使い方は終了です。

ExpressとWebSocketを使ったWebSocketのサンプルを作る準備

このエントリはリアルタイムWebハッカソンのハンズオン資料その2です。

前回の記事でnode.jsとnpmのインストールは完了しているものとします。

まずは必要なライブラリのインストール

$ npm install express jade less socket.io
npm info it worked if it ends with ok
npm info using npm@0.2.3-6
npm info fetch http://registry.npmjs.org/express/-/express@1.0.0rc4.tgz
npm info fetch http://registry.npmjs.org/less/-/less-1.0.36.tgz
npm info fetch http://registry.npmjs.org/socket.io/-/socket.io-0.5.3.tgz
npm info fetch http://registry.npmjs.org/jade/-/jade@0.5.3.tgz
npm info install express@1.0.0rc4
npm info install socket.io@0.5.3
npm info install less@1.0.36
npm info install jade@0.5.3
npm info activate socket.io@0.5.3
npm info activate less@1.0.36
npm info activate jade@0.5.3
npm info activate express@1.0.0rc4
npm info build Success: socket.io@0.5.3
npm info build Success: less@1.0.36
npm info build Success: jade@0.5.3
npm info build Success: express@1.0.0rc4
npm ok

ExpressはSinatraライクなnode.jsのWebアプリケーションフレームワーク
JadeHamlライクなnode.jsのテンプレートエンジンでlessCSS拡張lessのnode.js版。両方ともExpressが標準でサポートしている。
Expressがサポートしているテンプレートエンジンは他にもEJSHamlCoffeeKupがあり、CSS拡張もsassnode.js実装をサポートしてる。でまあ、好きなの使えばいいんだけど後述のジェネレータで作られたアプリでデフォルトで使われてるのがjadeとlessなので、ここではjadeとlessを使うことにする。

で、おもむろに以下を実行する

$ express sample
   create : sample
   create : sample/app.js
   create : sample/public/javascripts
   create : sample/logs
   create : sample/pids
   create : sample/public/images
   create : sample/public/stylesheets
   create : sample/public/stylesheets/style.less
   create : sample/test
   create : sample/test/app.test.js
   create : sample/views/partials
   create : sample/views/layout.jade
   create : sample/views/index.jade

すると、必要なファイルとディレクトリが作られアプリ基盤が完成する。
publicが静的ファイル置き場。(クライアントサイドの)JavaScriptCSSや画像なんかはここに置く。viewsはテンプレートファイル置き場。静的なHTMLならpublicに置けばいいんじゃないかな。testはテストコード置き場。logsはログファイルが作られる場所。pidsは複数プロセス立ち上げた時の管理用…だと思う。(ぶっちゃけ前まではpublicとviewsしかなくて、このエントリ書くために今実行してみたらその他のが出来てたので戸惑ってるところ)。
この状態ですでに実行可能になっているので実行してみます。

$ cd sample
$ node app.js
Express server listening on port 3000

ブラウザでlocalhost:3000にアクセスし、Welcome to Expressと表示されたら成功。

app.jsの中身はこんな感じ。

/**
 * Module dependencies.
 */

var express = require('express');

var app = module.exports = express.createServer();

// Configuration

app.configure(function(){
  app.set('views', __dirname + '/views');
  app.use(express.bodyDecoder());
  app.use(express.methodOverride());
  app.use(express.compiler({ src: __dirname + '/public', enable: ['less'] }));
  app.use(app.router);
  app.use(express.staticProvider(__dirname + '/public'));
});

app.configure('development', function(){
  app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 
});

app.configure('production', function(){
  app.use(express.errorHandler()); 
});

// Routes

app.get('/', function(req, res){
  res.render('index.jade', {
    locals: {
        title: 'Express'
    }
  });
});

// Only listen on $ node app.js

if (!module.parent) {
  app.listen(3000);
  console.log("Express server listening on port %d", app.address().port)
}

まあ、見たら大体分かると思うけどちょっと補足。

  • 16行目でCSS拡張としてlessを使うことを指定している。
  • 21,25行目でdevelopmentモードのときとproductionモードのときのエラー処理を設定している。基本はdevelopmentモードなんだけど、以下のようにNODE_ENVにproductionを指定して動かせばproductionモードになる。
$ NODE_ENV=production node app.js
  • 31行目で'/'以下にアクセスされたときの動作を設定している。見てのとおりjadeのテンプレートファイルのindex.jadeを変数titleに'Express'を束縛して描画する。テンプレートファイルの拡張子を見てどのエンジンを使うかを自動的に判断するので複数のテンプレートエンジンを共存させることも可能。だけどまあ1つのエンジンしか使わないなら、12行目のconfigureの中で次のように指定してやって拡張子を省略することもできる。
app.set('view engine', 'jade');

Cygwinや仮想化なしでnode.jsをWindowsへインストール

このエントリはリアルタイムWebハッカソンのハンズオン資料その1補足です。

どうしてもCygwinが嫌いだ。だけどVirtualBoxとかVMwareとかも嫌だって人もいるかもしれません。そういう人もなんとかまあnode.jsを使えるようにできないわけではありません。

注)茨の道です。普通にCygwin使うかVirtualBoxUbuntu入れることを推奨します。

1. msysgitをインストールしてGit Bashを使えるようにします。
2. Git Bash上で

$ git clone git://github.com/ajaxorg/node-builds

をして、node-builds/win32/binにパスを通す。これで一応nodeコマンドは使えるようになります。
ただ、必要なライブラリをnpmを使って取得することができません。なので……
3. Git Bash上で

$ git clone git://github.com/robrighter/node-boilerplate.git hoge
$ cd hoge
$ ./bin/initproject.sh

とすれば、express, Socket.ioなどをhoge/lib内に取得し、hoge/server.jsの先頭に

require(__dirname + "/lib/setup").ext( __dirname + "/lib").ext( __dirname + "/lib/express/support");

を記述してくれるのでこれで普通にrequireして使うことができるようになります。
expressやSocket.io以外のライブラリが使いたければ自分でlib内にgit cloneすればいいんじゃないかな。とはいえやったことはないので出来るかどうか知りません!誰かやってみて教えてくださいっ

node.jsとnpmのインストール


このエントリの記述は既に古いです。最新のインストール方法は下記のエントリを参照してください。
http://d.hatena.ne.jp/t_43z/20110503/1304421488


このエントリはリアルタイムWebハッカソンのハンズオン資料その1です。

Linux

普通に入れる

後述のnaveを使う方がいいと思う。っていうかnpmのインストール方法をどうしたいかによってnode.js本体のインストール方法が変わる。なので(naveを使わないにしても)最後まで読んでからやることを推奨。
普通にやるとこんな感じ。

$ sudo apt-get install build-essential libssl-dev curl
$ mkdir tmp
$ cd tmp
$ wget http://nodejs.org/dist/node-v0.2.3.tar.gz
$ tar -xvzf node-v0.2.3.tar.gz
$ cd node-v0.2.3
$ ./configure
$ make
$ sudo make install

必要なパッケージはもちろん環境によって違う。上記はUbuntu10.04の初期状態の場合。make testをする場合には、さらにapache2-utilsをinstallしておく必要がある。
npmは作者が推奨しているやり方が3通り、推奨していないやり方(sudoで実行しちゃう方法)が1通りある。
1. /usr/local以下をrootではなく自分のものにしてしまう方法。自分しか触らないマシンならいいんじゃねって話。

$ sudo chown -R $USER /usr/local
node本体をインストール
$ curl http://npmjs.org/install.sh | sh

一行目をnode本体のインストール前にやり、node本体のインストールの最後のsudoは付けない。

2. node本体を$HOME/local以下にインストールしておく方法
上述のnodeのインストールで./configureに--prefix=~/localのオプションを指定してやる。そうやってnode本体をインストールした後に

$ curl http://npmjs.org/install.sh | sh

を実行すればいい。

3. npmのインストール先を指定しておく方法
node本体は上述の方法でインストールしておく。

まず設定
$ cat >>~/.npmrc <

naveを使う

naveっていうのは、複数バージョンのnode.js環境を切り替えて使うためのツール。RubyのrvmとかPythonのvirtualenv的なもの。同じ趣旨のものにnvmもあるが、naveはnpmの作者自らが作っているのでその安心感がある。

$ sudo apt-get install build-essential libssl-dev git-core
$ mkdir work
$ cd work
$ git clone http://github.com/isaacs/nave.git
$ cd nave
$ ./nave.sh install latest

でnode本体のインストールが完了。

$ ./nave.sh use latest
$ curl http://npmjs.org/install.sh | sh

でnpmのインストールが完了。これでいれるとnode.js本体は$HOME/work/nave/installed/0.2.3以下に、npmは$HOME/work/nave/installed/0.2.3/lib/node/npmに入っている。
use をしないとnodeにパスが通らないので、

$HOME/work/nave/nave.sh use latest

を.bash_profileとかに入れとくといい。
追記) オフライン状態でnave.sh use latestすると、nave/installed以下が全て削除される現象(バグ?)が発生するようです。なので、ローカルのマシンだと.bash_profileに入れるのは危険かもしれません。

Windows

CygwinにしろVirtualBoxにしろ、環境ができたら上記のLinuxと同じ手順。素で入れるよりnave使う方がいいってのも同じ。

Cygwin嫌いな方は

VirtualBoxとかでUbuntu等を入れるのがいいでしょう。

Mac

MacPorts

node本体はあるんだけど、npmはない。
http://www.florian-kubis.de/2010/09/howto-install-node-js-on-mac-snow-leopard-with-express/が参考になりそう。

Homebrew

$ brew install node
$ brew install npm

あとはnpm用のパス設定

$ vim ~/.profile
export NODE_PATH=/usr/local/lib/node:$PATH
export PATH=/usr/local/share/npm/bin:$PATH
を追記

さくらのVPSの初期設定

Ubuntu 64bit32bti版入れました(64bit版は素で400MB近くメモリ食ってたので32bitに変えた)。

とりあえずアップデート(インストール時に最後にやってくれてるけど一応)

$ sudo apt-get update
$ sudo apt-get upgrade

SSH公開鍵を追加

ローカルマシンで
$ scp .ssh/id_rsa.pub ***.***.***.***:/home/meso

サーバで
$ mkdir .ssh
$ cat id_rsa.pub >> .ssh/authorized_keys
$ rm id_rsa.pub
$ chmod 600 .ssh/authorized_keys
$ sudo apt-get install vim
$ sudo vim /etc/ssh/sshd_config

PermitRootLogin yes

  • > PermitRootLogin no

#AuthorizedKeysFile %h/.ssh/authorized_keys

  • > AuthorizedKeysFile %h/.ssh/authorized_keys

#PasswordAuthentication yes

  • > PasswordAuthentication no

$ sudo /etc/init.d/ssh restart

あとはsshのポート変えたり、iptablesで接続ポート絞ったりとか色々

Node.js Knockout に参加しました

http://nodeknockout.com/

要は、node.jsを使ったアプリケーションを48時間で作成してね。っていうコンテスト。コードはgithubコミットし、herokujoyentにデプロイするのがルール。

僕は1人チームで参加しました。やっぱり、node.jsの特徴であるイベント駆動I/Oを生かしたものが作りたいと思い、WebSocketを使ってリアルタイムに複数人との対戦ができるタイピングゲームを作ることにしました。

herokuは、WebSocketの使用に制限があったので(同時接続64人までとか接続時間30秒までとか)、joyentにデプロイしました。っていうかこのjoyentのnode.js用のプラットフォームはすごく使い勝手が良かった。

完成品はhttp://mesolabs.no.de/です。ぶっちゃけ、テストも不十分だしコードも汚いand拙いものなんですが、なんとか形にはなりました(Windows 7 + Google Chromeな人が参加するとバグるっぽいです。Windows 7の人は是非Safariで)。

審査員やら一般の方の投票も終わりました。http://nodeknockout.com/teams/mesolabsで点数とコメントを見ることが出来ます。点数は伸び悩んだのですが、コメントを見ると勿体無いようなありがたいコメントを多く頂けました。

他のチームのアプリケーションを見ても、やはりWebSocketを使ったリアルタイムコミュニケーションを重視したものが多く、node.jsの向いている方向性がはっきりと示唆された大会であったようにも思います。

当初はほとんど触ったこともなかった状態だったので、参加するかどうか若干迷ったりもしたのですが、思い切って参加してみて本当によかったと思います。たくさん勉強になりました。

実装よりの話は、また次回にでもまとめて書きます。