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...
- 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();
}
}
- 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;
}
}
- Create a
WithCancellationToken
for the Async methods that don't accept aCancellationToken
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;
}
}
- Start the Postman app
- File => New
- Select "WebSocket request"
- Enter the following as the url
ws://localhost:8080/
- 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.