ぷろじぇくと、みすじら。

Visual Studio Codeのエディター部分(Monaco)のみをビルドする

Created at: | Tag: VSCode Monaco

Visual Studio CodeのエディターをWebページに埋め込むVisual Studio CodeのシンタックスハイライトをWebページで使うで Visual Studio Codeのエディター部分いわゆるMonaco EditorをCodeから引きはがして使っていました。

Monacoはそれ単体で配布されていたりリポジトリーがあったりするわけではないのですが、実はCodeのgulpfile.editor.jsにエディター部分(Monaco)だけをビルドするビルドタスクがあります。

まずCodeをビルドできるよう準備して、その後gulpで editor-distro タスクを実行すると以下のディレクトリにMonacoのビルド結果が出力されます。

このディレクトリー以下の一式をアプリケーションやWebサイトに配置して、エディターを組み込むことができます。Mimosaのmonaco-prebuiltはこのout-editor-minを突っ込んだものです。

ちなみにMonacoのライセンスはVisual Studio Codeの一部なのでMIT Licenseとなります。

Visual Studio CodeのエディターをWebページに埋め込む

Created at: | Tag: VSCode Monaco

Visual Studio CodeのシンタックスハイライトをWebページで使うという話を少し前に書きましたが、 今回はエディタ部分をWebページに埋め込んでみようという話です。

前にも少し触れましたが、Visual Studio Codeのエディター部分、いわゆるMonacoはVisual Studio Code以前からいろいろなところで埋め込まれて使われています。 Visual Studio Online "Monaco"を始め、TypeScript PlaygroundWinJS Playground、IE/EdgeのF12開発者ツールなどなど、 エディター部分の出来の良さというか埋め込んで使われることが念頭に置かれている感じがします。

動作サンプル

まずは動作するサンプルを貼り付けてみるので、触ってみてください。

適当なところでCtrl/Cmd + スペースを押せばきちんとコードの補完(IntelliSense)が表示されます。 Visual Studio Codeと同じようにJavaScriptモードはTypeScriptモードが元になっているので、型もちゃんと認識されています(helloメソッドの戻り値の型がstringになっています)。

hの後ろでコード補完を表示した

埋め込み方

肝心の埋め込み方ですが、エディター部分を埋め込むだけであればとても簡単です。

最初にエディターのJavaScriptファイルを用意する必要がありますが、 MimosaにあらかじめVisual Studio Codeからエディター部分をビルドしたものを同梱しているのでそれを使うと簡単です。 リポジトリの monaco-prebuilt 以下を引っこ抜いてきてもよいです。

ファイルの準備ができたらHTMLを書きます。エディターの領域をdiv要素で確保してください。

<div id="editor" style="height:200px;"></div>

エディターの表示サイズはエディターにしてほしい部分の要素のサイズによって確定するので、空の要素の場合、高さが0で何も表示できなくなります。 例えばコードの長さに応じた高さ、といったことはできないので注意が必要です。 また、このエディターとなりたい部分の要素の内容は空にしておいてください(エディターが実体化されても残るので)。

次にJavaScriptでMonacoエディターを生成します。

<script>
    require.config({
        baseUrl: '/mimosa/', // これは例によってMonaco(vsディレクトリ)が入っているディレクトリ
    });

    require(["vs/editor/editor.main"], function () {
        // エディター領域を取得する
        var editorE = document.querySelector('#editor');

        // エディターに表示する文字列を組み立てる
        var content = [
            '"use strict";',
            'const message = "コンニチハ!";',
            'class Greeter {',
            '    /** ハロー!! */',
            '    hello() {',
            '        return message;',
            '    }',
            '}',
            'new Greeter().h'
        ].join('\n');

        // エディターを生成する
        var editor = Monaco.Editor.create(editorE, {
            value: content,
            mode: "javascript",
            readOnly: false,
            //theme: "vs-dark",
            scrollBeyondLastLine: false,
            automaticLayout: true,
            autoSize: true,
            scrollbar: {
                handleMouseWheel: true
            }
        });

        // 何かイベントに応じて編集した文字列を取得する
        // console.log(editor.getValue());
    });
</script>

実質的にやることはrequire(["vs/editor/editor.main"])で読み込んで、Monaco.Editor.create メソッドでエディターを生成するということだけです。なんとお手軽。

Monaco.Editor.create メソッドの第二引数はオプションとなるオブジェクトです。 valueが初期文字列、modeが言語(csharpとかjavascriptとか)、readOnlyは編集可能かどうか、それ以外はICommonEditorOptions インターフェースあたりをご覧ください。

これだと表示するだけですので、最終的には編集結果を取得するというのが普通です。 Monaco.Editor.create メソッドの戻り値であるエディターのインスタンスに対して、 getValue メソッドを呼び出すことで編集中の文字列を取得できます。

まとめ

というわけで割と手軽に高性能エディターを自分のアプリケーションやWebページに組み込めるようになりました。 一部Visual Studio Codeの本体側に依存している機能(diagnostics)などもあるのでそういうものは使えなかったりするのですが、 それはそれとしても利用価値はあるのではと思います。

今回は単純に埋め込むところまでなので、次はこのエディターの拡張方法について書くかもしれません。

雑なHTTPサーバーをC#で書いてみる

Created at: | Tag: CSharp HTTP NET

人生のうち一度はHTTPサーバーを書くという話をしたら意外とそうでもなくてあれー?ってなったのですが、それはそれとして。

「HTTPサーバーには人生の大切な事が全てつまっている」と言った人がいるかどうかは定かではないですが、 雑なサーバーを書いてみるのはまあまあ簡単で理解も進んで面白いのでおすすめですよ、というわけで書いてみましょうという話。

アプリにWebサーバーを組み込みたくなったりして雑なHTTPサーバーを書いてみることというのがたまにあります。 そんな時、現代ではどの言語やプラットフォームでもパッケージやライブラリであっという間にサーバーを組み込めるので自分で書くことはないかもしれません。

ですが今時だとArduinoのような省メモリであることを必要とされる環境でもHTTPサーバーが必要になったとき、 ほぼ固定機能のHTTPサーバーを実装してAPIやWebページからコントロールできるように…ということもよくあると思いますので経験や知識は無駄ではないと思います。多分。

この記事ではC#で書きますが、お好みのプラットフォームなどで書いてみるとよいかと思います。

対象読者

雑なHTTPサーバーとは

おおよそこんな感じ。

要するにお遊びなのでRFCとかも読まないし実際適当です。ほとんどのブラウザーなどからおおよそアクセスして何か表示できればよい程度を目指します。

ただし外部に出ると死ぬ(セキュリティーとか)ような感じもあるのでまじめに使う前提ではありません。

流れとHTTPについて

流れについて一応雑に説明すると…

  1. HTTPサーバー: クライアントからの接続を待ち受ける
  2. HTTPクライアント: サーバーへ接続する
  3. HTTPサーバー: クライアントからの接続を受け入れる
  4. HTTPクライアント: サーバーへリクエストを送信する
  5. HTTPサーバー: クライアントからのリクエストを解釈する
  6. HTTPサーバー: レスポンスをクライアントへ送信する
  7. HTTPクライアント: レスポンスを解釈する

となっています。ですのでHTTPサーバー部分を作るので以下の部分を作ればよいことになります。

  1. HTTPサーバー: クライアントからの接続を待ち受ける
  2. HTTPサーバー: クライアントからの接続を受け入れる
  3. HTTPサーバー: クライアントからのリクエストを解釈する
  4. HTTPサーバー: レスポンスをクライアントへ送信する

HTTPについてですが、ご存知の通りリクエストとレスポンスというメッセージを送受信するプロトコルです。

リクエストメッセージはこんな感じです。

POST / HTTP/1.0[CR][LF]
User-Agent: Nantoka/1.0[CR][LF]
X-Nantoka: Kantoka[CR][LF]
[CR][LF]
Body

CR+LFで区切られていて、CR+LFだけの行が来るまでがヘッダー部分、それ以降がボディ部分となっていてあったりなかったりする部分です。

レスポンスメッセージはこんな感じ。おおよそ同じです。一行目(スタート行)がちょっと違いますがそれ以外の形は同じです。

