背景中的網路通訊

如果要在網路不在前景時繼續通訊,應用程式可以使用背景任務和以下兩種選項之一。

  • 套筒經紀人。 如果你的應用程式使用通訊端來建立長時間連線,那麼當它離開前景狀態時,便可將通訊端的擁有權委派給系統通訊端代理程式。 接著,中介程式會:當流量到達通訊端時啟動你的應用程式;將所有權轉回你的應用程式;然後由你的應用程式處理傳入的流量。
  • 控制通道觸發條件。

在背景任務中執行網路操作

  • 當收到封包且需要執行短暫任務時,使用 SocketActivityTrigger 來啟動背景任務。 完成任務後,背景任務應終止以節省電力。
  • 當收到封包且需要執行長壽命任務時,使用 ControlChannelTrigger 啟動背景任務。

網路相關條件與旗標

  • 在背景任務 BackgroundTaskBuilder.AddCondition 中加入 InternetAvailable條件,以延遲觸發背景任務直到網路堆疊運行。 這種條件能省電,因為背景任務要等網路連線才會執行。 此條件無法即時觸發啟動。

不管你用哪種觸發器,都要在背景任務上設定 IsNetworkRequested ,確保網路在背景任務執行時能保持連線。 這會告訴背景任務基礎架構在執行任務時維持網路運作,即使裝置已進入連網待機模式。 如果你的背景任務沒有使用 IsNetworkRequested,那麼在 Connected 待機模式下(例如手機螢幕關閉時),你的背景任務將無法存取網路。

通訊端代理程式與 SocketActivityTrigger

如果你的應用程式使用 DatagramSocketStreamSocketStreamSocketListener 連線,則應使用 SocketActivityTrigger 和通訊端代理程式,以便在應用程式未位於前景時,當有傳輸流量到達你的應用程式時收到通知。

為了讓你的應用程式在應用程式未啟用時接收並處理接字上收到的資料,應用程式必須在啟動時執行一次性設定,並在轉換到非啟用狀態時,將套接字所有權轉移給套接程式代理。

一次性設定步驟為:建立觸發程序、為該觸發程序註冊背景工作,以及為通訊端代理程式啟用通訊端:

  • 建立一個 SocketActivityTrigger ,並用 TaskEntryPoint 參數註冊一個背景任務,該觸發器用來處理收到封包的程式碼。
            var socketTaskBuilder = new BackgroundTaskBuilder();
            socketTaskBuilder.Name = _backgroundTaskName;
            socketTaskBuilder.TaskEntryPoint = _backgroundTaskEntryPoint;
            var trigger = new SocketActivityTrigger();
            socketTaskBuilder.SetTrigger(trigger);
            _task = socketTaskBuilder.Register();
  • 在繫結套接字之前,請先在該套接字上呼叫 EnableTransferOwnership 方法。
           _tcpListener = new StreamSocketListener();

           // Note that EnableTransferOwnership() should be called before bind,
           // so that tcpip keeps required state for the socket to enable connected
           // standby action. Background task Id is taken as a parameter to tie wake pattern
           // to a specific background task.  
           _tcpListener.EnableTransferOwnership(_task.TaskId, SocketActivityConnectedStandbyAction.Wake);
           _tcpListener.ConnectionReceived += OnConnectionReceived;
           await _tcpListener.BindServiceNameAsync("my-service-name");

當通訊端已正確設定完成後,當應用程式即將暫停時,請在該通訊端上呼叫 TransferOwnership,將其轉移給通訊端代理程式。 中介程式會監控通訊端,並在收到資料時啟用背景任務。 以下範例包含一個工具 TransferOwnership 函式,用於執行 StreamSocketListen socket 的傳輸。 (請注意,不同類型的通訊端各自都有自己的 TransferOwnership 方法,因此您必須針對要轉移其擁有權的通訊端,呼叫對應的方法。您的程式碼可能會包含一個多載的 TransferOwnership 輔助函式,為您使用的每種通訊端類型各提供一個實作,讓 OnSuspending 程式碼保持易於閱讀。)

