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 |