重新想象 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 = ;
// 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"), ""); // 开始等待 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 = ; 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 > )
{
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[]; 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, ));
// 指定等待连接队列中允许的最大数
_listener.Listen(); 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, , 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 = ; 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 == ) continue; // 如果是空字节则不做处理('\0') client.ReceivedByte.Add(b);
} // 把当前接收到的数据转换为字符串。用于判断是否包含自定义的结束符
string receivedString = UTF8Encoding.UTF8.GetString(client.Buffer, , count); // 如果该 Socket 在网络缓冲区中没有排队的数据 并且 接收到的数据中有自定义的结束符时
if (client.Socket.Connected && client.Socket.Available == && 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, , client.Buffer.Length, , 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, , 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
[源码下载]