應用程式會將通訊端的所有權轉移給通訊端代理程式,並使用下列其中一種適當的方法傳遞背景工作的 ID:


// declare int _transferOwnershipCount as a field.

private async void TransferOwnership(StreamSocketListener tcpListener)
{
    await tcpListener.CancelIOAsync();

    var dataWriter = new DataWriter();
    ++_transferOwnershipCount;
    dataWriter.WriteInt32(_transferOwnershipCount);
    var context = new SocketActivityContext(dataWriter.DetachBuffer());
    tcpListener.TransferOwnership(_socketId, context);
}

private void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();

    TransferOwnership(_tcpListener);
    deferral.Complete();
}

在你背景任務的事件處理程式中:

  • 首先,先取得背景任務延遲,這樣你才能用非同步方法處理事件。
var deferral = taskInstance.GetDeferral();
  • 接著,從事件參數中擷取 SocketActivityTriggerDetails,找出事件被提出的原因:
var details = taskInstance.TriggerDetails as SocketActivityTriggerDetails;
    var socketInformation = details.SocketInformation;
    switch (details.Reason)
  • 如果事件是因通訊端活動而引發,請在通訊端上建立一個 DataReader,以非同步方式載入讀取器,然後再根據你的應用程式設計使用資料。 請注意,您必須將 socket 的控制權交還給 socket broker,才能再次收到後續 socket 活動的通知。

以下範例中,接收到的文字會以吐司形式顯示。

case SocketActivityTriggerReason.SocketActivity:
            var socket = socketInformation.StreamSocket;
            DataReader reader = new DataReader(socket.InputStream);
            reader.InputStreamOptions = InputStreamOptions.Partial;
            await reader.LoadAsync(250);
            var dataString = reader.ReadString(reader.UnconsumedBufferLength);
            ShowToast(dataString);
            socket.TransferOwnership(socketInformation.Id); /* Important! */
            break;
  • 如果事件是因為 Keep Alive 計時器過期而觸發,那麼你的程式碼應該會透過 socket 傳送一些資料,以維持 socket 存活並重新啟動 keep alive 計時器。 再次強調,必須將通訊端的擁有權交還給通訊端代理程式,才能接收後續的事件通知:
case SocketActivityTriggerReason.KeepAliveTimerExpired:
            socket = socketInformation.StreamSocket;
            DataWriter writer = new DataWriter(socket.OutputStream);
            writer.WriteBytes(Encoding.UTF8.GetBytes("Keep alive"));
            await writer.StoreAsync();
            writer.DetachStream();
            writer.Dispose();
            socket.TransferOwnership(socketInformation.Id); /* Important! */
            break;
  • 如果事件是因為 socket 已關閉而被觸發,請重新建立 socket,並確保在建立新的 socket 後,將其所有權移交給 socket broker。 在此範例中,主機名稱與埠號會儲存在本地設定中,以便用來建立新的套接字連線:
case SocketActivityTriggerReason.SocketClosed:
            socket = new StreamSocket();
            socket.EnableTransferOwnership(taskInstance.Task.TaskId, SocketActivityConnectedStandbyAction.Wake);
            if (ApplicationData.Current.LocalSettings.Values["hostname"] == null)
            {
                break;
            }
            var hostname = (String)ApplicationData.Current.LocalSettings.Values["hostname"];
            var port = (String)ApplicationData.Current.LocalSettings.Values["port"];
            await socket.ConnectAsync(new HostName(hostname), port);
            socket.TransferOwnership(socketId);
            break;
  • 處理完活動通知後,別忘了完成延期申請:
  deferral.Complete();

欲完整示範 SocketActivityTrigger 與套接程式代理的使用範例,請參見 SocketActivityStreamSocket 範例。 套接字的初始化在 Scenario1_Connect.xaml.cs 中進行,背景任務實作則在 SocketActivityTask.cs。

