• 重新想象 Windows 8 Store Apps (68)


    [源码下载]


    重新想象 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; // 如果是空字节则不做处理('')
    
                    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() + "
    "; }, data);
            }
        }
    }



    OK
    [源码下载]

  • 相关阅读:
    commons-logging.jar 和 log4j.jar 的关系
    百钱买百鸡
    reflect
    golang结构体、接口、反射
    golang文件操作
    sqlx使用说明
    go example
    goroutine
    生成二维码
    method&interface
  • 原文地址:https://www.cnblogs.com/webabcd/p/3391694.html
Copyright © 2020-2023  润新知