重新想象 Windows 8 Store Apps (68) - 后台任务: 控制通道(ControlChannel)

原文: 重新想象 Windows 8 Store Apps (68) - 后台任务: 控制通道(ControlChannel)

[源码下载]


重新想象 Windows 8 Store Apps (68) - 后台任务: 控制通道(ControlChannel)



作者:webabcd


介绍
重新想象 Windows 8 Store Apps 之 后台任务

  • 控制通道(ControlChannel)



示例
1、客户端与服务端做 ControlChannel 通信的关键代码
ControlChannelHelper/AppContext.cs

/*

 * 本例通过全局静态变量来实现 app 与 task 的信息共享,以便后台任务可以获取到 app 中的相关信息

 * 

 * 注:

 * 也可以通过 Windows.ApplicationModel.Core.CoreApplication.Properties 保存数据,以实现 app 与 task 的信息共享

 */



using System.Collections.Concurrent;

using Windows.Networking.Sockets;



namespace ControlChannelHelper

{

    public class AppContext

    {

        /// <summary>

        /// 从 ControlChannel 接收到的数据

        /// </summary>

        public static ConcurrentQueue<string> MessageQueue = new ConcurrentQueue<string>();



        /// <summary>

        /// 客户端 socket

        /// </summary>

        public static StreamSocket ClientSocket;

    }

}

ControlChannelHelper/SocketControlChannel.cs

/*

 * 实现一个 socket tcp 通信的 ControlChannel,client 将在此 ControlChannel 中实时接收数据

 * 

 * 注:

 * win8 client 和 socket server 不能部署在同一台机器上,否则会抛出异常:{参考的对象类型不支持尝试的操作。 (异常来自 HRESULT:0x8007273D)}

 */



using System;

using System.Threading.Tasks;

using Windows.ApplicationModel.Background;

using Windows.Foundation;

using Windows.Networking;

using Windows.Networking.Sockets;

using Windows.Storage.Streams;



namespace ControlChannelHelper

{

    public class SocketControlChannel : IDisposable

    {

        // ControlChannel

        public ControlChannelTrigger Channel { get; set; }



        // 客户端 socket

        private StreamSocket _socket;

        // 用于发送数据

        private DataWriter _dataWriter;

        // 用于接收数据

        private DataReader _dataReader;



        // 向服务端发送心跳的间隔时间,单位为分钟,最小 15 分钟

        private uint _serverKeepAliveInterval = 15;

        // ControlChannel 的标识

        private string _channelId = "myControlChannel";



        public SocketControlChannel()

        {



        }



        public async Task<string> CreateChannel()

        {

            Dispose();



            try

            {

                // 实例化一个 ControlChannel

                Channel = new ControlChannelTrigger(_channelId, _serverKeepAliveInterval, ControlChannelTriggerResourceType.RequestHardwareSlot);

            }

            catch (Exception ex)

            {

                Dispose();

                return "控制通道创建失败:" + ex.ToString();

            }



            // 注册用于向服务端 socket 发送心跳的后台任务,需要在 manifest 中做相关配置

            var keepAliveBuilder = new BackgroundTaskBuilder();

            keepAliveBuilder.Name = "myControlChannelKeepAlive";

            // 注:如果走的是 WebSocket 协议,则系统已经为其内置了发送心跳的逻辑,此处直接指定为 Windows.Networking.Sockets.WebSocketKeepAlive 即可

            keepAliveBuilder.TaskEntryPoint = "BackgroundTaskLib.ControlChannelKeepAlive";

            keepAliveBuilder.SetTrigger(Channel.KeepAliveTrigger); // 到了发送心跳的间隔时间时则触发,本例是 15 分钟

            keepAliveBuilder.Register();



            // 注册用于向用户显示通知的后台任务,需要在 manifest 中做相关配置

            var pushNotifyBuilder = new BackgroundTaskBuilder();

            pushNotifyBuilder.Name = "myControlChannelPushNotification";

            pushNotifyBuilder.TaskEntryPoint = "BackgroundTaskLib.ControlChannelPushNotification";

            pushNotifyBuilder.SetTrigger(Channel.PushNotificationTrigger); // 在 ControlChannel 中收到了推送过来的数据时则触发

            pushNotifyBuilder.Register();



            try

            {

                _socket = new StreamSocket();

                AppContext.ClientSocket = _socket;



                // 在 ControlChannel 中通过指定的 StreamSocket 通信

                Channel.UsingTransport(_socket);



                // client socket 连接 server socket

                await _socket.ConnectAsync(new HostName("192.168.6.204"), "3366");



                // 开始等待 ControlChannel 中推送过来的数据,如果 win8 client 和 socket server 部署在同一台机器上,则此处会抛出异常

                ControlChannelTriggerStatus status = Channel.WaitForPushEnabled();



                if (status != ControlChannelTriggerStatus.HardwareSlotAllocated && status != ControlChannelTriggerStatus.SoftwareSlotAllocated)

                    return "控制通道创建失败:" + status.ToString();



                // 发送数据到服务端

                _dataWriter = new DataWriter(_socket.OutputStream);

                string message = "hello " + DateTime.Now.ToString("hh:mm:ss") + "^";

                _dataWriter.WriteString(message);

                await _dataWriter.StoreAsync();



                // 接收数据

                ReceiveData();

            }

            catch (Exception ex)

            {

                Dispose();

                return "控制通道创建失败:" + ex.ToString();

            }



            return "ok";

        }



        // 开始接收此次数据

        private void ReceiveData()

        {

            uint maxBufferLength = 256;



            try

            {

                var buffer = new Windows.Storage.Streams.Buffer(maxBufferLength);

                var asyncOperation = _socket.InputStream.ReadAsync(buffer, maxBufferLength, InputStreamOptions.Partial);

                asyncOperation.Completed = (IAsyncOperationWithProgress<IBuffer, uint> asyncInfo, AsyncStatus asyncStatus) =>

                {

                    switch (asyncStatus)

                    {

                        case AsyncStatus.Completed:

                        case AsyncStatus.Error:

                            try

                            {

                                IBuffer bufferRead = asyncInfo.GetResults();

                                uint bytesRead = bufferRead.Length;

                                _dataReader = DataReader.FromBuffer(bufferRead);



                                // 此次数据接收完毕

                                ReceiveCompleted(bytesRead);

                            }

                            catch (Exception ex)

                            {

                                AppContext.MessageQueue.Enqueue(ex.ToString());

                            }

                            break;

                        case AsyncStatus.Canceled:

                            AppContext.MessageQueue.Enqueue("接收数据时被取消了");

                            break;

                    }

                };

            }

            catch (Exception ex)

            {

                AppContext.MessageQueue.Enqueue(ex.ToString());

            }

        }



        public void ReceiveCompleted(uint bytesRead)

        {

            // 获取此次接收到的数据

            uint bufferLength = _dataReader.UnconsumedBufferLength;

            string message = _dataReader.ReadString(bufferLength);



            // 将接收到的数据放到内存中,由 PushNotificationTrigger 触发的后台任进行处理(当然也可以在此处处理)

            AppContext.MessageQueue.Enqueue(message);



            // 开始接收下一次数据

            ReceiveData();

        }



        // 释放资源

        public void Dispose()

        {

            lock (this)

            {

                if (_dataWriter != null)

                {

                    try

                    {

                        _dataWriter.DetachStream();

                        _dataWriter = null;

                    }

                    catch (Exception ex)

                    {



                    }

                }



                if (_dataReader != null)

                {

                    try

                    {

                        _dataReader.DetachStream();

                        _dataReader = null;

                    }

                    catch (Exception exp)

                    {



                    }

                }



                if (_socket != null)

                {

                    _socket.Dispose();

                    _socket = null;

                }



                if (Channel != null)

                {

                    Channel.Dispose();

                    Channel = null;

                }

            }

        }

    }

}


2、客户端辅助类
BackgroundTaskLib/ControlChannelKeepAlive.cs

/*

 * 用于向服务端 socket 发送心跳的后台任务

 * 

 * 注:

 * 如果走的是 WebSocket 协议,则系统已经为其内置了发送心跳的逻辑

 * 只需要将 BackgroundTaskBuilder.TaskEntryPoint 设置为 Windows.Networking.Sockets.WebSocketKeepAlive 即可,而不需要再自定义此后台任务

 */



using ControlChannelHelper;

using System;

using Windows.ApplicationModel.Background;

using Windows.Networking.Sockets;

using Windows.Storage.Streams;



namespace BackgroundTaskLib

{

    public sealed class ControlChannelKeepAlive : IBackgroundTask

    {

        public void Run(IBackgroundTaskInstance taskInstance)

