DEV Community

Masui Masanori
Masui Masanori

Posted on • Edited on

3 2

【ASP.NET Core】【TypeScript】Send messages with WebSockets

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"
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>());
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Startup.cs

...
namespace WebRtcSample
{
    public class Startup
    {
...
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
...
            app.MapWebSocketHolder("/ws");
...
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
            });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

websocket-message.ts

export type WebsocketMessage = {
    event: string,
    messageType: 'text'
    data: any,
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

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)
...
Enter fullscreen mode Exit fullscreen mode

Though it will be occurred after shutting down, I want to avoid it.
So I use "IHostApplicationLifetime" to get shutting down event.

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}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

Qodo Takeover

Introducing Qodo Gen 1.0: Transform Your Workflow with Agentic AI

Rather than just generating snippets, our agents understand your entire project context, can make decisions, use tools, and carry out tasks autonomously.

Read full post

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay