ソケットプログラミング - TCP/IPサーバの例

RubyなどのLLでソケット通信のプログラムを書いていると、あまりにも簡単に書けすぎて、ついついC言語でのソケット通信の書き方を忘れてしまいます。

ということで、おさらいのためにシンプルなechoサーバをCで書いてみました。おおまかな説明はソース中にコメントで書いてありますので省きます。

server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <sys/socket.h>
#include <netdb.h>

#define BUFFER_SIZE 512
#define READ_TIMEOUT 10000      // 10 seconds

/* get*info系のエラーメッセージを出力する */
void put_gaierror(const char *tag, int error_code)
{
    // errnoにエラーが格納されている場合もある
    if (error_code == EAI_SYSTEM) {
        perror(tag);
    } else {
        fprintf(stderr, "%s: %s\n", tag, gai_strerror(error_code));
    }
    return;
}

/* アドレス構造体の情報を可読化して出力する */
void print_nameinfo(const char *tag, const struct sockaddr *addr,
        socklen_t addrlen)
{
    char host[NI_MAXHOST];
    char serv[NI_MAXSERV];
    int error_code;

    // ホスト名もサービス名も逆引きしない
    error_code = getnameinfo(addr, addrlen, host, sizeof(host),
            serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV);
    if (error_code != 0) {
        put_gaierror("getnameinfo", error_code);
        strcpy(host, "***");
        strcpy(serv, "***");
    }

    printf("%s %s (%s)\n", tag, host, serv);

    return;
}

/* 接続待ち受けソケットを開く */
int open_socket(const char *service)
{
    struct addrinfo hints;
    struct addrinfo *addr_set;
    struct addrinfo *addr;
    int error_code;
    int sock = -1;
    int on = 1;

    memset(&hints, 0, sizeof(hints));
    hints.ai_flags = AI_PASSIVE;        // 全てのネットワークアドレスで待ち受ける
    hints.ai_family = AF_UNSPEC;        // 任意のプロトコルファミリーで待ち受ける
    hints.ai_socktype = SOCK_STREAM;    // TCPを指定

    error_code = getaddrinfo(NULL, service, &hints, &addr_set);
    if (error_code != 0) {
        put_gaierror("getaddrinfo", error_code);
        return -1;
    }

    // 順番にソケット作成を試行
    for (addr = addr_set; addr; addr = addr->ai_next) {
        sock = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
        if (sock < 0) {
            perror("socket");
            continue;
        }

        // TIME_WAIT時もすぐにbindできるようにSO_REUSEADDRオプションを指定
        if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) {
            perror("setsockopt");       // このエラーはスルー
        }

        if (bind(sock, addr->ai_addr, addr->ai_addrlen) != 0) {
            perror("bind");
            close(sock);
            sock = -1;
            continue;
        }

        if (listen(sock, SOMAXCONN) != 0) {
            perror("listen");
            close(sock);
            sock = -1;
            continue;
        }

        // 成功
        print_nameinfo("Listening", addr->ai_addr, addr->ai_addrlen);
        break;
    }

    freeaddrinfo(addr_set);

    return sock;
}

/* クライアントとの通信のメインルーチン */
int run(int sock)
{
    struct pollfd read_fd;
    int poll_count;
    char buf[BUFFER_SIZE];
    char *write_ptr;
    ssize_t read_len;
    ssize_t write_len;

    memset(&read_fd, 0, sizeof(read_fd));
    read_fd.fd = sock;
    read_fd.events = POLLIN;

    // 単純に延々と受け取ったデータをオウム返しするだけ
    while (1) {
        // 30秒間データ受信を待つ
        poll_count = poll(&read_fd, 1, READ_TIMEOUT);
        if (poll_count < 0) {
            if (errno == EINTR) {       // 割り込みによる中断の場合はやり直し
                continue;
            }
            perror("poll");
            close(sock);
            return -1;
        } else if (poll_count == 0) {  // タイムアウトしたら終了
            printf("Connection timeout.\n");
            break;
        }
        // POLLINを一つpollしているだけなので、特にreventsのチェックはしない

        read_len = read(sock, buf, sizeof(buf));
        if (read_len < 0) {
            if (errno == EINTR) {       // 割り込みによる中断の場合はやり直し
                continue;
            }
            perror("read");
            close(sock);
            return -1;
        } else if (read_len == 0) {     // EOF
            printf("Connection closed.\n");
            break;
        }

        // 面倒なのでしていないが、本当はPOLLOUTもpollすべき
        write_ptr = buf;
        while (read_len > 0) {
            write_len = write(sock, write_ptr, read_len);
            if (write_len < 0) {
                if (errno == EINTR) {   // 割り込みによる中断の場合はやり直し
                    continue;
                }
                perror("write");
                close(sock);
                return -1;
            }
            write_ptr += write_len;
            read_len -= write_len;
        }
    }

    close(sock);

    return 0;
}

