サーバの起動
この節ではPOPサーバが起動するまでのコードを解説します。
オプションの解析
最初は順当にプログラムの頭から見ていくことにしましょう。Rubyプログラムには、Cのmain
関数やJavaのMain
クラスのような決まった開始地点はありませんが、私はいつもmain
メソッドを自分で書くことにしています。
では、main
メソッドの冒頭を以下に示します。
def main mode = :inetd port = POPd.default_port parser = OptionParser.new parser.banner = "Usage: #{File.basename($0)} [--standalone] [--port=NUM]" parser.on('-i', '--inetd', 'Enables inetd mode (default)') { mode = :inetd } parser.on('-s', '--standalone', 'Enables standalone server') { mode = :standalone } parser.on('-p', '--port=NUM', "Port number to listen to (default: #{port})") {|num| port = num.to_i } parser.on('--help', 'Prints this message') { puts parser.help exit 0 } begin parser.parse! rescue OptionParser::ParseError => err $stderr.puts err.message $stderr.puts parser.help exit 1 end : : end
このコードではコマンドラインオプションを解析しています。POPdでは、optparseライブラリ(OptionParser
クラス)を使ってコマンドラインオプションを解析します。
OptionParser
でコマンドラインオプションを解析するには、次のような手順を踏みます。
OptionParser.new
メソッドでOptionParser
オブジェクトを作るOptionParser#on
メソッドでコマンドラインオプションを登録するOptionParser#parse!
メソッドで実際にARGVを解析する
OptionParser#on
メソッドにはいろいろと機能があるのですが、上記のコードに登場している基本的な使い方だけマスターしておけばとりあえずは十分でしょう。OptionParser#on
メソッドの主な使用パターンは以下のとおりです。
パターン | 解説 |
parser.on('-i', '説明') { …… } | ショートオプション「-i」を登録する。 第2引数はオプションの説明で、ヘルプメッセージを生成するときに使われる。 |
parser.on('-i', '--inetd', '説明') { …… } | ショートオプション「-i」と、それと同等の効果を持つロングオプション「--inetd」を登録する。第3引数はオプションの説明で、ヘルプメッセージを生成するときに使われる。 |
parser.on('--inetd', '説明') { …… } | ロングオプション「--inetd」を登録する。第2引数はオプションの説明で、ヘルプメッセージを生成するときに使われる。 |
parser.on('--port=NUM', '説明') {|num| …… } | 引数を持つロングオプション「--port」を登録する。引数はブロック引数numに渡ってくる。第2引数はオプションの説明で、ヘルプメッセージを生成するときに使われる。 |
また、OptionParser#banner=
メソッドで登録した文字列はヘルプメッセージの冒頭に出力されます。ヘルプメッセージはOptionParser#help
メソッドで得られます。
初めて見たときには、OptionParser
を使ったコードはややこしく見えるかもしれません。最初のうちは、動くコードをコピー&ペーストして自分のプログラムに応用していくのがいいでしょう。使っていればそのうち慣れてしまいます。
サーバの起動
続いてサーバを起動するコードを見ていきます。main
メソッドの続きを以下に示します。
accounts = POPd::AccountTable.new(POPd::Maildir) logger = Syslog.open('popd', Syslog::LOG_PID, Syslog::LOG_DAEMON) popd = POPd.new(accounts, logger)
この部分はPOPd
オブジェクトを設定に合わせて作成しています。POPd
クラスはサーバ全体を表現するクラスです。ここで例えば、POPd::Maildir
を適切なクラスに変更してやれば、別のメールボックス形式にも対応できるように設計してあります。
また、POPd#initialize
は以下のとおり、accounts
とlogger
をインスタンス変数に代入するだけです。
class POPd def initialize(accounts, logger) @accounts = accounts @logger = logger end
main
メソッドのコード2行目のSyslog.open
では、OSに用意されているロギングインターフェイスに接続しています。第1引数は自プログラム名、第2引数はログ形式の指定、第3引数は自プログラムの分類です。上記のようにSyslog.open
を呼び出すと、書き出されるログは以下のような形式になります。
Oct 29 11:12:11 tukumo popd[7947]: connection from 192.168.1.34
Syslog.open
メソッドの詳細については、本稿では説明しません。リファレンスマニュアルのSyslogモジュールの項を見てください。
さて、main
メソッドの続きに戻りましょう。main
メソッドの末尾、サーバを起動する部分を以下に示します。
case mode when :standalone daemon { popd.listen port } when :inetd popd.inetd else raise 'must not happen' end
変数mode
が:standalone
ならスタンドアロンモード(デーモンモード)、mode
が:inetd
ならinetdモードで起動します。
以下、スタンドアロンモードの場合とinetdモードの場合に分けて説明していきます。
デーモンになる
スタンドアローンモードの場合、特に知識が必要になる個所は2点あります。以下の2点です。
- デーモンになる方法
- TCP接続を受け付ける方法
1点目のデーモンになるコードは、daemon
メソッドで実装されています。2点目のTCP接続を受け付けるコードは、POPd#listen
メソッドで実装されています。
まずdaemon
メソッドから見ていきましょう。daemon
メソッドは自プロセスをデーモンにするメソッドです。以下にdaemon
メソッドのコードを示します。
def daemon return yield if $DEBUG fork { Process.setsid fork { Dir.chdir '/' STDIN.reopen '/dev/null', 'r' STDOUT.reopen '/dev/null', 'w' STDERR.reopen '/dev/null', 'w' trap(:INT, 'IGNORE') trap(:TERM, 'EXIT') trap(:HUP, 'IGNORE') yield } exit! } exit! end
このメソッドがやることは4つあります。
fork
メソッドとProcess.setsid
メソッドを呼び出す- ルートディレクトリ(/)にchdirする
- 標準入出力を「/dev/null」にリダイレクトする
- INTシグナル(Ctrl-Cによる割り込み)、TERMシグナル、HUPシグナルのハンドラを登録する
まず、1.のfork
メソッドとsetsid
メソッドが一番重要です。このメソッドはそれぞれfork()システムコールとsetsid()システムコールに対応しており、fork()→setsid()→fork()の順番で呼び出すことによってプロセスをデーモン化します。
次に、2.3.4.はサーバプログラムとしての作法です。
ルートディレクトリにchdirするのは、ファイルシステムをアンマウントできなくなる問題を防ぐためです。UNIXでは、ファイルシステムに含まれるディレクトリがプロセスのカレントディレクトリになっていると、そのファイルシステムをアンマウントできないという仕様があります。そこで例えば、システム管理者がうっかりして、マウントしたCD-ROMファイルシステムからサーバを起動したと考えてみましょう。この場合、サーバのカレントディレクトリはCD-ROMファイルシステムなので、サーバが終了するまでCD-ROMをアンマウントできなくなってしまいます。このような問題を防ぐため、サーバはひとまずルートディレクトリにchdirするのが作法なのです。
もっとも、必ずルートディレクトリにchdirしなければいけないわけではありません。サーバが動作するために適切なディレクトリが特にあるなら、そのディレクトリにchdirしても構いません。
次に、3.の標準入出力を「/dev/null」へリダイレクトする理由は、うっかり標準入出力を使ってしまったときにエラーにならないようにするためです。標準入出力は端末につながっていますが、デーモンから端末に対して入出力を行うとエラーになってしまうのです。プログラムが完璧に書けていればそんなことは起こらないはずですが、完璧なプログラムは滅多にありませんから、念のために実行しておきましょう。
最後に4.のシグナルハンドラ設定は一般的な慣習のようなものです。INTシグナル(割り込み)はサーバでは大抵無視します。TERMシグナルは一般にサーバを停止するときに使われます。HUPシグナルは設定ファイルなどを読み直すときに使われます。
それから、daemon
メソッドではexit!
メソッドを使っているところも特徴的です。exit!
メソッドは_exit()システムコールに対応するRubyのメソッドです。exit!
メソッドには、通常のexit
メソッドと違って入出力バッファをフラッシュしない、などの特徴があります。このメソッドで通常通りexit
メソッドを使ってしまうと、ここまでにプログラムが出力した内容が二重になってしまうなどの問題が発生します。
このように、デーモンになる手順はなかなか面倒です。面倒ではありますが、手順は決まっているので、上記のコードをコピー&ペーストすれば済むことでもあります。私は上記のコードに関しては特に著作権を主張しないので、皆さんがデーモンになるコードを書くときは自由にコピーして使ってください。
TCP接続を待つ
次に、スタンドアロンモードの第2の難関、TCP接続を受け付けるためのコードを見ていきます。TCP接続を受け付けるコードを実装しているのはPOPd#listen
メソッドです。POPd#listen
メソッドのコードを以下に示します。
def listen(port) server = TCPServer.new(port) while true Thread.fork(server.accept) {|sock| @logger.notice "connection from #{sock.peer_ipaddr}" service sock, sock } end end
TCP接続を受け付けるためには、以下の2つの手順が必要です。
TCPServer.new
メソッドを呼んで、TCPServer
オブジェクトを作成するTCPServer#accept
メソッドを呼んで、実際にTCP接続を待つ
まず、TCPServer.new
メソッドでTCPServer
オブジェクトを作ります。第1引数は待ち受けるポート番号です。POP3では110番がデフォルトのポートすなわちウェルノウンポート(well-known port)なので、一般的には「110」を渡せばよいでしょう。
ただし、1023番以下のポートはスーパーユーザー(root)権限がないと接続を待てないので、一般ユーザー権限でPOPdを動かすときは「--port」オプションを使ってもっと大きなポート番号を指定してください。
ちなみに、TCPServer
オブジェクトの実体は、TCP接続待ち受け用のソケットです。このソケットをサーバソケット(server socket)と言います。
さて次に、TCPServer#accept
メソッドを呼びます。TCPServer#accept
メソッドは、クライアントから接続があるまで待ち、接続が完了したら、クライアントと接続済みになったTCPSocket
オブジェクトを返します。既に説明したように、TCPSocket
オブジェクトに対しては普通のFile
オブジェクトと同じようにgets
メソッドやprint
メソッドを使って入出力が可能です。
複数のクライアントの並行処理
TCP接続を待ち受けるときの基本は以上の通りですが、上記のPOPd#listen
メソッドでは、もう一工夫を加えています。
スタンドアロンモードの場合、クライアントからのTCP接続を次々に受け付けなければいけません。そこで次のようなコードを書いたとしましょう。
server = TCPServer.new(port) while true sock = server.accept service sock, sock end
ところが、これでは問題があります。クライアントからの接続はいつ来るか分かりませんから、もしかすると、既にクライアントと接続してservice
メソッドを実行しているあいだに、別のクライアントからも接続要求が来てしまうかもしれません。もし、現在処理中のクライアントの処理が非常に長時間かかってしまうと、そのあいだ次のクライアントはずっと待たされてしまいます。
そこで、複数のクライアントを並行処理できるように、Rubyのスレッドを使うことを考えましょう。次のようにコードを改善してみました。
server = TCPServer.new(port) while true sock = server.accept Thread.fork { service sock, sock } end
これで問題はないでしょうか? ……残念ながら、これは間違いです。Thread.fork
を呼んだ時点でスレッドが1つ増えて並行処理が始まりますから、生成されたスレッドがブロックを実行してsock
を参照する前に、while
ループの次の回が実行されて、ローカル変数sock
が上書きされてしまう可能性があります。
ですからこのループは、さきほど見せたPOPd#listen
メソッドのように、Thread.fork
の引数を使って書かなければいけないのです。正しいコードは以下のとおりです。
server = TCPServer.new(port) while true Thread.fork(server.accept) {|sock| service sock, sock } end
こう書くことで、server.accept
の返り値(接続済のTCPSocket
オブジェクト)がブロック引数sock
に渡されます。そしてブロック引数sock
は、スレッドローカルなので、別のスレッドに変数を上書きされてしまうことはなくなります。
スタンドアロンモードとinetdモードの共通化
続いてPOPd#service
メソッドに進みましょう。POPd#service
メソッドのコードを以下に示します。
def service(input, output) @input = input @output = output session end
さきほどのPOPd#listen
メソッドの最後は次のようになっていました。
service sock, sock
従って、スタンドアロンモードならば@input
(入力元)も@output
(出力先)もソケットということになります。
また、inetdモードなら次のようにservice
メソッドを呼び出します。
service STDIN, STDOUT
これでinetdモードのときはめでたくSTDIN
(標準入力)が入力、STDOUT
(標準出力)が出力となります。
ここまでのコードでスタンドアロンモードとinetdモードの違いが共通化できたので、以降はどちらのモードでもコードは完全に同じになります。