23

I want to implement a simple chat server with the new System.Net.WebSockets classes in .NET 4.5 and later (on Windows 8.1). However, I only find examples making use of those classes in an ASP.NET environment (especially the ones here: http://www.codemag.com/Article/1210051)

I don't have such one, and would like to implement the websocket server as "raw" as possible, but without having to reimplement all the websocket protocol as Microsoft hopefully already did that in .NET 4.5.

I thought of simply instantiating a new WebSocket class like I'd do with a normal Socket, but the constructor is protected. So I went to create a class inheriting from it, but then I noticed I had to implement so many abstract methods and properties that it looked like I'm rewriting the whole logic (especially because I had to implement things like State or SendAsync).

I'm afraid that the MSDN documentation didn't help me. The documentation there has a pre-release status and many comments just say "TBD" or "when its implemented".

John Saunders
  • 160,644
  • 26
  • 247
  • 397
Ray
  • 7,940
  • 7
  • 58
  • 90
  • possible duplicate of [is there in C# SSL WebSocket Client that is .net 4.0?](http://stackoverflow.com/questions/13386604/is-there-in-c-sharp-ssl-websocket-client-that-is-net-4-0) – MethodMan May 27 '15 at 18:16
  • 1
    yeah, it seems the System.Net.WebSocket types are pretty Context centric.. specifically the HttpContext object http://www.codeproject.com/Articles/618032/Using-WebSocket-in-NET-Part .. with that said, System.Net.WebSockets doesn't seem to be intended to be for an assembly for Hosting WebSocket Servers.. – Brett Caswell May 27 '15 at 19:44
  • Here is a couple reference links (MDN): General Information - https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_servers C# WebSocketServer Implementation (doesn't use .NET 4.5 WebSockets) https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server .. I'm cleaning up the VB.NET code sample on there presently.. I put it on there awhile back as a draft. it hasn't received any revisions. – Brett Caswell May 28 '15 at 14:32
  • I understand your consideration. Ideally, a project wouldn't be implementing the protocal; However, the websocket protocal isn't particularly difficult to implement.. .. and.. it does give you the most *raw* form to work with. – Brett Caswell May 28 '15 at 14:40

4 Answers4

18

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.

frankhommers
  • 1,169
  • 12
  • 26
Ian Goldby
  • 5,609
  • 1
  • 45
  • 81
9

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!

Pedro Villa Verde
  • 2,312
  • 2
  • 19
  • 19
9

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>
GoTo
  • 5,966
  • 2
  • 28
  • 31
JDPeckham
  • 2,414
  • 2
  • 23
  • 26
2

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]
Peter Morris
  • 20,174
  • 9
  • 81
  • 146