/* シグナルによる中断を検出するためのフラグ */
static int _interrupted = 0;

/* シグナルハンドラ内では中断フラグをセットするだけ */
void sigint_action(int signum)
{
    _interrupted = 1;
    return;
}

/* サーバのメインルーチン */
int start_server(int accept_socket)
{
    struct sigaction action;

    // Ctrl+Cで終了するようにシグナルの動作を設定する
    memset(&action, 0, sizeof(action));
    action.sa_handler = sigint_action;
    action.sa_flags = SA_RESETHAND;     // 一度呼ばれたらデフォルト動作に戻す
    if (sigaction(SIGINT, &action, NULL) != 0) {
        perror("sigaction");
        return -1;
    }

    // 終了した子プロセスがゾンビプロセスにならないように設定する
    memset(&action, 0, sizeof(action));
    action.sa_handler = SIG_IGN;
    action.sa_flags = SA_NOCLDWAIT;
    if (sigaction(SIGCHLD, &action, NULL) != 0) {
        perror("sigaction");
        return -1;
    }

    // シグナルで中断されるまでループを続ける
    while (!_interrupted) {
        // sockaddr_storage構造体は
        // 全てのアドレスファミリー用のsockaddr構造体を
        // 収容できるサイズを持つことが保証されているため、
        // プロトコル非依存の通信ではこれを用いる
        struct sockaddr_storage addr;
        socklen_t addrlen;
        int connection_socket;
        pid_t pid;

        // クライアントからの接続待ち受け
        addrlen = sizeof(addr);
        connection_socket = accept(accept_socket, (struct sockaddr*)&addr,
                &addrlen);
        if (connection_socket < 0) {
            if (errno == EINTR) {       // 割り込みによる中断の場合はやり直し
                continue;
            }
            perror("accept");
            break;
        }
        print_nameinfo("Connected from", (struct sockaddr*)&addr, addrlen);

        // クライアントとの通信は別プロセスで処理
        pid = fork();
        if (pid < 0) {
            perror("fork");
            close(connection_socket);
            break;
        } else if (pid == 0) {
            // 子プロセスは接続待ち受けソケットを閉じて
            // メインルーチンを開始する
            // 処理が終わったらプロセス終了
            int status;
            close(accept_socket);
            status = run(connection_socket);
            exit(status);
        }
        close(connection_socket);       // 親プロセスは引き続き接続待ち受け
    }

    if (_interrupted) {
        printf("Interrupted.\n");
    }

    // まだ生きている子プロセスは全部終了させる
    // 自分がSIGTERMで死なないように一回だけ無視させる
    memset(&action, 0, sizeof(action));
    action.sa_handler = SIG_IGN;
    action.sa_flags = SA_RESETHAND;
    if (sigaction(SIGTERM, &action, NULL) != 0) {
        perror("sigaction");
    }
    if (killpg(0, SIGTERM) != 0) {
        perror("killpg");
    }

    return 0;
}

int main(int argc, char **argv)
{
    int sock;
    int status;

    if (argc < 2) {
        printf("usage: %s <service>\n", argv[0]);
        return -1;
    }

    sock = open_socket(argv[1]);
    if (sock < 0) {
        fprintf(stderr, "cannot open socket.\n");
        return -1;
    }

    status = start_server(sock);

    close(sock);

    printf("Done.\n");

    return status;
}

長っ!

しかもこれでもまだポーリングやシグナルの扱いに穴があるので、常時起動させておくサーバ用途なんかには使えません。

ちなみに

似たようなサーバをRubyで書いてみると…。

tcpserver.rb
#!/usr/bin/env ruby
# coding: utf-8

require 'socket'

READ_TIMEOUT = 10

if ARGV.empty?
  puts "Usage: #{$0} <service>"
  exit -1
end

server = TCPServer.open(ARGV.shift)
addr = server.addr
puts "Listening #{addr[3]} (#{addr[1]})"

begin
  loop do
    Thread.start(server.accept) do |io|
      peer = io.peeraddr
      puts "Connected from #{peer[3]} (#{peer[1]})"
      begin
        loop do
          reads, writes, excepts = IO.select([io], nil, nil, READ_TIMEOUT)
          raise 'Connection timeout.' unless reads
          raise 'Connection closed.' unless line = io.gets
          io.write line
        end
      rescue RuntimeError
        puts $!.message
      end
      io.close
    end
  end
