You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
14 KiB
391 lines
14 KiB
1 year ago
|
#if !BESTHTTP_DISABLE_SIGNALR
|
||
|
|
||
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
|
||
|
using BestHTTP.SignalR.Messages;
|
||
|
using System.Text;
|
||
|
|
||
|
namespace BestHTTP.SignalR.Hubs
|
||
|
{
|
||
|
public delegate void OnMethodCallDelegate(Hub hub, string method, params object[] args);
|
||
|
public delegate void OnMethodCallCallbackDelegate(Hub hub, MethodCallMessage methodCall);
|
||
|
|
||
|
public delegate void OnMethodResultDelegate(Hub hub, ClientMessage originalMessage, ResultMessage result);
|
||
|
public delegate void OnMethodFailedDelegate(Hub hub, ClientMessage originalMessage, FailureMessage error);
|
||
|
public delegate void OnMethodProgressDelegate(Hub hub, ClientMessage originialMessage, ProgressMessage progress);
|
||
|
|
||
|
/// <summary>
|
||
|
/// Represents a clientside Hub. This class can be used as a base class to encapsulate proxy functionalities.
|
||
|
/// </summary>
|
||
|
public class Hub : IHub
|
||
|
{
|
||
|
|
||
|
#region Public Properties
|
||
|
|
||
|
/// <summary>
|
||
|
/// Name of this hub.
|
||
|
/// </summary>
|
||
|
public string Name { get; private set; }
|
||
|
|
||
|
/// <summary>
|
||
|
/// Server and user set state of the hub.
|
||
|
/// </summary>
|
||
|
public Dictionary<string, object> State
|
||
|
{
|
||
|
// Create only when we need to.
|
||
|
get
|
||
|
{
|
||
|
if (state == null)
|
||
|
state = new Dictionary<string, object>();
|
||
|
return state;
|
||
|
}
|
||
|
}
|
||
|
private Dictionary<string, object> state;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Event called every time when the server sends an order to call a method on the client.
|
||
|
/// </summary>
|
||
|
public event OnMethodCallDelegate OnMethodCall;
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region Privates
|
||
|
|
||
|
/// <summary>
|
||
|
/// Table of the sent messages. These messages will be removed from this table when a Result message is received from the server.
|
||
|
/// </summary>
|
||
|
private Dictionary<UInt64, ClientMessage> SentMessages = new Dictionary<ulong, ClientMessage>();
|
||
|
|
||
|
/// <summary>
|
||
|
/// Methodname -> callback delegate mapping. This table stores the server callable functions.
|
||
|
/// </summary>
|
||
|
private Dictionary<string, OnMethodCallCallbackDelegate> MethodTable = new Dictionary<string, OnMethodCallCallbackDelegate>();
|
||
|
|
||
|
/// <summary>
|
||
|
/// A reusable StringBuilder to save some GC allocs
|
||
|
/// </summary>
|
||
|
private StringBuilder builder = new StringBuilder();
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
Connection IHub.Connection { get; set; }
|
||
|
|
||
|
public Hub(string name)
|
||
|
:this(name, null)
|
||
|
{
|
||
|
|
||
|
}
|
||
|
|
||
|
public Hub(string name, Connection manager)
|
||
|
{
|
||
|
this.Name = name;
|
||
|
(this as IHub).Connection = manager;
|
||
|
}
|
||
|
|
||
|
#region Public Hub Functions
|
||
|
|
||
|
/// <summary>
|
||
|
/// Registers a callback function to the given method.
|
||
|
/// </summary>
|
||
|
public void On(string method, OnMethodCallCallbackDelegate callback)
|
||
|
{
|
||
|
MethodTable[method] = callback;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Removes callback from the given method.
|
||
|
/// </summary>
|
||
|
/// <param name="method"></param>
|
||
|
public void Off(string method)
|
||
|
{
|
||
|
MethodTable[method] = null;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Orders the server to call a method with the given arguments.
|
||
|
/// </summary>
|
||
|
/// <returns>True if the plugin was able to send out the message</returns>
|
||
|
public bool Call(string method, params object[] args)
|
||
|
{
|
||
|
return Call(method, null, null, null, args);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Orders the server to call a method with the given arguments.
|
||
|
/// The onResult callback will be called when the server successfully called the function.
|
||
|
/// </summary>
|
||
|
/// <returns>True if the plugin was able to send out the message</returns>
|
||
|
public bool Call(string method, OnMethodResultDelegate onResult, params object[] args)
|
||
|
{
|
||
|
return Call(method, onResult, null, null, args);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Orders the server to call a method with the given arguments.
|
||
|
/// The onResult callback will be called when the server successfully called the function.
|
||
|
/// The onResultError will be called when the server can't call the function, or when the function throws an exception.
|
||
|
/// </summary>
|
||
|
/// <returns>True if the plugin was able to send out the message</returns>
|
||
|
public bool Call(string method, OnMethodResultDelegate onResult, OnMethodFailedDelegate onResultError, params object[] args)
|
||
|
{
|
||
|
return Call(method, onResult, onResultError, null, args);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Orders the server to call a method with the given arguments.
|
||
|
/// The onResult callback will be called when the server successfully called the function.
|
||
|
/// The onProgress callback called multiple times when the method is a long running function and reports back its progress.
|
||
|
/// </summary>
|
||
|
/// <returns>True if the plugin was able to send out the message</returns>
|
||
|
public bool Call(string method, OnMethodResultDelegate onResult, OnMethodProgressDelegate onProgress, params object[] args)
|
||
|
{
|
||
|
return Call(method, onResult, null, onProgress, args);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Orders the server to call a method with the given arguments.
|
||
|
/// The onResult callback will be called when the server successfully called the function.
|
||
|
/// The onResultError will be called when the server can't call the function, or when the function throws an exception.
|
||
|
/// The onProgress callback called multiple times when the method is a long running function and reports back its progress.
|
||
|
/// </summary>
|
||
|
/// <returns>True if the plugin was able to send out the message</returns>
|
||
|
public bool Call(string method, OnMethodResultDelegate onResult, OnMethodFailedDelegate onResultError, OnMethodProgressDelegate onProgress, params object[] args)
|
||
|
{
|
||
|
IHub thisHub = this as IHub;
|
||
|
|
||
|
// Start over the counter if we are reached the max value if the long type.
|
||
|
// While we are using this property only here, we don't want to make it static to avoid another thread synchronization, neither we want to make it a Hub-instance field to achieve better debuggability.
|
||
|
|
||
|
long newValue, originalValue;
|
||
|
do
|
||
|
{
|
||
|
originalValue = thisHub.Connection.ClientMessageCounter;
|
||
|
newValue = (originalValue % long.MaxValue) + 1;
|
||
|
} while (System.Threading.Interlocked.CompareExchange(ref thisHub.Connection.ClientMessageCounter, newValue, originalValue) != originalValue);
|
||
|
|
||
|
// Create and send the client message
|
||
|
return thisHub.Call(new ClientMessage(this, method, args, (ulong)thisHub.Connection.ClientMessageCounter, onResult, onResultError, onProgress));
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region IHub Implementation
|
||
|
|
||
|
bool IHub.Call(ClientMessage msg)
|
||
|
{
|
||
|
IHub thisHub = this as IHub;
|
||
|
|
||
|
if (!thisHub.Connection.SendJson(BuildMessage(msg)))
|
||
|
return false;
|
||
|
|
||
|
SentMessages.Add(msg.CallIdx, msg);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Return true if this hub sent the message with the given id.
|
||
|
/// </summary>
|
||
|
bool IHub.HasSentMessageId(UInt64 id)
|
||
|
{
|
||
|
return SentMessages.ContainsKey(id);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Called on the manager's close.
|
||
|
/// </summary>
|
||
|
void IHub.Close()
|
||
|
{
|
||
|
SentMessages.Clear();
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Called when the client receives an order to call a hub-function.
|
||
|
/// </summary>
|
||
|
void IHub.OnMethod(MethodCallMessage msg)
|
||
|
{
|
||
|
// Merge the newly received states with the old one
|
||
|
MergeState(msg.State);
|
||
|
|
||
|
if (OnMethodCall != null)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
OnMethodCall(this, msg.Method, msg.Arguments);
|
||
|
}
|
||
|
catch(Exception ex)
|
||
|
{
|
||
|
HTTPManager.Logger.Exception("Hub - " + this.Name, "IHub.OnMethod - OnMethodCall", ex);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
OnMethodCallCallbackDelegate callback;
|
||
|
if (MethodTable.TryGetValue(msg.Method, out callback) && callback != null)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
callback(this, msg);
|
||
|
}
|
||
|
catch(Exception ex)
|
||
|
{
|
||
|
HTTPManager.Logger.Exception("Hub - " + this.Name, "IHub.OnMethod - callback", ex);
|
||
|
}
|
||
|
}
|
||
|
else if (OnMethodCall == null)
|
||
|
HTTPManager.Logger.Warning("Hub - " + this.Name, string.Format("[Client] {0}.{1} (args: {2})", this.Name, msg.Method, msg.Arguments.Length));
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Called when the client receives back messages as a result of a server method call.
|
||
|
/// </summary>
|
||
|
void IHub.OnMessage(IServerMessage msg)
|
||
|
{
|
||
|
ClientMessage originalMsg;
|
||
|
|
||
|
UInt64 id = (msg as IHubMessage).InvocationId;
|
||
|
if (!SentMessages.TryGetValue(id, out originalMsg))
|
||
|
{
|
||
|
// This can happen when a result message removes the ClientMessage from the SentMessages dictionary,
|
||
|
// then a late come progress message tries to access it
|
||
|
HTTPManager.Logger.Warning("Hub - " + this.Name, "OnMessage - Sent message not found with id: " + id.ToString());
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
switch(msg.Type)
|
||
|
{
|
||
|
case MessageTypes.Result:
|
||
|
ResultMessage result = msg as ResultMessage;
|
||
|
|
||
|
// Merge the incoming State before firing the events
|
||
|
MergeState(result.State);
|
||
|
|
||
|
if (originalMsg.ResultCallback != null)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
originalMsg.ResultCallback(this, originalMsg, result);
|
||
|
}
|
||
|
catch(Exception ex)
|
||
|
{
|
||
|
HTTPManager.Logger.Exception("Hub " + this.Name, "IHub.OnMessage - ResultCallback", ex);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SentMessages.Remove(id);
|
||
|
|
||
|
break;
|
||
|
|
||
|
case MessageTypes.Failure:
|
||
|
FailureMessage error = msg as FailureMessage;
|
||
|
|
||
|
// Merge the incoming State before firing the events
|
||
|
MergeState(error.State);
|
||
|
|
||
|
if (originalMsg.ResultErrorCallback != null)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
originalMsg.ResultErrorCallback(this, originalMsg, error);
|
||
|
}
|
||
|
catch(Exception ex)
|
||
|
{
|
||
|
HTTPManager.Logger.Exception("Hub " + this.Name, "IHub.OnMessage - ResultErrorCallback", ex);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SentMessages.Remove(id);
|
||
|
break;
|
||
|
|
||
|
case MessageTypes.Progress:
|
||
|
if (originalMsg.ProgressCallback != null)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
originalMsg.ProgressCallback(this, originalMsg, msg as ProgressMessage);
|
||
|
}
|
||
|
catch(Exception ex)
|
||
|
{
|
||
|
HTTPManager.Logger.Exception("Hub " + this.Name, "IHub.OnMessage - ProgressCallback", ex);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
|
||
|
#region Helper Functions
|
||
|
|
||
|
/// <summary>
|
||
|
/// Merges the current and the new states.
|
||
|
/// </summary>
|
||
|
#if BESTHTTP_SIGNALR_WITH_JSONDOTNET
|
||
|
private void MergeState(IDictionary<string, Newtonsoft.Json.Linq.JToken> state)
|
||
|
#else
|
||
|
private void MergeState(IDictionary<string, object> state)
|
||
|
#endif
|
||
|
{
|
||
|
if (state != null && state.Count > 0)
|
||
|
foreach (var kvp in state)
|
||
|
this.State[kvp.Key] = kvp.Value;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Builds a JSon string from the given message.
|
||
|
/// </summary>
|
||
|
private string BuildMessage(ClientMessage msg)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
builder.Append("{\"H\":\"");
|
||
|
builder.Append(this.Name);
|
||
|
builder.Append("\",\"M\":\"");
|
||
|
builder.Append(msg.Method);
|
||
|
builder.Append("\",\"A\":");
|
||
|
|
||
|
string jsonEncoded = string.Empty;
|
||
|
|
||
|
// Arguments
|
||
|
if (msg.Args != null && msg.Args.Length > 0)
|
||
|
jsonEncoded = (this as IHub).Connection.JsonEncoder.Encode(msg.Args);
|
||
|
else
|
||
|
jsonEncoded = "[]";
|
||
|
|
||
|
builder.Append(jsonEncoded);
|
||
|
|
||
|
builder.Append(",\"I\":\"");
|
||
|
builder.Append(msg.CallIdx.ToString());
|
||
|
builder.Append("\"");
|
||
|
|
||
|
// State, if any
|
||
|
if (msg.Hub.state != null && msg.Hub.state.Count > 0)
|
||
|
{
|
||
|
builder.Append(",\"S\":");
|
||
|
|
||
|
jsonEncoded = (this as IHub).Connection.JsonEncoder.Encode(msg.Hub.state);
|
||
|
builder.Append(jsonEncoded);
|
||
|
}
|
||
|
|
||
|
builder.Append("}");
|
||
|
|
||
|
return builder.ToString();
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
HTTPManager.Logger.Exception("Hub - " + this.Name, "Send", ex);
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
// reset the StringBuilder instance, to reuse next time
|
||
|
builder.Length = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endregion
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|