C# アプリ間通信を調べてたら、クラス・インスタンス共有を知った

概要と経緯

関係ありそうな主要キーワード

「クライアント」は、アクションを起こす・投げる側
「サーバー」は、アクションを受け取って、何かする側

Microsoft .NET の公式記事(上記リンク)がすごく役に立ったので、そのソースをベースに書き進めています

それぞれ見出し的に

の3つを書いています

クラスが共有できるらしい。書いてみる

サーバー側

一部抜粋

{
  // IPCチャンネルをサーバー側として作成&登録
  // これにより、自身はサーバー "ipc://remote" としてアクセスを受け付ける
  IpcServerChannel serverChannel = new IpcServerChannel("remote");
  ChannelServices.RegisterChannel(serverChannel);

  // Counter という型、クラスを「ウチのこれ使えますよ~」とアプリの外側に周知させる
  // これによりクライアント(相手)は "ipc://remote/counter" にアクセスして Counterクラスを受け取れるようになる
  RemotingConfiguration.RegisterWellKnownServiceType( typeof(Counter), "counter",   ellKnownObjectMode.Singleton );

  // 呼び出し待ち状態となる
  Console.WriteLine("Listening on {0}", serverChannel.GetChannelUri());
}

Counterクラス(型)

public class Counter : MarshalByRefObject {

  private int count = 0;

  public int Count { get {
    return(count++);
  } }

}

先ほど「ウチのこれ使えますよ~」と周知処理させていた型です。
自前で好きなものを作りましょう。
なお、この型は「名前空間の外」に書いておきましょう
そうしないと、

型 ApplicationName.Counter, ApplicationName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null を読み込めません