rescue Interrupt
  puts 'Interrupted.'
end

server.close
puts 'Done.'

こんなに短く書けちゃいます。*1

または、

#!/usr/bin/env ruby
# coding: utf-8

require 'gserver'

READ_TIMEOUT = 10

if ARGV.empty?
  puts "Usage: #{$0} <service>"
  exit -1
end

class EchoServer < GServer
  def serve(io)
    loop do
      reads, writes, excepts = IO.select([io], nil, nil, READ_TIMEOUT)
      raise 'Connection timeout.' unless reads
      raise 'Connection closed.' unless line = io.gets
      io.write line
    end
  rescue RuntimeError
    puts $!.message
  end
end

server = EchoServer.new(ARGV.shift)
server.audit = true
server.start
begin
  sleep
rescue Interrupt
  puts 'Interrupted.'
end
server.stop
puts 'Done.'

こういう書き方もあります。

個人的には後者のほうが読みやすいかな?と思いますが、このレベルの違いなら好みで選んでもいいかもしれませんね。

*1:マルチプロセス/マルチスレッドの違いなどの細かな相違点はありますが。

Rubyのcase式と===演算子について

前フリ

奥様、知ってらした?Rubycase式ってすっごくパワフルなんですって!単なる同値判定で振り分けるだけじゃなくて、例えばこんなこともできちゃうらしいんですのよ!

case gets.chomp
when /おはよう/
  puts 'おはようございます、お目覚めはいかがですか?'
when /こんにちは/
  puts 'こんにちは、いい天気ですね。'
when /こんばんは/
  puts 'こんばんは、遅くまでお疲れ様です。'
else
  puts '何か御用でしょうか?'
end

あたくし、今までcase式の実力を見くびっていましたわ…!

ラク

どうしてこんなことができるのかというと、リファレンスマニュアルにも書いてあることなのですが、case式は===演算子を使ったif〜elsif〜end式と等価な処理を行うように実装されているからなんですね。

要は、この===演算子がキモなわけです。

/おはよう/ === 'おはようございます'  #=> true

みたいな式が成り立つからこそ、上述のようなcase式が書けちゃうんですね。

===演算子の正体

他の言語を使っていると、ついつい「===演算子は==演算子よりも更に厳密な同値判定演算子でしょ?」なんて思ってしまいますが、ところがどっこい、Rubyの場合はむしろ===演算子のほうが柔軟な場合もあり得るのです。

Object#===の説明を見てみるとわかりますが、基本的にこの===演算子は、サブクラスで再定義されていない限り、単にObject#==を呼び出すだけなんですね。

で、実際に===演算子を再定義している組み込みクラスにどんなものがあるかというと、

Module
kind_of?を呼んでいるだけ
Proc
callの別名
Range
include?を呼んでいるだけ
Regexp
=~matchとほぼ同じ*1

の4つのみとなっており、それぞれ上記のような実装となっています。いずれも「同値判定」とはやや趣の異なる処理ですね。

つまり、少なくとも組み込みクラスの範囲において、===演算子が==演算子よりも厳密な同値判定であるケースは皆無というわけです。Rubyにおける===演算子は、他の言語における===演算子とは全く別物と割り切るべきである、と言えます。

===は非可換な子

もう一つ気をつけるべき点は、これは===演算子に限ったことではないのですが、Rubyではこの辺の二項演算子がほとんどメソッドとして実装されている関係上、その判定がどのように処理されるかはレシーバに依存します。つまり、「どちらが左辺になるかによって結果が変わることがある」のですね。

例えば先ほどの

/おはよう/ === 'おはようございます'  #=> true

は、

'おはようございます' === /おはよう/  #=> false

と書いてしまうと成り立ちません。何故ならば、レシーバ、つまり左辺に位置しているオブジェクトはStringクラスのインスタンスであり、Stringクラスの===演算子は==演算子、それ即ち同値判定だからです。

活用してみよう

case式と===演算子のこのような性質を知っていれば、例えばオブジェクトのクラスによって処理を振り分けるコードも

case obj
when Array
  puts '配列ですね!'
when Hash
  puts 'ハッシュですね!'
when String, Symbol
  puts '文字列かシンボルですね!'
when Numeric
  puts '数値ですね!'
else
  puts 'わかりません!'
end

というように書けますし、例えば時間帯によって処理を振り分けるコードも

case Time.now.hour
when (6..8)
  puts 'おはようございます!'
when (9..11)
  puts '遅刻しませんでしたか!?'
when (12..13)
  puts 'お昼ですね!カレーですか!?'
