@blog.justoneplanet.info

日々勉強

緊急地震速報 by Extensionを作ったときの感謝をこめて

「緊急地震速報 by Extension」を作って経験したことなどをまとめました。今回は後編です。前編は会社のブログに書きました。

僕は生産設備を持っていませんので食料をはじめとした物流における支援は難しいのかもしれません。しかしながらシステムエンジニアとして間接的な支援や情報における支援はできます。僕は、天災における「破壊」を修復するのは人々の「生産」と考えるとともに、その「生産」の一部分を担う者として頑張っていきたいと思います。

東北地方太平洋沖地震の後、余震が頻発し緊急地震速報の警告音も頻繁に耳にするようになりました。警告音に焦らされる一方で、本当は落ち着いて揺れに対処しなければならないと思っていました。そこで緊迫感を与えないような緊急地震速報を出せないものかと今回の制作にいたりました。(なので初期バージョンには音がなかったのです)

また、本当は「緊急地震速報 by Chrome Extension」という名前で登録しようとしたのですが「Chrome」というワードは使用ができないので「緊急地震速報 by Extension」という名前になりました。

■プロキシ経由での接続

大きな会社などではプロキシ経由でインターネットに接続するようになっていて配信サーバと接続ができないようでした。

変更前

ポート12001番を使用していました。

var hosts = [
    '123.123.123.123:12001'
];

変更後

ポート443番を使用するように変更しました。

var hosts = [
    '123.123.123.123:443'
];

HTTPSで使用するポートを用いることにより一部の方の接続が可能になりました。

さらに変更後

ポート443番、80、8080番、12001番を使用するように変更しました。

var hosts = [
    '123.123.123.123:443',
    '123.123.123.123:80',
    '123.123.123.123:8080',
    '123.123.123.123:12001'
];

これによって判明したのは1/3〜1/4の利用者の方は443にしか接続できなかった事です。

参考

■マルチバイト文字列のbroadcast

今回、Websocketサーバからbroadcastする際、node-websocket-serverを使用したのですが日本語を正しく送信できませんでした。

解決策

wsServer.broadcast('\\u3092');

こうすることでクライアント側には\u3092という文字列が送信されるようになります。

ちょっとライブラリの中をのぞいてみます。

server.js

broadcastメソッドです。

this.broadcast = function(data) {
    manager.forEach(function(client) {
        clientWrite(client, data);
    });
};

上述のとおりforEachで回してます。clientWriteメソッドも確認します。

function clientWrite(client, data) {
  if (client && client._state === 4) {
    client.write(data, 'utf8');
  }
}

utf8としていされてwriteされています。

manager.js
Manager.prototype.forEach = function(callback, thisArg) {
    var current = this._head;
    while (current !== null) {
        callback.call(thisArg, current.connection);
        current = current._next;
    }
};

this._headは連結リストの先頭の要素のようです。

this._head = client;

clientオブジェクトが入っているようです。

var client = {
    id: connection.id,
    _next: null,
    connection: connection
};

接続IDと接続、次の要素を保持しています。

connection.js
function write(connection, data) {
    debug(connection.id, 'write: ', (new Buffer(data)).inspect());
    if (connection._socket.writable) {
        return connection._socket.write(data, 'binary');
    }
    return false;
}

binaryと指定されていますね。完全に読み切るには少々時間がかかるので適宜更新します。

参考

■正規表現

情報はtwitterのUser Streams API経由で取得していたのですが、ご利用者の方から震度やマグニチュードだけを簡略化して欲しいとの要望がありました。そこで正規表現を使用し文字列を整形して表示することにしました。ところが一部のご利用者には元の文章がそのまま表示されていました。

解決策

以下のようにして日本語をマッチさせるときに16進表現を使用することで解決しました。

var reg = new RegExp('\u9707\u5EA6([3-9]\u5F37*\u5F31*)', 'ig');
text.match(reg);
var scale = RegExp.$1;
// filter : simplify
if(localStorage["simplify"] !== "false"){
    var reg = new RegExp('([0-9]*/[0-9]*/[0-9]*)', 'ig');
    text.match(reg);
    var date = RegExp.$1;

    var reg = new RegExp('([0-9]*\:[0-9]*)', 'ig');
    text.match(reg);
    var time = RegExp.$1;

    var reg = new RegExp('\u30DE\u30B0\u30CB\u30C1\u30E5\u30FC\u30C9([0-9\.]*)', 'ig');
    text.match(reg);
    var magnitude = RegExp.$1;

    var reg = new RegExp('\u3001(.*?)\u306E', 'ig');
    text.match(reg);
    var place = RegExp.$1;
    
    if(date != '' && time != '' && place != '' && scale != '' && magnitude != ''){
        text = date + " " + time + " " + place + "\n\u9707\u5EA6" + scale + "\n\u30DE\u30B0\u30CB\u30C1\u30E5\u30FC\u30C9" + magnitude;
    }
}

// filter : scale
switch(localStorage["scale"]){
    case "7":
        if(parseInt(scale, 10) < 7){return false;}
        break;
    case "6":
        if(parseInt(scale, 10) < 6){return false;}
        break;
    case "5":
        if(parseInt(scale, 10) < 5){return false;}
        break;
    case "4":
        if(parseInt(scale, 10) < 4){return false;}
        break;
}

ブラウザ側の言語設定は各々異なります。通信時の文字コードを指定しても解決するかもしれません。

■twitterとの接続

node.jsを使い、以下のようなコードでtwitterと接続してます。

var client = http.createClient(
    80,
    "stream.twitter.com",
    false,
    false,
    {
        "username" : "hogehoge",
        "password" : "fugafuga"
    }
);
var request = client.request(
    "GET",
    "/1/statuses/filter.json?follow=123456789",
    {
        "host" : "stream.twitter.com"
    }
);
request.end();
request.on(
    "response",
    function(response){
        //sys.puts("response");
        var chunk = '';
        response.on(
            "data",
            function(data){
                // データ受信処理
            }
        );
        response.on(
            'end',
            function(){
                // twitterから接続を切られた時
            }
        );
    }
);

twitter切断時

実はたまに接続を切られます。切断される可能性を考慮し以下のようなコードを使用してます。

var getRequest = function(){
    var request = client.request(
        "GET",
        "/1/statuses/filter.json?follow=123456789",
        {
            "host" : "stream.twitter.com"
        }
    );
    request.end();
    request.on(
        "response",
        function(response){
            response.on(
                "data",
                function(data){
                    // データ受信処理
                }
            );
            response.on(
                'end',
                function(){
                    // twitterから接続を切られた時
                    r = getRequest();// 再接続
                }
            );
        }
    );
    return request;
}
var r = getRequest();

通信は必ず切れるものと考えてコードを書かなくてはいけませんね。さらに実は以下のようなスクリプトも実行してます。

request.on(
    "response",
    function(response){
        //sys.puts("response");
        var chunk = '';
        response.on(
            "data",
            function(data){
                // データ受信処理
            }
        );
        response.on(
            'end',
            function(){
                // twitterから接続を切られた時
                throw new Error('twitter disconnected me!!');
            }
        );
    }
);

node.jsにおいてcatchできなかったErrorはプロセスを終了させます。従って以下のようなシェルスクリプトと組み合わせてプロセスが終了してないか定期的にチェックし、終了していた場合は再起動することで接続を維持しています。

#!/bin/sh
while true
do
    isAlive=`ps -ef | grep "my-websocket-server.js" | grep -v grep | wc -l`
     if [ $isAlive -ge 1 ]; then
        echo "process is alive"
    else
        node my-websocket-server.js.js
        echo "process is dead"
    fi
    sleep 3
done

■アップデート

拡張機能のアップデートは自動で行われますが、新バージョンを登録してすぐに全ユーザのアップデートが完了するわけではありません。意外にも早く直後にアップデートされる方もいれば、時間がかかる方もいます。1時間で10%程度の方がアップデートされるようです。一方、90%のユーザはアップデート前のクライアントでサーバに接続しますので考慮して更新しなければなりません。

■起動

ちなみにですが以下のコマンドでWebSocketサーバを起動してます。

nohup ./check.sh > log.txt &

接続数を調べるには以下のようなコマンドを使ってます。

lsof -i:12345 | grep "node" | wc -l

■twitterからの文字列

システムの設計の問題にもなりますが、twitterからの文字列が正しくパースできないようなケースが稀にあります。入力は受け手側が意図する形式になるとは限らないということです。

文字列をパースできるように条件分岐する
但し、ブラックリスト方式に近いので未知の正しくない文字列はパース出来ない
データストアを作って保存に対してbroadcastのトリガを引かせる
全てのサーバが正しくパースできなかった事はないので非常に有用だが、失敗する確率はわずかながらある

