Partilhar via


How to use a WebSocket with a network trigger (XAML)

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

This topic shows how to maintain a WebSocket network connection using ControlChannelTrigger in a Windows Store app when an app is in the background.

What you need to know

Technologies

Prerequisites

  • The following information applies to any connected or network-aware Windows Store app that depends on network connections using WebSockets to always be connected. This topic applies to apps written in C++/XAML and apps using the .NET Framework 4.5 in C#, VB.NET, or managed C++ on Windows 8 and Windows Server 2012.

    This topic does not apply to apps written in JavaScript or a foreground app in JavaScript with an in-process C# or C++ binary. Background network connectivity using a network trigger with ControlChannelTrigger is not supported by a JavaScript app. For information on background tasks that apply to JavaScript apps, see Supporting your app with background tasks. For information on background network connectivity supported by a JavaScript app, see Staying connected in the background (HTML).

Instructions

Step 1: Using WebSockets with ControlChannelTrigger

Some special considerations apply when using MessageWebSocket or StreamWebSocket with ControlChannelTrigger. There are some transport-specific usage patterns and best practices that should be followed when using a MessageWebSocket or StreamWebSocket with ControlChannelTrigger. In addition, these considerations affect the way that requests to receive packets on the StreamWebSocket are handled. Requests to receive packets on the MessageWebSocket are not affected.

The following usage patterns and best practices should be followed when using MessageWebSocket or StreamWebSocket with ControlChannelTrigger:

  • An outstanding socket receive must be kept posted at all times. This is required to allow the push notification tasks to occur.
  • The WebSocket protocol defines a standard model for keep-alive messages. The WebSocketKeepAlive class can send client-initiated WebSocket protocol keep-alive messages to the server. The WebSocketKeepAlive class should be registered as the TaskEntryPoint for a KeepAliveTrigger by the app.

Some special considerations affect the way that requests to receive packets on the StreamWebSocket are handled. In particular, when using a StreamWebSocket with the ControlChannelTrigger, your app must use a raw async pattern for handling reads instead of the await model in C# and VB.NET or Tasks in C++.

Using the raw async pattern allows Windows to synchronize the IBackgroundTask.Run method on the background task for the ControlChannelTrigger with the return of the receive completion callback. The Run method is invoked after the completion callback returns. This ensures that the app has received the data/errors before the Run method is invoked.

It is important to note that the app has to post another read before it returns control from the completion callback. It is also important to note that the DataReader cannot be directly used with the MessageWebSocket or StreamWebSocket transport since that breaks the synchronization described above. It is not supported to use the DataReader.LoadAsync method directly on top of the transport. Instead, the IBuffer returned by the IInputStream.ReadAsync method on the StreamWebSocket.InputStream property can be later passed to DataReader.FromBuffer method for further processing.

The following sample shows how to use a raw async pattern for handling reads on the StreamSocket.

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);
   }
}

The read completion handler is guaranteed to fire before the IBackgroundTask.Run method on the background task for the ControlChannelTrigger is invoked. Windows has internal synchronization to wait for an app to return from the read completion callback. The app typically quickly processes the data or the error from the MessageWebSocket or StreamWebSocket in the read completion callback. The message itself is processed within the context of the IBackgroundTask.Run method. In this sample below, this point is illustrated by using a message queue that the read completion handler inserts the message into and the background task later processes.

The following sample shows the read completion handler to use with a raw async pattern for handling reads on the 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);
}

An additional detail for Websockets is the keep-alive handler. The WebSocket protocol defines a standard model for keep-alive messages.

The WebSocketKeepAlive class can send client-initiated WebSocket protocol keep-alive messages to the server. The WebSocketKeepAlive class should be registered as the TaskEntryPoint for a KeepAliveTrigger by the app. When using MessageWebSocket or StreamWebSocket, the WebSocketKeepAlive class should be registered as the TaskEntryPoint for a KeepAliveTrigger to allow the app to be unsuspended and send keep-alive messages to the server (remote endpoint). This should be done as part of the background registration app code as well as in the package manifest.

This task entry point of Windows.Sockets.WebSocketKeepAlive needs to be specified in two places:

  • When creating KeepAliveTrigger trigger in the source code (see example below).
  • In the app package manifest for the keepalive background task declaration.

The following sample adds a network trigger notification and a keepalive trigger under the <Application> element in an app manifest.

  <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> 

An app must be extremely careful when using an await statement in the context of a ControlChannelTrigger and an asynchronous operation on a StreamWebSocket, MessageWebSocket, or StreamSocket. A Task<bool> object can be used to register a ControlChannelTrigger for push notification and WebSocket keep-alives on the StreamWebSocket and connect the transport. As part of the registration, the StreamWebSocket transport is set as the transport for the ControlChannelTrigger and a read is posted. The Task.Result will block the current thread until all steps in the task execute and return statements in message body. The task is not resolved until the method returns either true or false. This guarantees that the whole method is executed. The Task can contain multiple await statements that are protected by the Task. This pattern should be used with the ControlChannelTrigger object when a StreamWebSocket or MessageWebSocket is used as the transport. For those operations that may take a long period of time to complete (a typical async read operation, for example), the app should use the raw async pattern discussed previously.

The following sample registers ControlChannelTrigger for push notification and WebSocket keep-alives on the StreamWebSocket.

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
}

For more information on using MessageWebSocket or StreamWebSocket with ControlChannelTrigger, see the ControlChannelTrigger StreamWebSocket sample.

Step 2: Previous steps

For more information on how to create a lock screen app to receive background network notifications that use network triggers, see Quickstart: Create a lock screen app that uses background network triggers.

For more information on how to use network triggers to deliver notifications to a lock screen app, see How to use network triggers to deliver notifications to a lock screen app.

For more information on how to write a background task to receive background network notifications that use network triggers, see How to write a background task for a network trigger.

For more information on how to re-establish a network trigger and a transport connection, see How to re-establish a network trigger and transport connection.

Step 3: Further steps

For more information on guidelines and checklists for using network triggers, see Guidelines and checklist for using network triggers.

Other resources

Adding support for networking

Background Networking

Badge overview

Connecting with WebSockets(XAML)

Lock screen overview

Staying connected in the background

Supporting your app with background tasks

Tile and tile notification overview

Toast notification overview

Transferring data in the background

Troubleshooting and debugging network connections

Reference

ControlChannelTrigger

MessageWebSocket

StreamWebSocket

Windows.ApplicationModel.Background

Windows.Networking.Sockets

Samples

Background task sample

ControlChannelTrigger StreamWebSocket sample

Lock screen apps sample

Push and periodic notifications client-side sample

Raw notifications sample