メイン

2章 アーカイブ

2007年09月05日

2章.Hello, dRuby (その1)

「2章.Hello, dRuby」に入りました。まずは「Hello, World.」ですね。

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

2.1 Hello, World.

まずはサーバー側となるputs00.rbの方からソースコード(リスト2.1)を、順に見ていくことにします。私はRubyをちゃんと勉強していないので、ついでにRubyのことも詳しめに見てみることにしてみようと思います。

require 'drb/drb'

Cでいうincludeみたいなもののようです。使いたいモジュール(モジュールと言う呼称でいいのかはわからないが)をこれで含めると、インタプリタがそっちを先に読みに行って、そこに定義されているものが使えるようになる。と言う感じに理解しておきます。

class Puts

classでクラスを定義できます。

def initialize(stream=$stdout)

defはメソッドを定義できるようです。で、initializeって名前のメソッドはコンストラクタのようです。
$で始まるものはグローバルな変数です。なので$stdoutはグローバル変数のうちのひとつです。標準出力を指し示す組み込み変数(あらかじめ定義されている変数)のようです。

@stream = stream

Rubyでは、@がついてるやつはインスタンス変数になります。ここでは引数のstreamをインスタンス変数の@streamに代入してるってことですね。

uri = ARGV.shift

 うごご、わからん。でもなんとなく想像はつく。調べてみると、ARGVはどうやらコマンドライン引数が入ってくる配列のようです。(Cのように、0番目にプログラム名が入ってきたりはしません。)で、shiftは、先頭の一要素を取るメソッドのようです。shiftが実行されると、先頭の要素は取り除かれ、他の要素はひとつずつ詰められるようです。ほうほう。puts00は、実行時にコマンドライン引数の先頭にURIを指定してもらう仕様であるということですね。

DRb.start_service(uri, Puts.new)

ついにきましたDRb。解説文を見てみると

3. サービスの場所を示す名前であるURIと、URIに関連付けるオブジェクトを指定して dRubyのサービスを開始します。これによってリクエストを待機するスレッドも準備されます。URIに関しては後述します。

2.1.1 印字サーバ

とあります。にゃるほど。「リクエストを待機するスレッドも準備されます。」と言うところが重要そうだな思いつつ次のステートメントを見てみます。

sleep

 これはスレッドを止めるやつのようです。むむむ、止まっちゃうんだよね。で、このプログラムはsleepが最後の行だからもし復帰したら終わっちゃうんだよね。

 ちなみにスレッドとは、処理を実行する単位とか、処理の実行者を指したり、処理の流れそのものを指したりします。プロセス内の処理を実際に実行しているのはスレッドになります。どこかから起動されたRubyのプロセスは必ずひとつ以上のスレッドを持ちます。最初に立ち上げられるのはメインスレッドと言うみたいです。同プロセス内のスレッドは、プロセス空間(メモリの場とか)を共有します。Rubyでのマルチスレッドの話はまた後で出てくるみたいなので楽しみにしておきます。

 さて、ここのsleepは「スクリプトが終了してしまわないように、メインスレッドをsleepさせます。」とあります。

 そもそもsleepってどこのメソッドなのかなあ。Kernelモジュールのsleepってやつが呼ばれてるのかしら。。KernelモジュールはWindowsだと多分kernel32.dllをロードして使えるようにしてるのかなあ?...っと予想通りwin32.cでやっていました。windows.hをincludeしてるWin32SDKプログラミングですね。濃いのでRuby本体のソースはこれ以上追いかけないようにして。。。。組み込み関数のsleepの説明を見てみると、

sec 秒だけプログラムの実行を停止します。sec には浮動小数点数も指定できます。sec が省略された場合、他スレッドからの Thread#run などで明示的に起こさない限り永久にスリープします。戻り値は実際に停止していた秒数(整数)です。

