c# .net

csharp .net WebSocket Server

kimbs0301 2025. 2. 23. 14:43

asp.net WebSocketServer

 

.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

</Project>

 

Properties/launchSettings.json

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "WebSocketServer": {
      "commandName": "Project",
      "launchBrowser": false,
      "launchUrl": "swagger",
      "applicationUrl": "http://localhost:2121",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "dotnetRunMessages": true
    }
  }
}

 

Program.cs

using System;
using System.Net.WebSockets;

namespace WebSocketServer;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        var app = builder.Build();

        var webSocketOptions = new WebSocketOptions
        {
            KeepAliveInterval = TimeSpan.FromMinutes(2)
        };

        app.UseWebSockets(webSocketOptions);

        app.UseMiddleware<WebSocketMiddleware>();

        app.Run();
    }
}

 

Model.cs

using System;

namespace WebSocketServer;

public class EchoMessage
{
	public int Sequence { get; set; }
	public string Content { get; set; } = string.Empty;
	public Int64 CurrentTime { get; set; } = 0;
}

 

ConnectionManager.cs

using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;

namespace WebSocketServer;

public class ConnectionManager
{
	private static readonly ConcurrentDictionary<string, WebSocket> SocketDict = new();

	public string GetId(WebSocket socket) => SocketDict.FirstOrDefault(p => p.Value == socket).Key;

	public void AddSocket(WebSocket socket)
	{
		string socketId = Guid.NewGuid().ToString(); // CreateConnectionId
		SocketDict.TryAdd(socketId, socket);
	}

	public async Task RemoveSocket(WebSocket socket, string? description = "Connection closed")
	{
		string id = GetId(socket);
		if (string.IsNullOrEmpty(id) == false)
			SocketDict.TryRemove(id, out _);

		if (socket.State != WebSocketState.Aborted)
		{
			try
			{
				await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, description, CancellationToken.None);

				socket.Dispose();
			}
			catch (Exception) { }
		}
	}
}

 

WebSocketMiddleware.cs

using System;
using System.Buffers;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;

namespace WebSocketServer;

public sealed class WebSocketMiddleware
{
	private readonly RequestDelegate Next;
	private WebSocketHandler WebSocketHandler { get; set; }

	public WebSocketMiddleware(RequestDelegate next)
	{
		Next = next;

		var connectionManager = new ConnectionManager();

		var webSocketHandler = new ChatHandler(connectionManager);

		WebSocketHandler = webSocketHandler;
	}

	public async Task Invoke(HttpContext context) // InvokeAsync
	{
		if (context.WebSockets.IsWebSocketRequest == false)
		{
			await Next.Invoke(context);
			return;
		}

		if (context.Request.Path != "/connect")
		{
			context.Response.StatusCode = StatusCodes.Status400BadRequest;
            
			await Next.Invoke(context);

			return;
		}
		string? username = context.Request.Query["username"];

		WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(new WebSocketAcceptContext
		{
			DangerousEnableCompression = true,
			KeepAliveInterval = TimeSpan.Zero
		});

		if (string.IsNullOrEmpty(username) == true)
		{
			if (socket.State != WebSocketState.Aborted)
			{
				await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, cancellationToken: CancellationToken.None);

				socket.Dispose();
			}
			return;
		}

		WebSocketHandler.OnConnected(socket, username);

		var recvByteArray = ArrayPool<byte>.Shared.Rent(1024 * 4);
		WebSocketMessageFlags socketMessageFlags = CreateMessageFlags(endOfMessage: true, compressMessage: true);

		while (socket.State == WebSocketState.Open)
		{
			 var recvBuffer = new ArraySegment<byte>(recvByteArray);

			WebSocketReceiveResult result;
			try
			{
				result = await socket.ReceiveAsync(recvBuffer, CancellationToken.None);
			}
			catch (Exception ex)
			{
				if (socket.State == WebSocketState.Open)
					Console.WriteLine($"Error: {ex.Message}");

				await WebSocketHandler.OnDisconnectedAsync(socket);

				break;
			}

			if (result.MessageType == WebSocketMessageType.Binary)
			{
				if (result.Count < 1) continue; // next

				EchoMessage? echoMessage = TryDeserializeClientMessage(Encoding.UTF8.GetString(recvByteArray, 0, result.Count));
				if (echoMessage == null)
				{
					await WebSocketHandler.OnDisconnectedAsync(socket);

					break;
				}

				var sendBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(echoMessage)));

				try
				{
					await socket.SendAsync(sendBuffer, WebSocketMessageType.Binary, socketMessageFlags, CancellationToken.None);
				}
				catch (Exception ex)
				{
					Console.WriteLine($"Error: {ex.Message}");
				}
			}

			else if (result.MessageType == WebSocketMessageType.Text)
			{
				await WebSocketHandler.OnDisconnectedAsync(socket);

				break;
			}

			else if (result.MessageType == WebSocketMessageType.Close)
				break;
		}

		ArrayPool<byte>.Shared.Return(recvByteArray);
	}

	private EchoMessage? TryDeserializeClientMessage(string message)
	{
		try
		{
			return JsonSerializer.Deserialize<EchoMessage>(message);
		}
		catch (Exception ex)
		{
			Console.WriteLine($"Error: invalid message format {ex.Message}");
			return null;
		}
	}

	private static WebSocketMessageFlags CreateMessageFlags(bool endOfMessage, bool compressMessage)
	{
		WebSocketMessageFlags flags = WebSocketMessageFlags.None;

		if (endOfMessage == true)
			flags |= WebSocketMessageFlags.EndOfMessage;

		if (compressMessage == false)
			flags |= WebSocketMessageFlags.DisableCompression;

		return flags;
	}
}

 

WebSocketHandler.cs

using System;
using System.Net.WebSockets;

namespace WebSocketServer;

public abstract class WebSocketHandler
{
	protected ConnectionManager ConnectionManager { get; set; }

	public WebSocketHandler(ConnectionManager connectionManager)
	{
		ConnectionManager = connectionManager;
	}

	public virtual void OnConnected(WebSocket socket, string username)
	{
		ConnectionManager.AddSocket(socket);
	}

	public virtual async Task OnDisconnectedAsync(WebSocket socket)
	{
		await ConnectionManager.RemoveSocket(socket, null);
	}
}

public sealed class MySocketHandler : WebSocketHandler
{
	public MySocketHandler(ConnectionManager connectionManager) : base(connectionManager)
	{

	}
}

 

클라이언트에게 보내는 패킷은 모두 압축되어 전송되어진다.
WebAPI 기능은 제외하였다.

'c# .net' 카테고리의 다른 글

c# .net IpAddress 목록  (0) 2025.03.17
csharp .net WebSocket Client  (0) 2025.02.23
c# .net Socket NetworkStream  (1) 2024.11.10
c# .net Binary BigEndian  (0) 2024.11.10
c# SocketAsyncEvent Server  (0) 2024.11.09