commit aa9e3dbcd72baa38c29c98bf146361642cd99a7c Author: Tom Date: Mon Jun 24 22:28:40 2024 +0000 Common networking stuffs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbbd0b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ \ No newline at end of file diff --git a/Abstract/HandlerManager.cs b/Abstract/HandlerManager.cs new file mode 100644 index 0000000..7b7bf07 --- /dev/null +++ b/Abstract/HandlerManager.cs @@ -0,0 +1,38 @@ +using Serilog; + +namespace CommonSocketLibrary.Abstract +{ + public abstract class HandlerManager + { + private readonly IDictionary _handlers; + protected readonly ILogger _logger; + + public IDictionary Handlers { get => _handlers; } + + + public HandlerManager(ILogger logger) + { + _handlers = new Dictionary(); + _logger = logger; + } + + + protected void Add(int op, Handler handler) + { + _handlers.Add(op, handler); + } + + public async Task Execute(Client sender, int opcode, T val) + { + if (opcode < 0 || !_handlers.TryGetValue(opcode, out Handler? handler) || handler == null) + { + _logger.Warning("Invalid opcode received: " + opcode); + return; + } + + await Execute(sender, handler, val); + } + + protected abstract Task Execute(Client sender, Handler handler, T value); + } +} \ No newline at end of file diff --git a/Abstract/HandlerTypeManager.cs b/Abstract/HandlerTypeManager.cs new file mode 100644 index 0000000..34c4309 --- /dev/null +++ b/Abstract/HandlerTypeManager.cs @@ -0,0 +1,45 @@ +using Serilog; + +namespace CommonSocketLibrary.Abstract +{ + public abstract class HandlerTypeManager + { + private readonly IDictionary _types; + public IDictionary HandlerTypes { get => _types; } + protected readonly ILogger _logger; + + + public HandlerTypeManager(ILogger logger, HandlerManager handlers) + { + _types = new Dictionary(); + _logger = logger; + + GenerateHandlerTypes(handlers.Handlers); + } + + + private void GenerateHandlerTypes(IDictionary handlers) + { + foreach (var entry in handlers) + { + if (entry.Value == null) + { + _logger.Error($"Failed to link websocket handler #{entry.Key} due to null value."); + continue; + } + + var type = entry.Value.GetType(); + var target = FetchMessageType(type); + if (target == null) + { + _logger.Error($"Failed to link websocket handler #{entry.Key} due to no match for {target}."); + continue; + } + _types.Add(entry.Key, target); + _logger.Debug($"Linked websocket handler #{entry.Key} to type {target.AssemblyQualifiedName}."); + } + } + + protected abstract Type? FetchMessageType(Type handlerType); + } +} \ No newline at end of file diff --git a/Abstract/SocketClient.cs b/Abstract/SocketClient.cs new file mode 100644 index 0000000..2356cf2 --- /dev/null +++ b/Abstract/SocketClient.cs @@ -0,0 +1,164 @@ +using Serilog; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; + +namespace CommonSocketLibrary.Abstract +{ + public abstract class SocketClient : IDisposable + { + private ClientWebSocket? _socket; + private CancellationTokenSource? _cts; + + protected readonly ILogger _logger; + protected readonly JsonSerializerOptions _options; + + public bool Connected { get; set; } + public int ReceiveBufferSize { get; } = 8192; + + + public SocketClient(ILogger logger, JsonSerializerOptions options) + { + _logger = logger; + _options = options; + Connected = false; + } + + public async Task ConnectAsync(string url) + { + if (_socket != null) + { + if (_socket.State == WebSocketState.Open) return; + else _socket.Dispose(); + } + + _socket = new ClientWebSocket(); + _socket.Options.RemoteCertificateValidationCallback = (o, c, ch, er) => true; + _socket.Options.UseDefaultCredentials = false; + if (_cts != null) _cts.Dispose(); + _cts = new CancellationTokenSource(); + await _socket.ConnectAsync(new Uri(url), _cts.Token); + await Task.Factory.StartNew(ReceiveLoop, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + await OnConnection(); + } + + public async Task DisconnectAsync() + { + if (_socket == null || _cts == null) return; + // TODO: requests cleanup code, sub-protocol dependent. + if (_socket.State == WebSocketState.Open) + { + _cts.CancelAfter(TimeSpan.FromMilliseconds(500)); + await _socket.CloseOutputAsync(WebSocketCloseStatus.Empty, "", CancellationToken.None); + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + } + + Connected = false; + _socket.Dispose(); + _socket = null; + _cts.Dispose(); + _cts = null; + } + + public void Dispose() => DisconnectAsync().Wait(); + + private async Task ReceiveLoop() + { + if (_socket == null || _cts == null) return; + + var loopToken = _cts.Token; + MemoryStream? outputStream = null; + WebSocketReceiveResult? receiveResult = null; + var buffer = new byte[ReceiveBufferSize]; + try + { + while (!loopToken.IsCancellationRequested) + { + outputStream = new MemoryStream(ReceiveBufferSize); + do + { + receiveResult = await _socket.ReceiveAsync(buffer, _cts.Token); + if (receiveResult.MessageType != WebSocketMessageType.Close) + outputStream.Write(buffer, 0, receiveResult.Count); + } + while (!receiveResult.EndOfMessage); + if (receiveResult.MessageType == WebSocketMessageType.Close) break; + outputStream.Position = 0; + await ResponseReceived(outputStream); + } + } + catch (TaskCanceledException) { } + finally + { + outputStream?.Dispose(); + } + } + + public async Task SendRaw(string content) + { + if (!Connected) return; + + var bytes = new byte[1024 * 4]; + var array = new ArraySegment(bytes); + var total = Encoding.UTF8.GetBytes(content).Length; + var current = 0; + + while (current < total) + { + var size = Encoding.UTF8.GetBytes(content.Substring(current), array); + await _socket.SendAsync(array, WebSocketMessageType.Text, true, _cts.Token); + current += size; + } + await OnMessageSend(-1, content); + } + + public async Task Send(int opcode, T data) + { + try + { + var message = GenerateMessage(opcode, data); + var content = JsonSerializer.Serialize(message, _options); + + var bytes = Encoding.UTF8.GetBytes(content); + var array = new ArraySegment(bytes); + var total = bytes.Length; + var current = 0; + + while (current < total) + { + var size = Encoding.UTF8.GetBytes(content.Substring(current), array); + await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts.Token); + current += size; + } + await OnMessageSend(opcode, content); + } + catch (Exception e) + { + Connected = false; + _logger.Error(e, "Failed to send a message: " + opcode); + } + } + + private async Task ResponseReceived(Stream stream) + { + try + { + var data = await JsonSerializer.DeserializeAsync(stream); + await OnResponseReceived(data); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to read or execute a websocket message."); + } + finally + { + stream.Dispose(); + } + } + + protected abstract Message GenerateMessage(int opcode, T data); + protected abstract Task OnResponseReceived(Message? content); + protected abstract Task OnMessageSend(int opcode, string? content); + protected abstract Task OnConnection(); + } +} \ No newline at end of file diff --git a/Common/IWebSocketHandler.cs b/Common/IWebSocketHandler.cs new file mode 100644 index 0000000..9518926 --- /dev/null +++ b/Common/IWebSocketHandler.cs @@ -0,0 +1,10 @@ +using CommonSocketLibrary.Abstract; + +namespace CommonSocketLibrary.Common +{ + public interface IWebSocketHandler + { + int OperationCode { get; } + Task Execute(SocketClient sender, Data data); + } +} \ No newline at end of file diff --git a/Common/WebSocketClient.cs b/Common/WebSocketClient.cs new file mode 100644 index 0000000..ea85368 --- /dev/null +++ b/Common/WebSocketClient.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using CommonSocketLibrary.Abstract; +using Serilog; + +namespace CommonSocketLibrary.Common +{ + public class WebSocketClient : SocketClient + { + private readonly HandlerManager _handlerManager; + private readonly HandlerTypeManager _handlerTypeManager; + + public WebSocketClient( + ILogger logger, + HandlerManager handlerManager, + HandlerTypeManager typeManager, + JsonSerializerOptions serializerOptions + ) : base(logger, serializerOptions) + { + _handlerManager = handlerManager; + _handlerTypeManager = typeManager; + } + + protected override WebSocketMessage GenerateMessage(int opcode, T data) + { + return new WebSocketMessage() + { + OpCode = opcode, + Data = data + }; + } + + protected override async Task OnResponseReceived(WebSocketMessage? data) + { + if (data == null) + return; + + string content = data.Data?.ToString() ?? string.Empty; + _logger.Verbose("RX #" + data.OpCode + ": " + content); + + if (!_handlerTypeManager.HandlerTypes.TryGetValue(data.OpCode, out Type? type) || type == null) + return; + + var obj = JsonSerializer.Deserialize(content, type, _options); + await _handlerManager.Execute(this, data.OpCode, obj); + } + + protected override async Task OnMessageSend(int opcode, string? content) + { + _logger.Verbose("TX #" + opcode + ": " + content); + } + + protected override async Task OnConnection() + { + } + } +} \ No newline at end of file diff --git a/Common/WebSocketHandlerManager.cs b/Common/WebSocketHandlerManager.cs new file mode 100644 index 0000000..43409e4 --- /dev/null +++ b/Common/WebSocketHandlerManager.cs @@ -0,0 +1,23 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Serilog; + +namespace CommonSocketLibrary.Socket.Manager +{ + public class WebSocketHandlerManager : HandlerManager + { + public WebSocketHandlerManager(ILogger logger) : base(logger) + { + } + + protected void Add(IWebSocketHandler handler) + { + Add(handler.OperationCode, handler); + } + + protected override async Task Execute(WebSocketClient sender, IWebSocketHandler handler, T value) + { + await handler.Execute(sender, value); + } + } +} \ No newline at end of file diff --git a/Common/WebSocketHandlerTypeManager.cs b/Common/WebSocketHandlerTypeManager.cs new file mode 100644 index 0000000..4f3354a --- /dev/null +++ b/Common/WebSocketHandlerTypeManager.cs @@ -0,0 +1,26 @@ +using CommonSocketLibrary.Abstract; +using CommonSocketLibrary.Common; +using Serilog; + +namespace CommonSocketLibrary.Socket.Manager +{ + public abstract class WebSocketHandlerTypeManager : HandlerTypeManager + { + public WebSocketHandlerTypeManager(ILogger logger, HandlerManager handlers) : base(logger, handlers) + { + } + + protected override Type? FetchMessageType(Type handlerType) + { + if (handlerType == null) + return null; + + var name = handlerType.Namespace + "." + handlerType.Name; + name = name.Replace(".Handlers.", ".Data.") + .Replace("Handler", "Message") + .Replace("MessageMessage", "Message"); + + return handlerType.Assembly.GetType(name); + } + } +} \ No newline at end of file diff --git a/Common/WebSocketMessage.cs b/Common/WebSocketMessage.cs new file mode 100644 index 0000000..0206aaf --- /dev/null +++ b/Common/WebSocketMessage.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace CommonSocketLibrary.Common +{ + public class WebSocketMessage + { + [JsonPropertyName("op")] + public int OpCode { get; set; } + + [JsonPropertyName("d")] + public object? Data { get; set; } + } +} \ No newline at end of file diff --git a/CommonSocketLibrary.csproj b/CommonSocketLibrary.csproj new file mode 100644 index 0000000..aebdfd9 --- /dev/null +++ b/CommonSocketLibrary.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + +