mirror of
https://github.com/vdemydiuk/mtapi.git
synced 2026-07-27 18:47:55 +00:00
369 lines
12 KiB
C#
Executable File
369 lines
12 KiB
C#
Executable File
using System.Net.WebSockets;
|
|
using System.Text;
|
|
|
|
namespace MtClient
|
|
{
|
|
public interface IRpcLogger
|
|
{
|
|
public void Debug(string message);
|
|
public void Info(string message);
|
|
public void Warn(string message);
|
|
public void Error(string message);
|
|
}
|
|
|
|
public class MtRpcClient
|
|
{
|
|
public MtRpcClient(string host, int port, IRpcLogger? logger = null)
|
|
{
|
|
host_ = host;
|
|
port_ = port;
|
|
logger_ = logger ?? new StubLogger();
|
|
|
|
receiveThread_ = new Thread(new ThreadStart(DoReceive));
|
|
sendThread_ = new Thread(new ThreadStart(DoWrite));
|
|
}
|
|
|
|
public async Task Connect()
|
|
{
|
|
logger_.Debug($"MtRpcClient.Connect: started to {host_}:{port_}");
|
|
|
|
try
|
|
{
|
|
await ws_.ConnectAsync(new Uri($"ws://{host_}:{port_}/ws"), CancellationToken.None);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger_.Error($"MtRpcClient.Connect failed: {ex.Message}");
|
|
throw new Exception($"Failed connection to {host_}:{port_}");
|
|
}
|
|
|
|
receiveThread_.Start();
|
|
sendThread_.Start();
|
|
|
|
logger_.Debug($"MtRpcClient.Connect: success.");
|
|
}
|
|
|
|
public async void Disconnect()
|
|
{
|
|
logger_.Debug($"MtRpcClient.Disconnect: {host_}:{port_}");
|
|
|
|
try
|
|
{
|
|
if (ws_.State == WebSocketState.Open)
|
|
await ws_.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
|
|
ws_.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger_.Warn($"MtRpcClient.Disconnect: {ex.Message}");
|
|
}
|
|
|
|
sendWaiter_.Set();
|
|
sendThread_.Join();
|
|
receiveThread_.Join();
|
|
|
|
logger_.Debug($"MtRpcClient.Disconnect: success");
|
|
}
|
|
|
|
public string? SendCommand(int expertHandle, int commandType, string payload, int timeout = 10000) // 10 sec
|
|
{
|
|
CommandTask<string> commandTask = new();
|
|
int commandId;
|
|
lock (tasks_)
|
|
{
|
|
commandId = nextCommandId++;
|
|
tasks_[commandId] = commandTask;
|
|
}
|
|
|
|
MtCommand command = new(expertHandle, commandType, commandId, payload);
|
|
Send(command);
|
|
|
|
var response = commandTask.WaitResponse(timeout);
|
|
lock (tasks_)
|
|
{
|
|
tasks_.Remove(commandId);
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
public HashSet<int>? RequestExpertsList()
|
|
{
|
|
CommandTask<object> requestTask = new();
|
|
lock (service_requests_)
|
|
{
|
|
service_requests_[ServiceRequestType.ExpertList] = requestTask;
|
|
}
|
|
|
|
MtServiceRequest request = new(ServiceRequestType.ExpertList);
|
|
Send(request);
|
|
|
|
var response = requestTask.WaitResponse(10000); // 10 sec
|
|
lock (service_requests_)
|
|
{
|
|
service_requests_.Remove(ServiceRequestType.ExpertList);
|
|
}
|
|
|
|
return response as HashSet<int>;
|
|
}
|
|
|
|
private void Send(MtMessage message)
|
|
{
|
|
lock (pendingMessages_)
|
|
{
|
|
pendingMessages_.Enqueue(message);
|
|
}
|
|
sendWaiter_.Set();
|
|
}
|
|
|
|
private async void DoWrite()
|
|
{
|
|
while(ws_.State == WebSocketState.Open)
|
|
{
|
|
MtMessage? message = null;
|
|
lock(pendingMessages_)
|
|
{
|
|
if (pendingMessages_.Count > 0)
|
|
message = pendingMessages_.Dequeue();
|
|
}
|
|
|
|
if (message == null)
|
|
{
|
|
sendWaiter_.WaitOne();
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
string msgStr = message.Serialize();
|
|
logger_.Debug($"MtRpcClient.DoWrite: sending message: {msgStr}");
|
|
byte[] bytes = Encoding.ASCII.GetBytes(msgStr);
|
|
await ws_.SendAsync(bytes, WebSocketMessageType.Text, true, CancellationToken.None);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger_.Error($"MtRpcClient.DoWrite: {e.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private async void DoReceive()
|
|
{
|
|
try
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
{
|
|
var messageBuffer = WebSocket.CreateClientBuffer(1024, 64);
|
|
while (ws_.State == WebSocketState.Open)
|
|
{
|
|
int recvCount = 0;
|
|
WebSocketReceiveResult result;
|
|
do
|
|
{
|
|
result = await ws_.ReceiveAsync(messageBuffer, CancellationToken.None);
|
|
var byteArray = messageBuffer.Array;
|
|
if (byteArray != null)
|
|
{
|
|
ms.Write(byteArray, messageBuffer.Offset, result.Count);
|
|
recvCount += result.Count;
|
|
}
|
|
}
|
|
while (!result.EndOfMessage);
|
|
|
|
if (result.MessageType == WebSocketMessageType.Close)
|
|
{
|
|
logger_.Info($"MtRpcClient.DoReceive: close signal {result.CloseStatusDescription}");
|
|
Disconnected?.Invoke(this, EventArgs.Empty);
|
|
break;
|
|
}
|
|
else if (result.MessageType == WebSocketMessageType.Text)
|
|
{
|
|
var msg = Encoding.UTF8.GetString(ms.ToArray(), 0, recvCount);
|
|
OnReceive(msg);
|
|
}
|
|
ms.Seek(0, SeekOrigin.Begin);
|
|
ms.Position = 0;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger_.Warn($"MtRpcClient.DoReceive: Exception in receive - {ex.Message}");
|
|
ConnectionFailed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
private void OnReceive(string msg)
|
|
{
|
|
logger_.Debug($"MtRpcClient.OnReceive: {msg}");
|
|
|
|
if (string.IsNullOrEmpty(msg))
|
|
{
|
|
logger_.Warn("MtRpcClient.OnReceive: Invalid message (null or empty)");
|
|
return;
|
|
}
|
|
|
|
var pieces = msg.Split(";", 2);
|
|
|
|
if (pieces.Length != 2
|
|
|| string.IsNullOrEmpty(pieces[0])
|
|
|| string.IsNullOrEmpty(pieces[1]))
|
|
{
|
|
logger_.Warn("MtRpcClient.OnReceive: Invalid message format.");
|
|
return;
|
|
}
|
|
|
|
MessageType msgType;
|
|
try
|
|
{
|
|
var msgTypeValue = int.Parse(pieces[0]);
|
|
msgType = (MessageType)Enum.ToObject(typeof(MessageType), msgTypeValue);
|
|
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger_.Error($"MtRpcClient.OnReceive: Parse MessageType failed. {e.Message}");
|
|
return;
|
|
}
|
|
|
|
var message = MtMessageParser.Parse(msgType, (pieces[1]));
|
|
if (message == null)
|
|
{
|
|
logger_.Error("MtRpcClient.OnReceive: Failed parse message payload");
|
|
return;
|
|
}
|
|
|
|
switch (message.MsgType)
|
|
{
|
|
case MessageType.Event:
|
|
if (message is MtEvent e)
|
|
MtEventReceived?.Invoke(this, new(e.ExpertHandle, e.EventType, e.Payload));
|
|
break;
|
|
case MessageType.Response:
|
|
if (message is MtResponse response)
|
|
ProcessResponse(response);
|
|
break;
|
|
case MessageType.ExpertList:
|
|
if (message is MtExpertListMsg expertListMsg)
|
|
ProcessExpertList(expertListMsg.Experts);
|
|
break;
|
|
case MessageType.ExpertAdded:
|
|
if (message is MtExpertAddedMsg expertAddedMsg)
|
|
ExpertAdded?.Invoke(this, new(expertAddedMsg.ExpertHandle));
|
|
break;
|
|
case MessageType.ExpertRemoved:
|
|
if (message is MtExpertRemovedMsg expertRemovedMsg)
|
|
ExpertRemoved?.Invoke(this, new(expertRemovedMsg.ExpertHandle));
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void ProcessResponse(MtResponse msg)
|
|
{
|
|
var handle = msg.ExpertHandle;
|
|
var commandId = msg.CommandId;
|
|
var payload = msg.Payload;
|
|
|
|
logger_.Debug($"MtRpcClient.ProcessResponse: {handle}, {commandId}, [{payload}]");
|
|
|
|
lock (tasks_)
|
|
{
|
|
if (tasks_.TryGetValue(commandId, out CommandTask<string>? value))
|
|
value.SetResponse(payload);
|
|
}
|
|
}
|
|
|
|
private void ProcessExpertList(HashSet<int> experts)
|
|
{
|
|
logger_.Debug($"MtRpcClient.ProcessExpertList: experts count - {experts.Count}");
|
|
|
|
lock (service_requests_)
|
|
{
|
|
if (service_requests_.TryGetValue(ServiceRequestType.ExpertList, out CommandTask<object>? value))
|
|
value.SetResponse(experts);
|
|
}
|
|
}
|
|
|
|
public event EventHandler<EventArgs>? ConnectionFailed;
|
|
public event EventHandler<EventArgs>? Disconnected;
|
|
public event EventHandler<MtExpertEventArgs>? ExpertAdded;
|
|
public event EventHandler<MtExpertEventArgs>? ExpertRemoved;
|
|
public event EventHandler<MtEventArgs>? MtEventReceived;
|
|
|
|
private readonly ClientWebSocket ws_ = new();
|
|
private readonly string host_;
|
|
private readonly int port_;
|
|
private readonly Queue<MtMessage> pendingMessages_ = [];
|
|
|
|
private readonly Thread receiveThread_;
|
|
private readonly Thread sendThread_;
|
|
private readonly EventWaitHandle sendWaiter_ = new AutoResetEvent(false);
|
|
|
|
private int nextCommandId = 0;
|
|
private readonly Dictionary<int, CommandTask<string>> tasks_ = [];
|
|
private readonly Dictionary<ServiceRequestType, CommandTask<object>> service_requests_ = [];
|
|
|
|
private readonly IRpcLogger logger_;
|
|
}
|
|
|
|
internal class CommandTask<T>
|
|
{
|
|
private readonly EventWaitHandle responseWaiter_ = new AutoResetEvent(false);
|
|
private T? response_;
|
|
private readonly object locker_ = new();
|
|
|
|
public T? WaitResponse(int time)
|
|
{
|
|
responseWaiter_.WaitOne(time);
|
|
lock (locker_)
|
|
{
|
|
return response_;
|
|
}
|
|
}
|
|
|
|
public void SetResponse(T result)
|
|
{
|
|
lock (locker_)
|
|
{
|
|
response_ = result;
|
|
}
|
|
responseWaiter_.Set();
|
|
}
|
|
}
|
|
|
|
public class MtEventArgs(int expertHandle, int eventType, string payload) : EventArgs
|
|
{
|
|
public int ExpertHandle { get; } = expertHandle;
|
|
public int EventType { get; } = eventType;
|
|
public string Payload { get; } = payload;
|
|
}
|
|
|
|
public class MtExpertListEventArgs(HashSet<int> experts) : EventArgs
|
|
{
|
|
public HashSet<int> Experts { get; } = experts;
|
|
}
|
|
|
|
public class MtExpertEventArgs(int expert) : EventArgs
|
|
{
|
|
public int Expert { get; }= expert;
|
|
}
|
|
|
|
internal class StubLogger : IRpcLogger
|
|
{
|
|
public void Debug(string message)
|
|
{
|
|
}
|
|
|
|
public void Error(string message)
|
|
{
|
|
}
|
|
|
|
public void Info(string message)
|
|
{
|
|
}
|
|
|
|
public void Warn(string message)
|
|
{
|
|
}
|
|
}
|
|
} |