■サーバ負荷

node.jsの素晴らしさを実感する毎日ですが、接続数が1万数千を超えるとさすがに0.5〜1秒程度遅延します。早く多くの方に届けるべき情報ではあるものの、一方で多くの方が接続すると遅延が生じやすくなります。サーバは個人で賄っているので限度があります。非常に難しい問題です。財閥の末裔だったらデータセンターごと買うのになと思う毎日です。

■まとめ

僕は水や電気の節約には賛成です。しかしながら根拠が不明瞭な自粛ムードには反対です。冒頭で述べました通り、破壊を修復するのは生産であり、手を止めて遠慮する事では無いと思います。何もできることがないと考えた結論の自粛より、いつも通り働き、学び、生活する方が生産的だと僕は考えます。まだまだ対応できていない事ばかりで完璧ではありませんが今後とも宜しくお願いいたします。早く余震が収まるといいですね。(●´⌓`●)

拡張機能への要望

今まで通り@mi_eqbotまでお願いいたします。自分のブログよりも確実にチェックしてます。w

その他開発などへの興味

@mitsuaki_iまでお願いいたします。

node.jsでモジュールを作る

以下のようなコードを考える。

var sys = require('sys');

var dog = {
    "pochi" : 16,
    "john"  : 10
};
var getDogAge = function(name){
    return dog[name];
}

sys.puts('pochi is ' + getDogAge('pochi') + ' years old.');

以下のようにモジュール化すると非常にすっきりする。

■モジュール側

以下のように記述する。ちなみにgetメソッドなどは予約されていて使用できないようだ。

var dog = {
    "pochi" : 16,
    "john"  : 10
};
exports.getAge = function(name){
    return dog[name];
}

クライアント側から使用するメソッドやプロパティはexportオブジェクトのプロパティとする。それ以外のグローバル空間にある変数や関数はクライアント側からは見えず、プライベート扱いとなる。

■クライアント側

以下のようにして使用する。

var sys = require('sys');
var dog = require('./dog.js');
sys.puts('pochi is ' + dog.getAge('pochi') + ' years old.');

また以下のようにした場合は

var Dog = function(name){
    var self = this;
    self.name = name;
    self.cry = function(){
        sys.puts(self.name);
    }
}
module.exports = Dog

以下のように使用することができる。

var Dog = require('./dog.js');
var dog = new Dog();

すっきり。簡単。(●´ω`●)

■参考

pywebsocketをインストールする

■pywebsocket単体で動作させる

draft75

vi /home/pywebsocket-0.5.2/src/mod_pywebsocket/standalone.py

以下の部分を

parser.add_option('--allow-draft75', dest='allow_draft75',
                      action='store_true', default=False,
                      help='Allow draft 75 handshake')

以下のように変更する。

parser.add_option('--allow-draft75', dest='allow_draft75',
                      action='store_true', default=True,
                      help='Allow draft 75 handshake')

起動

python /home/pywebsocket-0.5.2/src/mod_pywebsocket/standalone.py -p 8800 -d /home/pywebsocket-0.5.2/src/example

WebSocketで8800ポートにアクセスする。(●´ω`●)

■apacheモジュールとして動作させる

http-devel

yum install http-devel

mod_python

wget http://archive.apache.org/dist/httpd/modpython/mod_python-3.3.1.tgz
tar xvzf mod_python-3.3.1.tgz
cd mod_python-3.3.1
./configure –with-apxs=/usr/sbin/apxs –with-python=/usr/bin/python
make
make install
設定
vi /etc/httpd/conf.d/python.conf

以下の記述の下に

LoadModule python_module modules/mod_python.so

以下の記述を付加する。

AddHandler mod_python .py

mod_pywebsocket

wget http://pywebsocket.googlecode.com/files/mod_pywebsocket-0.5.2.tar.gz
tar xvzf mod_pywebsocket-0.5.2
cd pywebsocket-0.5.2/src
python setup.py build
python setup.py install
設定
vi /etc/httpd/conf.d/python_mod_pywebsocket.conf

以下を記述する。

<IfModule python_module>
    PythonPath "sys.path+['/usr/lib/python2.4/site-packages/']"
    PythonOption mod_pywebsocket.handler_root /var/www/html/wsh
    PythonHeaderParserHandler mod_pywebsocket.headerparserhandler
    PythonOption mod_pywebsocket.allow_draft75 On
</IfModule>
/etc/init.d/httpd restart

起動

cp /home/pywebsocket-0.5.2/src/example/echo_wsh.py /var/www/html/wsh/

WebSocketクライアントで/echoにアクセスする。(●´ω`●)

