Common networking stuffs
This commit is contained in:
commit
aa9e3dbcd7
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
38
Abstract/HandlerManager.cs
Normal file
38
Abstract/HandlerManager.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Abstract
|
||||||
|
{
|
||||||
|
public abstract class HandlerManager<Client, Handler>
|
||||||
|
{
|
||||||
|
private readonly IDictionary<int, Handler> _handlers;
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
public IDictionary<int, Handler> Handlers { get => _handlers; }
|
||||||
|
|
||||||
|
|
||||||
|
public HandlerManager(ILogger logger)
|
||||||
|
{
|
||||||
|
_handlers = new Dictionary<int, Handler>();
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void Add(int op, Handler handler)
|
||||||
|
{
|
||||||
|
_handlers.Add(op, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Execute<T>(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<T>(Client sender, Handler handler, T value);
|
||||||
|
}
|
||||||
|
}
|
45
Abstract/HandlerTypeManager.cs
Normal file
45
Abstract/HandlerTypeManager.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Abstract
|
||||||
|
{
|
||||||
|
public abstract class HandlerTypeManager<Client, Handler>
|
||||||
|
{
|
||||||
|
private readonly IDictionary<int, Type> _types;
|
||||||
|
public IDictionary<int, Type> HandlerTypes { get => _types; }
|
||||||
|
protected readonly ILogger _logger;
|
||||||
|
|
||||||
|
|
||||||
|
public HandlerTypeManager(ILogger logger, HandlerManager<Client, Handler> handlers)
|
||||||
|
{
|
||||||
|
_types = new Dictionary<int, Type>();
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
GenerateHandlerTypes(handlers.Handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void GenerateHandlerTypes(IDictionary<int, Handler> 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);
|
||||||
|
}
|
||||||
|
}
|
164
Abstract/SocketClient.cs
Normal file
164
Abstract/SocketClient.cs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
using Serilog;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Abstract
|
||||||
|
{
|
||||||
|
public abstract class SocketClient<Message> : 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<byte>(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<T>(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<byte>(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<Message>(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<T>(int opcode, T data);
|
||||||
|
protected abstract Task OnResponseReceived(Message? content);
|
||||||
|
protected abstract Task OnMessageSend(int opcode, string? content);
|
||||||
|
protected abstract Task OnConnection();
|
||||||
|
}
|
||||||
|
}
|
10
Common/IWebSocketHandler.cs
Normal file
10
Common/IWebSocketHandler.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Common
|
||||||
|
{
|
||||||
|
public interface IWebSocketHandler
|
||||||
|
{
|
||||||
|
int OperationCode { get; }
|
||||||
|
Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data);
|
||||||
|
}
|
||||||
|
}
|
56
Common/WebSocketClient.cs
Normal file
56
Common/WebSocketClient.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Common
|
||||||
|
{
|
||||||
|
public class WebSocketClient : SocketClient<WebSocketMessage>
|
||||||
|
{
|
||||||
|
private readonly HandlerManager<WebSocketClient, IWebSocketHandler> _handlerManager;
|
||||||
|
private readonly HandlerTypeManager<WebSocketClient, IWebSocketHandler> _handlerTypeManager;
|
||||||
|
|
||||||
|
public WebSocketClient(
|
||||||
|
ILogger logger,
|
||||||
|
HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager,
|
||||||
|
HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager,
|
||||||
|
JsonSerializerOptions serializerOptions
|
||||||
|
) : base(logger, serializerOptions)
|
||||||
|
{
|
||||||
|
_handlerManager = handlerManager;
|
||||||
|
_handlerTypeManager = typeManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override WebSocketMessage GenerateMessage<T>(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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
Common/WebSocketHandlerManager.cs
Normal file
23
Common/WebSocketHandlerManager.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Socket.Manager
|
||||||
|
{
|
||||||
|
public class WebSocketHandlerManager : HandlerManager<WebSocketClient, IWebSocketHandler>
|
||||||
|
{
|
||||||
|
public WebSocketHandlerManager(ILogger logger) : base(logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Add(IWebSocketHandler handler)
|
||||||
|
{
|
||||||
|
Add(handler.OperationCode, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task Execute<T>(WebSocketClient sender, IWebSocketHandler handler, T value)
|
||||||
|
{
|
||||||
|
await handler.Execute(sender, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
Common/WebSocketHandlerTypeManager.cs
Normal file
26
Common/WebSocketHandlerTypeManager.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using CommonSocketLibrary.Abstract;
|
||||||
|
using CommonSocketLibrary.Common;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace CommonSocketLibrary.Socket.Manager
|
||||||
|
{
|
||||||
|
public abstract class WebSocketHandlerTypeManager : HandlerTypeManager<WebSocketClient, IWebSocketHandler>
|
||||||
|
{
|
||||||
|
public WebSocketHandlerTypeManager(ILogger logger, HandlerManager<WebSocketClient, IWebSocketHandler> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
Common/WebSocketMessage.cs
Normal file
13
Common/WebSocketMessage.cs
Normal file
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
25
CommonSocketLibrary.csproj
Normal file
25
CommonSocketLibrary.csproj
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Abstract/" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MathParser.org-mXparser" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.1-dev-10391" />
|
||||||
|
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
Loading…
Reference in New Issue
Block a user