你可能會注意到範例在建立新套接字或取得現有套接字時,會立即呼叫 TransferOwnership ,而不是像本主題所述使用 OnSuspending even handler 來執行。 這是因為範例著重於示範 SocketActivityTrigger,且在執行時並未將該套接字用於其他活動。 你的應用程式可能會比較複雜,應該用 OnSuspending 來判斷何時呼叫 TransferOwnership

控制通道觸發器

首先,確保你正確使用控制通道觸發器(CCT)。 如果你使用的是 DatagramSocketStreamSocketStreamSocketListener 連線,我們建議你使用 SocketActivityTrigger。 你可以用 CCT 來管理 StreamSocket,但它們會佔用更多資源,而且可能無法在 Connected 待機模式下運作。

如果你使用 WebSockets、IXMLHTTPRequest2System.Net.Http.HttpClientWindows,Web.Http.HttpClient,然後你必須使用 ControlChannelTrigger

ControlChannelTrigger 與 WebSockets

Important

本節所述功能(帶有 WebSockets 的 ControlChannelTrigger)在 Windows SDK 10.0.15063.0 版本及之後版本中得到支援。

使用 MessageWebSocketStreamWebSocket 搭配 ControlChannelTrigger 時,需特別注意。 使用 MessageWebSocketStreamWebSocket 搭配 ControlChannelTrigger 時,應遵循一些傳輸專用的使用模式與最佳實務。 此外,這些考量也會影響 StreamWebSocket 上接收封包請求的處理方式。 在 MessageWebSocket 上接收封包的請求不會受到影響。

使用 MessageWebSocketStreamWebSocket 搭配 ControlChannelTrigger 時,應遵循以下使用模式與最佳實務:

  • 必須隨時保持一個未完成的通訊端接收作業處於已提交狀態。 這是讓推播通知任務得以執行的必要條件。
  • WebSocket 協定定義了一種保持活著訊息的標準模型。 WebSocketKeepAlive 類別可向伺服器發送由用戶端發起的 WebSocket 協定 keep-alive 訊息。 WebSocketKeepAlive 類別應該被應用程式註冊為 KeepAliveTrigger 的 TaskEntryPoint。

一些特殊考量會影響 StreamWebSocket 上接收封包的請求處理方式。 特別是在使用 StreamWebSocket 搭配 ControlChannelTrigger 時,應用程式必須使用原始非同步模式來處理讀取,而非 C# 和 VB.NET 中的 waitit 模型或 C++ 中的 Tasks。 原始非同步模式在本節後面的程式碼範例中會說明。

使用原始非同步模式,Windows 能同步 ControlChannelTrigger 背景任務的 IBackgroundTask.Run 方法與接收完成回調的回傳。 Run 方法會在完成回調回傳後被呼叫。 這確保應用程式在 執行方法被 呼叫前已收到資料或錯誤。

值得注意的是,應用程式必須在完成回呼函式返回控制權之前,再次發出讀取要求。 同時也要注意, DataReader 不能直接與 MessageWebSocketStreamWebSocket 傳輸一起使用,因為這會破壞上述的同步。 不支援直接在傳輸端上方使用 DataReader.LoadAsync 方法。 取而代之的是,IInputStream.ReadAsync 方法在 StreamWebSocket.InputStream 屬性上回傳的 IBuffer 可以之後傳給 DataReader.FromBuffer 方法進行進一步處理。

以下範例展示了如何使用原始非同步模式來處理 StreamWebSocket 上的讀取。

void PostSocketRead(int length)
{
    try
    {
        var readBuf = new Windows.Storage.Streams.Buffer((uint)length);
        var readOp = socket.InputStream.ReadAsync(readBuf, (uint)length, InputStreamOptions.Partial);
        readOp.Completed = (IAsyncOperationWithProgress<IBuffer, uint>
            asyncAction, AsyncStatus asyncStatus) =>
        {
            switch (asyncStatus)
            {
                case AsyncStatus.Completed:
                case AsyncStatus.Error:
                    try
                    {
                        // GetResults in AsyncStatus::Error is called as it throws a user friendly error string.
                        IBuffer localBuf = asyncAction.GetResults();
                        uint bytesRead = localBuf.Length;
                        readPacket = DataReader.FromBuffer(localBuf);
                        OnDataReadCompletion(bytesRead, readPacket);
                    }
                    catch (Exception exp)
                    {
                        Diag.DebugPrint("Read operation failed:  " + exp.Message);
                    }
                    break;
                case AsyncStatus.Canceled:

                    // Read is not cancelled in this sample.
                    break;
           }
       };
   }
   catch (Exception exp)
   {
       Diag.DebugPrint("failed to post a read failed with error:  " + exp.Message);
   }
}

讀取完成處理程序在 ControlChannelTrigger 背景任務的 IBackgroundTask 執行方法被呼叫前,必定會啟動。 Windows 具有內部同步機制,會等待應用程式從讀取完成回呼函式返回。 應用程式通常會在讀取完成回呼中,快速處理來自 MessageWebSocketStreamWebSocket 的資料或錯誤。 訊息本身是在 IBackgroundTask.Run 方法的上下文中處理的。 以下範例中,透過使用訊息佇列說明此點,讀取完成處理器將訊息插入該佇列,背景任務則由後續處理。

以下範例顯示搭配原始非同步模式使用的讀取完成處理常式,用於處理 StreamWebSocket 上的讀取作業。

public void OnDataReadCompletion(uint bytesRead, DataReader readPacket)
{
    if (readPacket == null)
    {
        Diag.DebugPrint("DataReader is null");

        // Ideally when read completion returns error,
        // apps should be resilient and try to
        // recover if there is an error by posting another recv
        // after creating a new transport, if required.
        return;
    }
    uint buffLen = readPacket.UnconsumedBufferLength;
    Diag.DebugPrint("bytesRead: " + bytesRead + ", unconsumedbufflength: " + buffLen);

    // check if buffLen is 0 and treat that as fatal error.
    if (buffLen == 0)
    {
        Diag.DebugPrint("Received zero bytes from the socket. Server must have closed the connection.");
        Diag.DebugPrint("Try disconnecting and reconnecting to the server");
        return;
    }

    // Perform minimal processing in the completion
    string message = readPacket.ReadString(buffLen);
    Diag.DebugPrint("Received Buffer : " + message);

    // Enqueue the message received to a queue that the push notify
    // task will pick up.
    AppContext.messageQueue.Enqueue(message);

    // Post another receive to ensure future push notifications.
    PostSocketRead(MAX_BUFFER_LENGTH);
}

WebSocket 的另一個細節是保活處理常式。 WebSocket 協定定義了一種保持活著訊息的標準模型。

使用 MessageWebSocketStreamWebSocket 時,請註冊一個 WebSocketKeepAlive 類別實例作為 KeepAliveTrigger 的 TaskEntryPoint ,讓應用程式能夠解除暫停,並定期向伺服器(遠端端點)發送 keep-alive 訊息。 這也應作為背景註冊應用程式程式碼以及套件資訊清單的一部分來完成。

Windows.Sockets.WebSocketKeepAlive 的工作進入點需要在兩個地方指定:

  • 在原始碼中建立 KeepAliveTrigger 觸發器時(見下方範例)。
  • 在應用程式套件的 manifest 裡,針對 keepalive 背景任務宣告。

以下範例會在應用程式資訊清單中的 <Application> 元素下,新增網路觸發程序通知和保持連線觸發程序。

  <Extensions>
    <Extension Category="windows.backgroundTasks"
         Executable="$targetnametoken$.exe"
         EntryPoint="Background.PushNotifyTask">
      <BackgroundTasks>
        <Task Type="controlChannel" />
      </BackgroundTasks>
    </Extension>
    <Extension Category="windows.backgroundTasks"
         Executable="$targetnametoken$.exe"
         EntryPoint="Windows.Networking.Sockets.WebSocketKeepAlive">
      <BackgroundTasks>
        <Task Type="controlChannel" />
      </BackgroundTasks>
    </Extension>
  </Extensions>

應用程式在 ControlChannelTrigger 中使用 wait 語句,以及在 StreamWebSocketMessageWebSocketStreamSocket 上執行非同步操作時,必須非常小心。 Task<bool> 物件可用來在 StreamWebSocket 上註冊 ControlChannelTrigger,以用於推播通知和 WebSocket 保活,並連線傳輸。 在註冊過程中,系統會將 StreamWebSocket 設定為 ControlChannelTrigger 的傳輸,並發出讀取要求。 Task.Result 會阻塞目前執行緒,直到任務中的所有步驟都執行完畢,並在訊息主體中回傳語句。 該任務直到方法返回真或假時才會被解決。 這確保整個方法都能被執行。 任務可以包含多個受任務保護的等待語句。 當使用 StreamWebSocketMessageWebSocket 作為傳輸時,應搭配 ControlChannelTrigger 物件使用此模式。 對於可能需要長時間完成的操作(例如典型的非同步讀取操作),應用程式應使用前述原始非同步模式。

以下範例會在 StreamWebSocket 上註冊 ControlChannelTrigger,以用於推播通知及 WebSocket 保持連線。

private bool RegisterWithControlChannelTrigger(string serverUri)
{
    // Make sure the objects are created in a system thread
    // Demonstrate the core registration path
    // Wait for the entire operation to complete before returning from this method.
    // The transport setup routine can be triggered by user control, by network state change
    // or by keepalive task
    Task<bool> registerTask = RegisterWithCCTHelper(serverUri);
    return registerTask.Result;
}

async Task<bool> RegisterWithCCTHelper(string serverUri)
{
    bool result = false;
    socket = new StreamWebSocket();

    // Specify the keepalive interval expected by the server for this app
    // in order of minutes.
    const int serverKeepAliveInterval = 30;

    // Specify the channelId string to differentiate this
    // channel instance from any other channel instance.
    // When background task fires, the channel object is provided
    // as context and the channel id can be used to adapt the behavior
    // of the app as required.
    const string channelId = "channelOne";

    // For websockets, the system does the keepalive on behalf of the app
    // But the app still needs to specify this well known keepalive task.
    // This should be done here in the background registration as well
    // as in the package manifest.
    const string WebSocketKeepAliveTask = "Windows.Networking.Sockets.WebSocketKeepAlive";

    // Try creating the controlchanneltrigger if this has not been already
    // created and stored in the property bag.
    ControlChannelTriggerStatus status;

    // Create the ControlChannelTrigger object and request a hardware slot for this app.
    // If the app is not on LockScreen, then the ControlChannelTrigger constructor will
    // fail right away.
    try
    {
        channel = new ControlChannelTrigger(channelId, serverKeepAliveInterval,
                                   ControlChannelTriggerResourceType.RequestHardwareSlot);
    }
    catch (UnauthorizedAccessException exp)
    {
        Diag.DebugPrint("Is the app on lockscreen? " + exp.Message);
        return result;
    }

    Uri serverUriInstance;
    try
    {
        serverUriInstance = new Uri(serverUri);
    }
    catch (Exception exp)
    {
        Diag.DebugPrint("Error creating URI: " + exp.Message);
        return result;
    }

    // Register the apps background task with the trigger for keepalive.
    var keepAliveBuilder = new BackgroundTaskBuilder();
    keepAliveBuilder.Name = "KeepaliveTaskForChannelOne";
    keepAliveBuilder.TaskEntryPoint = WebSocketKeepAliveTask;
    keepAliveBuilder.SetTrigger(channel.KeepAliveTrigger);
    keepAliveBuilder.Register();

    // Register the apps background task with the trigger for push notification task.
    var pushNotifyBuilder = new BackgroundTaskBuilder();
    pushNotifyBuilder.Name = "PushNotificationTaskForChannelOne";
    pushNotifyBuilder.TaskEntryPoint = "Background.PushNotifyTask";
    pushNotifyBuilder.SetTrigger(channel.PushNotificationTrigger);
    pushNotifyBuilder.Register();

    // Tie the transport method to the ControlChannelTrigger object to push enable it.
    // Note that if the transport' s TCP connection is broken at a later point of time,
    // the ControlChannelTrigger object can be reused to plug in a new transport by
    // calling UsingTransport API again.
    try
    {
        channel.UsingTransport(socket);

        // Connect the socket
        //
        // If connect fails or times out it will throw exception.
        // ConnectAsync can also fail if hardware slot was requested
        // but none are available
        await socket.ConnectAsync(serverUriInstance);

        // Call WaitForPushEnabled API to make sure the TCP connection has
        // been established, which will mean that the OS will have allocated
        // any hardware slot for this TCP connection.
        //
        // In this sample, the ControlChannelTrigger object was created by
        // explicitly requesting a hardware slot.
        //
        // On systems that without connected standby, if app requests hardware slot as above,
        // the system will fallback to a software slot automatically.
        //
        // On systems that support connected standby,, if no hardware slot is available, then app
        // can request a software slot by re-creating the ControlChannelTrigger object.
        status = channel.WaitForPushEnabled();
        if (status != ControlChannelTriggerStatus.HardwareSlotAllocated
            && status != ControlChannelTriggerStatus.SoftwareSlotAllocated)
        {
            throw new Exception(string.Format("Neither hardware nor software slot could be allocated. ChannelStatus is {0}", status.ToString()));
        }

        // Store the objects created in the property bag for later use.
        CoreApplication.Properties.Remove(channel.ControlChannelTriggerId);

        var appContext = new AppContext(this, socket, channel, channel.ControlChannelTriggerId);
        ((IDictionary<string, object>)CoreApplication.Properties).Add(channel.ControlChannelTriggerId, appContext);
        result = true;

        // Almost done. Post a read since we are using streamwebsocket
        // to allow push notifications to be received.
        PostSocketRead(MAX_BUFFER_LENGTH);
    }
    catch (Exception exp)
    {
         Diag.DebugPrint("RegisterWithCCTHelper Task failed with: " + exp.Message);

         // Exceptions may be thrown for example if the application has not
         // registered the background task class id for using real time communications
         // broker in the package manifest.
    }
    return result;
}

