重新想象 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
[源碼下載]