■ベンチマーク

正確性は微妙だが100クライアントからの接続でのロードアベレージは、「node : pywebsoket(standalone) : pywebsoket(apache) = 0.15 : 0.25 : 0.8」となった。最初の2つの差は微妙だったが、Apacheモジュールとして動作させた時のリソースの消費量は明らかに大きいようだ。

node-base64をインストールする

どうやら環境によっては以下のエラーが出る。

../base64.cc:138: error: 'malloc' was not declared in this scope
../base64.cc:141: error: 'free' was not declared in this scope

■解決策

ソースをダウンロードしbase64.ccに以下のラインを追加する。

#import <stdlib.h>

その後、以下のコマンドでインストールする。

npm install dir/

最後のスラッシュは必要なかったかもしれない。

WebSocketsに触ってみる

WebSocketsでWebアプリケーションは次の段階を迎える。node.jsと共に触ってみる。((((o゚▽゚)o)))

■準備

npmをインストールする

node.jsのパッケージ管理システムである。

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

node-websocket-serverをインストールする

WebSocketsに適した感じのサーバだ。

npm install http://github.com/miksago/node-websocket-server/tarball/master

expressをインストールする

node.jsのフレームワークとでもいうんだろうか。

npm install http://github.com/visionmedia/express/tarball/master

■サーバー側(server.js)

サーバーを作る。→各種イベントをセットアップする。→特にデータが来たらブロードキャストする。といった流れ。

var sys     = require('sys');
var ws      = require('websocket-server');
var express = require('express');
var app     = express.createServer();
var coord   = [];

// set up the websokets server
app.configure(function(){
    app.use(express.staticProvider(__dirname + '/public'));// 公開ディレクトリをセットアップ
});
var server = ws.createServer({"server" : app});

// set up the events of server
server.addListener(
    'listening',
    function(){
        sys.log('listening');
    }
);
server.addListener(
    'connection',
    function(conn){
        sys.log('connection');
        if(coord.length > 0){// 最初の接続で最後の位置をクライアントに反映させる
            conn.send(coord[coord.length - 1]);
        }
        conn.addListener(
            'message',
            function(message){
                coord.push(message);
                server.broadcast(message);
            }
        );
    }
);
server.addListener(
    'close',
    function(conn){
        sys.log('close');
    }
);
server.listen(80);

■クライアント側(/public/client.js)

ドラッグ&ドロップで座標を送信する感じ。データが飛んできたら逆に座標をセットする感じ。ちなみにGoogle Chromeでしか動作テストはしてない。

$(function(){
    var conn = new WebSocket("ws://192.168.11.3/");
    conn.onmessage = function(e){// データが飛んできた時
        var data = JSON.parse(e.data);
        $('p#box').css({
            "left" : data['left'],
            "top"  : data['top']
        });
    }
    $('p#box').draggable({
        "cursor" : "pointer",
        "drag"   : function(e, ui){// ドラッグした時
            if(conn){
                conn.send(JSON.stringify(ui.position));
            }
        }
    });
});

html(/public/index.html)

まぁコンナ感じ。logo.pngにはchromiumのロゴを使用させていただいた。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>title</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.7.3/jquery-ui.min.js"></script>
<script type="text/javascript" src="client.js"></script>
</head>

<body>
<p id="box"><img src="./img/logo.png" /></p>
</body>
</html>

■完成

サーバーを起動。

node server.js

キタ━━━━━━(゚∀゚)━━━━━━ !!!!!

websockets

片方を動かすともう片方に反映される!

node.jsを触ってみる

既に開いてるポートを使いたいので、apacheを止めて80番のポートを使用し実験する。

/etc/init.d/httpd stop

以下のコードを使用する。オフィシャルサイトに載ってるのをちょっと改造しただけのもの。(・・。)ゞ

var http = require('http');
http.createServer(function(req, res){
    res.writeHead(200, {"Content-Type": "text/plain"});
    res.end('Hello World\n');
}).listen(80, "192.168.11.3");
console.log('Server running at http://192.168.11.3:80/');
node sample.js

ブラウザで(http://192.168.11.3/)アクセスしてみる。

Hello World

ひゃー。通じる喜び。シンプルな素晴らしさ。ヾ(@^▽^@)ノあー素敵。