欲了解更多關於使用 MessageWebSocketStreamWebSocket 搭配 ControlChannelTrigger 的資訊,請參閱 ControlChannelTrigger StreamWebSocket 範例

ControlChannelTrigger 與 HttpClient

使用 HttpClient 搭配 ControlChannelTrigger 時,需特別注意。 使用 ControlChannelTriggerHttpClient 時,應遵循一些傳輸專用的使用模式與最佳實務。 此外,這些考量也會影響在 HttpClient 上接收封包的請求處理方式。

請注意,目前使用 SSL 的 Httpclient 不支援網路觸發功能與 ControlChannelTrigger。   使用 HttpClient 搭配 ControlChannelTrigger 時,應遵循以下使用模式與最佳實務:

  • 應用程式可能需要在 System.Net.Http 命名空間的 HttpClientHttpClientHandler 物件上設定各種屬性與標頭,然後再將請求傳送到特定 URI。
  • 應用程式可能需要先提出初始請求,先正確測試並設定傳輸,然後才能建立 HttpClient 傳輸以搭配 ControlChannelTrigger 使用。 一旦應用程式判斷傳輸可以正確設定,就可以將 HttpClient 物件設定為與 ControlChannelTrigger 物件搭配使用的傳輸物件。 此程序旨在防止某些情況中斷透過傳輸層建立的連線。 使用 SSL 搭配憑證時,應用程式可能需要顯示對話框以輸入 PIN 或選擇多個憑證。 可能需要代理驗證和伺服器認證。 若代理或伺服器認證失效,連線可能會被關閉。 應用程式處理這些認證過期問題的一種方法是設定計時器。 當需要 HTTP 重定向時,無法保證能可靠建立第二個連線。 初步測試要求會先確保應用程式在使用 HttpClient 物件作為 ControlChannelTrigger 物件的傳輸機制之前,能夠使用最新的重新導向 URL。

與其他網路傳輸不同,HttpClient 物件無法直接傳遞至 ControlChannelTrigger 物件的 UsingTransport 方法。 相反地,必須特別建構一個 HttpRequestMessage 物件,以配合 HttpClient 物件與 ControlChannelTrigger 使用。 HttpRequestMessage 物件是使用 RtcRequestFactory.Create 方法建立的。 建立的 HttpRequestMessage 物件會傳給 UsingTransport 方法。

以下範例展示了如何構建一個 HttpRequestMessage 物件,以配合 HttpClient 物件與 ControlChannelTrigger 使用。

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Windows.Networking.Sockets;

public HttpRequestMessage httpRequest;
public HttpClient httpClient;
public HttpRequestMessage httpRequest;
public ControlChannelTrigger channel;
public Uri serverUri;

private void SetupHttpRequestAndSendToHttpServer()
{
    try
    {
        // For HTTP based transports that use the RTC broker, whenever we send next request, we will abort the earlier
        // outstanding http request and start new one.
        // For example in case when http server is taking longer to reply, and keep alive trigger is fired in-between
        // then keep alive task will abort outstanding http request and start a new request which should be finished
        // before next keep alive task is triggered.
        if (httpRequest != null)
        {
            httpRequest.Dispose();
        }

        httpRequest = RtcRequestFactory.Create(HttpMethod.Get, serverUri);

        SendHttpRequest();
    }
        catch (Exception e)
    {
        Diag.DebugPrint("Connect failed with: " + e.ToString());
        throw;
    }
}

一些特殊考量會影響在 HttpClient 上發送 HTTP 請求以啟動回應的方式。 特別是在使用 ControlChannelTriggerHttpClient 時,應用程式必須用任務來處理傳送,而非等待模型。

使用 HttpClient 時,對於 ControlChannelTrigger 的背景工作,IBackgroundTask.Run 方法與接收完成回呼的返回之間沒有同步機制。 因此,應用程式只能在 Run 方法中使用阻擋 HttpResponseMessage 技術,並等待收到完整回應。

使用 ControlChannelTriggerHttpClientStreamSocketMessageWebSocketStreamWebSocket 傳輸有明顯不同。 HttpClient 的接收回呼會透過 Task 傳遞給應用程式,因為它是由 HttpClient 程式碼傳遞的。 這表示 ControlChannelTrigger 推播通知任務會在資料或錯誤被派遣到應用程式後立即觸發。 在下方範例中,程式碼將 HttpClient.SendAsync 方法回傳的 responseTask 儲存到全域儲存中,推送通知任務會在此擷取並內嵌處理。

以下範例展示了如何處理 HttpClient 上的傳送請求,當搭配 ControlChannelTrigger 使用時。

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Windows.Networking.Sockets;

private void SendHttpRequest()
{
    if (httpRequest == null)
    {
        throw new Exception("HttpRequest object is null");
    }

    // Tie the transport method to the controlchanneltrigger object to push enable it.
    // Note that if the transport' s TCP connection is broken at a later point of time,
    // the controlchanneltrigger object can be reused to plugin a new transport by
    // calling UsingTransport API again.
    channel.UsingTransport(httpRequest);

    // Call the SendAsync function to kick start the TCP connection establishment
    // process for this http request.
    Task<HttpResponseMessage> httpResponseTask = httpClient.SendAsync(httpRequest);

    // Call WaitForPushEnabled API to make sure the TCP connection has been established,
    // which will mean that the OS will have allocated any hardware slot for this TCP connection.
    ControlChannelTriggerStatus status = channel.WaitForPushEnabled();
    Diag.DebugPrint("WaitForPushEnabled() completed with status: " + status);
    if (status != ControlChannelTriggerStatus.HardwareSlotAllocated
        && status != ControlChannelTriggerStatus.SoftwareSlotAllocated)
    {
        throw new Exception("Hardware/Software slot not allocated");
    }

    // The HttpClient receive callback is delivered via a Task to the app.
    // The notification task will fire as soon as the data or error is dispatched
    // Enqueue the responseTask returned by httpClient.sendAsync
    // into a queue that the push notify task will pick up and process inline.
    AppContext.messageQueue.Enqueue(httpResponseTask);
}

