3 Hello, dRuby.

dRubyでも世界のはじまりは Hello, World です。

この章では、dRubyを使った小さなスクリプトを通じてdRubyの提供する 基本的な機能と、スクリプトを書く上での約束ごとを学びます。

3.1 Hello, World.

文字列を印字するサーバを準備し、 このサーバを利用して"Hello, World."を印字させる実験をしてみましょう。

この実験では二つのプロセスが登場します。 一つは文字列を印字するサーバ、もう一つは印字サーバを利用して'Hello, Wolrd.'を putsするクライアントです。

3.1.1 印字サーバ

印字するサーバputs00.rbを示します。

List 3.1 puts00.rb

# puts00.rb
require 'drb/drb'                                        # (1)

class Puts                                               # (2)
  def initialize(stream=$stdout)
    @stream = stream
  end

  def puts(str)
    @stream.puts(str)
  end
end

uri = ARGV.shift
DRb.start_service(uri, Puts.new)                        # (3)
puts DRb.uri
sleep                                                   # (4)

簡単にスクリプトの説明します。

  1. dRubyを利用するには require 'drb/drb'が必要です。
  2. putsだけをするPutsクラスを定義します。印字サービスの主処理です。
  3. サービスの場所を示す名前であるURIと、URIに関連付けるオブジェクトを指定して dRubyのサービスを開始します。これによってリクエストを待機するスレッドも 準備されます。URIに関しては後述します。
  4. スクリプトが終了してしまわないように、メインスレッドをsleepさせます。

ではサーバを起動しましょう。 ターミナル1で次のようにputs00.rbを実行します。 puts00.rbは引数でサービスの場所を示すURIを指定できます。

[ターミナル1]
% ruby puts00.rb druby://localhost:12345
druby://localhost:12345

サーバは終了せずリクエストが届くのを待機しつづけます。 印字サービスのURIを出力したあと、シェルに戻っていないことを確かめてください。

3.1.2 irbからの利用

つづいてクライアントです。irbを使って実験してみましょう。 ターミナルをもう一つ用意(ターミナル2)してirbを起動し、次のスクリプトを入力して下さい。

[ターミナル2]
% irb
irb(main):001:0> require 'drb/drb' 
=> true

dRubyを利用するには'drb/drb'をrequireしなくてはなりません。

irb(main):002:0> there = DRbObject.new_with_uri('druby://localhost:12345') 
=> #<DRb::DRbObject: ... 略">

DRbObject.new_with_uri()は、URIを指定して分散オブジェクトへの参照を返します。 URIはputs00.rbを起動したときに与えたものを指定します。 生成された参照(DRbObject)をthereという変数に覚えておきます。

http://www2a.biglobe.ne.jp/%7eseki/ruby/note20.jpg DRbObject.new_with_uri(uri)は2.0から導入されました。 1.3系ではDRbObject.new(nil, uri)です。 2.0でも1.3と同様に生成することもできます。

irb(main):003:0> there.puts('Hello, World.') 
=> nil

印字サーバのオブジェクトのputsメソッドを呼び出しました。 サーバを起動したターミナル1に、'Hello, World.'が印字されているはずです。 確認して下さい。

[ターミナル1]
% ruby puts00.rb druby://localhost:12345
druby://localhost:12345
Hello, World.

どうですか?うまく'Hello, World.'が印字されていましたか? irbでタイプしたスクリプトによって、サーバのターミナルに文字が出力されたのです。 いまタイプした数行で印字サービスを利用できたのが感じられたでしょうか?

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2puts00.jpg

図3.1 印字サーバとirbによるクライアント

まだピンとこなければ、違う文字列も印字させましょう。 サーバのターミナルも観察しながらirbにスクリプトを入力して下さい。

[ターミナル2]
irb(main):004:0> there.puts('R is for Ruby.') 
=> nil

サーバのターミナルに文字列が表示されたでしょうか?

[ターミナル1]
R is for Ruby.

クライアントのthereという変数は印字サービスのオブジェクトを参照しています。 thereにputsを送ることでサーバのputsメソッドが起動され、サーバの標準出力に 文字列が印字されたのです。

今度はサーバを停止させたらどうなるか実験してみましょう。 [Ctrl]-Cで停止させます。*1

