.NET Remoting IPC 双方向通信

MS的にはWCFにすべきだったのかもしれないが、同一コンピュータ上でのプロセス間通信としてIPCを利用した方式を使ってみた。

以下のURLを読んでおくといい。コード例は特に最後のURLが短くて分かりやすい。

概要

IPCを利用してクライアントとサーバの2プロセス間で

  • クライアントからサーバ側のメソッドを呼び出す。
  • クライアントからサーバ側にコールバック関数を登録して、サーバ側から→クライアントに呼び返す。

プロジェクトを3つ作成する。

  1. サーバとクライアントで共有するクラスなどを定義するDLLのプロジェクト
  2. サーバ側実行モジュール
  3. クライアント側実行モジュール

DLLのプロジェクト

サーバ⇔クライアントで互いに参照するものはMarshalByRefObject、受け渡すデータはSerializableにする。

  • サーバ→クライアント方向のコールバック関数で受け渡すデータのクラスを定義する。Serializableにする。
  • コールバック関数のdelegateを定義する。↑で作ったSerializableのクラスをパラメーターで受け取るようにする。
  • コールバック関数のdelegateを格納するクラスを定義する。MarshalByRefObjectにする。
    • このクラスをクライアント側で生成して、サーバに参照を渡す形になる。
    • 特に[OneWay]は不要。IPCは下回りは名前付きパイプだから?
  • サーバ側で公開するインターフェースを定義する。MarshalByRefObjectにする。
    • このクラスはサーバ側で生成して、クライアントから参照する形になる。
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.Web.Script.Serialization;

    // コールバック関数
    public delegate void DelegateOnEvent(EventArg eventArg);
    
    /// <summary>
    /// サーバ→クライアントへ渡すパラメタ
    /// </summary>
    [Serializable]
    public class EventArg : ISerializable
    {
        public Dictionary<string, string> params;

        public EventArg(Dictionary<string, string> params)
        {
            this.params = params;
        }

        /// <summary>
        /// 逆シリアル化
        /// </summary>
        /// <param name="info"></param>
        /// <param name="context"></param>
        [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
        protected EventArg(SerializationInfo info, StreamingContext context)
        {
            // 自前でやるときは実装する。以下は一例。
            var ser = new JavaScriptSerializer();
            var json = info.GetString("params");
            this.configuration = ser.Deserialize<Dictionary<string, string>>(json);
        }

        /// <summary>
        /// シリアル化
        /// </summary>
        /// <param name="info"></param>
        /// <param name="context"></param>
        [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)]
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            // 自前でやるときは実装する。以下は一例。
            var ser = new JavaScriptSerializer();
            var json = ser.Serialize(params);
            info.AddValue("params", json);
        }
    }

    /// <summary>
    /// クライアントがコールバック関数を登録する際に使用する。
    /// </summary>
    public class EventCallbackSink : MarshalByRefObject
    {
        public event DelegateOnEvent OnEvent;

        public EventCallbackSink()
        {
        }

        public void CallbackToClient(EventArg evt)
        {
            if (OnEvent != null)
            {
                OnEvent(evt);
            }
        }
    }

    /// <summary>
    /// サーバが外部プロセスに対して公開するインタフェース
    /// </summary>
    public class IpcServerApi : MarshalByRefObject
    {
        private static List<DelegateOnEvent> eventListeners = new List<DelegateOnEvent>();

        public void AddEventListener(DelegateOnEvent listener) 
        {
            eventListeners.Add(listener);
        }

        public static void RaiseEventToClient(Dictionary<string, string> params)
        {
            EventArg evt = new EventArg(params);

            foreach (var listener in eventListeners)
            {
                listener(evt);
            }
        }
        
        /// <summary>
        /// このインターフェースの有効期間を無制限にする。
        /// </summary>
        /// <returns></returns>
        public override object InitializeLifetimeService()
        {
            return null;
        }
    }

サーバ側

  • IpcChannelを生成する→RegisterChannel()
    • 双方向の場合IpcServerChannelではなく、IpcChannel。
    • BinaryServerFormatterSinkProvider の BinFormatter.TypeFilterLevel = TypeFilterLevel.Fullを指定する。
      • 指定しないと「セキュリティ レベルでは逆シリアル化することが許可されていません」エラーとなる。
  • RemotingConfiguration.RegisterWellKnownServiceType() あるいは RemotingServices.Marshal() で公開する。
using System.Collections.Generic;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;

            // IPCチャンネルの生成
            string portName = "aaaaaaaa";
            var channelProperties = new Dictionary<string, string>();
            channelProperties.Add("portName", portName);
            var channel = new IpcChannel(channelProperties, null,
                new BinaryServerFormatterSinkProvider
                {
                    TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full,
                });
            ChannelServices.RegisterChannel(channel, true);

            var server = new IpcServerApi();

            RemotingServices.Marshal(server, "bbbbbbbb", typeof(IpcServerApi));

            // ...中略...

            // イベント送信時
            var params = new Dictionary<string, string>();
            server.RaiseEventToClient(params);

クライアント側

  • IpcChannelを生成する→RegisterChannel()
    • 双方向の場合IpcClientChannelではない。
    • BinaryServerFormatterSinkProvider の BinFormatter.TypeFilterLevel = TypeFilterLevel.Fullを指定する。
  • Activator.GetObject()でサーバ側のオブジェクトを取得する。
  • コールバック関数の登録
using System;
using System.Collections.Generic;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Ipc;

            // IPCチャンネルの生成
            string portName = "cccccccc";
            var channelProperties = new Dictionary<string, string>();
            channelProperties.Add("portName", portName);
            var channel = new IpcChannel(channelProperties, null,
                new BinaryServerFormatterSinkProvider
                {
                    TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full,
                });
            ChannelServices.RegisterChannel(channel, true);

            // リモートオブジェクトを取得
            var server = Activator.GetObject(typeof(IpcServerApi), "ipc://aaaaaaaa/bbbbbbbb") as IpcServerApi;
            // リスナの登録
            var callbackSink = new EventCallbackSink();
            callbackSink.OnEvent += new DelegateOnEvent(EventListener);
            var callbackDelegate = new DelegateOnEvent(callbackSink.CallbackToClient);
            server.AddEventListener(callbackDelegate);

        public void EventListener(EventArg evt)
        {
        }

問題点

  • クライアント側がプロセスを終了して、また起動して、リスナーを登録しなおした時、何かおかしい。
    • EventCallbackSink()を生成したクライアント プロセスが終了しても、サーバ側のMarshalByRefObjectが残っていておかしい?
    • InitializeLifetimeService()をもう少しキチンと実装すべきなのか?