when (14..16)
  puts 'シエスタである。邪魔をしてはいけない。'
when (17..18)
  puts 'お疲れ様です!さっさと帰りましょう!'
when (19..20)
  puts '夕飯はカレーですね!'
when (21..23)
  puts 'さっさと歯磨いて寝やがってください!'
when (0..5)
  puts '深夜アニメ好きですね!'
end

みたいに書けます。if〜elsif〜endで書くよりもだいぶすっきり書けますし、意味もわかりやすいですね。

更なる活用

独自に===演算子を定義してやれば、さらにパワフルな振り分けだってできてしまいます。

ただし、その場合に注意しなければならないのは、「case式ではwhen節に指定されたオブジェクトをレシーバとして===メソッドが呼び出される」という点です。

つまり、

case foo
when bar
  puts 'bingo!'
else
  puts 'oops.'
end

というコードは

if bar === foo
  puts 'bingo!'
else
  puts 'oops.'
end

と同義です。

前述の通り===演算子は非可換ですので、この場合はfooオブジェクトではなく、barオブジェクトのクラスに===演算子を定義してやる必要があります。

ついでに==演算子

せっかくなので==演算子の正体も調べてみましょう。

この==演算子という奴は、言語によって同値判定だったり同一判定だったりしますが、Rubyにおける==演算子の機能は…これもまた===演算子と同じく、「レシーバとなるオブジェクトのクラスに依存する」ことになります。というのも、==演算子は===演算子以上に各クラスで詳細に定義されているんですね。

例えば、一番大元となるObject#==は、C言語レベルでのポインタの比較、つまり完全な同一判定です。しかし実際は殆どのクラスで独自に再定義されていますので、Object#==が使用されるケースはそう多くはありません。

それぞれの==演算子が実際に何をやっているかはリファレンスマニュアル(もしくはソースコード)を見てみないとわかりませんが、大抵の場合は

[ 1, 2, 3 ] == [ 1, 2, 3 ]  #=> true
{ :a => 5 } == { :a => 5 }  #=> true

のように、「同値判定」として自然な結果になるように実装されています。

==演算子に関しては、異なるクラス同士で比較してtrueになり得るパターンはそうないと思いますし*2、===演算子に比べれば気をつけなければいけないケースも少なそうです。

*1:戻り値がtrueもしくはfalseのみ

*2:Numericのサブクラス同士の比較くらいでしょうか?

Rubyのメソッド呼び出し順

※以下の記述は ruby 1.9.1-p129 を前提としています。

Ruby逆引きハンドブックの「SECTION-40 呼び出されるメソッドの決定方法」(p.145)が大変参考になったので、復習を兼ねて実際に試してみました。

基本的にクラスの継承ツリーを辿っていくわけですが、Rubyの場合は特異メソッドやモジュールのMix-inなどが絡んでくるため、そのあたりの関係をちゃんと認識しておく必要があります。

インスタンスメソッド

次のようなコードを書いて実行してみます。

instance_method.rb
#!/usr/bin/env ruby
# coding: utf-8

class Object
  def foo
    [ "Objectクラスのメソッド" ]
  end
end

module SuperIncludeModule
  def foo
    [ "親クラスでincludeしたモジュールのメソッド" ] + super
  end
end

class SuperClass
  include SuperIncludeModule

  def foo
    [ "親クラスのメソッド" ] + super
  end
end

module SubIncludeParentModule
  def foo
    [ "自クラスでincludeしたモジュールの親モジュールのメソッド" ] + super
  end
end

module SubIncludeModule
  include SubIncludeParentModule

  def foo
    [ "自クラスでincludeしたモジュールのメソッド" ] + super
  end
end

class SubClass < SuperClass
  include SubIncludeModule

  def foo
    [ "自クラスのメソッド" ] + super
  end
end

bar = SubClass.new

def bar.foo
  [ "特異メソッド" ] + super
end

module SubExtendModule
  def foo
    [ "extendしたモジュールのメソッド" ] + super
  end
end
bar.extend SubExtendModule

puts bar.foo
実行結果
$ ruby instance_method.rb 
特異メソッド
extendしたモジュールのメソッド
自クラスのメソッド
自クラスでincludeしたモジュールのメソッド
自クラスでincludeしたモジュールの親モジュールのメソッド
親クラスのメソッド
親クラスでincludeしたモジュールのメソッド
Objectクラスのメソッド

includeextend はメソッド定義を上書きするわけではなく、継承ツリーの合間に挟まっていく形になるのだということがわかります。

ちなみに、Rubyではトップレベルで定義された関数は、Objectクラスのプライベートメソッド*1となりますので、上のコードの

class Object
  def foo
    [ "Objectクラスのメソッド" ]
  end
end

この部分を

def foo
  [ "トップレベルのメソッド" ]
end

こう書き換えると、

$ ruby instance_method.rb 
特異メソッド
extendしたモジュールのメソッド
自クラスのメソッド
自クラスでincludeしたモジュールのメソッド
自クラスでincludeしたモジュールの親モジュールのメソッド
親クラスのメソッド
親クラスでincludeしたモジュールのメソッド
トップレベルのメソッド

こんな感じの実行結果になります。

クラスメソッド

クラスメソッドの場合はもう少し複雑になります。細かな解説はRuby逆引きハンドブックのp.147あたりを読んで頂くとして、実際にコードを書いて実行してみます。

class_method.rb
#!/usr/bin/env ruby
# coding: utf-8

class Object
  def foo
    [ "Objectクラスのメソッド" ]
  end
end

class Module
  def foo
    [ "Moduleクラスのメソッド" ] + super
  end
end

class Class
  def foo
    [ "Classクラスのメソッド" ] + super
  end
end

class SuperClass
  def self.foo
    [ "親クラスのクラスメソッド" ] + super
  end
end

class SubClass < SuperClass
  def self.foo
    [ "自クラスのクラスメソッド" ] + super
  end
end

module SuperExtendParentModule
  def foo
    [ "親クラスをextendしたモジュールの親モジュールのメソッド" ] + super
  end
end

module SuperExtendModule
  include SuperExtendParentModule

  def foo
    [ "親クラスをextendしたモジュールのメソッド" ] + super
  end
end
SuperClass.extend SuperExtendModule

module SubExtendModule
  def foo
    [ "自クラスをextendしたモジュールのメソッド" ] + super
  end
end
SubClass.extend SubExtendModule

puts SubClass.foo
実行結果
$ ruby class_method.rb 
自クラスのクラスメソッド
自クラスをextendしたモジュールのメソッド
親クラスのクラスメソッド
親クラスをextendしたモジュールのメソッド
親クラスをextendしたモジュールの親モジュールのメソッド
Classクラスのメソッド
Moduleクラスのメソッド
Objectクラスのメソッド

ここで注目したいのは SuperClass.extendSuperClass 及び SubClass の定義の後に実行している部分で、ここからも「継承ツリーに継ぎ足していく形になるので定義の順番は関係ない」のだということがわかります。

複雑な例

以下のようなコードを考えてみます。

complex_mixin.rb
#!/usr/bin/env ruby
# coding: utf-8

def foo
  [ "トップレベル" ]
end

module ParentModule
  def foo
    [ "親モジュール" ] + super
  end
end

class SuperClass
  include ParentModule

  def foo
    [ "親クラス" ] + super
  end
end

module ChildModule
  include ParentModule

  def foo
    [ "子モジュール" ] + super
  end
end

class SubClass < SuperClass
  include ParentModule
  include ChildModule

  def foo
    [ "自クラス" ] + super
  end
end

bar = SubClass.new

bar.extend ParentModule

puts bar.foo

ParentModule が、

  1. SuperClass でincludeされている
  2. ChildModule でincludeされている
  3. SubClass でincludeされている
  4. SubClassのインスタンス をextendしている

と、都合4回使われています。この場合 "親モジュール" がどの時点で表れるのか、一瞬考え込んでしまいますが、実行結果は

$ ruby complex_mixin.rb 
自クラス
子モジュール
親クラス
親モジュール
トップレベル

と、特におかしなところもない、自然な結果になってくれます。

Module#includeObject#extend が、同じモジュールが指定された場合は2回目以降を無視してくれるためですね。

Module#ancestors

Module#ancestors メソッドを使うと、上述のメソッド探索順を簡単に確認することができます。

例えば、先ほどの instance_method.rb の末尾、

puts bar.foo

puts bar.class.ancestors

と書き換えて実行すると、

$ ruby instance_method.rb 
SubClass
SubIncludeModule
SubIncludeParentModule
SuperClass
SuperIncludeModule
Object
Kernel
BasicObject

と、大体似たような結果を得ることができます。

ただし、見てわかる通り、ここには 特異クラス は表示されないため、完全な探索経路が得られるわけではないようです。

Ruby逆引きハンドブック

Ruby逆引きハンドブック

*1:レシーバを指定できない=関数形式でしか呼び出せないメソッド

TwitterFoxのパフォーマンス改善

(2009/06/26追記)TwitterFoxのバージョンアップによってこの問題は解消しました。1.8.2以降であれば下記の修正は必要ありません。

