バックグラウンドタスクでソケットをリッスンする – SocketActivityTrigger

SocketActivityTrigger

UWPのBackgroundTaskは、さまざまなトリガーによってプロセスの開始を可能にします。

その中でもSocketActivityTriggerというものを使うと、システムが受信したソケットに反応してUWPのバックグラウンドタスクを開始することができます。

2種類の方法

SocketActivityTriggerを利用したバックグラウンドソケット通信には2種類存在します。

  • フォアグランドでConnectしたソケットをバックグラウンドへ渡す
  • フォアグラウンドでリッスンしているリスナーをバックグラウンドへ渡す

前者の場合、かずきさんのブログ公式のサンプルに載っているのでそちらを参考にしてください。

 

今回のサンプルは後者の場合です。フォアグラウンドでソケットリッスンを開始し、バックグラウンドタスクへソケットリスナーを渡し、バックグラウンドでコネクションを確立します。

ソケットが閉じてもOSが再起動してもリッスンを再開します。

サンプル

サンプルはここに置いているので参考にしてください。

https://github.com/garicchi/uwp-background-socket

ポイント

SocketActivityTriggerにはポイントがいくつかあります。

まず普通にBackgroundTaskBuilderでタスクを生成しますが、TriggerにはSocketActivityTriggerを渡します。

//バックグラウンドタスクを登録する
//NameはなんでもいいけどTaskEntryPointはバックグランドタスクの {名前空間}.{クラス名} にしないとだめ
var socketTaskBuilder = new BackgroundTaskBuilder();
socketTaskBuilder.Name = "MySocketBackgroundTask";
socketTaskBuilder.TaskEntryPoint = "BackgroundSocketComponent.SocketListenTask";

//バックグラウンドタスクでSocketを待ち受けるためのトリガー
//これのおかげでバックグラウンドタスクがSocketに反応できる
var trigger = new SocketActivityTrigger();
socketTaskBuilder.SetTrigger(trigger);
var task = socketTaskBuilder.Register();

その後、StreamSocketListenerでソケット待ち受けをしますが、バインドする前にEnableTransferOwnershipメソッドを呼んで、バックグラウンドにリスナーを渡す許可をしなければいけません。

ここでSocketActivityConnectedStandbyAction列挙体を入れる必要があります。公式サンプルではWakeになってますがDoNotWakeにしてください。(じゃないと動かない)

//ソケットリスナー
var socketListener = new StreamSocketListener();
var hostname = NetworkInformation.GetHostNames().Where(q => q.Type == HostNameType.Ipv4).First();
var port = textPort.Text;
//バックグラウンドタスクとポート番号を合わせるためにローカル設定に入れておく
ApplicationData.Current.LocalSettings.Values["SocketPort"] = port;
//バックグラウンドタスクとソケットIDを合わせるためにローカル設定に入れておく
ApplicationData.Current.LocalSettings.Values["SocketId"] = socketId;
//バックグラウンドタスクにソケットリスナーの権限を渡すことを許可
//第2引数はDoNotWakeにしないとBind時にエラーになる
socketListener.EnableTransferOwnership(task.TaskId, SocketActivityConnectedStandbyAction.DoNotWake);
//ホスト名とポート番号でバインドする
await socketListener.BindEndpointAsync(hostname, port);

バインド完了したら以下の2文でソケットリスナーをバックグラウンドに渡します。

私のサンプルではバインド直後にいれていますが、OnSuspending時にやってもいいと思います。

//ここから下はSuspendingイベントに入れてもいい
//ソケットリスナーをバックグランドタスクに渡すためにIOを止める
await socketListener.CancelIOAsync();
//バックグランドタスクに権限を渡す
socketListener.TransferOwnership(socketId);

バックグラウンドタスクではTriggerDetailのReasonプロパティを使用して、どのソケットイベントがきているかを判断します。

var details = taskInstance.TriggerDetails as SocketActivityTriggerDetails;
var socketInformation = details.SocketInformation;

