C# アプリ間通信を調べてたら、クラス・インスタンス共有を知った
概要と経緯
- アプリ同士の通信をやりたい
- いろいろ方法はあるっぽい
- Socket通信ってので実現できるらしいが、古い記事が多かったり、面倒くさそうで回避
- 別の方法調べたらもう少し楽できそうだったのでそっちの方法に。
関係ありそうな主要キーワード
「クライアント」は、アクションを起こす・投げる側
「サーバー」は、アクションを受け取って、何かする側
Microsoft .NET の公式記事(上記リンク)がすごく役に立ったので、そのソースをベースに書き進めています
それぞれ見出し的に
- クラス(型)のみ共有
- クラス(型) および インスタンス共有
- クラス(型) および インスタンス共有 および 引数を送る
の3つを書いています
クラスが共有できるらしい。書いてみる
1.サーバー側処理
一部抜粋
{
// 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 の簡単な説明としては
リモート処理をサポートするアプリケーションで、 アプリケーションのドメインの境界を越えてオブジェクトにアクセスできるようにします。
との事。(なるほど分からん)
MarshalByRefObject クラスの力を借りるだけで面倒くさそうな処理がいとも簡単に...すごいですね。
さて、次はクライアント側のソースです
2.クライアント側処理
先程のサーバー側では 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クラス(をインスタンス化させたもの)と値自体は共有されない事に注意してください
しかもこの状態ではまだ「サーバーからクラスを受け取って、クライアントで利用しているだけ」に過ぎないので
双方向通信とはちょっと言いづらいですね。
そこで、もうちょっとそれっぽくしてみます。
今度はインスタンスそのものを共有できる方法です
インスタンスも共有が出来るらしい。書いてみる
A.サーバー側
//フィールドにインスタンスを用意
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
クラスのインスタンスに替えます
B.クライアント側
//フィールドにインスタンス用の変数を用意(あとで代入される)
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()
で受け取れるようになります。