※以下の内容は、私の中途半端な知識に基づいた適当な改造となっております。この内容をそのまま適用した場合、何らかの問題が発生するかもしれません。そのあたりは自己責任ということでお願い致します。
※間違っている部分へのツッコミは大歓迎です。というか、是非よろしくお願い致します。

前置き

TwitterFox、便利ですよね。私みたいな、それほどヘビーでもないけど、完全にライトなわけでもない…という中途半端なついたったー(ついったらー?)には、非常にお手軽でありがたいツールです。

そのTwitterFoxが2009/06/11に1.8にバージョンアップし、見た目的にも内部的にも大幅に変更が加えられました。*1

基本的には歓迎できる改良点ばかりなのですが、一つだけ、どうしても気になる点が…。

タイムラインの取得時に、今まで感じていなかった「引っかかり」のようなものを感じるようになってしまったんですね。私はよくニコニコ動画を見ているのですが、TwitterFoxが新しいステータスを受信するたびに動画再生が一瞬停止してしまったりして、正直ちょっとストレスを感じるようになってしまいました。

この機会に他のツールを探してみる、という選択肢もあったのですが、それなりにTwitterFoxを気に入っていますし、ボトルネックを探して自分で解決する方法をとってみることにしました。

原因

ここまでで既に前置きが長くなってしまいましたので…途中経過はばっさりと省略して。

結論から言うと、DB(SQLiteファイル)への書き込み処理がボトルネックとなっていた模様です。

components/nsTwitterFox.js の683行目から始まる TwitterNotifier#retrieveTimeline 、及び同ファイルの前半部分に定義されている User , Status, DirectMessage 各クラスの insertIntoDB メソッドを追ってみるとわかるのですが、どうやら

  • ステータスが1つ更新される度に3つのINSERT文が実行される
  • それらはトランザクションに纏められておらず、バラバラに実行される

という状態になっているらしいんですね。

トランザクションの外にあるINSERT文は、それ自体が一つの小さなトランザクションとして処理されますので、例えば1度の更新で20のステータスを受信した場合、20*3=60回ものトランザクションが実行されることになります。

さすがにそれでは重くもなりますよね…。

対応

ということで、やっつけ対応ですが、 TwitterNotifier#retrieveTimeline の中で msg.insertIntoDB を呼び出しているforループ全体を db.exec("BEGIN TRANSACTION");db.exec("COMMIT TRANSACTION"); で囲ってやります。

こんな感じ。

  retrieveTimeline: function(obj, req, method) {
    if (obj) {
      // Added 'unread' flag if the message is new in any of stored messages
      this.log("Get " + obj.length + " " + method);
      db.exec("BEGIN TRANSACTION"); // ← 追加
      for (var i in obj) {

        var msg;
        var result;

        if (method == "messages") {
          msg = new DirectMessage(obj[i]);
          result = msg.insertIntoDB(this._accounts[this._user].id);
        }
        else {
          msg = new Status(obj[i], method);
          result = msg.insertIntoDB(this._accounts[this._user].id);
        }
        msg.type = method;
        if (result) {
          this._unread[method].push(msg.id);
          this._newMessages.push(msg);
        }
      }
      db.exec("COMMIT TRANSACTION"); // ← 追加
    }
  },

これで何個ステータスを受信しようが1つのトランザクションで纏めて処理されますので、処理にかかる時間は最小限に抑えられるはずです。

実際に私の環境では、この変更を加えて以降、今のところ「引っかかり」は感じていません。やったねたえちゃん!

その他の変更

私版TwitterFoxでは、他にも個人的な好みにより、以下の修正が加えてあります。

components/nsTwitterFox.js の14行目

var MAX_STORED_MESSAGES = 40;

var MAX_STORED_MESSAGES = 100;

に変更。

更に、同じく components/nsTwitterFox.js の555〜556行目

      var count = this._unread[type].length;
      if (count < 20) count = 20;

      var count = this._unread[type].length + 20;
      if (count < MAX_STORED_MESSAGES) count = MAX_STORED_MESSAGES;

に変更。

見ていただければわかるかと思いますが、通常は最大100件、未読数が80件以上ある場合は未読数+20件まで一度に表示できるように変更してあります。

このあたりはタイムラインの速さによって色々調整してみるのもいいかもしれません。

パッチ

ここまでの変更をパッチに纏めると、こんな感じになります。