HTTP/1.0 200 OK[CR][LF]
Server: Nantoka/1.0[CR][LF]
X-Hauhau: Maumau[CR][LF]
[CR][LF]
Body

詳しくはRFC 7230をご覧ください(HTTP 1.1ですが)。実は折り返せたりして(obsoleteだけど)、本当にまじめに実装しようとすると結構面倒だったりします。

サーバーを書いてみる

と、雑な説明をしたところでサーバーを書いてみます。

クライアントからの接続を待ち受ける

まずは「クライアントからの接続を待ち受ける」部分を作ってみます。

クライアントからの接続というのはTCP接続を待ち受けるいわゆるLISTENするということで一般的にはSocketを使ったりします。 とはいえSocket直接というのはプリミティブすぎるので.NET FrameworkにはTcpListenerクラスという便利クラスが用意されているのでそれを使うと簡単です。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

// Startメソッドはブロッキングではなくて、そのまま終わってしまうので適当に止めておく
Console.ReadLine();

接続待ち受けを開始するにはTcpListenerをどのIPアドレスとポートで待ち受けるか指定して作って、Startメソッドを呼び出すだけです。

ここではLoopback(127.0.0.1と::1、いわゆるlocalhost)のポート12345で待ち受ける指定にしています。 Loopbackにしておくと自身のコンピューターからしか接続できない状態となりますが、Loopback以外にはAny(何でも)や特定のIPアドレス(イーサネットアダプタのアドレス)などを指定できます。

そして実行してブラウザーから http://localhost:12345/ にアクセスすると……接続できませんとも言わずに何も起きないはずです。

クライアントからの接続を受け入れる

TcpListenerで待ち受ける状態になっても、「クライアントからの接続を受け入れる」処理がないのでクライアントはまだ待たされている状態です。

というわけで受け入れる処理を書きます。AcceptTcpClientAsyncメソッドを呼び出すことで受け入れが行われ、クライアントとの接続であるTcpClientクラスのインスタンスが返ってきます。 返ってきたTcpClientはIDisposableを実装しているのでusingしておきます。Disposeすればクライアントとの接続を切断できます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
{
    // 接続元を出力しておく
    Console.WriteLine(tcpClient.Client.RemoteEndPoint);
}

// Startメソッドはブロッキングではなくて、そのまま終わってしまうので適当に止めておく
Console.ReadLine();

このコードを実行してブラウザーから http://localhost:12345/ にアクセスすると……多分すぐにアクセスできませんでしたというメッセージが表示されるようになるはずです。

これでデータの流れはありませんが接続の「待ち受け」→「受け入れ」→「切断」の一連ができました。

しかし、この実装では1回しか受け入れられないのでもう一度アクセスすると突き刺さります。ということで何度も受け入れられるように無限ループにしておきます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);
    }
}

クライアントからのリクエストを解釈する(フリをする)

次はリクエストを読み出します。が、とりあえずいったん読んで解釈したフリをすることにします。

ほとんどのブラウザーやユーザーエージェントのリクエストはGETリクエストであって、返したものをそのまま表示したり解釈したりするので最悪読み捨てても大丈夫だったりします。

クライアントとのデータの送受信はTcpClientのGetStreamメソッドで得られるStreamのインスタンスを利用します。このStreamを読めばクライアントから受信、書き込めばクライアントへ送信ということです。もちろんStreamなのでStreamReader/StreamWriterを通すと簡単に読み書きできます。

StreamReaderを作ったら、ReadLineAsyncメソッドでヘッダー部分の終わり、つまりCR+LFのみの行(=空行)まで読み込みます。 その際、読み込んだ行をコンソールなどにデバッグ目的で適当に書き出しておきます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);

        // ヘッダー部分を全部読む
        string line;
        do
        {
            line = await reader.ReadLineAsync();
            // 読んだ行を出力しておく
            Console.WriteLine(line);
        } while (!String.IsNullOrWhiteSpace(line));
    }
}

このコードを実行してブラウザーから http://localhost:12345/ にアクセスしてみると、コンソールなどに以下のようなブラウザーのリクエストが出力されるはずです。

GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, image/jxr, */*
Accept-Language: en-US,en;q=0.7,ja;q=0.3
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.14251
Accept-Encoding: gzip, deflate
Host: localhost:12345
Connection: Keep-Alive

とりあえず今のところはこれでよいということにします。

レスポンスをクライアントへ送信する

いよいよクライアントに対してレスポンスを返します。

レスポンスは1行目がステータスライン(ステータスコードとかを含む行)、その後ヘッダー部分、ボディ部分を送信すれば出来上がりです。つまりStreamWriterに対して書き込んでいくだけの簡単なお仕事です。

200なステータスで、Content-Typeが指定されていて、ボディがあれば大体受け入れてもらえます。というわけでこんな感じのレスポンスを返すということにします。

HTTP/1.0 200 OK
Content-Type: text/plain; charset=UTF-8

Hello! Konnichiwa!

これを今までのコードに組み込むとこうなります。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // 接続元を出力しておく
        Console.WriteLine(tcpClient.Client.RemoteEndPoint);

        // ヘッダー部分を全部読んで捨てる
        string line;
        do
        {
            line = await reader.ReadLineAsync();
            // 読んだ行を出力しておく
            Console.WriteLine(line);
        } while (!String.IsNullOrWhiteSpace(line));

        // レスポンスを返す
        // ステータスライン
        await writer.WriteLineAsync("HTTP/1.0 200 OK");
        // ヘッダー部分
        await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
        await writer.WriteLineAsync(); // 終わり
        // これ以降ボディ
        await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
    }
}

そしてこのコードを実行してブラウザーから http://localhost:12345/ にアクセスしてみます。"Hello! Konnichiwa!"と表示されたら成功です。

というわけで超雑なHTTPサーバーができました。なんと32行。この通り、データを返すだけならばとても簡単ですね。あとはこれをベースにいじって遊んでみるとよいのではないでしょうか。

まとめ

HTTPサーバーを書いてみると、クライアントとサーバー、接続やリクエスト、レスポンスといったものを少し身近に感じることができるようになると思います。 一度書いていじってみるのは個人的にはとてもおすすめなのです。Extraとしてちょっといじったものを載せておきます。

Extra: リクエストを解釈する

ヘッダー部分を読み捨てましたが、ヘッダー部分(とリクエストライン)を解釈するともっとそれっぽくなります。

リクエストラインやヘッダー部分にはリクエストパスやリクエストに関する情報が含まれています。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    using (var tcpClient = await tcpListener.AcceptTcpClientAsync())
    using (var stream = tcpClient.GetStream())
    using (var reader = new StreamReader(stream))
    using (var writer = new StreamWriter(stream))
    {
        // ヘッダー部分を全部読んでおく
        var requestHeaders = new List<string>();
        while (true)
        {
            var line = await reader.ReadLineAsync();
            if (String.IsNullOrWhiteSpace(line))
            {
                break;
            }
            requestHeaders.Add(line);
        }

        // 一行目(リクエストライン)は [Method] [Path] HTTP/[HTTP Version] となっている
        var requestLine = requestHeaders.FirstOrDefault();
        var requestParts = requestLine?.Split(new[] { ' ' }, 3);
        if (!requestHeaders.Any() || requestParts.Length != 3)
        {
            await writer.WriteLineAsync("HTTP/1.0 400 Bad Request");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Bad Request");
            return;
        }

        // 接続元を出力しておく
        Console.WriteLine("{0} {1}", tcpClient.Client.RemoteEndPoint, requestLine);

        // パス
        var path = requestParts[1];
        if (path == "/")
        {
            // / のレスポンスを返す
            await writer.WriteLineAsync("HTTP/1.0 200 OK");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
        }
        else if (path == "/hauhau")
        {
            // /hauhau のレスポンスを返す
            await writer.WriteLineAsync("HTTP/1.0 200 OK");
            await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
            await writer.WriteLineAsync();
            await writer.WriteLineAsync("Hauhau!!");
        }
    }
}

パスが取れれば、ルーティングのテーブルを作って振り分けて…とかもやってみたくなりますね。

Extra: 複数リクエストをさばく

今までの実装は async/await を使ってはあったもののリクエストを受けて流す部分は1つなので並列で処理を実行することができません。 例えばある一つのリクエストの処理に時間がかかっている間はほかのリクエストを受け入れることができなくなるということです。

そこで.NET FrameworkにはTaskという便利なものがあるので空いているスレッドに処理を回すことができます。

var tcpListener = new TcpListener(IPAddress.Loopback, 12345);
tcpListener.Start();

while (true)
{
    var tcpClient = await tcpListener.AcceptTcpClientAsync();

    // 接続を受け入れたら後続の処理をTaskに放り込む
    Task.Run(async () =>
    {
        using (var stream = tcpClient.GetStream())
        using (var reader = new StreamReader(stream))
        using (var writer = new StreamWriter(stream))
        {
            // ヘッダー部分を全部読んでおく
            var requestHeaders = new List<string>();
            while (true)
            {
                var line = await reader.ReadLineAsync();
                if (String.IsNullOrWhiteSpace(line))
                {
                    break;
                }
                requestHeaders.Add(line);
            }

            // 一行目(リクエストライン)は [Method] [Path] HTTP/[HTTP Version] となっている
            var requestLine = requestHeaders.FirstOrDefault();
            var requestParts = requestLine?.Split(new[] { ' ' }, 3);
            if (!requestHeaders.Any() || requestParts.Length != 3)
            {
                await writer.WriteLineAsync("HTTP/1.0 400 Bad Request");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Bad Request");
                return;
            }

            // 接続元を出力しておく
            Console.WriteLine("{0} {1}", tcpClient.Client.RemoteEndPoint, requestLine);

            // パス
            var path = requestParts[1];
            if (path == "/")
            {
                // / のレスポンスを返す
                await writer.WriteLineAsync("HTTP/1.0 200 OK");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Hello! Konnichiwa! @ " + DateTime.Now); // 動的感を出す
            }
            else if (path == "/hauhau")
            {
                // 遅い処理をシミュレートするマン
                await Task.Delay(5000); // 5秒

                // /hauhau のレスポンスを返す
                await writer.WriteLineAsync("HTTP/1.0 200 OK");
                await writer.WriteLineAsync("Content-Type: text/plain; charset=UTF-8");
                await writer.WriteLineAsync();
                await writer.WriteLineAsync("Hauhau!!");
            }
        }
    });
}

まじめにやるときにはTaskと接続の管理とかが必要ですが、こんな感じで並列化ができます。

ちなみに~Async(非同期メソッド)の場合、IO待ちの際にスレッドを手放すことが多くなるので効率よく処理できるようになります。

ご報告

Created at: | Tag:

ガルパンはいいぞ

DNS-SD(Bonjour)をUniversal Windows Platformから利用してサービスを探す

Created at: | Tag: DNS-SD UWP

Windows 10からDNS-SD/mDNS、いわゆるAppleのBonjourをネイティブサポートするようになりました。 要するに何ができるかというとIPアドレスやホスト名を知らなくても同じネットワークにいるデバイスとサービスを探すことができる仕組みです。

例えば、iTunesが他のコンピューターのiTunes 共有ライブラリを見つけるといったことに利用されています。そのちょっと便利な機能をUWPのAPIを通して利用できるようになっています。

アプリからDNS-SDでサービスを探す

さて、どうやって使うかですがまあまあ簡単で、通常のデバイスを列挙するのと同様にDeviceInformation クラスを利用します……が、列挙しようと思ってFindAllAsyncメソッドを使うと何も返ってこず(awaitすると進まない)、暫くするとエラーが出ます。

この挙動が正しいのかどうかよくわかりませんが、ともかく何やらうまく動きません。

そこでDeviceInformation.CreateWatcher メソッドを利用します。 このメソッドはデバイスがネットワークに追加されたり削除されたりというのを監視する方法です。この方法でならば列挙できます。

Watcherを作ってStartメソッドを呼び、監視を始めるとデバイスが見つかりAddedイベントが呼び出されますのでそこで適当に情報を保持しておけばよいです。 ちなみに監視して列挙終了時の EnumerationCompleted イベントが発生しないので完了を待つというのはやめた方がいいでしょう(多分それがFindAllAsyncが返ってこない原因な気がします)。

とりあえず、ネットワークにあるAndroid TV(正確にはAndroid TVが持つリモコン用サービス)を列挙するソースコードはこんな感じなります。

using Windows.Devices.Enumeration;

// ...

// DNS-SDのサービス名
var serviceName = "_androidtvremote._tcp";
// DNS-SDを利用する
var protocolId = "{4526e8c1-8aac-4153-9b16-55e86ada0e54}";
// デバイス検索文字列
var aqsFilter = $"System.Devices.AepService.ProtocolId:={protocolId} AND System.Devices.Dnssd.Domain:=\"local\" AND System.Devices.Dnssd.ServiceName:=\"{serviceName}\"";
// 取得するプロパティ
var properties = new[] {
    "System.Devices.Dnssd.HostName", // ホスト名 (自称)
    "System.Devices.IpAddress", // IPアドレスの配列(string[])
};

var watcher = DeviceInformation.CreateWatcher(aqsFilter, properties, DeviceInformationKind.AssociationEndpointService);
watcher.Added += (sender, args) =>
{
    Debug.WriteLine("Added: " + args.Id);
    foreach (var prop in args.Properties)
    {
        var value = prop.Value is string[] ? (String.Join(", ", (string[])prop.Value)) : prop.Value + "";
        Debug.WriteLine($"  - {prop.Key}: {value}");
    }
};
watcher.Updated += (sender, args) =>
{
    Debug.WriteLine("Updated: " + args.Id);
    foreach (var prop in args.Properties)
    {
        var value = prop.Value is string[] ? (String.Join(", ", (string[])prop.Value)) : prop.Value + "";
        Debug.WriteLine($"  - {prop.Key}: {value}");
    }
};
watcher.Start();

実行するとこんな感じにホスト名やIPアドレスなどの情報が取れます。

DnsSd#KJ-55X8500C._androidtvremote._tcp.local#0
  - System.ItemNameDisplay: KJ-55X8500C
  - System.Devices.DeviceInstanceId: 
  - System.Devices.Icon: 
  - System.Devices.GlyphIcon: 
  - System.Devices.InterfaceEnabled: 
  - System.Devices.IsDefault: 
  - System.Devices.PhysicalDeviceLocation: 
  - System.Devices.Aep.CanPair: False
  - System.Devices.Aep.IsPaired: False
  - {A35996AB-11CF-4935-8B61-A6761081ECDF} 18: 
  - {A35996AB-11CF-4935-8B61-A6761081ECDF} 13: 
  - System.Devices.Dnssd.HostName: Android.local
  - System.Devices.IpAddress: 192.168.1.100

アプリはこの情報をもとにサーバー(サービス)に接続すれば、相手の名前をあらかじめ知っていなくてもアクセスできる、というわけです。

手順をおさらいすると、DeviceWatcherを作るCreateWatcherメソッドの引数に渡すAQS文字列と呼ばれる検索フィルター文字列に、 DNS-SDプロトコル({4526e8c1-8aac-4153-9b16-55e86ada0e54})を使うことを指定して、さらにDNS-SDのサービス名(_daap.tcpなど)を指定して、検索を実行すれば見つかるという形になっています。例えばサービス名に_daap.tcpを指定するとネットワーク上のiTunesの共有ライブラリを探すことができます。

ちなみにこのUniversalなAPIはいわゆる従来のデスクトップアプリケーションからも呼び出すことができます。

制限事項

得られたホスト名(.localで終わる名前)をHttpClientなどに渡しても名前解決されますが、たまに失敗するのでIPアドレスを使った方がいいかもしれません。

なお、どうやら試した範囲ではサービス名を指定せずにネットワークに存在するサービスをすべて列挙する、ということはできないようです。System.Devices.Dnssd.ServiceNameを指定しないで検索すると何も返ってこないことに注意が必要です。

また、Hyper-Vが有効の場合にも動作しないという話もあります。