        {

            if (taskInstance == null)

                return;



            // 获取 ControlChannel

            var channelEventArgs = taskInstance.TriggerDetails as IControlChannelTriggerEventDetails;

            ControlChannelTrigger channel = channelEventArgs.ControlChannelTrigger;



            if (channel == null)

                return;



            string channelId = channel.ControlChannelTriggerId;



            // 发送心跳

            SendData();

        }



        private async void SendData()

        {

            // 发送心跳到 server socket

            DataWriter dataWriter = new DataWriter(AppContext.ClientSocket.OutputStream);

            string message = "hello " + DateTime.Now.ToString("hh:mm:ss") + "^";

            dataWriter.WriteString(message);

            await dataWriter.StoreAsync();

        }

    }

}

BackgroundTaskLib/ControlChannelPushNotification.cs

/*

 * 用于向用户显示通知的后台任务,需要在 manifest 中做相关配置

 */



using ControlChannelHelper;

using NotificationsExtensions.ToastContent;

using System;

using Windows.ApplicationModel.Background;

using Windows.Networking.Sockets;

using Windows.UI.Notifications;



namespace BackgroundTaskLib

{

    public sealed class ControlChannelPushNotification : IBackgroundTask

    {

        public void Run(IBackgroundTaskInstance taskInstance)

        {

            if (taskInstance == null)

                return;



            // 获取 ControlChannel

            var channelEventArgs = taskInstance.TriggerDetails as IControlChannelTriggerEventDetails;

            ControlChannelTrigger channel = channelEventArgs.ControlChannelTrigger;



            if (channel == null)

                return;



            string channelId = channel.ControlChannelTriggerId;



            try

            {

                string messageReceived;



                // 将从 ControlChannel 中接收到的信息,以 toast 的形式弹出

                while (AppContext.MessageQueue.Count > 0)

                {

                    bool result = AppContext.MessageQueue.TryDequeue(out messageReceived);

                    if (result)

                    {

                        IToastText01 templateContent = ToastContentFactory.CreateToastText01();

                        templateContent.TextBodyWrap.Text = messageReceived;

                        templateContent.Duration = ToastDuration.Short; 

                        IToastNotificationContent toastContent = templateContent;

                        ToastNotification toast = toastContent.CreateNotification();



                        ToastNotifier toastNotifier = ToastNotificationManager.CreateToastNotifier();

                        toastNotifier.Show(toast);

                    }

                }

            }

            catch (Exception ex)

            {



            }

        }

    }

}


3、客户端
BackgroundTask/ControlChannel.xaml

<Page

    x:Class="XamlDemo.BackgroundTask.ControlChannel"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="using:XamlDemo.BackgroundTask"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    mc:Ignorable="d">



    <Grid Background="Transparent">

        <StackPanel Margin="120 0 0 0">

            

            <TextBlock Name="lblMsg" FontSize="14.667" />

            

            <Button Name="btnCreateChannel" Content="创建一个 ControlChannel" Margin="0 10 0 0" Click="btnCreateChannel_Click" />

            

        </StackPanel>

    </Grid>

</Page>

BackgroundTask/ControlChannel.xaml.cs

/*

 * 演示如何创建一个基于 socket tcp 通信的 ControlChannel,client 将在此 ControlChannel 中实时接收数据

 * 

 * 注:

 * 不能在模拟器中运行

 * RTC - Real Time Communication 实时通信

 * win8 client 和 socket server 不能部署在同一台机器上,否则会抛出异常:{参考的对象类型不支持尝试的操作。 (异常来自 HRESULT:0x8007273D)}

 */



using System;

using ControlChannelHelper;

using Windows.ApplicationModel.Background;

using Windows.UI.Xaml;

using Windows.UI.Xaml.Controls;

using Windows.UI.Popups;



namespace XamlDemo.BackgroundTask

{

    public sealed partial class ControlChannel : Page

    {

        public ControlChannel()

        {

            this.InitializeComponent();

        }



        private async void btnCreateChannel_Click(object sender, RoutedEventArgs e)

        {

            // 如果 app 在锁屏上,则可以通过 ControlChannelTrigger 触发指定的后台任务

            BackgroundAccessStatus status = BackgroundExecutionManager.GetAccessStatus();

            if (status == BackgroundAccessStatus.Unspecified)

            {

                status = await BackgroundExecutionManager.RequestAccessAsync();

            }

            if (status == BackgroundAccessStatus.Denied)

            {

                await new MessageDialog("请先将此 app 添加到锁屏").ShowAsync();

                return;

            }



            // 创建一个基于 socket tcp 通信的 ControlChannel,相关代码参见:ControlChannelHelper 项目

            SocketControlChannel channel = new SocketControlChannel();

            string result = await channel.CreateChannel();



            lblMsg.Text = result;

        }

    }

}


