Fixed several socket issues. Added backoff for reconnection.

This commit is contained in:
Tom 2024-08-10 19:31:08 +00:00
parent aa9e3dbcd7
commit ab90d47b89
8 changed files with 207 additions and 106 deletions

View File

@ -2,41 +2,55 @@
namespace CommonSocketLibrary.Abstract namespace CommonSocketLibrary.Abstract
{ {
public abstract class HandlerTypeManager<Client, Handler> public interface ICodedOperation
{
int OperationCode { get; }
}
public interface IMessageTypeManager
{
Type? GetMessageTypeByCode(int code);
}
public abstract class MessageTypeManager<Handler> : IMessageTypeManager where Handler : ICodedOperation
{ {
private readonly IDictionary<int, Type> _types; private readonly IDictionary<int, Type> _types;
public IDictionary<int, Type> HandlerTypes { get => _types; }
protected readonly ILogger _logger; protected readonly ILogger _logger;
public HandlerTypeManager(ILogger logger, HandlerManager<Client, Handler> handlers) public MessageTypeManager(IEnumerable<Handler> handlers, ILogger logger)
{ {
_types = new Dictionary<int, Type>(); _types = new Dictionary<int, Type>();
_logger = logger; _logger = logger;
GenerateHandlerTypes(handlers.Handlers); GenerateHandlerTypes(handlers);
} }
public Type? GetMessageTypeByCode(int code)
private void GenerateHandlerTypes(IDictionary<int, Handler> handlers)
{ {
foreach (var entry in handlers) _types.TryGetValue(code, out Type? type);
return type;
}
private void GenerateHandlerTypes(IEnumerable<Handler> handlers)
{
foreach (var handler in handlers)
{ {
if (entry.Value == null) if (handler == null)
{ {
_logger.Error($"Failed to link websocket handler #{entry.Key} due to null value."); _logger.Error($"Failed to link websocket handler due to null value.");
continue; continue;
} }
var type = entry.Value.GetType(); var type = handler.GetType();
var target = FetchMessageType(type); var target = FetchMessageType(type);
if (target == null) if (target == null)
{ {
_logger.Error($"Failed to link websocket handler #{entry.Key} due to no match for {target}."); _logger.Error($"Failed to link websocket handler #{handler.OperationCode} due to no match for {target}.");
continue; continue;
} }
_types.Add(entry.Key, target); _types.Add(handler.OperationCode, target);
_logger.Debug($"Linked websocket handler #{entry.Key} to type {target.AssemblyQualifiedName}."); _logger.Debug($"Linked websocket handler #{handler.OperationCode} to type {target.AssemblyQualifiedName}.");
} }
} }

View File

@ -1,51 +1,56 @@
using CommonSocketLibrary.Backoff;
using Serilog; using Serilog;
using System.Collections;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace CommonSocketLibrary.Abstract namespace CommonSocketLibrary.Abstract
{ {
public abstract class SocketClient<Message> : IDisposable public abstract class SocketClient<Message> : IDisposable where Message : class
{ {
private ClientWebSocket? _socket; protected ClientWebSocket? _socket;
private CancellationTokenSource? _cts; protected CancellationTokenSource? _cts;
private readonly int ReceiveBufferSize = 8192;
protected readonly ILogger _logger; protected readonly ILogger _logger;
protected readonly JsonSerializerOptions _options; protected readonly JsonSerializerOptions _options;
private bool _disposed;
public bool Connected { get; set; } public event EventHandler<EventArgs> OnConnected;
public int ReceiveBufferSize { get; } = 8192; public event EventHandler<SocketDisconnectionEventArgs> OnDisconnected;
public SocketClient(ILogger logger, JsonSerializerOptions options) public SocketClient(ILogger logger, JsonSerializerOptions options)
{ {
_logger = logger; _logger = logger;
_options = options; _options = options;
Connected = false; _disposed = false;
} }
public async Task ConnectAsync(string url) protected async Task ConnectAsync(string url)
{ {
if (_socket != null) if (_socket != null)
{ {
if (_socket.State == WebSocketState.Open) return; if (_socket.State == WebSocketState.Open) return;
else _socket.Dispose(); else if (!_disposed) _socket.Dispose();
} }
_socket = new ClientWebSocket(); _socket = new ClientWebSocket();
_socket.Options.RemoteCertificateValidationCallback = (o, c, ch, er) => true; _socket.Options.RemoteCertificateValidationCallback = (o, c, ch, er) => true;
_socket.Options.UseDefaultCredentials = false; _socket.Options.UseDefaultCredentials = false;
_disposed = false;
if (_cts != null) _cts.Dispose(); if (_cts != null) _cts.Dispose();
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
await _socket.ConnectAsync(new Uri(url), _cts.Token); await _socket.ConnectAsync(new Uri(url), _cts.Token);
await Task.Factory.StartNew(ReceiveLoop, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); await Task.Factory.StartNew(ReceiveLoop, _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
await OnConnection(); OnConnected?.Invoke(this, EventArgs.Empty);
} }
public async Task DisconnectAsync() public async Task DisconnectAsync(SocketDisconnectionEventArgs args)
{ {
if (_socket == null || _cts == null) return; if (_disposed || _socket == null || _cts == null)
// TODO: requests cleanup code, sub-protocol dependent. return;
if (_socket.State == WebSocketState.Open) if (_socket.State == WebSocketState.Open)
{ {
_cts.CancelAfter(TimeSpan.FromMilliseconds(500)); _cts.CancelAfter(TimeSpan.FromMilliseconds(500));
@ -53,14 +58,20 @@ namespace CommonSocketLibrary.Abstract
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
} }
Connected = false; OnDisconnected?.Invoke(this, args);
_socket.Dispose(); _socket.Dispose();
_socket = null; _socket = null;
_cts.Dispose(); _cts.Dispose();
_cts = null; _cts = null;
} }
public void Dispose() => DisconnectAsync().Wait(); public void Dispose()
{
if (_disposed)
return;
_disposed = true;
}
private async Task ReceiveLoop() private async Task ReceiveLoop()
{ {
@ -87,78 +98,84 @@ namespace CommonSocketLibrary.Abstract
await ResponseReceived(outputStream); await ResponseReceived(outputStream);
} }
} }
catch (TaskCanceledException) { } catch (WebSocketException wse)
{
string data = string.Join(string.Empty, wse.Data.Cast<DictionaryEntry>().Select(e => e.Key + "=" + e.Value));
_logger.Error($"Websocket connection problem while receiving data [state: {_socket.State}][code: {wse.ErrorCode}][data: {data}]");
}
catch (TaskCanceledException)
{
_logger.Error($"Socket's receive loop got canceled forcefully [state: {_socket.State}]");
}
finally finally
{ {
outputStream?.Dispose(); if (_socket.State.ToString().Contains("Close") || _socket.State == WebSocketState.Aborted)
await DisconnectAsync(new SocketDisconnectionEventArgs(_socket.CloseStatus.ToString()!, _socket.CloseStatusDescription ?? string.Empty));
} }
} }
public async Task SendRaw(string content) protected async Task Reconnect(IBackoff backoff, Action reconnect)
{ {
if (!Connected) return; while (true)
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); try
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); TimeSpan delay = backoff.GetNextDelay();
await _socket.SendAsync(array, WebSocketMessageType.Text, current + size >= total, _cts.Token); await Task.Delay(delay);
current += size; reconnect.Invoke();
backoff.Reset();
break;
}
catch (Exception)
{
_logger.Error("Unable to reconnect to server.");
} }
await OnMessageSend(opcode, content);
}
catch (Exception e)
{
Connected = false;
_logger.Error(e, "Failed to send a message: " + opcode);
} }
} }
private async Task ResponseReceived(Stream stream) private async Task ResponseReceived(Stream stream)
{ {
Message? data = null;
try try
{ {
var data = await JsonSerializer.DeserializeAsync<Message>(stream); data = await JsonSerializer.DeserializeAsync<Message>(stream, _options);
await OnResponseReceived(data);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error(ex, "Failed to read or execute a websocket message."); _logger.Error(ex, "Failed to read a websocket message.");
} }
finally finally
{ {
stream.Dispose(); stream.Dispose();
} }
if (data == null)
{
_logger.Error("Failed to read a websocket message.");
return;
}
try
{
await OnResponseReceived(data);
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to execute a websocket message.");
}
} }
protected abstract Message GenerateMessage<T>(int opcode, T data);
protected abstract Task OnResponseReceived(Message? content); protected abstract Task OnResponseReceived(Message? content);
protected abstract Task OnMessageSend(int opcode, string? content); }
protected abstract Task OnConnection();
public class SocketDisconnectionEventArgs : EventArgs
{
public string Status { get; }
public string Reason { get; }
public SocketDisconnectionEventArgs(string status, string reason)
{
Status = status;
Reason = reason;
}
} }
} }

View File

@ -0,0 +1,32 @@
namespace CommonSocketLibrary.Backoff
{
public class ExponentialBackoff : IBackoff
{
private int _initial;
private int _current;
private int _maximum;
public ExponentialBackoff(int initial, int maximum)
{
if (maximum < initial)
throw new InvalidOperationException("Initial backoff cannot be larger than maximum backoff.");
_initial = initial;
_maximum = maximum;
Reset();
}
public TimeSpan GetNextDelay()
{
_current = Math.Min(_current * 2, _maximum);
return TimeSpan.FromMilliseconds(_current);
}
public void Reset()
{
_current = _initial / 2;
}
}
}

8
Backoff/IBackoff.cs Normal file
View File

@ -0,0 +1,8 @@
namespace CommonSocketLibrary.Backoff
{
public interface IBackoff
{
TimeSpan GetNextDelay();
void Reset();
}
}

View File

@ -2,9 +2,8 @@ using CommonSocketLibrary.Abstract;
namespace CommonSocketLibrary.Common namespace CommonSocketLibrary.Common
{ {
public interface IWebSocketHandler public interface IWebSocketHandler : ICodedOperation
{ {
int OperationCode { get; }
Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data); Task Execute<Data>(SocketClient<WebSocketMessage> sender, Data data);
} }
} }

View File

@ -1,4 +1,6 @@
using System.Text.Json; using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using CommonSocketLibrary.Abstract; using CommonSocketLibrary.Abstract;
using Serilog; using Serilog;
@ -6,21 +8,22 @@ namespace CommonSocketLibrary.Common
{ {
public class WebSocketClient : SocketClient<WebSocketMessage> public class WebSocketClient : SocketClient<WebSocketMessage>
{ {
private readonly HandlerManager<WebSocketClient, IWebSocketHandler> _handlerManager; protected IDictionary<int, IWebSocketHandler> _handlers;
private readonly HandlerTypeManager<WebSocketClient, IWebSocketHandler> _handlerTypeManager; private readonly MessageTypeManager<IWebSocketHandler> _messageTypeManager;
public WebSocketClient( public WebSocketClient(
ILogger logger, IEnumerable<IWebSocketHandler> handlers,
HandlerManager<WebSocketClient, IWebSocketHandler> handlerManager, MessageTypeManager<IWebSocketHandler> typeManager,
HandlerTypeManager<WebSocketClient, IWebSocketHandler> typeManager, JsonSerializerOptions serializerOptions,
JsonSerializerOptions serializerOptions ILogger logger
) : base(logger, serializerOptions) ) : base(logger, serializerOptions)
{ {
_handlerManager = handlerManager; _handlers = handlers.ToDictionary(h => h.OperationCode, h => h);
_handlerTypeManager = typeManager; _messageTypeManager = typeManager;
} }
protected override WebSocketMessage GenerateMessage<T>(int opcode, T data)
protected WebSocketMessage GenerateMessage<T>(int opcode, T data)
{ {
return new WebSocketMessage() return new WebSocketMessage()
{ {
@ -29,28 +32,61 @@ namespace CommonSocketLibrary.Common
}; };
} }
protected override async Task OnResponseReceived(WebSocketMessage? data) protected override async Task OnResponseReceived(WebSocketMessage? message)
{ {
if (data == null) if (message == null)
return; return;
string content = data.Data?.ToString() ?? string.Empty; string content = message.Data?.ToString() ?? string.Empty;
_logger.Verbose("RX #" + data.OpCode + ": " + content); _logger.Verbose("RX #" + message.OpCode + ": " + content);
if (!_handlerTypeManager.HandlerTypes.TryGetValue(data.OpCode, out Type? type) || type == null) var type = _messageTypeManager.GetMessageTypeByCode(message.OpCode);
if (type == null)
{
return; return;
}
var obj = JsonSerializer.Deserialize(content, type, _options); var data = JsonSerializer.Deserialize(content, type, _options);
await _handlerManager.Execute(this, data.OpCode, obj); if (!_handlers.TryGetValue(message.OpCode, out IWebSocketHandler? handler) || handler == null)
{
return;
}
await handler.Execute(this, data);
} }
protected override async Task OnMessageSend(int opcode, string? content) public async Task Send<T>(int opcode, T data)
{ {
_logger.Verbose("TX #" + opcode + ": " + content); if (_socket == null)
} return;
try
{
var message = GenerateMessage(opcode, data);
var content = JsonSerializer.Serialize(message, _options);
protected override async Task OnConnection() 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;
}
_logger.Verbose("TX #" + opcode + ": " + content);
}
catch (Exception e)
{
if (_socket.State.ToString().Contains("Close") || _socket.State == WebSocketState.Aborted)
{
await DisconnectAsync(new SocketDisconnectionEventArgs(_socket.CloseStatus.ToString()!, _socket.CloseStatusDescription ?? string.Empty));
_logger.Warning($"Socket state on closing = {_socket.State} | {_socket.CloseStatus?.ToString()} | {_socket.CloseStatusDescription}");
}
_logger.Error(e, $"Failed to send a websocket message [op code: {opcode}]");
}
} }
} }
} }

View File

@ -4,9 +4,9 @@ using Serilog;
namespace CommonSocketLibrary.Socket.Manager namespace CommonSocketLibrary.Socket.Manager
{ {
public abstract class WebSocketHandlerTypeManager : HandlerTypeManager<WebSocketClient, IWebSocketHandler> public abstract class WebSocketMessageTypeManager : MessageTypeManager<IWebSocketHandler>
{ {
public WebSocketHandlerTypeManager(ILogger logger, HandlerManager<WebSocketClient, IWebSocketHandler> handlers) : base(logger, handlers) public WebSocketMessageTypeManager(IEnumerable<IWebSocketHandler> handlers, ILogger logger) : base(handlers, logger)
{ {
} }

View File

@ -13,13 +13,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MathParser.org-mXparser" Version="6.0.0" /> <PackageReference Include="MathParser.org-mXparser" Version="6.0.0" />
<PackageReference Include="Serilog" Version="4.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.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.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Trace" Version="4.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>