switch (details.Reason)
{

まず、ソケットリッスン状態でアクセプトできるソケットが来た場合、ConnectionAcceptedになります。

Acceptは自動で行われるのでここが呼び出された時点でアクセプトされています。

その後、StreamSocketListenerでアクセプトされたソケットを取得する必要がありますがイベントでしか取得できないので、イベントがちゃんと発火されるまで2秒ぐらいまちます。

コネクションを張れたら、このバックグラウンドタスクに再度受信したソケットの権限を渡します。

こうすることでアクセプトされたソケットに来る信号も、このタスクで取得できるようになります。

//コネクションがアクセプトされたなら(自動でアクセプトされる)
case SocketActivityTriggerReason.ConnectionAccepted:
    //SocketInformationにはStreamSocketに値がある場合とStreamSocketListenerの2パターンがあるのでSocketKindで判別
    if (socketInformation.SocketKind == SocketActivityKind.StreamSocketListener)
    {
        //StreamSocketListenerを取得
        var socket = socketInformation.StreamSocketListener;
        //AcceptされたSocketがこのイベントで取得できるがこのイベントが発火するまでプロセスを落としてはいけないので
        //Task.Delayで2秒ぐらい待つ
        socket.ConnectionReceived += (s, e) =>
        {
            //アクセプトされたソケットを取得したらソケットリスナー登録時同様、このタスクにソケットを受信できるように設定
            var socketClient = e.Socket;
            socketClient.EnableTransferOwnership(taskInstance.Task.TaskId);
            socketClient.TransferOwnership(socketId);

            ShowToast(string.Format("Connect {0}", socketClient.Information.LocalAddress.DisplayName));
            //ソケットリスナーは破棄しないと次回リスナー起動時に死ぬ
            socketInformation.StreamSocketListener.Dispose();
        };
        await Task.Delay(2000);
        
    }

    break;

ソケットにデータが書き込まれたときはSocketActivityになります。

ここは普通にDataReaderでデータを1バイト読み取ります。

ソケットはsocketInformationのStreamSocketから取得します。

//ソケットにデータが来た時
case SocketActivityTriggerReason.SocketActivity:
    //1バイト読んでトーストで表示
    using (var reader = new DataReader(socketInformation.StreamSocket.InputStream))
    {
        uint readNum = 1;
        await reader.LoadAsync(readNum);
        var data = reader.ReadString(readNum);
        ShowToast(string.Format("DataReceived {0}",data.ToString()));
    }
    socketInformation.StreamSocket.TransferOwnership(socketId);
    break;

ソケットが閉じられたときは再度Listenをします。

ここが不思議なところなのですが、ソケットが閉じられたときだけじゃなく、OSが起動した時、SocketClosedが2回呼び出されます。

つまりSocketClosedが呼びされたタイミングでListenを開始すればOSが再起動してもListenを継続できます。

しかし2回呼び出されるのは厄介なので今リッスンしてるソケットの数で判断しています。

//ソケットが閉じられたとき
case SocketActivityTriggerReason.SocketClosed:
    //ソケットが閉じられたとき、以下の処理を読んで再度リッスンすればよいが
    //OSが再起動したとき、SocketClosedが2回呼ばれる(謎)
    //2回の呼び出しの違いはSocketの数なのでSocketの数で1回目を判定して再度リッスン
    if (SocketActivityInformation.AllSockets.Count == 0)
    {
        var socketListener = new StreamSocketListener();
        var hostname = NetworkInformation.GetHostNames().Where(q => q.Type == HostNameType.Ipv4).First();
        var port = ApplicationData.Current.LocalSettings.Values["SocketPort"].ToString();

        socketListener.EnableTransferOwnership(taskInstance.Task.TaskId, 
            SocketActivityConnectedStandbyAction.DoNotWake);

        await socketListener.BindEndpointAsync(hostname, port);

        await socketListener.CancelIOAsync();

        socketListener.TransferOwnership(socketId);
        ShowToast(string.Format("{0}:{1} restart socket listen",hostname,port));
    }
    break;

動作例

AndroidのアプリであるSimpleSocketTesterからソケットを送ってみています。

この手法は公式のサンプルにもないので少々難しいですね。質問は@garicchiにて受け付けています。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください