Intro
This time, I try sending messages between two clients with WebSockets.
Microsoft Docs have a WebSocket sample.
But it only can send messages myself.
Because I want to connect other clients and send messages each other, I will try change the sample.
Environments
- .NET ver.5.0.100
- NLog.Web.AspNetCore ver.4.9.3
package.json (client side)
{
"dependencies": {
"ts-loader": "^8.0.10",
"tsc": "^1.20150623.0",
"typescript": "^4.0.5",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"ws": "^7.4.0"
}
}
Add Middleware
Because I don't want to add all controlling WebSocket codes in Startup.cs, I create custom middleware and separate them.
According to the documents, middlewares' constructor must have "RequestDelegate" and must have "Invoke" or "InvokeAsync" methods.
WebSocketMiddleware.cs (Full)
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace WebRtcSample.WebSockets
{
public class WebSocketMiddleware
{
private readonly RequestDelegate next;
private readonly IWebSocketHolder webSocket;
public WebSocketMiddleware(RequestDelegate next,
IWebSocketHolder webSocket)
{
this.next = next;
this.webSocket = webSocket;
}
public async Task InvokeAsync(HttpContext context)
{
if(context.WebSockets.IsWebSocketRequest == false)
{
context.Response.StatusCode = 400;
return;
}
await webSocket.AddAsync(context);
}
}
}
Middlewares also can be injected dependencies.
So except the first argument, I also can add other dependencies as mentioned above.
Use extension methods
To use the middleware class, I use an extension method.
WebSocketMiddleware.cs (Full)
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace WebRtcSample.WebSockets
{
public static class WebSocketHolderMapper
{
public static IApplicationBuilder MapWebSocketHolder(this IApplicationBuilder app, PathString path)
{
return app.Map(path, (app) => app.UseMiddleware<WebSocketMiddleware>());
}
}
}
Startup.cs
...
namespace WebRtcSample
{
public class Startup
{
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.MapWebSocketHolder("/ws");
...
}
}
}
Use WebSockets
To use WebSockets, I don't need to add any packages.
Only I need to add "app.UseWebSockets();" in Startup.cs.
Startup.cs (Full)
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WebRtcSample.WebSockets;
namespace WebRtcSample
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddControllers();
// to hold connected WebSocket clients, this class must be singleton
services.AddSingleton<IWebSocketHolder, WebSocketHolder>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseStaticFiles();
app.UseWebSockets();
app.MapWebSocketHolder("/ws");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Connecting WebSockets and holding WebSocket clients
To send messages to other connected WebSocket clients, I must hold WebSocket instances after connectted.
IWebSocketHolder.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace WebRtcSample.WebSockets
{
public interface IWebSocketHolder
{
Task AddAsync(HttpContext context);
}
}
WebSocketHolder.cs
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Http;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace WebRtcSample.WebSockets
{
public class WebSocketHolder: IWebSocketHolder
{
private readonly ILogger<WebSocketHolder> logger;
private readonly ConcurrentDictionary<string, WebSocket> clients = new ();
public WebSocketHolder(ILogger<WebSocketHolder> logger)
{
this.logger = logger;
}
public async Task AddAsync(HttpContext context)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
if(clients.TryAdd(CreateId(), webSocket))
{
await EchoAsync(webSocket);
}
}
private string CreateId()
{
return Guid.NewGuid().ToString();
}
private async Task EchoAsync(WebSocket webSocket)
{
// for sending data
byte[] buffer = new byte[1024 * 4];
// Receiving and Sending messages until connections will be closed.
while(true)
{
WebSocketReceiveResult result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
if(result.CloseStatus.HasValue)
{
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
clients.TryRemove(clients.First(w => w.Value == webSocket));
webSocket.Dispose();
break;
}
// Send to all clients
foreach(var c in clients)
{
await c.Value.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
}
}
}
}
}
- Creating a WebSockets middleware for ASP .NET Core 3 | radu's blog
- GitHub - nenoNaninu/AspNetCoreSimpleWebSocketChat
Client side
I create a simple page to use WebSockets with TypeScript and ws.
HomeController.cs
using Microsoft.AspNetCore.Mvc;
namespace WebRtcSample.Controllers
{
public class HomeController: Controller
{
[Route("")]
public ActionResult Index()
{
return View("Views/Index.cshtml");
}
}
}
Index.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello WebRTC</title>
<meta charset="utf-8">
</head>
<body>
<textarea id="input_message"></textarea>
<button onclick="Page.connect()">Connect</button>
<button onclick="Page.send()">Send</button>
<button onclick="Page.close()">Close</button>
<div id="received_text_area"></div>
<script src="js/main.js"></script>
</body>
</html>
main.page.ts
import { WebsocketMessage } from "./websocket-message";
let ws: WebSocket|null = null;
export function connect() {
// Replace XXX.XXX.XXX.XXX to server PC's IP address
ws = new WebSocket('ws://localhost:5000/ws');
ws.onopen = () => sendMessage({
event: 'message',
messageType: 'text',
data: 'connected',
});
ws.onmessage = data => {
const message = <WebsocketMessage>JSON.parse(data.data);
switch(message.messageType) {
case 'text':
addReceivedMessage(message.data);
break;
default:
console.log(data);
break;
}
};
}
export function send() {
const messageArea = document.getElementById('input_message') as HTMLTextAreaElement;
sendMessage({
event: 'message',
messageType: 'text',
data: messageArea.value,
});
}
export function close() {
if(ws == null) {
console.warn('WebSocket was null');
return;
}
ws.close();
ws = null;
}
function addReceivedMessage(message: string) {
const receivedMessageArea = document.getElementById('received_text_area') as HTMLElement;
const child = document.createElement('div');
child.textContent = message;
receivedMessageArea.appendChild(child);
}
function sendMessage(message: WebsocketMessage) {
if (ws == null) {
console.warn('WebSocket was null');
return;
}
ws.send(JSON.stringify(message));
}
websocket-message.ts
export type WebsocketMessage = {
event: string,
messageType: 'text'
data: any,
}
Exception on shutting down
Not I can connect two or more clients.
But when I shut down the application by Ctrl + C, I will get an exception.
2020-11-13 19:01:46.0232|1|ERROR|Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware|An unhandled exception has occurred while executing the request. Microsoft.AspNetCore.Connections.ConnectionAbortedException: The connection was aborted because the server is shutting down and request processing didn't complete within the time specified by HostOptions.ShutdownTimeout.
at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
...
Though it will be occurred after shutting down, I want to avoid it.
So I use "IHostApplicationLifetime" to get shutting down event.
- IHostApplicationLifetime Interface (Microsoft.Extensions.Hosting) | Microsoft Docs
- Application Shutdown in ASP.NET Core with IApplicationLifetime – { Think Rethink }
WebSocketHolder.cs (Full)
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Http;
using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace WebRtcSample.WebSockets
{
public class WebSocketHolder: IWebSocketHolder
{
private readonly ILogger<WebSocketHolder> logger;
private readonly ConcurrentDictionary<string, WebSocket> clients = new ();
private CancellationTokenSource source = new ();
private CancellationToken cancel;
public WebSocketHolder(ILogger<WebSocketHolder> logger,
IHostApplicationLifetime applicationLifetime)
{
this.logger = logger;
cancel = source.Token;
// on shutting down the application, "OnShutDown()" will be invoked.
applicationLifetime.ApplicationStopping.Register(OnShutdown);
}
private void OnShutdown()
{
source.Cancel();
}
public async Task AddAsync(HttpContext context)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
if(clients.TryAdd(CreateId(), webSocket))
{
await EchoAsync(webSocket);
}
}
private string CreateId()
{
return Guid.NewGuid().ToString();
}
private async Task EchoAsync(WebSocket webSocket)
{
try
{
// for sending data
byte[] buffer = new byte[1024 * 4];
while(true)
{
WebSocketReceiveResult result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), cancel);
if(result.CloseStatus.HasValue)
{
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancel);
clients.TryRemove(clients.First(w => w.Value == webSocket));
webSocket.Dispose();
break;
}
// Send to all clients
foreach(var c in clients)
{
await c.Value.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancel);
}
}
}
catch(OperationCanceledException ex)
{
// After canceling the task, OperationCanceledException will be occurred
logger.LogError($"Error {ex.Message}");
}
}
}
}
Top comments (0)