個人的には node.js は2回くらい飽きてしまって、3周目くらいです。
よく考えたらこのブログでは一度も node.js に触れてなかったなーと思ったのと、最近触ってないから忘れてそうだな...という思いからエントリにしてみました。
node.js が面白いと言われている理由の一つに非同期処理があります。そして非同期を面白くする題材として websocket があります。今日はその websocket を使って、サンプルを作ります。
物としては、twitter の filter stream から、instagr.am と picplz.com の画像URLを収集し、それをクライアントにブロードキャストします。クライアントはそれを受けて Growl 風にポップアップ表示するという物です。
まずサイトに仕上げるには、静的ファイルサーバが必要になります。
どのサイトでも紹介してそうですが、適当ながら動くレベルの物を作ります。
var sys = require('sys'),
fs = require('fs'),
url = require('url'),
http = require('http');
// static file server
var extmap = {'.htm': 'text/html', '.css': 'text/css'};
var server = http.createServer(function(req, res){
var uri = url.parse(req.url).pathname;
if (uri.match('/$')) uri = '/index.html'
try {
var filename = __dirname + '/static' + uri;
var contenttype = extmap[filename.substr(filename.lastIndexOf("."), 4)] || 'application/octet-stream';
var rs = fs.createReadStream(filename);
res.writeHead(200, {'Content-Type': contenttype});
sys.pump(rs, res);
} catch(e) {
console.log(e);
res.sendHeader(404, {"Content-Type": "text/plain"});
res.write("Not Found\n");
res.close();
}
});
server.listen(8080);
これを server.js というファイル名で保存して
# node server.js
と実行すると、static フォルダ配下のファイルが http://localhost:8080/ でサーブされる仕組みです。次に、websocket を扱える様にします。既存のサーバ上に websocket を乗せるには以下の様にサーバを指定すればokです。(おそらくこれは sugyan さんへのヒントになるのかもしれませんが)
var ws = websocket.createServer({server: server});
クライアント側の実装だとこんな感じですね。
var connection = new WebSocket('ws://' + location.host);
connection.onopen = function(event) {
}
connection.onmessage = function(event) {
}
さて、下準備は出来たので twitter stream を扱いましょう。twitter stream API はまだ basic 認証で扱えます。http-basic-auth モジュールを使いましょう。なお、Ubuntu 向けに launchpad から提供されている nodejs には node-waf が入っていませんので、依存で入る base64 モジュールのビルドに失敗します。どうしても試したい人は nodejs をソースからビルドしましょう。twitter filter stream を扱うコードは以下の様になります。
var basicauth = require('http-basic-auth');
var basicauthclient = basicauth.createClient(80, 'stream.twitter.com', false, false, account)
var req = basicauthclient.request('GET', '/1/statuses/filter.json?track=picplz,instagr', {'host': 'stream.twitter.com'})
req.end();
req.on('response', function (res) {
res.on('data', function(chunk) {
})
})
account は username と password をキーに持つオブジェクトです。ご自分のアカウントを設定しましょう。twitter stream を受信したら、tweet の entities から URL 一覧を取得し、instagr.am と picplz.com の画像URLを取得しましょう。picplz.com はURLの後ろに /thumb/400 を足すだけですが、instagr.am は API にアクセスしなければなりません。
tweet 受信時の処理は以下の様になりました。
req.on('response', function (res) {
res.on('data', function(chunk) {
try {
var tweet = JSON.parse(chunk);
// parse entities urls
[].forEach.call(tweet.entities.urls, function(item) {
// pick images of instagr.am or picplz.com
if (item.url.match('^http://instagr.am/p/')) {
var client = http.createClient(80, 'instagr.am');
var req = client.request('GET', '/api/v1/oembed/?format=json&maxheight=330&url=' + item.url, {'host': 'instagr.am'});
req.on('response', function(res){
res.on('data', function(chunk){
var url = JSON.parse(chunk).url
console.log(url)
ws.broadcast(url)
});
});
req.end();
}
if (item.url.match('^http://picplz.com/')) {
var url = item.url + '/thumb/400'
console.log(url)
ws.broadcast(url)
}
});
} catch(e) { console.log(e) }
});
});
サーバ部は出来上がりですね。最後にクライアント部を作りましょう。
$(function() {
var connection = new WebSocket('ws://' + location.host);
connection.onopen = function(event) {
$('#fotoflo').empty();
}
connection.onmessage = function(event) {
var x = (Math.random() * ($(document).width() - 330)).toFixed();
var y = (Math.random() * ($(document).height() - 330)).toFixed();
var item = $('<div/>')
.addClass('popup-image')
.css({'left': x + 'px', 'top': y + 'px', 'position': 'absolute', 'display': 'none'})
.appendTo('body')
$('<img/>')
.attr('src', event.data)
.bind('load', function() {
$(item).fadeIn(500).delay(10000).fadeOut(500, function() {
$(this).remove()
})
}).appendTo(item)
}
})
こんな感じでしょうか。受信する度にランダムな位置へ画像をフェイドイン表示し、数秒経過したら消すというオーソドックスな物です。以下、リポジトリの場所は示しますが、サーバの全体ソースを載せておきます。
var sys = require('sys'),
fs = require('fs'),
url = require('url'),
http = require('http'),
websocket = require('websocket-server'),
basicauth = require('http-basic-auth');
// load twitter account
var account = JSON.parse(fs.readFileSync(__dirname + '/config.json', 'utf8'))
// static file server
var extmap = {'.htm': 'text/html', '.css': 'text/css'};
var server = http.createServer(function(req, res){
var uri = url.parse(req.url).pathname;
if (uri.match('/$')) uri = '/index.html'
try {
var filename = __dirname + '/static' + uri;
var contenttype = extmap[filename.substr(filename.lastIndexOf("."), 4)] || 'application/octet-stream';
var rs = fs.createReadStream(filename);
res.writeHead(200, {'Content-Type': contenttype});
sys.pump(rs, res);
} catch(e) {
console.log(e);
res.sendHeader(404, {"Content-Type": "text/plain"});
res.write("Not Found\n");
res.close();
}
});
server.listen(80);
// listen websocket server on the server
var ws = websocket.createServer({server: server});
// twitter filter stream
var basicauthclient = basicauth.createClient(80, 'stream.twitter.com', false, false, account)
var req = basicauthclient.request('GET', '/1/statuses/filter.json?track=picplz,instagr', {'host': 'stream.twitter.com'})
req.end();
req.on('response', function (res) {
res.on('data', function(chunk) {
try {
var tweet = JSON.parse(chunk);
// parse entities urls
[].forEach.call(tweet.entities.urls, function(item) {
// pick images of instagr.am or picplz.com
if (item.url.match('^http://instagr.am/p/')) {
var client = http.createClient(80, 'instagr.am');
var req = client.request('GET', '/api/v1/oembed/?format=json&maxheight=330&url=' + item.url, {'host': 'instagr.am'});
req.on('response', function(res){
res.on('data', function(chunk){
var url = JSON.parse(chunk).url
console.log(url)
ws.broadcast(url)
});
});
req.end();
}
if (item.url.match('^http://picplz.com/')) {
var url = item.url + '/thumb/400'
console.log(url)
ws.broadcast(url)
}
});
} catch(e) { console.log(e) }
});
});
簡単ですね!server.js と同じフォルダに config.json というファイルがあるので、そのファイル内を twitter アカウントで修正して頂ければ動きます。
mattn/node-image-stream - GitHubみなさんもぜひ面白い物作ってみて下さい。
display images from twitter stream
https://github.com/mattn/node-image-stream