DEV Community

Masui Masanori
Masui Masanori

Posted on • Edited on

【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

Top comments (0)