@blog.justoneplanet.info

日々勉強

Objective-Cで乱数を生成する

以下の方法で5~10までの乱数が取得できる。

int rand = 5.0 + arc4random() % 6;

上述では初期化と生成がセットになっている。

■おまけ

C由来の方法も使用できる。

srand([[NSDate date] timeIntervalSinceReferenceDate]);
int rand = rand();

初期化にはユニークな値を使用する。

androidでローカルファイルにアクセスする

■読込

try {
    InputStream inputStream = openFileInput(FILENAME);
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    String str;
    while((str = reader.readLine()) != null) {
    }
    reader.close();
    inputStream.close();
}
catch (FileNotFoundException e) {
    e.printStackTrace();
}
catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}
catch (IOException e) {
    e.printStackTrace();
}

■書込

try {
    OutputStream outputStream = openFileOutput(FILENAME, MODE_PRIVATE);
    PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"));
    printWriter.append(str);
    printWriter.close();
}
catch (FileNotFoundException e) {
    e.printStackTrace();
}
catch (UnsupportedEncodingException e) {
    e.printStackTrace();
}

非同期

巨大なファイルの場合に同期読込がANRを発生させることがある。従って、AsyncTaskを使って非同期にファイルの読込書込を行う。

AsyncFileReadTask.java
package info.justoneplanet.android.sample.localfile;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;

import android.content.Context;
import android.os.AsyncTask;

public class AsyncFileReadTask extends AsyncTask<String, String, String> {
    
    private Context mContext;
    private Observer mObserver;

    AsyncFileReadTask(Context context, Observer observer) {
        mContext = context;
        mObserver = observer;
    }

    @Override
    protected String doInBackground(String... params) {
        String data = "";
        try {
            InputStream inputStream = mContext.openFileInput(params[0]);
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            String str;
            while((str = reader.readLine()) != null) {
                data += str;
            }
            reader.close();
            inputStream.close();
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        return data;
    }
    
    @Override
    protected void onPostExecute(String result) {
        mObserver.onReadFile(result);
    }
    
    public interface Observer {
        public void onReadFile(String result);
    }
}
AsyncFileWriteTask.java
package info.justoneplanet.android.sample.localfile;

import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;

import android.content.Context;
import android.os.AsyncTask;

public class AsyncFileWriteTask extends AsyncTask<String, String, Boolean> {

    private Context mContext;
    private Observer mObserver;

    AsyncFileWriteTask(Context context, Observer observer) {
        mContext = context;
        mObserver = observer;
    }
    
    @Override
    protected Boolean doInBackground(String... params) {
        try {
            OutputStream outputStream = mContext.openFileOutput(params[0], Context.MODE_PRIVATE);
            PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, "UTF-8"));
            printWriter.append(params[1]);
            printWriter.close();
            return Boolean.TRUE;
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return Boolean.FALSE;
    }
    
    @Override
    protected void onPostExecute(Boolean result) {
        mObserver.onWriteFile(result);
    }
    
    public interface Observer {
        public void onWriteFile(Boolean result);
    }
}
クライアントコード

上述の2つのクラスは以下のように使用する。

package info.justoneplanet.android.sample.localfile;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.EditText;

public class LocalFileActivity extends Activity implements AsyncFileReadTask.Observer, AsyncFileWriteTask.Observer {
    
    private static final String FILENAME = "sample.txt";
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        // ファイルの読込
        AsyncFileReadTask task = new AsyncFileReadTask(getApplicationContext(), this);
        task.execute(FILENAME);
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        
        EditText editText = (EditText) findViewById(R.id.edit_text);
        String str = editText.getText().toString();
        if (str.length() == 0) {
            deleteFile(FILENAME);
            return;
        }
        
        // ファイルの書込
        AsyncFileWriteTask task = new AsyncFileWriteTask(getApplicationContext(), this);
        task.execute(FILENAME, str);
    }

    @Override
    public void onReadFile(String result) {
        EditText editText = (EditText) findViewById(R.id.edit_text);
        editText.append(result);
    }

    @Override
    public void onWriteFile(Boolean result) {
        Log.e("file", "written");
    }
}

■削除

deleteFile(FILENAME);

androidでアプリ内課金を使う

■必要なもの

  • Android Marketの開発者アカウント
  • Google checkoutの財務情報登録

■サンプルアプリのダウンロード

以下のような手順で行う。

  1. AVD Managerを起動
  2. Avalilable packages
  3. Third party Add-ons
  4. Google Inc.
  5. Google Market Billing package, revision 1をチェック

[sdkdir]/extras/google/market_billingに保存される。

■公開鍵の追記

  • プロフィールの編集でライセンスとアプリ内課金の公開鍵をコピーする
  • src/com/example/dungeons/Security.javaにペーストする
String base64EncodedPublicKey = "your public key here";

■プロジェクトのインポート

プロジェクトで右クリックし新規プロジェクトを選択し、androidのプロジェクトを新たに作る。

※但し、既存のソースから新規作成するようにしてください。

■パッケージ名の変更

com.exampleというパッケージ名は使用できないのでパッケージ名を変更する。

  1. パッケージ・エクスプローラのウィンドウでパッケージを選択する
  2. eclipseメニューのリファクタリングから名前変更する
  3. Manifestのパッケージ名が変わらないようなのでファイルを開いて修正する

手動でパッケージ名の変更をおこなう場合

以下の項目を変更する。

  • ディレクトリ名
  • マニフェストファイル
  • package
  • import

■デバッグ

以下の部分を

public static final boolean DEBUG = false;

以下のように変更する。

public static final boolean DEBUG = true;

■マーケットへの登録

通常のアプリと同様に署名してマーケットにアップする必要がある。

但し、公開せずに下書きのまま保存する。

■アイテムの登録

ダッシュボードのアプリケーションのタイトルの下の「アプリ内サービス」からアイテムを登録する。

サンプルアプリのアプリ内サービス IDはそれぞれ「sword_001」と「potion_001」で公開する必要がある。

■実機でテスト

署名されているアプリケーションからリクエストをする必要があるので、以下のコマンドをクライアントで実行しアップロードしたapkファイルを直接端末にインストールする。

./adb install Dungeous.apk

※USBから直接インストールすると、RESULT_DEVELOPER_ERRORというエラーが返ってきて正しくテストできない。

参考

Cのスコープ

あんまり書くこともない気がする。

int counter = 0;// グローバル

int main(int argc, char *argv[])
{
    int counter = 5;// ローカル
    printf("%d", counter);
}

ちなみにブロックスコープはちゃんと存在する。

int main(int argc, char *argv[])
{
    int counter = 5;// ローカル
    printf("%d", counter);
    if(1){
        int x = 10;
    }
    // この空間ではxは不可視
}

■static変数

関数に紐づいた静的な変数で呼び出しごとに初期化されない変数。

int count()
{
    static int counter = 0;
    return counter++;
}
int main(int argc, char *argv[])
{
    printf("%d", count());// 0
    printf("%d", count());// 1
    printf("%d", count());// 2
}

緊急地震速報 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までお願いいたします。

ディフィー・ヘルマン鍵共有

盗聴の可能性のある通信路を使って、暗号鍵の共有を可能にする暗号プロトコルである。

#python
g = 3;
p = 2147483647;

#A
a = 11;
A = g**a % p;

#B
b = 10;
B = g**b % p;

#A
B**a % p;
#B
A**b % p;

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();

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

■参考

UITextViewをEditableにしてplaceholderを実装する

UITextFieldでは以下のようにすることでプレースホルダーを使用することができる。

UITextField *field = [[UITextField alloc] init];
[field setFrame:CGRectMake(10, 120, 190, 35)];
[field setPlaceholder:@"入力してください"];

複数行のテキスト入力欄が必要な場合はUITextViewをEditableにして対応するが、上述のような手法でプレースホルダーを用いることはできない。

■実装

UIPlaceHolderTextView.h

#import "UIPlaceHolderTextView.h"

@implementation UIPlaceHolderTextView
@synthesize placeholder;
@synthesize placeholderColor;
@synthesize placeholderLabel;

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [placeholderLabel release]; placeholderLabel = nil;
    [placeholderColor release]; placeholderColor = nil;
    [placeholder release]; placeholder = nil;
    [super dealloc];
}

- (void)awakeFromNib {
    [super awakeFromNib];
    [self setPlaceholder:@""];
    [self setPlaceholderColor:[UIColor lightGrayColor]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
}

- (id)initWithFrame:(CGRect)frame {
    if((self = [super initWithFrame:frame])) {
        [self setPlaceholder:@""];
        [self setPlaceholderColor:[UIColor lightGrayColor]];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
    }
    return self;
}

// 表示テキストに変更があった時
- (void)textChanged:(NSNotification *)notification {
    if([[self placeholder] length] == 0) {
        return;
    }
    
    if([[self text] length] == 0) {
        [[self viewWithTag:999] setAlpha:1];
    }
    else {
        [[self viewWithTag:999] setAlpha:0];
    }
}

- (void)setText:(NSString *)text {
    [super setText:text];
    [self textChanged:nil];
}

// drawRect時にplaceholderのUILabelを準備してViewに追加する
- (void)drawRect:(CGRect)rect {
    if([[self placeholder] length] > 0) {
        if (placeholderLabel == nil) {
            placeholderLabel = [[UILabel alloc] initWithFrame:CGRectMake(8,8,self.bounds.size.width - 16,0)];
            placeholderLabel.lineBreakMode = UILineBreakModeWordWrap;
            placeholderLabel.numberOfLines = 0;
            placeholderLabel.font = self.font;
            placeholderLabel.backgroundColor = [UIColor clearColor];
            placeholderLabel.textColor = self.placeholderColor;
            placeholderLabel.alpha = 0;
            placeholderLabel.tag = 999;
            [self addSubview:placeholderLabel];
        }
        
        placeholderLabel.text = self.placeholder;
        [placeholderLabel sizeToFit];
        [self sendSubviewToBack:placeholderLabel];
    }
    
    if([[self text] length] == 0 && [[self placeholder] length] > 0 ) {
        [[self viewWithTag:999] setAlpha:1];
    }
    
    [super drawRect:rect];
}
@end

クライアントコード

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

UIPlaceHolderTextView *view = [[UIPlaceHolderTextView alloc] init];
[view setPlaceholder:@"(^o^)"];

■参考

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モジュールとして動作させた時のリソースの消費量は明らかに大きいようだ。