以下範例展示了如何透過 ControlChannelTrigger 讀取 HttpClient 收到的回應。

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public string ReadResponse(Task<HttpResponseMessage> httpResponseTask)
{
    string message = null;
    try
    {
        if (httpResponseTask.IsCanceled || httpResponseTask.IsFaulted)
        {
            Diag.DebugPrint("Task is cancelled or has failed");
            return message;
        }
        // We' ll wait until we got the whole response.
        // This is the only supported scenario for HttpClient for ControlChannelTrigger.
        HttpResponseMessage httpResponse = httpResponseTask.Result;
        if (httpResponse == null || httpResponse.Content == null)
        {
            Diag.DebugPrint("Cannot read from httpresponse, as either httpResponse or its content is null. try to reset connection.");
        }
        else
        {
            // This is likely being processed in the context of a background task and so
            // synchronously read the Content' s results inline so that the Toast can be shown.
            // before we exit the Run method.
            message = httpResponse.Content.ReadAsStringAsync().Result;
        }
    }
    catch (Exception exp)
    {
        Diag.DebugPrint("Failed to read from httpresponse with error:  " + exp.ToString());
    }
    return message;
}

欲了解更多關於使用 HttpClient 搭配 ControlChannelTrigger 的資訊,請參閱 ControlChannelTrigger HttpClient 範例

ControlChannelTrigger 搭配 IXMLHttpRequest2

使用 IXMLHTTPRequest2 搭配 ControlChannelTrigger 時,需特別注意。 使用 IXMLHTTPRequest2 搭配 ControlChannelTrigger 時,應遵循一些傳輸專用的使用模式與最佳實務。 使用 ControlChannelTrigger 不會影響 IXMLHTTPRequest2 上 HTTP 請求的傳送或接收方式。

使用 IXMLHTTPRequest2 搭配 ControlChannelTrigger 時的使用模式與最佳實務

  • IXMLHTTPRequest2 物件作為傳輸時,其壽命僅有一個請求/回應。 當使用 ControlChannelTrigger 物件時,只需建立並設定一次 ControlChannelTrigger 物件,然後反覆呼叫 UsingTransport 方法,每次都會關聯一個新的 IXMLHTTPRequest2 物件,方便使用。 應用程式應在提供新的 IXMLHTTPRequest2 物件前刪除先前的 IXMLHTTPRequest2 物件,以確保應用程式不會超過分配的資源限制。
  • 應用程式可能需要先呼叫 SetPropertySetRequestHeader 方法來設定 HTTP 傳輸,然後再呼叫 Send 方法。
  • 應用程式可能需要先發送 初始傳送請求 ,以測試並正確設定傳輸,然後才能建立與 ControlChannelTrigger 共用的傳輸。 一旦應用程式判斷傳輸已正確設定,即可將 IXMLHTTPRequest2 物件設定為與 ControlChannelTrigger 搭配使用的傳輸物件。 此程序旨在防止某些情況破壞透過傳輸層建立的連線。 使用 SSL 搭配憑證時,應用程式可能需要顯示對話框以輸入 PIN 或選擇多個憑證。 可能需要代理驗證和伺服器認證。 若代理或伺服器認證失效,連線可能會被關閉。 應用程式處理這些認證過期問題的一種方法是設定計時器。 當需要 HTTP 重定向時,無法保證能可靠建立第二個連線。 初始測試請求將確保應用程式在使用 IXMLHTTPRequest2 物件作為 ControlChannelTrigger 物件所使用的傳輸機制之前,能夠使用最新的重新導向 URL。

欲了解更多關於使用 IXMLHTTPRequest2 搭配 ControlChannelTrigger 的資訊,請參閱 ControlChannelTrigger with IXMLHTTPRequest2 範例

重要 API