4、服务端
SocketServerTcp/ClientSocketPacket.cs

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;



namespace SocketServerTcp

{

    /// <summary>

    /// 对客户端 Socket 及其他相关信息做一个封装

    /// </summary>

    public class ClientSocketPacket

    {

        /// <summary>

        /// 客户端 Socket

        /// </summary>

        public System.Net.Sockets.Socket Socket { get; set; }



        private byte[] _buffer;

        /// <summary>

        /// 为该客户端 Socket 开辟的缓冲区

        /// </summary>

        public byte[] Buffer

        {

            get

            {

                if (_buffer == null)

                    _buffer = new byte[64];



                return _buffer;

            }

        }



        private List<byte> _receivedByte;

        /// <summary>

        /// 客户端 Socket 发过来的信息的字节集合

        /// </summary>

        public List<byte> ReceivedByte

        {

            get

            {

                if (_receivedByte == null)

                    _receivedByte = new List<byte>();



                return _receivedByte;

            }

        }

    }

}

SocketServerTcp/Main.cs

/*

 * 从以前写的 wp7 demo 中直接复制过来的,用于演示如何通过 ControlChannel 实时地将信息以 socket tcp 的方式推送到 win8 客户端

 * 

 * 注:

 * 本例通过一个约定结束符来判断是否接收完整,其仅用于演示,实际项目中请用自定义协议。可参见:XamlDemo/Communication/TcpDemo.xaml.cs

 */



using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Windows.Forms;



using System.Net.Sockets;

using System.Net;

using System.Threading;

using System.IO;



namespace SocketServerTcp

{

    public partial class Main : Form

    {

        SynchronizationContext _syncContext;



        System.Timers.Timer _timer;



        // 信息结束符,用于判断是否完整地读取了客户端发过来的信息,要与客户端的信息结束符相对应(本例只用于演示,实际项目中请用自定义协议)

        private string _endMarker = "^";



        // 服务端监听的 socket

        private Socket _listener;



        // 实例化 ManualResetEvent,设置其初始状态为无信号

        private ManualResetEvent _signal = new ManualResetEvent(false);



        // 客户端 Socket 列表

        private List<ClientSocketPacket> _clientList = new List<ClientSocketPacket>();



        public Main()

        {

            InitializeComponent();



            // UI 线程

            _syncContext = SynchronizationContext.Current;



            // 启动后台线程去运行 Socket 服务

            Thread thread = new Thread(new ThreadStart(LaunchSocketServer));

            thread.IsBackground = true;

            thread.Start();

        }



        private void LaunchSocketServer()

        {

            // 每 10 秒运行一次计时器所指定的方法,群发信息

            _timer = new System.Timers.Timer();

            _timer.Interval = 10000d;

            _timer.Elapsed += new System.Timers.ElapsedEventHandler(_timer_Elapsed);

            _timer.Start();



            // TCP 方式监听 3366 端口

            _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            _listener.Bind(new IPEndPoint(IPAddress.Any, 3366));

            // 指定等待连接队列中允许的最大数

            _listener.Listen(10);





            while (true)

            {

                // 设置为无信号

                _signal.Reset();



                // 开始接受客户端传入的连接

                _listener.BeginAccept(new AsyncCallback(OnClientConnect), null);



                // 阻塞当前线程,直至有信号为止

                _signal.WaitOne();

            }

        }



        private void _timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)

        {

            // 每 10 秒给所有连入的客户端发送一次消息

            SendData(string.Format("webabcd 对所有人说:大家好! 【信息来自服务端 {0}】", DateTime.Now.ToString("hh:mm:ss")));

        }



        private void OnClientConnect(IAsyncResult async)

        {

            ClientSocketPacket client = new ClientSocketPacket();

            // 完成接受客户端传入的连接的这个异步操作,并返回客户端连入的 socket

            client.Socket = _listener.EndAccept(async);



            // 将客户端连入的 Socket 放进客户端 Socket 列表

            _clientList.Add(client);



            OutputMessage(((IPEndPoint)client.Socket.LocalEndPoint).Address + " 连入了服务器");

            SendData("一个新的客户端已经成功连入服务器。。。 【信息来自服务端】");



            try

            {

                // 开始接收客户端传入的数据

                client.Socket.BeginReceive(client.Buffer, 0, client.Buffer.Length, SocketFlags.None, new AsyncCallback(OnDataReceived), client);

            }

            catch (SocketException ex)

            {

                // 处理异常

                HandleException(client, ex);

            }



            // 设置为有信号

            _signal.Set();

        }



        private void OnDataReceived(IAsyncResult async)

        {

            ClientSocketPacket client = async.AsyncState as ClientSocketPacket;



            int count = 0;



            try

            {

                // 完成接收数据的这个异步操作,并返回接收的字节数

                if (client.Socket.Connected)

                    count = client.Socket.EndReceive(async);

            }

            catch (SocketException ex)

            {

                HandleException(client, ex);

            }



            // 把接收到的数据添加进收到的字节集合内

            // 本例采用 UTF8 编码,中文占用 3 字节,英文等字符与 ASCII 相同

            foreach (byte b in client.Buffer.Take(count))

            {

                if (b == 0) continue; // 如果是空字节则不做处理('\0')



                client.ReceivedByte.Add(b);

            }



            // 把当前接收到的数据转换为字符串。用于判断是否包含自定义的结束符

            string receivedString = UTF8Encoding.UTF8.GetString(client.Buffer, 0, count);



            // 如果该 Socket 在网络缓冲区中没有排队的数据 并且 接收到的数据中有自定义的结束符时

            if (client.Socket.Connected && client.Socket.Available == 0 && receivedString.Contains(_endMarker))

            {

                // 把收到的字节集合转换成字符串(去掉自定义结束符)

                // 然后清除掉字节集合中的内容,以准备接收用户发送的下一条信息

                string content = UTF8Encoding.UTF8.GetString(client.ReceivedByte.ToArray());

                content = content.Replace(_endMarker, "");

                client.ReceivedByte.Clear();



                // 发送数据到所有连入的客户端,并在服务端做记录

                SendData(content);

                OutputMessage(content);

            }



            try

            {

                // 继续开始接收客户端传入的数据

                if (client.Socket.Connected)

                    client.Socket.BeginReceive(client.Buffer, 0, client.Buffer.Length, 0, new AsyncCallback(OnDataReceived), client);

            }

            catch (SocketException ex)

            {

                HandleException(client, ex);

            }

        }



        /// <summary>

        /// 发送数据到所有连入的客户端

        /// </summary>

        /// <param name="data">需要发送的数据</param>

        private void SendData(string data)

        {

            byte[] byteData = UTF8Encoding.UTF8.GetBytes(data);



            foreach (ClientSocketPacket client in _clientList)

            {

                if (client.Socket.Connected)

                {

                    try

                    {

                        // 如果某客户端 Socket 是连接状态,则向其发送数据

                        client.Socket.BeginSend(byteData, 0, byteData.Length, SocketFlags.None, new AsyncCallback(OnDataSent), client);

                    }

                    catch (SocketException ex)

                    {

                        HandleException(client, ex);

                    }

                }

                else

                {

                    // 某 Socket 断开了连接的话则将其关闭,并将其清除出客户端 Socket 列表

                    // 也就是说每次向所有客户端发送消息的时候,都会从客户端 Socket 集合中清除掉已经关闭了连接的 Socket

                    client.Socket.Close();

                    _clientList.Remove(client);

                }

            }

        }



        private void OnDataSent(IAsyncResult async)

        {

            ClientSocketPacket client = async.AsyncState as ClientSocketPacket;



            try

            {

                // 完成将信息发送到客户端的这个异步操作

                int sentBytesCount = client.Socket.EndSend(async);

            }

            catch (SocketException ex)

            {

                HandleException(client, ex);

            }

        }



        /// <summary>

        /// 处理 SocketException 异常

        /// </summary>

        /// <param name="client">导致异常的 ClientSocketPacket</param>

        /// <param name="ex">SocketException</param>

        private void HandleException(ClientSocketPacket client, SocketException ex)

        {

            // 在服务端记录异常信息,关闭导致异常的 Socket,并将其清除出客户端 Socket 列表

            OutputMessage(client.Socket.RemoteEndPoint.ToString() + " - " + ex.Message);

            client.Socket.Close();

            _clientList.Remove(client);

        }



        // 在 UI 上输出指定信息

        private void OutputMessage(string data)

        {

            _syncContext.Post((p) => { txtMsg.Text += p.ToString() + "\r\n"; }, data);

        }

    }

}



OK
[源码下载]

你可能感兴趣的:(windows)