とあります。で、どこからでも呼べると。呼んだら、それを実行していたスレッドが止まると。ここでは引数を指定していないので、起こされない限り永遠に止まっていると言うことですね。で、もし起こされてもこのプログラムはsleepが最後の行だからそのまま終わると。ふむふむ。

 じゃあ、やっぱりDRb.start_serviceで新しいスレッドを起こしていないと何も出来ないはずです。ちょっとだけみてみるかな、難しかったらあきらめよう。。と、start_serviceメソッドの中では、DRbServerをnewしてるな、でDRbServerのinitializeの中で

@thread = run

としている。これかなあ。runをみてみると

def run
  Thread.start do
begin
  while true
    main_loop
  end
ensure
  @protocol.close if @protocol
  kill_sub_thread
end
  end
end

うごご、tabと半角スペースが雑じっててなんだかインデントがわからぬデス。。。っと、それはさて置き、RubyのThreadはstartメソッドにブロックを渡せるんですね。runメソッドではThread.startで新スレッドを生成、実行し、そのオブジェクトが勝手に返却されてると。で、そのスレッドの中のブロックはぐるぐる回ってるmain_loopがいる感じですね。ほうほう。とりあえず要求を受け付けるスレッドは作られてそうだということが確認できたので、puts00については満足。

まだサーバー側しかみていませんが、ソースコードが入ると長くなる予感。この調子でどこまで行けるか。。と一抹の不安を覚えつつ続きはまた後日。

2007年09月06日

2章.Hello, dRuby (その2)

「Hello, World.」の続きです。印字サーバー側(puts00)は終わったので、本の指示に従い起動したままほったらかしておきます。次はクライアント側、本の「2.1.2 irbからの利用」あたりからです。クライアント側はirbでやるんですね。irb(interactive ruby)は、Rubyと逐次対話しながら遊べる道具です。

there = DRbObject.new_with_uri('druby://localhost:12345')

と、irbでタイプします。

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

2.1.2 irbからの利用

ふむふむ、ちゃんと理解するためにDRbObject.new_with_uriの中を見てみると、

self.new(nil, uri)

してるだけのようです。selfってのは自分を指すのでしょう。が、selfの厳密な意味や仕組みを深く突っ込んでいくとハマりそうな気配がするのでちょっとうやむやにしておます。ここでは、DRbObjectのインスタンスが生成されている、という程度の理解で留めます。つまり、どんなオブジェクトがサーバーで共有されていようが、リモートオブジェクトの参照は、必ずDRbObjectだということですね。クライアント側からリモートオブジェクトにアクセスするには必ずDRbObjectを介するので、DRbObjectさえ理解すりゃアクセスの仕組みはわかると。そういうことと理解しておきます。

じゃあ、次にみるべきところはDRbObjectのinitializeかなあ。中を見ると、、、まず、第1引数のobjがnilかどうかで分岐しています。でもどちらも@uriと@refに設定していることに違いは無いようです。で、この場合は必ずnilなので、そっちのブロックを見ることにします。

@uri,option=DRbProtocol.uri_option(uri,DRb.config)

うごご、「@uri,option = ・・・」なんじゃこりゃ、全然わからん。一体@uriには何がセットされるんだ、、、で、30分以上悩みました。。が、これは多重代入と言うんですね。右オペランドと左オペランドの要素数が複数個でも入れれるだけ入れてくれると。ふむふむ。例えば

foo, bar = 1,2;p foo;p bar

とすると、

1
2

と出ます。実際には[1,2]と配列になって返却されているようですね。こんなことも知らないとは、、と言われそうですが知りませんでした。Ruby(というかスクリプト言語)の通信簿1ですからね。はい。またちょっと賢くなりました、さて、ここでは、DRbProtocol.uri_optionを呼び出し、uriとoptionを返してもらっています。引数になっているDRb.configあたりを追いかけると出てくるcurrent_serverは、いたるところで使われていてかなり重要そうな気がしますが、とりあえずこれは今回は置いておきます。なので、次は素直にDRbProtocol.uri_optionを見ていきます。(多分元のuri文字列を加工してるんだろうなという想像をしつつ。)

    def uri_option(uri, config, first=true)
      @protocol.each do |prot|
	begin
	  uri, opt = prot.uri_option(uri, config)
	  # opt = nil if opt == ''
	  return uri, opt
・・・

まず、@protocolは配列なので複数のプロトコルをサポートしているようです。デフォルトではDRbTCPSocketの一要素のみが設定されています。で、これが「druby://ホスト:ポート番号」のやつだと。でも、正確には「druby://ホスト:ポート番号?オプション」と言う形式で指定される仕様のようです。@protocolのuri_optionメソッドはDRbTCPSocketの同名メソッドに委譲しています。この中で指定されたURI文字列をパースして、ホストやポート番号、オプションに分解しているようですね(多分同じことはDRbServerの方でもやってるんでしょうが、main_loopの中まで見ていなかったので遭遇しなかったようです)。

@uri周りはこれ位で満足しました。次は@refです。もう一度DRbObjectのinitializeに戻って

@ref = DRbURIOption.new(option) unless option.nil?

これは、unlessだから、「optionがnullでなければoptionを@refに代入しろ、optionがnullならDRbURIOption.newして@refに代入しろ」ってことです。理由はわかんないけどオプションを単なる文字列じゃなくてDRbURIOptionオブジェクトとして持っておきたいってことなのだろうという程度の理解で次。
ってあれれーー??もう終わった。設定のみか。何か簡単すぎるような気もするが気にせず進めていきます。これで、irbでさっきタイプした

there = DRbObject.new_with_uri('druby://localhost:12345')

のthereに、uriやoptionが設定されたDRbObjectのインスタンスが代入されるんだよ、ってところまでは理解しました。で、引き続き次はirbで、

there.puts('Hello, World.')

と打つとサーバー側のputs00を動かしているターミナルに表示される。うん、すごい!!でもどうやってんだろ、DRbObjectには当然ながらputsなんてメソッドはありません。でも呼べてるんだから何かある。。と、これはどうやらmethod_missingメソッドの中でやっています。RubyのObjectクラスのリファレンスを見ると

method_missing(name, args, ... )

呼びだされたメソッドが定義されていなかった時、Ruby がこのメソッドを呼び出します。

とあります。DRbObjectではこのメソッドをオーバーライドして、その中でリモートのメソッド呼び出しをやっているんですね。この中身も見たいところだが、今日はもう時間切れ。とりあえず「Hello, World.」が出るのは出た!

後、クライアントからputsで文字列を渡すとサーバー側の標準出力に出るってのは確認できましたが、サーバー側のリモートオブジェクトからクライアント側の@stdoutには出せないのかとか、色々気になりはじめたことがあります。が、これらも今追求せずとも読み進めるにつれて明らかになっていくでしょう。

2007年09月07日

2章.Hello, dRuby (その3)

「2章.Hello, dRuby の3回目(その3)」ですが、前回のエントリでは見れなかったDRbObjectのmethod_missingの中が気になるので、今回は本を読み進めず、こちらの中の概要だけでも見てみることにしました。以下がDRbObjectのmethod_missingの全てです。

def method_missing(msg_id, *a, &b)
  if DRb.here?(@uri)
obj = DRb.to_obj(@ref)
DRb.current_server.check_insecure_method(obj, msg_id)
return obj.__send__(msg_id, *a, &b) 
  end
  succ, result = self.class.with_friend(@uri) do
    DRbConn.open(@uri) do |conn|
      conn.send_message(self, msg_id, a, b)
    end
  end
  if succ
    return result
  elsif DRbUnknown === result
    raise result
  else
    bt = self.class.prepare_backtrace(@uri, result)
result.set_backtrace(bt + caller)
    raise result
  end
end

すごくちょっとしかありません。これくらいならみれるかなあ。じゃあまず最初の分岐。

if DRb.here?(@uri)

これは、スクリプトを実行している自分がオブジェクトを公開しているサーバー自身かどうかを訊ねています。自分がオブジェクトを公開しているサーバーだったら、リモート呼び出しの仕組みを使わずにそのまま呼んじゃえという事だと思われます。対して、リモート呼び出しのときに実行されるのが以下。

succ, result = self.class.with_friend(@uri) do
  DRbConn.open(@uri) do |conn|
    conn.send_message(self, msg_id, a, b)
  end
end

かなり短いですが、結構ムズい。渡されるブロックが入れ子になって2つあります。多分yieldの2段呼びだと想像しつつ、この5行をもうしばらく眺める。ちなみにyieldとはメソッドにブロックを渡すとそれを任意のタイミングでメソッドの中から呼べるというものみたいです。こんなやつです。

def add 
  p '足します'
  yield(1,2) 
  p '足しました'
end
add do |a,b|
  p a + b
end

上のスクリプトを実行すると、下のように出力されます。

足します
3
足しました

 大抵の場合、yieldは上の例のようなヘボい使い方はせず、イテレータで回す時に使われるようです。yieldの説明はこれくらいにして、さっきの5行ですが、よく見ると単純で、多分サーバーへの接続を確保し、そこへメソッド呼び出しのメッセージを送るという処理なのだなと言う想像くらいは容易につきます。これをわざわざyieldでブロックを渡してやろうとしているということは、複数の接続先が配列などで管理されていて、そこから今回の送り先を選び出して送るか、または、送り先全てに対して順次送ったりするんだなと推測出来そう。っと、そういう前提を抱きつつ中を見てみることにします。

def self.with_friend(uri)
  friend = DRb.fetch_server(uri)
  return yield() unless friend
・・・

 。。。。はい、全然よくわかりません。。friendって何かしら。お友達関係にあるサーバーと連携できるのかな。とりあえずHello, World.レベルの単純なクライアントからの場合はfriendはnilっぽいですね。とりあえず今回はfriendはnilと読みかえることにします。だからこのreturn yieldも実行されて、中のDRbConn.open(@uri) do ・・・ブロックが動きます。

 DRbConnとは、DRbObjectの中で使われる単純なクラスで、具体的にはサーバーとの接続をプールしているみたいです。毎回繋げに行くのはコストが掛かるから、一度つなげた接続は使いまわして、メソッド呼び出しに掛かるコストを減らしてくれるんですね。で、えっと、DRbConn.openなので、これはクラスメソッドか。Rubyでは“self.メソッド名”と定義すると、クラスメソッドになるようです。なので、self.openメソッドを見てみます。

def self.open(remote_uri)  # :nodoc:
  begin
conn = nil
・・・
conn = self.new(remote_uri) unless conn
succ, result = yield(conn)
return succ, result
・・・

 実際のコードはpoolにいれたりpoolに照会したり、poolに対するアクセスをアトミックにしたりともっと長いけど、肝心なところだけ抜粋したつもりです。poolに何も入っていない最初のアクセスの場合は、connもnilのままなので、ここでDRbConnのインスタンスを作って、それ自身を渡してyieldですね。で、そうすると2段ブロックの中の方が動いてその結果を返すと。ふむふむ。ということで次はDRbConnのsend_messageです。

def send_message(ref, msg_id, arg, block)  # :nodoc:
  @protocol.send_request(ref, msg_id, arg, block)
  @protocol.recv_reply
end

ここでのrefはDRbObjectですね。msg_id, arg, blockは、method_missingに渡された引数そのままです。@protocolはDRbTCPSocketだということは以前のエントリで既に見たので、DRbTCPSocketのsend_requestを見てみます。

def send_request(ref, msg_id, arg, b)
  @msg.send_request(stream, ref, msg_id, arg, b)
end

ちなみに@msgは何物かというと

@msg = DRbMessage.new(config)

 と初期化されていました。他プロセスのオブジェクトに対して実際にメッセージを送るのはDRbMessageがやっているようです。で応答をrecv_replyで受け取ってsuccとresultに詰めるという感じです。succが成功or失敗の真偽値、resultがメソッドの戻り値です。ちなみにDRbMessageの中ではMarshalを使ってdumpしているようです。詳しくはまた今度見ることにします。

 終わった。送信部分の流れは大体わかりました。もしかして後はDRbServerのmain_loopの中さえ見りゃ、dRubyの基本的な内部構造にはさらっと目を通したことになる。か?

 ところで、yield2段を目で追うのがつらかったので、今回からデバッガを使い始めました。しかし、そこで少しハマりました。デバッガとかで見ていると、デバッガが変数のインスペクション情報を取得するために勝手にto_sとかを呼び、私自身が試そうとしているスクリプトよりも先にリモート呼び出しをしているので、既にDRbConnにプールされていました。くそー、まだ一度もリモートで呼び出ししていないからインスタンス出来てないはずなのにどこで作ってるんだよ、わからん、って思って悩んだというわけです。それは置いといて、デバッガもリモートオブジェクトだと言う意識がないということだなあ。デバッガも騙されるdRubyって。。ちょっとびっくりです。

 次から本に戻ります。どこを読んでたかなあ。あ、まだHello, World.か。