diff -u -r twitterfox-1.8.1-fx/components/nsTwitterFox.js twitternotifier@naan.net/components/nsTwitterFox.js
--- twitterfox-1.8.1-fx/components/nsTwitterFox.js      2009-06-10 09:40:18.000000000 +0900
+++ twitternotifier@naan.net/components/nsTwitterFox.js 2009-06-13 15:42:14.000000000 +0900
@@ -11,7 +11,7 @@
 var CLASS_NAME = "Twitter Notifier";
 var CONTRACT_ID = "@naan.net/twitternotifier;1";
 
-var MAX_STORED_MESSAGES = 40;
+var MAX_STORED_MESSAGES = 100;
 
 var TWEET_FRIENDS   = 0;
 var TWEET_MENTIONS  = 1;
@@ -552,8 +552,8 @@
     var type = obj.type;
     if (this._accounts[this._user]) {
       var ret = {};
-      var count = this._unread[type].length;
-      if (count < 20) count = 20;
+      var count = this._unread[type].length + 20;
+      if (count < MAX_STORED_MESSAGES) count = MAX_STORED_MESSAGES;
       if (type == "messages") {
         ret.msgs = this.restoreMessagesFromDB(this._accounts[this._user].id, count);
       }
@@ -684,6 +684,7 @@
     if (obj) {
       // Added 'unread' flag if the message is new in any of stored messages
       this.log("Get " + obj.length + " " + method);
+      db.exec("BEGIN TRANSACTION");
       for (var i in obj) {
 
         var msg;
@@ -703,6 +704,7 @@
           this._newMessages.push(msg);
         }
       }
+      db.exec("COMMIT TRANSACTION");
     }
   },
 

このパッチを %APPDATA%\Mozilla\Firefox\Profiles\(プロファイル名)\extensions\twitternotifier@naan.net\components\nsTwitterFox.js に対して適用すればOKです。

快適なTwitterFoxライフをお楽しみ下さい!

ついでに

1.8.1になってデフォルトフォントサイズが小さくなったようですので、見た目が大きく変わってちょっとギョっとするかもしれませんが、普通に設定画面*2からフォントサイズの変更ができますので、そこで大きめに設定してやればOKです。1.8以前はフォントサイズ12相当だった模様なので、私は12に設定しておきました。

*1:翌日06/12に早速1.8.1へのマイナーバージョンアップがありましたが、この記事で述べている範囲のことは1.8.1でもそのまま適用できます。

*2:TwitterFoxのアイコンを右クリック

WordPressプラグインの作り方 〜アクション編〜

以下の記述は WordPress 2.8 日本語版 を前提としています。

WordPressプラグインの作り方メモ、その2。

今回は、WordPressに用意されている2種類のフックのうちのもう一方、アクションを利用したプラグインを作成します。アクションフックを利用すると、任意の場所で任意の文字列を出力したり、他にも色々できたりします。

作成するプラグインですが、少し前にIE6で閲覧すると「アナログ」と表示するCSSが話題になりましたね。あれをパクって…そのままというのもアレなので、せっかくですから「アナログ」の代わりに「アナロ熊」を表示するようにしてみます。

こんな感じ。

ie6-analoguma.php
<?php
/*
Plugin Name: IE6 Analoguma
Plugin URI: http://d.hatena.ne.jp/shibason/20090612/1244799460
Description: IE6でアクセスされた際に、ページ右上にアナロ熊を表示します。
Version: 1.0
Author: shibason
Author URI: http://d.hatena.ne.jp/shibason/
*/

function add_analoguma_css() {
  $url = get_bloginfo('wpurl') . '/wp-content/plugins/ie6-analoguma/style.css';
  ?>
  <link rel="stylesheet" type="text/css" href="<?php echo $url; ?>" />
  <?php
}
add_action('wp_head', add_analoguma_css);

function add_analoguma_js() {
  $url = get_bloginfo('wpurl') . '/wp-content/plugins/ie6-analoguma/script.js';
  ?>
  <script type="text/javascript" src="<?php echo $url; ?>"></script>
  <?php
}
add_action('wp_footer', add_analoguma_js);
?>
script.js
if (/*@cc_on!@*/false && !window.XMLHttpRequest) {
  window.attachEvent('onload', function() {
    var div = document.createElement('div');
    div.setAttribute('id', 'analoguma');
    div.innerHTML = '      ┼╂┼<br />' +
        '    ∩_┃_∩<br />' +
        '    | ノ      ヽ<br />' +
        '   /  ●   ● |<br />' +
        '   |    ( _●_)  ミ<br />' +
        '  彡、   |∪|  、`\  <2011年7月24日までに<br />' +
        '  / __ ヽノ /´,>  )   IE6も停波しろクマー<br />' +
        ' (___)   / (_/<br />' +
        '  |       /<br />' +
        '  |  /\ \    アナロ熊<br />' +
        '  | /    )  )<br />' +
        '  ∪    (  \<br />' +
        '        \_)';
    document.body.appendChild(div);
    div.style.top = document.documentElement.scrollTop + 'px';
    window.attachEvent('onscroll', function() {
      div.style.top = document.documentElement.scrollTop + 'px';
    });
  });
}
style.css
@charset "utf-8";

