はやし雑記

はやしです

C#でプロセス間通信をする (WCF)

モチベーション

C#でプロセス間通信をしたので、備忘録代わりに残しておきます。

普段の生活で(研究とかで)Windows Presentation Foundation (WPF)を使って簡単なソフトウェアを作ることはよくあります。大抵はUIスレッドのみで完結し、たまにマルチスレッドにするくらいで、マルチプロセスにすることなんて滅多にありません。 しかし、最近とあるDLL内の関数を同一のプロセスから叩くと、なぜか正常に機能しないということがありました。なので、仕方なくマルチプロセスにした、という次第です。

普段はmacを使っていて、WPFも完全には理解できていないので、変なところがあるかもしれませんが、コメントなりPRなり頂けると幸いです。

github.com

Windows Communication Foundation (WCF)

今回はWCFというものを使ってプロセス間通信を行います。WCFとはなにか、については↓

www.atmarkit.co.jp

今回はこのWCFを使って、クライアントアプリからホストアプリ2つを呼び出す、という感じのものを作ります。

wcf_sample

今回作ったwcf_sampleについて軽く説明します。

f:id:hayashikunsan:20180603155452p:plain

Solution 'wcf_sample'はwcf_sample, host, wcfの3つから成ります。wcf_samplehostはWPFアプリで、wcfは.NET Frameworkのクラスライブラリです。

クライアントアプリwcf_sampleからホストアプリhostを呼び出すという感じの構成です。通信関連の処理はwcfにカプセル化し、wcf_sample, hostwcfを参照しています。

WCFで通信を行うためには、System.ServiceModelのアセンブリを参照する必要があるので、wcfSystem.ServiceModelを参照します。

f:id:hayashikunsan:20180603154125p:plain

実装するのは、以下の2つの内容です。

  1. ホストアプリに入力された内容を、クライアントアプリから取得する
  2. クライアントアプリの内容を、ホストアプリに反映させる

こんな感じで動きます。

https://www.youtube.com/watch?v=IELotyjpYF4


wcf_sample

wcf

wcfではWCFに関連する処理をカプセル化しています。

含まれるのは、以下のwcf.csのみです。

using System;
using System.ServiceModel;

namespace wcf
{
    public struct Data
    {
        public string text;
        public int number;
    }

    public class WCF
    {
        static string Address = "net.pipe://localhost/wcf_sample";

        [ServiceContract(CallbackContract = typeof(ICallback))]
        public interface IHost
        {
            [OperationContract(IsOneWay = true)]
            void LoadData();

            [OperationContract(IsOneWay = true)]
            void UpdateSlider(double val);
        }

        [ServiceContract]
        public interface ICallback
        {
            [OperationContract(IsOneWay = true)]
            void SendData(Data data);

            [OperationContract(IsOneWay = true)]
            void Send();
        }

        [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
        public class Host : IHost
        {
            
            public delegate Data LoadDataListener();
            public delegate void UpdateSliderListener(double val);
            private LoadDataListener loadDataListener;
            private UpdateSliderListener updateSliderListener;


            public void LoadData()
            {
                var data = loadDataListener();
                ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>();
                callback.SendData(data);
            }

            public void UpdateSlider(double val)
            {
                updateSliderListener(val);
                ICallback callback = OperationContext.Current.GetCallbackChannel<ICallback>();
                callback.Send();
            }

            public static void StartHost(string id, LoadDataListener loadDataListener, UpdateSliderListener updateSliderListener)
            {
                var host = new Host() { loadDataListener = loadDataListener, updateSliderListener = updateSliderListener };
                var serviceHost = new ServiceHost(host);
                var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);

                try
                {
                    serviceHost.AddServiceEndpoint(typeof(IHost), binding, Address + id);
                    serviceHost.Open();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
            }
        }

        public class Callback : ICallback
        {
            private string id;
            public delegate void DataHandler(string id, Data data);
            public delegate void Handler(string id);
            private DataHandler dataHandler;
            private Handler handler;

            public void SendData(Data data)
            {
                dataHandler(id, data);
            }

            public void Send()
            {
                handler(id);
            }

            public static IHost ConnectHost(string id, DataHandler dataHandler, Handler handler)
            {
                var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);

                Callback callback = new Callback() { id = id, dataHandler = dataHandler, handler = handler };

                var host = new DuplexChannelFactory<IHost>(callback,
                    new NetNamedPipeBinding(NetNamedPipeSecurityMode.None),
                    new EndpointAddress(Address + id)).CreateChannel();
                return host;
            }
        }
    }
}

ホストからクライアントに送る内容はstruct Dataを定義しています。

IHostICallbackでどのような形式で通信を行うかを定義します。この2つは別のソースファイルでも、名前空間が異なっていても構わないので、同一の内容である必要があります。

今回はwcfにまとめてしまっていますが、ホストアプリではIHostを実装したクラス(ここではHost)に実際に呼ばれたときの挙動を実装し、クライアントアプリではICallbackを実装したクラス(ここではCallback)にホストアプリからの応答を実装します。

今回の場合、クライアントアプリからIHost#LoadDataを叩くと、ホストアプリでHost#LoadDataが呼ばれ、その中でICallback#SendDataを叩くと、クライアントアプリのCallback#SendDataが呼ばれる、という流れです。

ホストアプリはホスティングをするためにServiceHost#openをする必要があります。ここでは、Host.StartHostに記述しています。 イメージとしてはサーバーを起動するような感じです。

クライアントアプリはホストに接続しに行きます。Callback.ConnectHostに記述しています。 今回のように双方向で通信を行う場合はDuplexChannelFactoryIHostを取得します。この取得したIHostを使って、ホストアプリにリクエストを送るような感じになります。

WCFで接続をする際には、ホストとクライアントで共通でユニークなアドレスを指定する必要があります。 今回の場合2つのホストを起動するので、Address = "net.pipe://localhost/wcf_sample"idを付与したものをアドレスとして指定しています。

このlocalhostにLAN内のIPを指定したら他のマシンのプロセスと通信できるんでしょうか?そのうち試してみたいと思います。

wcf_sample, host

wcf_samplehostはそれぞれ以下のような画面です。

f:id:hayashikunsan:20180603181921p:plain

wcf_sampleでReloadボタンを押すと、WCF経由でhostのTextとNumberのTexBoxに入れた内容が取得されます。

また、wcf_sampleのSliderを動かすと、その値がWCF経由でhostに送られ、hostのSliderに反映されます。

まず、クライアント側(wcf_sample)の主要な実装です。

private void StartHost()
{
    var p1 = Process.Start(@"..\..\..\host\bin\Debug\host.exe", "1");
    var p2 = Process.Start(@"..\..\..\host\bin\Debug\host.exe", "2");

    p1.WaitForInputIdle();
    p2.WaitForInputIdle();
    hosts = new WCF.IHost[] {
        WCF.Callback.ConnectHost("1", HandleData, Handle),
        WCF.Callback.ConnectHost("2", HandleData, Handle)
    };
}

private void HandleData(string id, Data data)
{
    switch (id)
    {
        case "1":
            H1TextTextBox.Text = data.text;
            H1NumberTextBox.Text = data.number.ToString();
            break;
        case "2":
            H2TextTextBox.Text = data.text;
            H2NumberTextBox.Text = data.number.ToString();
            break;
    }
    Console.WriteLine("HandleData");
}

private void Handle(string id)
{
    Console.WriteLine("Handle");
}

private void H1Load_Click(object sender, RoutedEventArgs e)
{
    hosts[0].LoadData();
}

private void H1Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    hosts[0].UpdateSlider(H1Slider.Value);
}

wcf_sampleが起動されたときに、hostを2つ起動します。(多分パスの指定はもっといい方法があると思います)このとき、コマンドライン引数でidを渡します。("1""2") hostが起動したら、接続して、返ってきたIHostを保持しておきます。

Reloadボタンが押されると、IHost#LoadDataが、Sliderの値が変更されるとIHost#UpdateSliderが叩かれます。

次にホスト側(host)の実装です。

public MainWindow()
{
    InitializeComponent();

    string[] args = Environment.GetCommandLineArgs();
    if (args.Length >= 2)
    {
        id = args[1];
        Title = "Host: " + id;
    }
    WCF.Host.StartHost(id, LoadData, UpdateSlider);
}

private Data LoadData()
{
    var data = Dispatcher.Invoke(new Func<Data>(() =>
    {
        int number = -1;
        int.TryParse(NumberTextBox.Text, out number);
        return new Data() { text = TextTextBox.Text, number = number };
    }));
    return data;
}

private void UpdateSlider(double val)
{
    Dispatcher.Invoke(new Action(() => Slider.Value = val));
}

ホストアプリが起動されると、Host.StartHostでホスティングを開始します。

クライアントアプリからIHost#LoadDataが呼ばれると、delegeteを経由して上記のLoadDataが呼ばれます。 ここで、TextBoxの内容を取得して、callbackによってクライアントに返ります。

1点注意すべきことは、WCF経由で呼ばれる処理はUIスレッドでは無いので、TextBoxなどのUIコンポーネントを普通に呼ぶとエラーが起きます。 なので、Dispatcher.Invokeを使いましょう。

How to run

hostをビルドしていないと、Process.Startができないので、まずhostをビルドしましょう。 その後、wcf_sampleを実行すれば動くはずです。

デバッグをする場合

wcf_sampleを実行すると、hostのデバッグがやりにくいですが、「デバッグ>プロセスにアタッチ」からプロセスにアタッチすると、ホストアプリでもブレークポイントを張ったりできます。

f:id:hayashikunsan:20180603192225j:plain

ホストアプリのほうでエラーが起きて、正常にCallbackに処理が返らないと、クライアント側でCommunicationObjectFaultedExceptionが出ます。 WCFの処理の途中でエラーが起きてもUIが固まったりしない(非UIスレッドなので)ので、わかりにくいですが、そういうときは、プロセスにアタッチすると良いでしょう。

あとがき

WPFは良いフレームワークだけど、C#を書くのがつらい…… SwiftでWPFを使いたいなぁ〜