2007年09月10日

2章.Hello, dRuby (その4)

本の続きに戻ります。サーバを停止させた状態でリモート呼び出しをしたらどうなるか実験してみます。puts00のサーバーを停止してthere.putsを実行っと。

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

2.1.2. irbからの利用

確かに本の通りにDRbConnErrorがでました。

def initialize(remote_uri)   # :nodoc:
  @uri = remote_uri
  @protocol = DRbProtocol.open(remote_uri, DRb.config)
end

場所はここ、DRbConnのインスタンス生成時のDRbProtocol.open。このメソッドは

def open(uri, config, first=true) 
  @protocol.each do |prot|
    begin
      return prot.open(uri, config)
    rescue DRbBadScheme
    rescue DRbConnError
      raise($!)
    rescue
      raise(DRbConnError, "#{uri} - #{$!.inspect}")
    end
  end
  ・・・

 です。このprot.openでは、実際にはDRbTCPSocketのクラスメソッドopenが呼ばれるので、この中のTCPSocket.openで例外が発生している感じですね。それをrescueでつかまえてraiseしてるのか。ふむふむ。ではopenは成功したけどその後に通信エラーになった場合はどうなるのかなあ。DbConnのopenの中身の一部です。

  ・・・
@pool.each do |c|
   if conn.nil? and c.uri == remote_uri
     conn = c if c.alive?
   else
     new_pool.push c
   end
end
@pool = new_pool
  ・・・

 プールから取る時の、「conn = c if c.alive?」で、この時点でサーバー側が死んでいたらそれを使わないようになっています。で、もしここでも接続は生きてたけど、送信までに死んだ場合は、recv_replyのどこかで例外が出るようです。
 ここでは、@poolの要素群をnew_poolという入れ物に一度移し替えて、それを@poolに戻しています。ただ今回使われることになった接続オブジェクトは、この時点ではnew_poolに入っていません。プールに接続オブジェクトを入れているのは、その下に記載されているのensureです。(ensureってのは、javaでいうfinallyみたいなやつです。)

@pool.unshift(conn)
@pool.pop.close while @pool.size > POOL_SIZE

 新しいconnをプール用配列の先頭に入れ、プールの要素数の上限を越えた要素は取り除いて閉じる。ってことか。whileも右側に書けるのですね。かっこいいコードだなあ。

 
 ところで、サーバーは無事生きているけど、リモートオブジェクトに存在しない操作を行なった場合はどうなるのでしょうか。ここではhogeメソッド(Putsに存在しない操作)を呼んでみました。

there.hoge

すると以下のようなresultになりました。ちなみに、成功/失敗が設定されるsuccの値はfalseになっています。