[ターミナル1]
^Cputs.rb:15:in `sleep': Interrupt 
        from puts.rb:15

さてここでthere.putsしたらどうなるのでしょう。

[ターミナル2]
irb(main):005:0> there.puts('Hello, again.')
DRb::DRbConnError: druby://localhost:12345 - #<Errno::EINVAL.... 略

DRb::DRbConnErrorという例外が発生しました。 DRbConnErrorはdRuby内部で通信エラーが発生したことを示します。 サーバが停止してしまったので、メソッド呼出しに失敗してしまったのです。

http://www2a.biglobe.ne.jp/%7eseki/ruby/note20.jpg DRb::DRbConnErrorは2.0から導入されました。1.3系ではErrno例外が発生します。

もう一度サーバを起動させましょう。

[ターミナル1]
% ruby puts00.rb
druby://localhost:12345

そしてthere.putsをもう一度試します。

[ターミナル2]
irb(main):006:0> there.puts('Hello, again.') 
=> nil

今度は例外が発生しませんでした。 'Hello, again.'がターミナル1に印字されていますか?

[ターミナル1]
% ruby puts00.rb
druby://localhost:12345
Hello, again.

ではirbを終了させて下さい。

[ターミナル2]
irb(main):007:0> exit

3.1.3 スクリプト版クライアント

最後にクライアントをスクリプトに書き直しての実験です。

List 3.2 hello00.rb

# hello00.rb
require 'drb/drb'                     # (1)

uri = ARGV.shift
there = DRbObject.new_with_uri(uri)   # (2)
there.puts('Hello, World.')           # (3)

このスクリプトはirbで実験した通りのスクリプトです。

  1. dRubyライブラリを読み込みます。
  2. URIで指定するリモートのオブジェクトの参照を作り、thereに覚えておきます。 thereのメソッド呼出しは、印字サーバのメソッド呼出しとなります。
  3. 印字サーバのputsメソッドを起動して、'Hello, World.'を印字させます。

hello00.rbをターミナル2で実行させます。 ターミナル1に'Hello, World.'が印字されましたか?

[ターミナル2]
% ruby hello00.rb druby://localhost:12345

単純な例を使ってdRubyを体験してみました。 簡単な手順でクライアント・サーバモデルのスクリプトが書けることが お分かりでしょうか?

3.1.4 dRubyのURI

先ほどの実験では説明なしに'druby://localhost:12345'というURIを使っていました。 この節ではTCPを使って接続する場合のdRubyのURIと、 DRb.start_serviceに与えるURIとオブジェクトの関係について解説します。

TCPを使って接続する場合のdRubyのURIの書式は次の通りです。

druby://ホスト名:ポート番号

URIはホスト名とポート番号から構成されます。 URIによってdRubyのサーバを特定することができます。

URIを使用するのは、サービスの起動時とDRbObjectの生成の場面です。 DRb.start_serviceはURIとそれに関連付けるオブジェクトを指定して dRubyのサービス(DRbServer)を開始します。 DRbServerにはサーバソケットの管理やソケットを待ち受けるスレッドなど サーバプログラミングに必要なもの一式が入っています。 具体的には、URIのホスト名とポート番号を用いて生成したTCPServerや、 acceptで接続を待ち受けるスレッドなどのオブジェクトです。 URIはDRbServerを特定する情報で、DRbServerは一つの固有なURIを持ちます。

DRb.start_service(uri, front)

URIと関連付けられたオブジェクトをリモートから参照するには、 DRbObject.new_with_uriを使用します。

there = DRbObject.new_with_uri(uri)

なお、DRbObject.new_with_uri(uri)はDRbObject.new(nil, uri)と等価です。

URIと関連付けられたオブジェクトは、そのサービスの入り口(受け付け担当)と なるため、フロントオブジェクトと呼びます。 DRbObject.new_with_uri()で作成したオブジェクトへのメソッド呼び出しは、 全てこのフロントオブジェクトに届くことになります。 実際のアプリケーションでは、アプリケーション内部のオブジェクトをそのまま URIに関連付けずに、アクセス制御をしたり、 複数の処理をまとめて行うユーティリティ的なメソッドを用意したりした、 特別なクラスをフロントに置くことが多いです。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2front3.jpg

図3.2 アプリケーションの入り口となるフロント

DRb.start_serviceで指定するURIは、ホスト名を省略したりポート番号を省略したり することができます。

  • ホスト名を省略するTCPServerのから自動的にホスト名を補います。
  • ホスト名を指定すると、指定したネットワークインターフェイスにのみにポートを開きます。
  • ポート番号に0を指定すると、システムが自動的に空いているポートを割り当てます。

具体的には次のように記述します。

  • 'druby://hostname:12345' - 全て指定した完全な形
  • 'druby://:12345' - ホスト名を省略した形式
  • 'druby://hostname:0' - ホスト名を指定するがポート番号を省略した形式
  • 'druby://:0' - ホスト名もポート番号も省略した形式
  • nil - ホスト名もポート番号も省略

それぞれ実験してみましょう。irbで試します。 require 'drb/drb'を行う起動オプション、-r drb/drbをつけて起動します。

DRb.start_serviceのあとにURIを返すメソッドDRb.uriを使って、 開始したサービスのURIを調べます。

% irb -r drb/drb
irb(main):001:0> DRb.start_service('druby://yourhost:12345'); DRb.uri 
=> "druby://yourhost:12345" 
irb(main):002:0> DRb.start_service('druby://:12346'); DRb.uri 
=> "druby://yourhost:12346"
irb(main):003:0> DRb.start_service('druby://yourhost:0'); DRb.uri 
=> "druby://yourhost:52359"
irb(main):004:0> DRb.start_service('druby://:0'); DRb.uri 
=> "druby://yourhost:52360"
irb(main):005:0> DRb.start_service(nil); DRb.uri 
=> "druby://yourhost:52361"
irb(main):006:0> exit

では、先ほどの'Hello, World.'を今度はURIの指定なしに試してみましょう。 以下、irbの出力で重要でないものは省略してあります。 また--prompt simpleでプロンプトをシンプルにしています。

[ターミナル1]
% ruby puts00.rb
druby://yourhost:52369   # ← 自動生成されたURI

[ターミナル2]
% irb --prompt simple -r drb/drb
>> uri = 'druby://yourhost:52369' # ←ターミナル1の出力に合わせて指定
>> there = DRbObject.new_with_uri(uri) 
>> there.puts('Hello, World.') 

[ターミナル1]
% ruby puts00.rb
druby://yourhost:52369
Hello, World.

最初の'Hello, World.'の実験ではlocalhostを指定していましたから、 ローカルループバックのインターフェイスのみを開きます。 このURIでは同じマシンでないと動作しませんでした。 今回の実験では全てのネットワークインターフェイスに対して サービスするのでネットワーク上のマシン間でも実験ができます。 もし複数のコンピュータを使える環境があるなら、ターミナル1とターミナル2を それぞれ別のコンピュータで起動してみて下さい。

[マシン1]
% ruby puts00.rb
druby://yourhost:52371   # ← 自動生成されたURI

[マシン2]
% irb --prompt simple -r drb/drb
>> uri = 'druby://yourhost:52371' # ←マシン1の出力に合わせて指定
>> there = DRbObject.new_with_uri(uri) 
>> there.puts('Hello, World.') 

[マシン1]
% ruby puts00.rb
druby://yourhost:52371
Hello, World.

3.2 Reminder

この節では簡単なアプリケーションを作ります。 このアプリケーションの仕様メモは次の通り。

  • だれでも書き込め、削除できるメモ帳。
  • ユーザインターフェイスはirb。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2rem02.jpg

図3.3 ターミナル1に配置されたReminderをターミナル2から参照する

まず、Reminderクラスを定義します。(reminder0.rb

Reminderは項目の追加、削除、一覧取得のメソッドを持つクラスです。 それぞれの項目にはシリアル番号が振られます。 この番号は削除の際に使用されます。

List 3.3 reminder0.rb

# reminder0.rb
class Reminder
  def initialize
    @item = {}
    @serial = 0
  end

  def [](key)
    @item[key]
  end

  def add(str)
    @serial += 1
    @item[@serial] = str
    @serial
  end

  def delete(key)
    @item.delete(key)
  end

  def to_a
    @item.keys.sort.collect do |k|
      [k, @item[k]]
    end
  end
end

ではirbを使ってReminderサーバを起動してみましょう。

[ターミナル1]
% irb --prompt simple -r reminder0.rb -r drb/drb
>> $KCODE='euc'
>> front = Reminder.new
>> DRb.start_service('druby://yourhost:12345', front)
>> DRb.uri
=> "druby://yourhost:12345"

フロントオブジェクトはReminderのインスタンスです。 日本語の文字列も保持するために、$KCODEを指定しています。 値はあなたの環境に合わせて指定して下さい。

続いてクライアントです。 まず項目をいくつか追加してみましょう。

[ターミナル2]
% irb --prompt simple -r drb/drb
>> $KCODE='euc'
>> r = DRbObject.new_with_uri('druby://yourhost:12345')
>> r.to_a
=> []
>> r.add('13:00 ミーティング')
=> 1
>> r.add('17:00 進捗報告')
=> 2
>> r.add('土曜日にDVDを返す')
=> 3
>> r.to_a
=> [[1, "13:00 ミーティング"], [2, "17:00 進捗報告"], [3, "土曜日にDVDを返す"]]

もう一つクライアントを増やして項目を削除/追加します。

[ターミナル3]
% irb --prompt simple -r drb/drb
>> $KCODE='euc'
>> r = DRbObject.new_with_uri('druby://yourhost:12345')
>> r.to_a
=> [[1, "13:00 ミーティング"], [2, "17:00 進捗報告"], [3, "土曜日にDVDを返す"]]
>> r.delete(2)
>> r.to_a
=> [[1, "13:00 ミーティング"], [3, "土曜日にDVDを返す"]]
>> r.add('15:00 進捗報告')
>> r.to_a
=> [[1, "13:00 ミーティング"], [3, "土曜日にDVDを返す"], [4, "15:00 進捗報告"]]

複数のクライアント(ターミナル2、ターミナル3)から同じReminderを操作しているので、 追加した項目がそれぞれから見えています。 Reminderはサーバ(ターミナル1)に存在し、クライアントにあるのは参照です。 二つのクライアントがReminderを共有しているわけです。

つづいてクライアントの寿命とReminderの寿命の違いについて 確かめたいと思います。 クライアントを全て終了させてからもう一度アクセスしてみましょう。

[ターミナル2]
>> exit

[ターミナル3]
>> exit

終了させました。 もう一度クライアントをセットアップしてReminderの様子を見てみましょう。

[ターミナル2]
% irb --prompt simple -r drb/drb
>> $KCODE='euc'
>> r = DRbObject.new_with_uri('druby://yourhost:12345')
>> r.to_a
=> [[1, "13:00 ミーティング"], [3, "土曜日にDVDを返す"], [4, "15:00 進捗報告"]]

うん。予想通り最後の操作の状態を維持しています。 Reminderはサーバにあるため、クライアントの終了/再起動に関係ないのです。

dRubyをアプリケーション(システム)に導入するメリットの一つは、 プロセスの寿命を超えたオブジェクトが簡単に利用できる点です。 従来では永続化が必要な場面の一部で、dRubyを永続化の代わりに使用することが できます。 身近な例ではWebアプリケーションの実装などが挙げられます。 従来であればデータベースにページの状態をストアしていくような場面で、 CGIのような寿命の短いプロセスとより長く動作するdRubyのサーバを組合せて 簡潔に記述することができます。

http://www2a.biglobe.ne.jp/%7eseki/ruby/d2rem03.jpg

図3.4 ターミナル1に配置されたReminderをターミナル3のCUIから操作する

クライアントからの操作をもう少し見易くするためにReminderCUIを書きましょう。 ReminderCUIはirbから利用されるのを前提とした キャラクタベースのユーザインターフェイスです。 項目を縦に一覧表示するlistメソッドと、 確認してから削除するdeleteメソッドを持っています。

List 3.4 reminder_cui0.rb

# reminder_cui0.rb
class ReminderCUI
  def initialize(reminder)
    @model = reminder
  end

  def list
    @model.to_a.each do |k, v|
      puts format_item(k, v)
    end
    nil
  end

  def add(str)
    @model.add(str)
  end

  def show(key)
    puts format_item(key, @model[key])
  end

  def delete(key)
    puts" [delete? (Y/n)]: #{@model[key]}"
    if /\s*n\s*/ =~ gets
      puts "canceled"
      return
    end
    @model.delete(key)
    list
  end

  private
  def format_item(key, str)
    sprintf("%3d: %s\n", key, str)
  end
end

ターミナル3でReminderCUIを起動して実験しましょう。

[ターミナル3]
% irb --prompt simple -r reminder_cui0.rb -r drb/drb 
>> $KCODE='euc'
>> there = DRbObject.new_with_uri('druby://yourhost:12345')
>> r = ReminderCUI.new(there)
>> r.list
  1: 13:00 ミーティング
  3: 土曜日にDVDを返す
  4: 15:00 進捗報告
=> nil
>> r.add('図書館にRHGをリクエストする')
>> r.list
  1: 13:00 ミーティング
  3: 土曜日にDVDを返す
  4: 15:00 進捗報告
  5: 図書館にRHGをリクエストする
=> nil
>> r.delete(1)
 [delete? (Y/n)]: 13:00 ミーティング
n                                    # ← 自分でタイプします
canceled
=> nil
>> r.delete(1)
 [delete? (Y/n)]: 13:00 ミーティング
y                                    # ← 自分でタイプします
  3: 土曜日にDVDを返す
  4: 15:00 進捗報告
  5: 図書館にRHGをリクエストする
=> nil

irbを使うと、ちょっとしたクラスを書くだけでも使い易いインターフェイスが 得られますね。 アプリケーション本体ーつまりターミナル1のReminderーをそのままに複数の ユーザインターフェイスを与えることができます。 後の章ではWebのインターフェイスを書きます。もう少し待って下さい。

この節ではdRubyを使って簡単な分散アプリケーションを書きました。 dRubyの雰囲気をつかめたでしょうか?

3.3 まとめ

  • dRubyを利用するときはrequire 'drb/drb'でライブラリをロードします。
  • dRubyスクリプトはDRb.start_serviceが必要です。
  • URIはDRbServerを特定します。
  • DRbServerは一つのオブジェクトと関連付けることができます。
  • URIをつかってこのオブジェクトへの参照を作ります。

*1どうやって停止させるのかは環境によって異なります