div#analoguma {
  display: block;
  position: absolute;
  top: 0;
  right: 0;
  width: auto;
  height: auto;
  line-height: 1.125;
  margin: 0;
  padding: 1em;
  border: 5px solid #174415;
  background-color: #4DE849;
  color: #000000;
  font-family: "MS Pゴシック","MS PGothic","Mona","mona-gothic-jisx0208.1990-0",sans-serif;
  font-size: 3em;
  text-align: left;
  filter: alpha(opacity=50);
}

メインのPHPファイル・JavaScriptファイル・CSSファイルの三部構成となっています。CSSファイルはトップJavaScriptファイルはボトムで読み込むのが良い、なんていう話もありますのでそんな感じで。

ちなみに、この add_action 関数にも優先度を指定することができるのですが、今回は読み込み順を考慮しなくていいので特に指定していません。

正直この程度の小ネタでいちいちファイルを分割するのは面倒ですが、そこはそれ、あくまでもサンプルということで。

これらのファイルをまとめて ie6-analoguma という名前のディレクトリにぶち込んで、そいつを wordpress/wp-content/plugins に放り込み、「プラグインの管理」画面で有効化します。

IE6で閲覧すると、


うっすらと出てきます。右上っていうかほとんど全画面ですね。

しかも、


スクロールしてもついてきます。

うぜぇ…。

WordPressプラグインの作り方 〜フィルタ編〜

以下の記述は WordPress 2.8 日本語版 を前提としています。

WordPressプラグインの作り方メモ、その1。

WordPressで記事を書く際も「はてな記法」的な独自記法を使いたい…というような要望は、記事本文へフィルタをかけるプラグインを作成することによって簡単に実現できます。*1

こんな感じ。

<?php
/*
Plugin Name: My Content Filter
Plugin URI: http://d.hatena.ne.jp/shibason/20090612/1244781801
Description: 記事本文ではてな記法ライクな独自記法を使用できるようにします。
Version: 1.0
Author: shibason
Author URI: http://d.hatena.ne.jp/shibason/
*/

function my_content_filter($content) {
  /* 小々見出し記法 */
  $patterns[] = '/(^|\n)\*\*\*([^\r\n]*)/';
  $replacements[] = '$1<h5>$2</h5>';
  /* 小見出し記法 */
  $patterns[] = '/(^|\n)\*\*([^\r\n]*)/';
  $replacements[] = '$1<h4>$2</h4>';
  /* 見出し記法 */
  $patterns[] = '/(^|\n)\*([^\r\n]*)/';
  $replacements[] = '$1<h3>$2</h3>';

  /* リンク記法(タイトルあり) */
  $patterns[] = '/\[(http:|https:)([^:\]]*):title=([^\]]*)\]/';
  $replacements[] = '<a href="$1$2" target="_blank">$3</a>';
  /* リンク記法(タイトルなし) */
  $patterns[] = '/\[(http:|https:)([^\]]*)\]/';
  $replacements[] = '<a href="$1$2" target="_blank">$1$2</a>';

  return preg_replace($patterns, $replacements, $content);
}
add_filter('the_content', my_content_filter, 9); 
?>

正直はてな記法とは比較するのもおこがましいようなシンプルなフィルタですが、そこはまぁサンプルですのでご勘弁を。*2

ポイントは add_filter 関数に渡す第三引数です。ここにはそのフィルタの優先度を指定します。この優先度が小さいフィルタほど先に適用されます。

第三引数を渡さなかった場合、デフォルトの 10 が優先度として使われるのですが、そうするとWordPress組み込みのデフォルトフィルタが先に適用されてしまい、改行が br タグに変換されてしまったりと、正規表現をかける上で色々面倒な状態になってしまいますので、明示的に10より小さい値を指定して先にフィルタをかけてやる必要があります。

こいつを my-content-filter.php とかなんとか適当な名前で保存して、 wordpress/wp-content/plugins ディレクトリに放り込み、「プラグインの管理」画面で有効化。

すると


こんな感じに入力した記事本文が


こんな感じで表示されるようになります。

めでたしめでたし。

*1:記法の処理そのものの難易度は別として。

*2:ちなみに、もっときちんとしたはてな記法を使いたい方は、既にプラグインが存在しますので、そちらをお使いください。