result undefined method `hoge' for #<Puts:0x29ba790 @stream=#<IO:0x294e758>>

2007年09月11日

[番外]今までの整理

ひとまず今までのところで学んだことを整理することにしました。

dRubyとは

  1. Rubyのオブジェクトをプロセス間でやり取りするための仕掛け
  2. dRubyは100%Rubyなので、変数が型に縛られないから楽に使える。IDLとかもいらない
  3. dRubyはプロセス間通信の仕組み自体もRubyで書かれているので、OSに依存しない

dRubyの仕組み

  1. URIに対してひとつのオブジェクトを紐付けられる
  2. 複数のプロトコルを扱える。デフォルトはDRbTCPSocket。自分で追加も可能
  3. URI文字列のパースや、メッセージをやり取りするときにProtocol関係のオブジェクトへ委譲されるため

  4. サーバー側でどんなオブジェクトを公開してようとも、クライアントが扱えるリモートオブジェクトは常にDRbObject
  5. DRbObjectを使った呼び出しであっても、自分自身がサーバー側であれば、ローカルのオブジェクトのメソッドを直接呼ぶ
  6. DRbObjectのmethod_missingからリモート呼び出しをしている
  7. メッセージの送受信を実際に行っているのはDRbMessage
  8. サーバーへの接続はDRbConnによってプールされ、接続の確立にかかるコストを抑えている

まだわからないところ

  1. puts00でサーバー側のPutsからクライアント側の@stdoutに出すにはどうしたらいいの?
  2. サーバー側は、マルチスレッドで並列にメッセージを処理している?
  3. まだmain_loop内を見ていないのでわからない
  4. fetch_serverのfriendって何?どういうときに使われるの?
  5. 通信時のMarshalを使った仕組みの詳細
  6. プールのところにある#FIXMEがなぜFIXMEなのか
  7. URIのoption引数の使い方、というか使われ方

2007年09月12日

[番外]単純な送信時のメッセージパッシングの雰囲気

今まで学んだ部分(クライアント側)をコミュニケーション図で整理してみました。が、正確ではありません。yieldや、moduleの扱いをどう描いたものか難しいですねえ。。クラスメソッドとインスタンスメソッドも区別していません。細かいところも結構端折ってます。なので、あくまでも雰囲気ということで。

(画像をクリックすると別窓に表示します)

2007年09月14日

2章.Hello, dRuby (その5)

本の続きに戻ります。「2.1.4 dRubyのURI」からです。小見出しの通り、DRbServerと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)

2.1.4 dRubyのURI

ふむふむです。今までのエントリで、さらっと中身を覗いたこともあり、大体感じはわかった。ちなみに、本文中のacceptとは、クライアントからの接続があるまで待ち続けるシステムコール(もしくはシステムコールを呼び出すことのできる何か)のことだと思います。ソケット通信とかのサーバーのプログラムを自分で書くときは、大抵の場合、acceptで待ち続け、クライアントからの要求に応じてスレッドを生成したり、forkして子プロセスを作ったりすることが多いと思います(forkとは分身の術のようなものです。)。こうすることでクライアントからの複数の要求を並列に処理することが出来ます。dRubyのDRbServerのmain_loopでも、スレッドを生成して並列に処理しているのだと思います(と想像)。で次と。

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

2.1.4 dRubyのURI

DRbServerで公開するリモートオブジェクトは、フロントオブジェクトと言うのですね。で、門番とか総合受付みたいなことをさせることが多いと。図がわかりやすいです。オブジェクトを公開しているDRbServerはURIによって特定されると言うことなので、URIの理解が大事なようです。本ではしばらくURIの話が続きます。URIは下記のように一部省略できるようです。

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

2.1.4 dRubyのURI

続いて、本に従いirbで実行っと。。ちゃんとできました!!別のコンピュータとも問題なくやり取り出来ますね、dRubyのオブジェクトのやり取りは、標準でTCPSocketを使っているため、同じコンピュータのプロセス間のみではなく、別コンピュータのプロセスともやり取りできます。可能性無限大。

。。。でも、あれれ?先日のエントリで確認した限りでは、「druby://」のプロトコルでは、ポート番号の後に引数を追加して何か出来るような気がしました。

druby://ホスト名:ポート番号?オプション

URIのパース部分の正規表現はこう


/^druby:\/\/(.*?):(\d+)(\?(.*))?$/

オプションは確かDRbURIOptionになっていたやつでした。これは何に使うんだろうなあ、という疑問がわきます。メッセージ送信部分のソース(DRbMessageのsend_request)を一応みてみると

def send_request(stream, ref, msg_id, arg, b) # :nodoc:
  ary = []
  ary.push(dump(ref.__drbref))
  ary.push(dump(msg_id.id2name))
  ary.push(dump(arg.length))
  arg.each do |e|
    ary.push(dump(e))
  end
  ary.push(dump(b))
  stream.write(ary.join(''))
rescue
  raise(DRbConnError, $!.message, $!.backtrace)
end

とあります。ここのref.__drbrefは、オプション引数DRbURIOptionそのものなので、dumpされた情報には確かに含まれているようです。おそらくサーバー側でのマーシャルデータ復元時に、DRbObjectのrecv_requestとかそのあたりでよきに計らってくれるようですが、ちゃんと追いかけることができていません。このあたりの世界は、もうちょっと本を読み進めてから再挑戦することにします。

でも、内部の仕組みはともかく、オプションの利用方法くらいは知りたいなあ。と思って調べたら、咳さんのはてなに載ってました。

URIから参照を作ろうとすると、サービスの入り口のオブジェクトしか参照できないことになるんだけど、数年前に追加されたオプションを使うともうちょっと先のオブジェクトを参照できる。

druby://localhost:12345?option

「?」の後の文字列はURIのオプション引数(?)となり、frontオブジェクトにこの文字列を問い合わせた結果のオブジェクトを参照する。問い合わせはメソッドに?以後の文字列を与えて行う。

I like Ruby too. - dRubyのURIのオプション

ふむふむ、そういうことか。(てか、最近のエントリでこのサイトも紹介してくれてる!)

雰囲気を味わうために、puts00.rbをちょっと改造してみます。

require 'drb/drb'

class Puts
 def initialize(stream=$stdout)
  @stream = stream
 end
 def puts(str)
  @stream.puts("#{str} [Puts]")
 end
end

class Puts2
 def initialize(stream=$stdout)
  @stream = stream
 end
 def puts(str)
  @stream.puts("#{str} [Puts2]")
 end
end

class Front
 def initialize
  @hash = {"puts" => Puts.new, "puts2" => Puts2.new }
 end
 def [](arg)
  @hash[arg]
 end
end

uri = ARGV.shift
DRb.start_service(uri, Front.new)
puts DRb.uri
sleep

Puts2という、Putsのようなものをもうひとつ定義し、それぞれのインスタンスをハッシュに入れて切り替えられるようにしただけです。
ちなみに{}で定義できるものをハッシュといい、キーと値のペアで情報を出し入れできる便利ないれものです。なお#{hoge}は式展開といい、文字列中にオブジェクトの内容を入れることができる書き方です。Ruby好きな人は式展開が好きらしいです。

はい、で、ハッシュのキーをオプションの文字列にしました。で、クライアント側のコードは

require 'drb/drb'

there = DRbObject.new_with_uri('druby://localhost:12345?puts')
there.puts('Hello, World.')

there2 = DRbObject.new_with_uri('druby://localhost:12345?puts2')
there2.puts('Hello, World.')

とかで。するとこんな風に出ます。一応出来たのは出来た。

Hello, World. [Puts]
Hello, World. [Puts2]


今回はこれにて終了。次はリマインダーのサンプルを見ていきます。

2007年09月20日

2章.Hello, dRuby (その6)

今日は、dRuby幸福の王子本のp.16 「2.2 Reminder」からです。ここでは、Reminderのアプリケーションを作って動かしてみます。いよいよdRubyを使ってアプリケーションを作成するときが来ました。要件は本に記載されている通りです。

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

2.2 Reminder

本の図を見ると大体の構造がわかりますね。公開されるReminderのオブジェクト(フロントオブジェクト)は以下です(転載)

# 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

メモの文字列はハッシュに入れられると、で、ハッシュには1からのシリアル番号が振られ、それがキーになり添字でアクセスできるようになっていると。はい、理解しました。で、本に従い、irbからサーバーとクライアントを立ち上げて、リマインダーに登録してみます。

r.add('13:00 ミーティング')
r.to_a
=> [[1, "13:00 ミーティング"]]

無事登録できましたね。それから本の通り、ターミナルの数を増やしたり、クライアントを落としてから再度立ち上げたりしてみましたが、今まで登録した内容はちゃんと残っています。めでたし。

ところで、ここで

  1. サーバーを落とす
  2. もう一度サーバーを立ち上げる
  3. クライアントから操作する

とすると、今までリマインダーに登録した情報は消えています。つまりリマインダーの情報はサーバー側が持っているのですね。ここをもうちょっと調べるために、通信の内容を間単に見てみます。

サーバー
192.168.0.4
ポート
9898
クライアント
192.168.0.2

としています。つまり、サーバーのURIは
druby://192.168.0.4:9898/
です。

クライアントから、

r = DRbObject.new_with_uri('druby://192.168.0.4:9898')
r.add 'foo'
r.add 'fooo'
r.add 'foooo'

としてみました。サーバーもクライアントもirbで起動しています。以下はそのパケットのダンプです。

(画像をクリックすると別窓に表示します)

送信のたびにデータ部(データ部とはTCPパケット内の、アプリケーションが自由に使えるデータが含まれる箇所の名前です。)のサイズが44,45,46と1byteずつ大きくなっているのは、addの引数の文字数の差だと思われます。サーバーから送られてくる15byteはメソッドの戻りだと思われます。
データの中身を見なくとも、送信されるデータが44byteと小さいですし、戻されるデータはリマインダーのデータ量に依存せず常に小さいので、本当にメソッド呼び出しだけしかしていない(クライアント側にリマインダーのフロントオブジェクトそのものが渡ってきているわけではない)ことがわかります。

 では次に、フロントオブジェクトそのものではなく、フロントオブジェクトが参照している他のインスタンス(@itemとか)について見てみます。サーバー側のirbでリマインダーに情報を追加してみます。

irb(main):008:0> r.add 'hogehoge'
=> 1

それからサーバー側でオブジェクトIDを見てみます。ちなみに、__id__で返される数値のことをここではオブジェクトIDと呼んでいます。これは、各オブジェクトの間で重複しないように付けられた識別番号(住民票コードのようなもの)です。もちろんプロセスが違っていれば同じ値が振られることもありますが、同一プロセスの中ではユニークになります。


irb(main):011:0> str = r[1]
=> "hogehoge"
irb(main):012:0> str.__id__
=> 21567584
irb(main):013:0> r[1].__id__
=> 21567584

ふむふむ、続いてクライアント側のirbで同様のことをしてみます。

irb(main):009:0> str = r[1]
=> "hogehoge"
irb(main):010:0> str.__id__
=> 21563792
irb(main):011:0> r[1].__id__
=> 21778008

 おや?クライアント側は、代入した時点で別のオブジェクトIDになっています。つまりstrとr[1]は別物です。これは多分、クライアント側の

irb(main):009:0> str = r[1]

 この行で、r[1]をサーバー側に問い合わせた時点で、オブジェクトの参照ではなく複製されたものが取得されるからでしょう。つまりr[1]は値渡しです。通常のRubyプログラミングと違いますね。このあたりは多分色々と難しいことが多そうなので、本を読み進めながら徐々に理解していきたいと思います。

 で、それから後ですが、ReminderCUIを作って遊び、「2.3 まとめ」をふむふむと読みました。はい、これで「2章:Hello, dRuby」は終わりにしたいと思います。結構長かったけど、dRubyの雰囲気は一応理解できた!つもりです。

 本だと、この次は「3章:eRuby」に続きますが、このサイトでは「3章:eRuby」を飛ばして「4章:参照渡しと値渡し」に入ります。「3章:eRuby」を飛ばす理由は、Webアプリケーションの話が入ってくると、多分dRubyそのもの以外に色々と面倒なことが出てきて混乱しそうだからです。このサイトでは、まずdRubyのことを学びたいので、Webアプリケーションで活用することは後回しにしたいと思います。

 「4章:参照渡しと値渡し」では、今日のこのエントリで見た付近の仕組みなども解説されているようなので、今から楽しみにしておきます。

 dRubyは結構脳ミソが刺激されていいです。

About 2章

ブログ「dRubyの正面」のカテゴリ「2章」に投稿されたすべてのエントリーのアーカイブのページです。過去のものから新しいものへ順番に並んでいます。

前のカテゴリは1章です。

次のカテゴリは4章です。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。