How to work with System.Net.WebSockets without ASP.NET?

Ian's answer definitely was good, but I needed a loop process. The mutex was key for me. This is a working .net core 2 example based on his. I can't speak to scalability of this loop.

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


namespace WebSocketServerConsole
{
    public class Program
    {
        static HttpListener httpListener = new HttpListener();
        private static Mutex signal = new Mutex();
        public static void Main(string[] args)
        {
            httpListener.Prefixes.Add("http://localhost:8080/");
            httpListener.Start();
            while (signal.WaitOne())
            {
                ReceiveConnection();
            }

        }

        public static async System.Threading.Tasks.Task ReceiveConnection()
        {
            HttpListenerContext context = await 
            httpListener.GetContextAsync();
            if (context.Request.IsWebSocketRequest)
            {
                HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
                WebSocket webSocket = webSocketContext.WebSocket;
                while (webSocket.State == WebSocketState.Open)
                {
                    await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello world")),
                        WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
            signal.ReleaseMutex();
        }
    }
}

and a test html page for it.

<!DOCTYPE html>
  <meta charset="utf-8" />
  <title>WebSocket Test</title>
  <script language="javascript" type="text/javascript">

  var wsUri = "ws://localhost:8080/";
  var output;

  function init()
  {
    output = document.getElementById("output");
    testWebSocket();
  }

  function testWebSocket()
  {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }

  function onOpen(evt)
  {
    writeToScreen("CONNECTED");
    doSend("WebSocket rocks");
  }

  function onClose(evt)
  {
    writeToScreen("DISCONNECTED");
  }

  function onMessage(evt)
  {
    writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
  }

  function onError(evt)
  {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  }

  function doSend(message)
  {
    writeToScreen("SENT: " + message);
    websocket.send(message);
  }

  function writeToScreen(message)
  {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }

  window.addEventListener("load", init, false);

  </script>

  <h2>WebSocket Test</h2>

  <div id="output"></div>

Here is my complete working example...

  1. Start up a host
namespace ConsoleApp1;

public static class Program
{
    public static async Task Main(string[] args)
    {
        IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
            .ConfigureServices(services =>
            {
                services.AddSingleton<Server>();
                services.AddHostedService<Server>();
            });
        IHost host = hostBuilder.Build();
        await host.RunAsync();
    }
}
  1. Create a server to accept clients and talk to them
using ConsoleApp15.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Net.WebSockets;
using System.Text;

namespace ConsoleApp15;
public class Server : IHostedService
{
    private readonly ILogger<Server> Logger;
    private readonly HttpListener HttpListener = new();

    public Server(ILogger<Server> logger)
    {
        Logger = logger ?? throw new ArgumentNullException(nameof(logger));
        HttpListener.Prefixes.Add("http://localhost:8080/");
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        Logger.LogInformation("Started");
        HttpListener.Start();
        while (!cancellationToken.IsCancellationRequested)
        {
            HttpListenerContext? context = await HttpListener.GetContextAsync().WithCancellationToken(cancellationToken);
            if (context is null)
                return;

            if (!context.Request.IsWebSocketRequest)
                context.Response.Abort();
            else
            {
                HttpListenerWebSocketContext? webSocketContext =
                    await context.AcceptWebSocketAsync(subProtocol: null).WithCancellationToken(cancellationToken);

                if (webSocketContext is null)
                    return;

                string clientId = Guid.NewGuid().ToString();
                WebSocket webSocket = webSocketContext.WebSocket;
                _ = Task.Run(async() =>
                {
                    while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
                    {
                        await Task.Delay(1000);
                        await webSocket.SendAsync(
                            Encoding.ASCII.GetBytes($"Hello {clientId}\r\n"),
                            WebSocketMessageType.Text,
                            endOfMessage: true,
                            cancellationToken);
                    }
                });

                _ = Task.Run(async() =>
                {
                    byte[] buffer = new byte[1024];
                    var stringBuilder = new StringBuilder(2048);
                    while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
                    {
                        WebSocketReceiveResult receiveResult =
                            await webSocket.ReceiveAsync(buffer, cancellationToken);
                        if (receiveResult.Count == 0)
                            return;

                        stringBuilder.Append(Encoding.ASCII.GetString(buffer, 0, receiveResult.Count));
                        if (receiveResult.EndOfMessage)
                        {
                            Console.WriteLine($"{clientId}: {stringBuilder}");
                            stringBuilder = new StringBuilder();
                        }
                    }
                });
            }
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Logger.LogInformation("Stopping...");
        HttpListener.Stop();
        Logger.LogInformation("Stopped");
        return Task.CompletedTask;
    }
}
  1. Create a WithCancellationToken for the Async methods that don't accept a CancellationToken parameter. This is so the server shuts down gracefully when told to.
namespace ConsoleApp15.Extensions;

public static class TaskExtensions
{
    public static async Task<T?> WithCancellationToken<T>(this Task<T> source, CancellationToken cancellationToken)
    {
        var cancellationTask = new TaskCompletionSource<bool>();
        cancellationToken.Register(() => cancellationTask.SetCanceled());

        _ = await Task.WhenAny(source, cancellationTask.Task);

        if (cancellationToken.IsCancellationRequested)
            return default;
        return source.Result;
    }
}
  1. Start the Postman app
  2. File => New
  3. Select "WebSocket request"
  4. Enter the following as the url ws://localhost:8080/
  5. Click [Connect]

I just stumbled on this link that shows how to implement a IHttpHandler using just the System.Net.WebSockets implementation. The handler is required as the .NET WebSocket implementation is dependent on IIS 8+.

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

namespace AspNetWebSocketEcho
{
    public class EchoHandler : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            if (context.IsWebSocketRequest)
                context.AcceptWebSocketRequest(HandleWebSocket);
            else
                context.Response.StatusCode = 400;
        }

        private async Task HandleWebSocket(WebSocketContext wsContext)
        {
            const int maxMessageSize = 1024;
            byte[] receiveBuffer = new byte[maxMessageSize];
            WebSocket socket = wsContext.WebSocket;

            while (socket.State == WebSocketState.Open)
            {
                WebSocketReceiveResult receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);

                if (receiveResult.MessageType == WebSocketMessageType.Close)
                {
                    await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                }
                else if (receiveResult.MessageType == WebSocketMessageType.Binary)
                {
                    await socket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", CancellationToken.None);
                }
                else
                {
                    int count = receiveResult.Count;

                    while (receiveResult.EndOfMessage == false)
                    {
                        if (count >= maxMessageSize)
                        {
                            string closeMessage = string.Format("Maximum message size: {0} bytes.", maxMessageSize);
                            await socket.CloseAsync(WebSocketCloseStatus.MessageTooLarge, closeMessage, CancellationToken.None);
                            return;
                        }

                        receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer, count, maxMessageSize - count), CancellationToken.None);
                        count += receiveResult.Count;
                    }

                    var receivedString = Encoding.UTF8.GetString(receiveBuffer, 0, count);
                    var echoString = "You said " + receivedString;
                    ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(echoString));

                    await socket.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
        }

        public bool IsReusable
        {
            get { return true; }
        }
    }
}

Hope it helped!


Yes.

The easiest way is to use an HTTPListener. If you search for HTTPListener WebSocket you'll find plenty of examples.

In a nutshell (pseudo-code)

HttpListener httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost/");
httpListener.Start();

HttpListenerContext context = await httpListener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
    HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
    WebSocket webSocket = webSocketContext.WebSocket;
    while (webSocket.State == WebSocketState.Open)
    {
        await webSocket.SendAsync( ... );
    }
}

Requires .NET 4.5 and Windows 8 or later.

Tags:

C#

.Net

Websocket