のようなエラーを目にすることになるかも…Σ(´ ` )

共有させるクラスは MarshalByRefObject を継承して作る

今回サンプルとして用意したCounterクラスは MarshalByRefObject というクラスを継承してこさえています。

MarshalByRefObject の簡単な説明としては

リモート処理をサポートするアプリケーションで、 アプリケーションのドメインの境界を越えてオブジェクトにアクセスできるようにします。

との事。(日本語でおk)
MarshalByRefObject クラスの力を借りるだけで面倒くさそうな処理がいとも簡単に…すごいですね。
さて、次はクライアント側のソースです

クライアント側

さっきのはIpcServerChannelですが、
こちらはIpcClientChannelクラスを用いています

一部抜粋

{
  // IPCチャンネルをクライアント側として作成&登録
  IpcClientChannel clientChannel = new IpcClientChannel();
  ChannelServices.RegisterChannel(clientChannel);

  // 「ウチのこれ使えますよ~」と別のアプリ(サーバー側)が触れ回っていた Counter という型、クラスを
  // クライアント側が自分のもとに引っ張り出して、扱えるようにする
  RemotingConfiguration.RegisterWellKnownClientType( typeof(Counter) , "ipc://remote/counter" );

  // 相手から引っ張り出してきたクラスをインスタンス化
  Counter counter = new Counter();

  // 実際に自分の所で使ってみる。
  Console.WriteLine("This is call number {0}.", counter.Count);
}

Counterクラス(型)

さて、こちらでも似たようなクラスをコードに書いてあげます
ただし、此方は中身がない状態でOKです。うわべだけ。処理なし。

//クライアント側
public class Counter : MarshalByRefObject
{   
  public int Count { get { throw null; } }
}


先ほどサーバー側で書いた Counterクラス と比較してみましょう

//サーバー側
public class Counter : MarshalByRefObject {

  private int count = 0;

  public int Count { get {
    return(count++);
  } }
}

内部の count フィールドは private (隠匿されている)なので、クライアント側は、使うときにその情報を知る必要はありません。
Countプロパティがあるという情報のみ知っていればOKな訳です。

寸劇風に書くと…

A「ねぇねぇ、サーバー側から Counter っていうクラスが送られてくるらしいよ」
A「ウチらにそれ使ってくれって上からお達しが来てさ~」
B「へ~、どういうクラスなの?」
A「さー、よくわかんないけど、説明書だけ※1 渡された。読んでみて」
B「どれどれ…えっと」
B『CounterクラスはMarshalByRefObjectを継承して作られているようです』
A「ふむふむ」
B『int型のCountプロパティがあるので、そこから数を受け取ってください』
B『実際は中にprivateフィールドとかあるんですが、貴方達がそれを知る必要はありません』
B「…だってさ」
A「なるほどね~、使う側は内部処理を気にする必要が無い…か。 わかりみが深~い」

※1の説明書が、クライアント側のソースに記載した「上辺だけのクラス」って訳です。
「一応、我々も使う立場なんで “定義だけ” は知っておいてね」みたいな意図ですね。

これら2つの処理をそれぞれ組み込んだ、サーバー側アプリ・クライアント側アプリを立ち上げて検証してみます

クライアント側で、受け取ったCounterクラスをインスタンス化させた後、使ってみます。

Windowフォームにボタンでも作っておいて、そこで押すたびに
この処理だけ走るようにしておけば、わかりやすいかも。

//クライアント側アプリにて、何かのClickイベント内で呼ぶ
{
  //これを実行(呼び出)した分だけ、内部カウンターが増える
  Console.WriteLine("This is call number {0}.", counter.Count);
}

こうすると、サーバー側で記載した処理能力を持つCounterクラスを、 クライアント側のソースコードで利用することができます。

クライアント側でボタンを押すたび、0から始まった値が順に増え続けるというものです
しかし、その値を保持しているのはあくまでCounterクラスをインスタンス化させたクライアント側のみであり、
ここ(クライアント側)でいくらボタンを押したからといって、
サーバー側のCounterクラス(をインスタンス化させたもの)と値自体は共有されない事に注意してください

しかもこの状態ではまだ「サーバーからクラスを受け取って、クライアントで利用しているだけ」に過ぎないので
双方向通信とはちょっと言いづらいですね。

そこで、もうちょっとそれっぽくしてみます。
今度はインスタンスそのものを共有できる方法です

インスタンスも共有が出来るらしい。書いてみる

サーバー側

//フィールドにインスタンスを用意
public Counter counter = new Counter();

//メイン処理
public void mainProcess()
{
  IpcServerChannel serverChannel = new IpcServerChannel("remote");
  ChannelServices.RegisterChannel(serverChannel);

  //処理を差し替え。
  //Counter クラスから作ったインスタンスを「これ使ってね~」と登録。
  //"ipc://remote/counter" で相手方が得られるのは変わらない
  RemotingServices.Marshal(Counter, "counter", typeof(Counter));

  // 呼び出し待ち状態となる
  Console.WriteLine("{0}で待ってまーす", serverChannel.GetChannelUri());
}

//Counterクラス
public class Counter : MarshalByRefObject {
  private int count = 0;
  public int Count { get {
    return(count++);
  } }
}

//用意できたら、何かのクリックイベント等で呼び出してみる
{
  Console.WriteLine("カウント呼び出し {0}",counter.Count);
}

RemotingConfiguration.RegisterWellKnownServiceTypeの代わりに
RemotingServices.Marshalが用いられました。

登録に用いる引数も、Counterクラス(型) から、Counterクラスのインスタンスに替えます

クライアント側

//フィールドにインスタンス用の変数を用意(あとで代入される)
public Counter counter = null;

//メイン処理
public void mainProcess(){
  IpcClientChannel clientChannel = new IpcClientChannel();
  ChannelServices.RegisterChannel(clientChannel);
  //処理を差し替え。
  //"ipc://remote/counter" から引っ張ってきた Counterクラスが使えるようになるのは変わらない
  var endPointProxy = Activator.GetObject(typeof(Counter), "ipc://remote/counter");
     
  //変換して使う
  this.counter = (Counter)endPointProxy;
}

//Counterクラス (うわべだけ)
public class Counter : MarshalByRefObject
{   
  public int Count { get { throw null; } }
}

//用意できたら、何かのクリックイベント等で呼び出してみる
{
  Console.WriteLine("カウント呼び出し {0}.", this.counter.Count);
}

RemotingConfiguration.RegisterWellKnownClientType の代わりに
Activator.GetObject といったものが出てきました。

Activator.GetObject() の戻り値は、

要求した既知のオブジェクトによって提供されたエンドポイントを指すプロキシ

との事。 (日本語でおk)

「”ipc://remote/counter” に繋いで引っ張ってきた、Counter型の”なにか”」っていう表現がわかりやすいでしょうか。

その “なにか” はサーバー側で登録した Counterクラスを実体化させたインスタンスです。

これをキャストして使う感じになるみたいです。
前の書き方だと、new Counter() でインスタンス化していましたが、
今回得るものはクラス(型)ではなく、インスタンス自体というのが大きな違いですね

あと、上記の様に endPointProxy 変数を用意しなくても、
こんな感じで、のっけからキャストするのももちろんアリです

Counter counter = (Counter)Activator.GetObject(typeof(Counter), "ipc://remote/counter");

と、まぁこれでプロセス間の通信というか、インスタンス共有ができたことになります
(間借りのほうが、まだ適切な表現だとは思いますが)

サーバー側とクライアント側でそれぞれ、counter.Count を呼び出すと
互いの呼び出した回数が、どちらから呼んでも上乗せされるのが確認できるかと思います。

インスタンスに限らず、引数も渡せるらしい。書いてみる

インスタンスが共有できるので、さらに改造して引数を渡せるようにしてみます
ここでは String をクライアント側がサーバー側に渡せるようにしています
先ほどと比べると、いささか複雑さを感じるかも (+_+;

サーバー側

//フィールドに ShareClass から作ったインスタンスを用意
public ShareClass shareInstance = new ShareClass();

//メイン処理
public void mainProcess()
{
  IpcServerChannel serverChannel = new IpcServerChannel("hoge_channel");
  ChannelServices.RegisterChannel(serverChannel);
	
  //インスタンスの共有登録をする前に、
  //前もってインスタンスのフィールドに対して、サーバー側処理のHogeMethodメソッドを登録しておく。
  shareInstance._fieldMsg += new Counter.CallEventHandler(HogeMethod);

  // shareInstance を「これ使ってね~」と登録。
  // "ipc://hoge_channel/share" で相手方が得られるのは変わらない
  RemotingServices.Marshal(shareInstance, "share", typeof(ShareClass));
}

//共有されるインスタンスの実処理
//最終的に、相手方にこのインスタンスの .Message() を呼んでもらう
public class ShareClass : MarshalByRefObject {

  private int count = 0;

  // 任意の処理を登録用させるための変数
  public CallEventHandler _fieldMsg;

  // デリゲートを定義して、登録した変数をメソッドとして扱えるようにする
  public delegate void CallEventHandler(string _message);

  // 受け皿に用意したメソッド。ワンクッション置く。
  // _fieldMsg に委譲してある処理(メソッド)に引数を渡して呼ぶ
  public void Message(string _message)
  {
      if (_fieldMsg != null)
      {
          this._fieldMsg(_message);
      }
  }
  public int Count { get { return(count++); }	}
}

//共有したインスタンスのMessage()を相手方が呼び出した場合、
//結果的にサーバー側でこのメソッドに引数が渡され、実行される。
public void HogeMethod(string _Message)
{
  //処理
  Console.WriteLine("相手方から処理が呼び出されました。送られた引数:{0}" , _Message);
}

クライアント側

//メイン処理
public void mainProcess()
{

  IpcClientChannel icc = new IpcClientChannel();
  ChannelServices.RegisterChannel(icc, false);

  //"ipc://hoge_channel/share" から引っ張ってきた、
  //ShareClassクラスのインスタンスが使えるようになるのは変わらない
  ShareClass shareInstance = (ShareClass)Activator.GetObject(typeof(Counter), "ipc://hoge_channel/share");

  shareInstance.Message("クライアント側から送るメッセージです。アイアム文字列引数!");
}

//ShareClassクラス定義(こちらは中身がなくてOK)
public class ShareClass : MarshalByRefObject
{   
  public int Count { get { throw null; } }

  public void Message(string sMessage) { }
}

こんな感じの実装だと、クライアント側でshareInstance.Message()を利用して送ったメッセージを、
サーバー側の HogeMethod() で受け取れるようになります。

他参考になったサイト