[win8] metro-desktopのプロセス間通信をWeb API風にする

[win8] MetroアプリからDesktopアプリへWCFで接続する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3387
[win8] metro アプリケーションからデスクトップアプリにプロセス間通信する | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3379

# 追記 2012/05/12
# 再度確認したところ、localhost によるループバック接続はパッケージを作った時は駄目で、Visual Studio からデバッグ実行しただけ接続できます。このあたり、hosts 書き換え、ip 指定でも駄目なので、別の方式を考えないと。以下は、参考のため残しておきます。
# 業務的には、別マシンに proxy を立てて localhost->proxy->localohst にすれば ok なんですが、もうちょっとうまい方法を考えますか。ネットワーク負荷がかかるし。

なところで、WCF を使ってプロセス間通信できることは確認できたのですが、WCF だと metro アプリのほうで web の参照設定をしないといけません。まぁ、製品的にサーバーが先に固定化されている場合はいいのですが、流動的に作っている場合は先にインターフェースを決めないといけないというのはちょっと酷です。

な訳で、Web API 風に GET コマンドのアドレスを使って metro アプリ(クライアント)から desktop アプリ(サーバー)へ送信できるようにします。

■クライアントの metro アプリ側

private async void Button_Click_1(object sender, RoutedEventArgs e)
{
    string text = textBox1.Text;
    string url = "http://localhost:8083/metro/method";

    HttpClient client = new HttpClient();
    HttpResponseMessage res = await client.GetAsync(url + "/" + textBox1.Text);
    string response = await res.Content.ReadAsStringAsync();
    textBox2.Text = response;
}

metro アプリのほうでは、アドレスを指定してサーバーに接続します。Web API の REST のように「http://localhost:8083/metro/method/masuda/1000」とか「http://localhost:8083/metro/method?name=masuda&num=1000」のように呼び出すことを想定します。ここのサンプルでは適当に呼出URLを作っているだけなので、URLを作るための適当なラッパを作ると良いでしょう。

応答は、XML 形式でもよいのですが、自由に。

■Web APIを提供する desktop アプリ

HttpListener.BeginGetContext メソッド (System.Net)
http://msdn.microsoft.com/ja-jp/library/system.net.httplistener.begingetcontext(v=vs.110).aspx

を参考にして、HttpListener クラスでサーバーを作ります。前回は、同期メソッドを使ったために backgroundWorker コンポーネントでスレッドを使いましたが、今回は非同期メソッドの BeginGetContext、EndGetContext メソッドを使います。上記のサンプルコードではコールバック関数が static になっていますが、下記のように普通の内部メソッドを使うことができます。つーか、内部メソッドを使ったほうが、ListBox などの GUI にアクセスできるので便利かと。

using System.Net;
using System.IO;

namespace SampleLocalWebApiServer
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        HttpListener listener;

        private void button1_Click(object sender, EventArgs e)
        {
            string url = "http://*:8083/metro/";
            // 開始
            listener = new HttpListener();
            listener.Prefixes.Add(url);

            listener.Start();
            listBox1.Items.Add("サーバー開始");
            listener.BeginGetContext(ListenerCallback, listener);
        }

        public void ListenerCallback(IAsyncResult result)
        {
            // HttpListener listener = (HttpListener)result.AsyncState;
            HttpListenerContext context;
            try
            {
                context = listener.EndGetContext(result);
            }
            catch
            {
                // stop メソッドで例外が発生するので、対処
                return;
            }
            // var content = listener.GetContext();
            var req = context.Request;
            var url = req.RawUrl;
            var res = context.Response;

            listBox1.Items.Add("受信");

            var output = new StreamWriter( res.OutputStream ) ;
            output.WriteLine(string.Format("called {0}", url));
            output.Close();

            // 次の受信の準備
            listener.BeginGetContext(ListenerCallback, listener);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            // 終了
            listener.Stop();
            listBox1.Items.Add("サーバー終了");
        }
    }
}

サーバーの停止なのですが、Stop か、Abort を呼び出します。が、ちょっと面倒なのは、Stop メソッドを呼び出した途端にコールバックが呼び出されるんですよね…で、EndGetContext メソッドの呼び出し時に例外が発生してしまうので、コードでは try-catch で、回避しています。異常終了の場合と区別がつかないので、もうちょっとなんとかしたいところですね。

呼び出した URL は、RawUrl プロパティで取得できます。URL プロパティでもいいのですが、RawURL のほうが、サーバー名とポート名を削ってくれるので、処理が楽なのです(名前が逆っぽいのは、見ないことにしよう)。

■実行してみる

サーバーを管理者権限で動かして、metro app からつなげてみます。

無事接続できていますね。サーバー側で適当に振り分けてやれば、Web API として動かすことができます。URL Encode/Decodeすれば文字列は簡単です。複雑な場合は POST で送って、適当なクラスでラップすればよいですかね。というか、シリアライズ機能を使えば ok かも。

カテゴリー: C#, windows 8 パーマリンク