Search code examples
c#asp.net-web-apiwebsocketiis-8

How to gracefully close a two-way WebSocket in .Net


I have a WebSocket server that accepts a stream of binary data from a client and responds with another stream of text data for every 4MB read. The server uses IIS 8 and asp.net web api.

Server

public class WebSocketController : ApiController
{
    public HttpResponseMessage Get()
    {
        if (!HttpContext.Current.IsWebSocketRequest)
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest);
        }

        HttpContext.Current.AcceptWebSocketRequest(async (context) =>
        {
            try
            {
                WebSocket socket = context.WebSocket;

                byte[] requestBuffer = new byte[4194304];
                int offset = 0;

                while (socket.State == WebSocketState.Open)
                {
                    var requestSegment = new ArraySegment<byte>(requestBuffer, offset, requestBuffer.Length - offset);
                    WebSocketReceiveResult result = await socket.ReceiveAsync(requestSegment, CancellationToken.None);

                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        // Send one last response before closing
                        var response = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Server got " + offset + " bytes\n"));
                        await socket.SendAsync(response, WebSocketMessageType.Text, true, CancellationToken.None);

                        // Close
                        await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                        break;
                    }

                    offset += result.Count;
                    if (offset == requestBuffer.Length)
                    {
                        // Regular response
                        var response = new ArraySegment<byte>(Encoding.UTF8.GetBytes("Server got 4194304 bytes\n"));
                        await socket.SendAsync(response, WebSocketMessageType.Text, true, CancellationToken.None);
                        offset = 0;
                    }
                }
            }
            catch (Exception ex)
            {
                // Log and continue
            }
        });

        return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols);
    }
}

The c# client uses the ClientWebSocket class to connect to the server and send requests. It creates a task for receiving responses from the server that runs in parallel with the request sending. When it is done sending the requests it calls CloseAsync on the socket and then waits for the Receive task to complete.

Client

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WebSocketClient
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                CallWebSocketServer().Wait();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static async Task CallWebSocketServer()
        {
            using (ClientWebSocket socket = new ClientWebSocket())
            {
                await socket.ConnectAsync(new Uri("ws://localhost/RestWebController"), CancellationToken.None);
                byte[] buffer = new byte[128 * 1024];

                Task receiveTask = Receive(socket);
                for (int i = 0; i < 1024; ++i)
                {
                    await socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Binary, true, CancellationToken.None);
                }
               
                await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                receiveTask.Wait();

                Console.WriteLine("All done");
            }
        }

        static async Task Receive(ClientWebSocket socket)
        {
            try
            {
                byte[] recvBuffer = new byte[64 * 1024];
                while (socket.State == WebSocketState.Open)
                {
                    var result = await socket.ReceiveAsync(new ArraySegment<byte>(recvBuffer), CancellationToken.None);
                    Console.WriteLine("Client got {0} bytes", result.Count);
                    Console.WriteLine(Encoding.UTF8.GetString(recvBuffer, 0, result.Count));
                    if (result.MessageType == WebSocketMessageType.Close)
                    {
                        Console.WriteLine("Close loop complete");
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception in receive - {0}", ex.Message);
            }
        }
    }
}

The problem is that the client blocks at the CloseAsync call.

What would be the correct way of gracefully closing the WebSocket in this scenario?


Solution

  • Figured this out.

    Server

    Basically, I had to call the ClientWebSocket.CloseOutputAsync (instead of the CloseAsync) method to tell the framework no more output is going to be sent from the client.

    await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
    

    Client

    Then in the Receive function, I had to allow for socket state WebSocketState.CloseSent to receive the Close response from the server

    static async Task Receive(ClientWebSocket socket)
    {
        try
        {
            byte[] recvBuffer = new byte[64 * 1024];
            while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent)
            {
                var result = await socket.ReceiveAsync(new ArraySegment<byte>(recvBuffer), CancellationToken.None);
                Console.WriteLine("Client got {0} bytes", result.Count);
                Console.WriteLine(Encoding.UTF8.GetString(recvBuffer, 0, result.Count));
                if (result.MessageType == WebSocketMessageType.Close)
                {
                    Console.WriteLine("Close loop complete");
                    break;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception in receive - {0}", ex.Message);
        }
    }