培训考核三期,新版培训,网页版培训登录器
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.
 
 

1443 lines
59 KiB

#if !BESTHTTP_DISABLE_SIGNALR_CORE
using System.Threading;
#if CSHARP_7_OR_LATER
using System.Threading.Tasks;
#endif
using BestHTTP.Futures;
using BestHTTP.SignalRCore.Authentication;
using BestHTTP.SignalRCore.Messages;
using System;
using System.Collections.Generic;
using BestHTTP.Logger;
using System.Collections.Concurrent;
using BestHTTP.PlatformSupport.Threading;
namespace BestHTTP.SignalRCore
{
public sealed class HubConnection : BestHTTP.Extensions.IHeartbeat
{
public static readonly object[] EmptyArgs = new object[0];
/// <summary>
/// Uri of the Hub endpoint
/// </summary>
public Uri Uri { get; private set; }
/// <summary>
/// Current state of this connection.
/// </summary>
public ConnectionStates State {
get { return (ConnectionStates)this._state; }
private set {
Interlocked.Exchange(ref this._state, (int)value);
}
}
private volatile int _state;
/// <summary>
/// Current, active ITransport instance.
/// </summary>
public ITransport Transport { get; private set; }
/// <summary>
/// The IProtocol implementation that will parse, encode and decode messages.
/// </summary>
public IProtocol Protocol { get; private set; }
/// <summary>
/// This event is called when the connection is redirected to a new uri.
/// </summary>
public event Action<HubConnection, Uri, Uri> OnRedirected;
/// <summary>
/// This event is called when successfully connected to the hub.
/// </summary>
public event Action<HubConnection> OnConnected;
/// <summary>
/// This event is called when an unexpected error happen and the connection is closed.
/// </summary>
public event Action<HubConnection, string> OnError;
/// <summary>
/// This event is called when the connection is gracefully terminated.
/// </summary>
public event Action<HubConnection> OnClosed;
/// <summary>
/// This event is called for every server-sent message. When returns false, no further processing of the message is done by the plugin.
/// </summary>
public event Func<HubConnection, Message, bool> OnMessage;
/// <summary>
/// Called when the HubConnection start its reconnection process after loosing its underlying connection.
/// </summary>
public event Action<HubConnection, string> OnReconnecting;
/// <summary>
/// Called after a successful reconnection.
/// </summary>
public event Action<HubConnection> OnReconnected;
/// <summary>
/// Called for transport related events.
/// </summary>
public event Action<HubConnection, ITransport, TransportEvents> OnTransportEvent;
/// <summary>
/// An IAuthenticationProvider implementation that will be used to authenticate the connection.
/// </summary>
public IAuthenticationProvider AuthenticationProvider { get; set; }
/// <summary>
/// Negotiation response sent by the server.
/// </summary>
public NegotiationResult NegotiationResult { get; private set; }
/// <summary>
/// Options that has been used to create the HubConnection.
/// </summary>
public HubOptions Options { get; private set; }
/// <summary>
/// How many times this connection is redirected.
/// </summary>
public int RedirectCount { get; private set; }
/// <summary>
/// The reconnect policy that will be used when the underlying connection is lost. Its default value is null.
/// </summary>
public IRetryPolicy ReconnectPolicy { get; set; }
/// <summary>
/// Logging context of this HubConnection instance.
/// </summary>
public LoggingContext Context { get; private set; }
/// <summary>
/// This will be increment to add a unique id to every message the plugin will send.
/// </summary>
private long lastInvocationId = 1;
/// <summary>
/// Id of the last streaming parameter.
/// </summary>
private int lastStreamId = 1;
/// <summary>
/// Store the callback for all sent message that expect a return value from the server. All sent message has
/// a unique invocationId that will be sent back from the server.
/// </summary>
private ConcurrentDictionary<long, InvocationDefinition> invocations = new ConcurrentDictionary<long, InvocationDefinition>();
/// <summary>
/// This is where we store the methodname => callback mapping.
/// </summary>
private ConcurrentDictionary<string, Subscription> subscriptions = new ConcurrentDictionary<string, Subscription>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// When we sent out the last message to the server.
/// </summary>
private DateTime lastMessageSentAt;
private DateTime lastMessageReceivedAt;
private DateTime connectionStartedAt;
private RetryContext currentContext;
private DateTime reconnectStartTime = DateTime.MinValue;
private DateTime reconnectAt;
private List<TransportTypes> triedoutTransports = new List<TransportTypes>();
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
private bool pausedInLastFrame;
public HubConnection(Uri hubUri, IProtocol protocol)
: this(hubUri, protocol, new HubOptions())
{
}
public HubConnection(Uri hubUri, IProtocol protocol, HubOptions options)
{
this.Context = new LoggingContext(this);
this.Uri = hubUri;
this.State = ConnectionStates.Initial;
this.Options = options;
this.Protocol = protocol;
this.Protocol.Connection = this;
this.AuthenticationProvider = new DefaultAccessTokenAuthenticator(this);
}
public void StartConnect()
{
if (this.State != ConnectionStates.Initial &&
this.State != ConnectionStates.Redirected &&
this.State != ConnectionStates.Reconnecting)
{
HTTPManager.Logger.Warning("HubConnection", "StartConnect - Expected Initial or Redirected state, got " + this.State.ToString(), this.Context);
return;
}
if (this.State == ConnectionStates.Initial)
{
this.connectionStartedAt = DateTime.Now;
HTTPManager.Heartbeats.Subscribe(this);
}
HTTPManager.Logger.Verbose("HubConnection", $"StartConnect State: {this.State}, connectionStartedAt: {this.connectionStartedAt.ToString(System.Globalization.CultureInfo.InvariantCulture)}", this.Context);
if (this.AuthenticationProvider != null && this.AuthenticationProvider.IsPreAuthRequired)
{
HTTPManager.Logger.Information("HubConnection", "StartConnect - Authenticating", this.Context);
SetState(ConnectionStates.Authenticating, null, this.defaultReconnect);
this.AuthenticationProvider.OnAuthenticationSucceded += OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed += OnAuthenticationFailed;
// Start the authentication process
this.AuthenticationProvider.StartAuthentication();
}
else
StartNegotiation();
}
#if CSHARP_7_OR_LATER
TaskCompletionSource<HubConnection> connectAsyncTaskCompletionSource;
public Task<HubConnection> ConnectAsync()
{
if (this.State != ConnectionStates.Initial && this.State != ConnectionStates.Redirected && this.State != ConnectionStates.Reconnecting)
throw new Exception("HubConnection - ConnectAsync - Expected Initial or Redirected state, got " + this.State.ToString());
if (this.connectAsyncTaskCompletionSource != null)
throw new Exception("Connect process already started!");
this.connectAsyncTaskCompletionSource = new TaskCompletionSource<HubConnection>();
this.OnConnected += OnAsyncConnectedCallback;
this.OnError += OnAsyncConnectFailedCallback;
this.StartConnect();
return connectAsyncTaskCompletionSource.Task;
}
private void OnAsyncConnectedCallback(HubConnection hub)
{
this.OnConnected -= OnAsyncConnectedCallback;
this.OnError -= OnAsyncConnectFailedCallback;
this.connectAsyncTaskCompletionSource.TrySetResult(this);
this.connectAsyncTaskCompletionSource = null;
}
private void OnAsyncConnectFailedCallback(HubConnection hub, string error)
{
this.OnConnected -= OnAsyncConnectedCallback;
this.OnError -= OnAsyncConnectFailedCallback;
this.connectAsyncTaskCompletionSource.TrySetException(new Exception(error));
this.connectAsyncTaskCompletionSource = null;
}
#endif
private void OnAuthenticationSucceded(IAuthenticationProvider provider)
{
HTTPManager.Logger.Verbose("HubConnection", "OnAuthenticationSucceded", this.Context);
this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
StartNegotiation();
}
private void OnAuthenticationFailed(IAuthenticationProvider provider, string reason)
{
HTTPManager.Logger.Error("HubConnection", "OnAuthenticationFailed: " + reason, this.Context);
this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
SetState(ConnectionStates.Closed, reason, this.defaultReconnect);
}
private void StartNegotiation()
{
HTTPManager.Logger.Verbose("HubConnection", "StartNegotiation", this.Context);
if (this.State == ConnectionStates.CloseInitiated)
{
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
return;
}
#if !BESTHTTP_DISABLE_WEBSOCKET
if (this.Options.SkipNegotiation && this.Options.PreferedTransport == TransportTypes.WebSocket)
{
HTTPManager.Logger.Verbose("HubConnection", "Skipping negotiation", this.Context);
ConnectImpl(this.Options.PreferedTransport);
return;
}
#endif
SetState(ConnectionStates.Negotiating, null, this.defaultReconnect);
// https://github.com/dotnet/aspnetcore/blob/master/src/SignalR/docs/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
// Send out a negotiation request. While we could skip it and connect right with the websocket transport
// it might return with additional information that could be useful.
UriBuilder builder = new UriBuilder(this.Uri);
if (builder.Path.EndsWith("/"))
builder.Path += "negotiate";
else
builder.Path += "/negotiate";
string query = builder.Query;
if (string.IsNullOrEmpty(query))
query = "negotiateVersion=1";
else
query = query.Remove(0, 1) + "&negotiateVersion=1";
builder.Query = query;
var request = new HTTPRequest(builder.Uri, HTTPMethods.Post, OnNegotiationRequestFinished);
request.Context.Add("Hub", this.Context);
if (this.AuthenticationProvider != null)
this.AuthenticationProvider.PrepareRequest(request);
request.Send();
}
private void ConnectImpl(TransportTypes transport)
{
HTTPManager.Logger.Verbose("HubConnection", "ConnectImpl - " + transport, this.Context);
switch (transport)
{
#if !BESTHTTP_DISABLE_WEBSOCKET
case TransportTypes.WebSocket:
if (this.NegotiationResult != null && !IsTransportSupported("WebSockets"))
{
SetState(ConnectionStates.Closed, "Couldn't use preferred transport, as the 'WebSockets' transport isn't supported by the server!", this.defaultReconnect);
return;
}
this.Transport = new Transports.WebSocketTransport(this);
this.Transport.OnStateChanged += Transport_OnStateChanged;
break;
#endif
case TransportTypes.LongPolling:
if (this.NegotiationResult != null && !IsTransportSupported("LongPolling"))
{
SetState(ConnectionStates.Closed, "Couldn't use preferred transport, as the 'LongPolling' transport isn't supported by the server!", this.defaultReconnect);
return;
}
this.Transport = new Transports.LongPollingTransport(this);
this.Transport.OnStateChanged += Transport_OnStateChanged;
break;
default:
SetState(ConnectionStates.Closed, "Unsupported transport: " + transport, this.defaultReconnect);
break;
}
try
{
if (this.OnTransportEvent != null)
this.OnTransportEvent(this, this.Transport, TransportEvents.SelectedToConnect);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "ConnectImpl - OnTransportEvent exception in user code!", ex, this.Context);
}
this.Transport.StartConnect();
}
private bool IsTransportSupported(string transportName)
{
// https://github.com/dotnet/aspnetcore/blob/master/src/SignalR/docs/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
// If the negotiation response contains only the url and accessToken, no 'availableTransports' list is sent
if (this.NegotiationResult.SupportedTransports == null)
return true;
for (int i = 0; i < this.NegotiationResult.SupportedTransports.Count; ++i)
if (this.NegotiationResult.SupportedTransports[i].Name.Equals(transportName, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
private void OnNegotiationRequestFinished(HTTPRequest req, HTTPResponse resp)
{
if (this.State == ConnectionStates.Closed)
return;
if (this.State == ConnectionStates.CloseInitiated)
{
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
return;
}
string errorReason = null;
switch (req.State)
{
// The request finished without any problem.
case HTTPRequestStates.Finished:
if (resp.IsSuccess)
{
HTTPManager.Logger.Information("HubConnection", "Negotiation Request Finished Successfully! Response: " + resp.DataAsText, this.Context);
// Parse negotiation
this.NegotiationResult = NegotiationResult.Parse(resp, out errorReason, this);
// Room for improvement: check validity of the negotiation result:
// If url and accessToken is present, the other two must be null.
// https://github.com/dotnet/aspnetcore/blob/master/src/SignalR/docs/specs/TransportProtocols.md#post-endpoint-basenegotiate-request
if (string.IsNullOrEmpty(errorReason))
{
if (this.NegotiationResult.Url != null)
{
this.SetState(ConnectionStates.Redirected, null, this.defaultReconnect);
if (++this.RedirectCount >= this.Options.MaxRedirects)
errorReason = string.Format("MaxRedirects ({0:N0}) reached!", this.Options.MaxRedirects);
else
{
var oldUri = this.Uri;
this.Uri = this.NegotiationResult.Url;
if (this.OnRedirected != null)
{
try
{
this.OnRedirected(this, oldUri, Uri);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnNegotiationRequestFinished - OnRedirected", ex, this.Context);
}
}
StartConnect();
}
}
else
ConnectImpl(this.Options.PreferedTransport);
}
}
else // Internal server error?
errorReason = string.Format("Negotiation Request Finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2}",
resp.StatusCode,
resp.Message,
resp.DataAsText);
break;
// The request finished with an unexpected error. The request's Exception property may contain more info about the error.
case HTTPRequestStates.Error:
errorReason = "Negotiation Request Finished with Error! " + (req.Exception != null ? (req.Exception.Message + "\n" + req.Exception.StackTrace) : "No Exception");
break;
// The request aborted, initiated by the user.
case HTTPRequestStates.Aborted:
errorReason = "Negotiation Request Aborted!";
break;
// Connecting to the server is timed out.
case HTTPRequestStates.ConnectionTimedOut:
errorReason = "Negotiation Request - Connection Timed Out!";
break;
// The request didn't finished in the given time.
case HTTPRequestStates.TimedOut:
errorReason = "Negotiation Request - Processing the request Timed Out!";
break;
}
if (errorReason != null)
{
this.NegotiationResult = new NegotiationResult();
this.NegotiationResult.NegotiationResponse = resp;
SetState(ConnectionStates.Closed, errorReason, this.defaultReconnect);
}
}
public void StartClose()
{
HTTPManager.Logger.Verbose("HubConnection", "StartClose", this.Context);
this.defaultReconnect = false;
switch(this.State)
{
case ConnectionStates.Initial:
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
break;
case ConnectionStates.Authenticating:
this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
this.AuthenticationProvider.Cancel();
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
break;
case ConnectionStates.Reconnecting:
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
break;
case ConnectionStates.CloseInitiated:
case ConnectionStates.Closed:
// Already initiated/closed
break;
default:
if (HTTPManager.IsQuitting)
{
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
}
else
{
SetState(ConnectionStates.CloseInitiated, null, this.defaultReconnect);
if (this.Transport != null)
this.Transport.StartClose();
}
break;
}
}
#if CSHARP_7_OR_LATER
TaskCompletionSource<HubConnection> closeAsyncTaskCompletionSource;
public Task<HubConnection> CloseAsync()
{
if (this.closeAsyncTaskCompletionSource != null)
throw new Exception("CloseAsync already called!");
this.closeAsyncTaskCompletionSource = new TaskCompletionSource<HubConnection>();
this.OnClosed += OnClosedAsyncCallback;
this.OnError += OnClosedAsyncErrorCallback;
// Avoid race condition by caching task prior to StartClose,
// which asynchronously calls OnClosedAsyncCallback, which nulls
// this.closeAsyncTaskCompletionSource immediately before we have
// a chance to read from it.
var task = this.closeAsyncTaskCompletionSource.Task;
this.StartClose();
return task;
}
void OnClosedAsyncCallback(HubConnection hub)
{
this.OnClosed -= OnClosedAsyncCallback;
this.OnError -= OnClosedAsyncErrorCallback;
this.closeAsyncTaskCompletionSource.TrySetResult(this);
this.closeAsyncTaskCompletionSource = null;
}
void OnClosedAsyncErrorCallback(HubConnection hub, string error)
{
this.OnClosed -= OnClosedAsyncCallback;
this.OnError -= OnClosedAsyncErrorCallback;
this.closeAsyncTaskCompletionSource.TrySetException(new Exception(error));
this.closeAsyncTaskCompletionSource = null;
}
#endif
public IFuture<TResult> Invoke<TResult>(string target, params object[] args)
{
Future<TResult> future = new Future<TResult>();
long id = InvokeImp(target,
args,
(message) =>
{
bool isSuccess = string.IsNullOrEmpty(message.error);
if (isSuccess)
future.Assign((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result));
else
future.Fail(new Exception(message.error));
},
typeof(TResult));
if (id < 0)
future.Fail(new Exception("Not in Connected state! Current state: " + this.State));
return future;
}
#if CSHARP_7_OR_LATER
public Task<TResult> InvokeAsync<TResult>(string target, params object[] args)
{
return InvokeAsync<TResult>(target, default(CancellationToken), args);
}
public Task<TResult> InvokeAsync<TResult>(string target, CancellationToken cancellationToken = default, params object[] args)
{
TaskCompletionSource<TResult> tcs = new TaskCompletionSource<TResult>();
long id = InvokeImp(target,
args,
(message) =>
{
if (cancellationToken.IsCancellationRequested)
{
tcs.TrySetCanceled(cancellationToken);
return;
}
bool isSuccess = string.IsNullOrEmpty(message.error);
if (isSuccess)
tcs.TrySetResult((TResult)this.Protocol.ConvertTo(typeof(TResult), message.result));
else
tcs.TrySetException(new Exception(message.error));
},
typeof(TResult));
if (id < 0)
tcs.TrySetException(new Exception("Not in Connected state! Current state: " + this.State));
else
cancellationToken.Register(() => tcs.TrySetCanceled());
return tcs.Task;
}
#endif
public IFuture<object> Send(string target, params object[] args)
{
Future<object> future = new Future<object>();
long id = InvokeImp(target,
args,
(message) =>
{
bool isSuccess = string.IsNullOrEmpty(message.error);
if (isSuccess)
future.Assign(message.item);
else
future.Fail(new Exception(message.error));
},
typeof(object));
if (id < 0)
future.Fail(new Exception("Not in Connected state! Current state: " + this.State));
return future;
}
#if CSHARP_7_OR_LATER
public Task<object> SendAsync(string target, params object[] args)
{
return SendAsync(target, default(CancellationToken), args);
}
public Task<object> SendAsync(string target, CancellationToken cancellationToken = default, params object[] args)
{
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
long id = InvokeImp(target,
args,
(message) =>
{
if (cancellationToken.IsCancellationRequested)
{
tcs.TrySetCanceled(cancellationToken);
return;
}
bool isSuccess = string.IsNullOrEmpty(message.error);
if (isSuccess)
tcs.TrySetResult(message.item);
else
tcs.TrySetException(new Exception(message.error));
},
typeof(object));
if (id < 0)
tcs.TrySetException(new Exception("Not in Connected state! Current state: " + this.State));
else
cancellationToken.Register(() => tcs.TrySetCanceled());
return tcs.Task;
}
#endif
private long InvokeImp(string target, object[] args, Action<Message> callback, Type itemType, bool isStreamingInvocation = false)
{
if (this.State != ConnectionStates.Connected)
return -1;
bool blockingInvocation = callback == null;
long invocationId = blockingInvocation ? 0 : System.Threading.Interlocked.Increment(ref this.lastInvocationId);
var message = new Message
{
type = isStreamingInvocation ? MessageTypes.StreamInvocation : MessageTypes.Invocation,
invocationId = blockingInvocation ? null : invocationId.ToString(),
target = target,
arguments = args,
nonblocking = callback == null,
};
SendMessage(message);
if (!blockingInvocation)
if (!this.invocations.TryAdd(invocationId, new InvocationDefinition { callback = callback, returnType = itemType }))
HTTPManager.Logger.Warning("HubConnection", "InvokeImp - invocations already contains id: " + invocationId, this.Context);
return invocationId;
}
internal void SendMessage(Message message)
{
if (HTTPManager.Logger.Level == Logger.Loglevels.All)
HTTPManager.Logger.Verbose("HubConnection", "SendMessage: " + message.ToString(), this.Context);
try
{
using (new WriteLock(this.rwLock))
{
var encoded = this.Protocol.EncodeMessage(message);
if (encoded.Data != null)
{
this.lastMessageSentAt = DateTime.Now;
this.Transport.Send(encoded);
}
}
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "SendMessage", ex, this.Context);
}
}
public DownStreamItemController<TDown> GetDownStreamController<TDown>(string target, params object[] args)
{
long invocationId = System.Threading.Interlocked.Increment(ref this.lastInvocationId);
var future = new Future<TDown>();
future.BeginProcess();
var controller = new DownStreamItemController<TDown>(this, invocationId, future);
Action<Message> callback = (Message msg) =>
{
switch (msg.type)
{
// StreamItem message contains only one item.
case MessageTypes.StreamItem:
{
if (controller.IsCanceled)
break;
TDown item = (TDown)this.Protocol.ConvertTo(typeof(TDown), msg.item);
future.AssignItem(item);
break;
}
case MessageTypes.Completion:
{
bool isSuccess = string.IsNullOrEmpty(msg.error);
if (isSuccess)
{
// While completion message must not contain any result, this should be future-proof
if (!controller.IsCanceled && msg.result != null)
{
TDown result = (TDown)this.Protocol.ConvertTo(typeof(TDown), msg.result);
future.AssignItem(result);
}
future.Finish();
}
else
future.Fail(new Exception(msg.error));
break;
}
}
};
var message = new Message
{
type = MessageTypes.StreamInvocation,
invocationId = invocationId.ToString(),
target = target,
arguments = args,
nonblocking = false,
};
SendMessage(message);
if (callback != null)
if (!this.invocations.TryAdd(invocationId, new InvocationDefinition { callback = callback, returnType = typeof(TDown) }))
HTTPManager.Logger.Warning("HubConnection", "GetDownStreamController - invocations already contains id: " + invocationId, this.Context);
return controller;
}
public UpStreamItemController<TResult> GetUpStreamController<TResult>(string target, int paramCount, bool downStream, object[] args)
{
Future<TResult> future = new Future<TResult>();
future.BeginProcess();
long invocationId = System.Threading.Interlocked.Increment(ref this.lastInvocationId);
string[] streamIds = new string[paramCount];
for (int i = 0; i < paramCount; i++)
streamIds[i] = System.Threading.Interlocked.Increment(ref this.lastStreamId).ToString();
var controller = new UpStreamItemController<TResult>(this, invocationId, streamIds, future);
Action<Message> callback = (Message msg) => {
switch (msg.type)
{
// StreamItem message contains only one item.
case MessageTypes.StreamItem:
{
if (controller.IsCanceled)
break;
TResult item = (TResult)this.Protocol.ConvertTo(typeof(TResult), msg.item);
future.AssignItem(item);
break;
}
case MessageTypes.Completion:
{
bool isSuccess = string.IsNullOrEmpty(msg.error);
if (isSuccess)
{
// While completion message must not contain any result, this should be future-proof
if (!controller.IsCanceled && msg.result != null)
{
TResult result = (TResult)this.Protocol.ConvertTo(typeof(TResult), msg.result);
future.AssignItem(result);
}
future.Finish();
}
else
{
var ex = new Exception(msg.error);
future.Fail(ex);
}
break;
}
}
};
var messageToSend = new Message
{
type = downStream ? MessageTypes.StreamInvocation : MessageTypes.Invocation,
invocationId = invocationId.ToString(),
target = target,
arguments = args,
streamIds = streamIds,
nonblocking = false,
};
SendMessage(messageToSend);
if (!this.invocations.TryAdd(invocationId, new InvocationDefinition { callback = callback, returnType = typeof(TResult) }))
HTTPManager.Logger.Warning("HubConnection", "GetUpStreamController - invocations already contains id: " + invocationId, this.Context);
return controller;
}
public void On(string methodName, Action callback)
{
On(methodName, null, (args) => callback());
}
public void On<T1>(string methodName, Action<T1> callback)
{
On(methodName, new Type[] { typeof(T1) }, (args) => callback((T1)args[0]));
}
public void On<T1, T2>(string methodName, Action<T1, T2> callback)
{
On(methodName,
new Type[] { typeof(T1), typeof(T2) },
(args) => callback((T1)args[0], (T2)args[1]));
}
public void On<T1, T2, T3>(string methodName, Action<T1, T2, T3> callback)
{
On(methodName,
new Type[] { typeof(T1), typeof(T2), typeof(T3) },
(args) => callback((T1)args[0], (T2)args[1], (T3)args[2]));
}
public void On<T1, T2, T3, T4>(string methodName, Action<T1, T2, T3, T4> callback)
{
On(methodName,
new Type[] { typeof(T1), typeof(T2), typeof(T3), typeof(T4) },
(args) => callback((T1)args[0], (T2)args[1], (T3)args[2], (T4)args[3]));
}
private void On(string methodName, Type[] paramTypes, Action<object[]> callback)
{
if (this.State >= ConnectionStates.CloseInitiated)
throw new Exception("Hub connection already closing or closed!");
this.subscriptions.GetOrAdd(methodName, _ => new Subscription())
.Add(paramTypes, callback);
}
/// <summary>
/// Remove all event handlers for <paramref name="methodName"/> that subscribed with an On call.
/// </summary>
public void Remove(string methodName)
{
if (this.State >= ConnectionStates.CloseInitiated)
throw new Exception("Hub connection already closing or closed!");
Subscription _;
this.subscriptions.TryRemove(methodName, out _);
}
internal Subscription GetSubscription(string methodName)
{
Subscription subscribtion = null;
this.subscriptions.TryGetValue(methodName, out subscribtion);
return subscribtion;
}
internal Type GetItemType(long invocationId)
{
InvocationDefinition def;
this.invocations.TryGetValue(invocationId, out def);
return def.returnType;
}
List<Message> delayedMessages;
internal void OnMessages(List<Message> messages)
{
this.lastMessageReceivedAt = DateTime.Now;
if (pausedInLastFrame)
{
if (this.delayedMessages == null)
this.delayedMessages = new List<Message>(messages.Count);
foreach(var msg in messages)
delayedMessages.Add(msg);
messages.Clear();
}
for (int messageIdx = 0; messageIdx < messages.Count; ++messageIdx)
{
var message = messages[messageIdx];
if (this.OnMessage != null)
{
try
{
if (!this.OnMessage(this, message))
continue;
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnMessage user code!", ex, this.Context);
}
}
switch (message.type)
{
case MessageTypes.Invocation:
{
Subscription subscribtion = null;
if (this.subscriptions.TryGetValue(message.target, out subscribtion))
{
for (int i = 0; i < subscribtion.callbacks.Count; ++i)
{
var callbackDesc = subscribtion.callbacks[i];
object[] realArgs = null;
try
{
realArgs = this.Protocol.GetRealArguments(callbackDesc.ParamTypes, message.arguments);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnMessages - Invocation - GetRealArguments", ex, this.Context);
}
try
{
callbackDesc.Callback.Invoke(realArgs);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnMessages - Invocation - Invoke", ex, this.Context);
}
}
}
break;
}
case MessageTypes.StreamItem:
{
long invocationId;
if (long.TryParse(message.invocationId, out invocationId))
{
InvocationDefinition def;
if (this.invocations.TryGetValue(invocationId, out def) && def.callback != null)
{
try
{
def.callback(message);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnMessages - StreamItem - callback", ex, this.Context);
}
}
}
break;
}
case MessageTypes.Completion:
{
long invocationId;
if (long.TryParse(message.invocationId, out invocationId))
{
InvocationDefinition def;
if (this.invocations.TryRemove(invocationId, out def) && def.callback != null)
{
try
{
def.callback(message);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnMessages - Completion - callback", ex, this.Context);
}
}
}
break;
}
case MessageTypes.Ping:
// Send back an answer
SendMessage(new Message() { type = MessageTypes.Ping });
break;
case MessageTypes.Close:
SetState(ConnectionStates.Closed, message.error, message.allowReconnect);
if (this.Transport != null)
this.Transport.StartClose();
return;
}
}
}
private void Transport_OnStateChanged(TransportStates oldState, TransportStates newState)
{
HTTPManager.Logger.Verbose("HubConnection", string.Format("Transport_OnStateChanged - oldState: {0} newState: {1}", oldState.ToString(), newState.ToString()), this.Context);
if (this.State == ConnectionStates.Closed)
{
HTTPManager.Logger.Verbose("HubConnection", "Transport_OnStateChanged - already closed!", this.Context);
return;
}
switch (newState)
{
case TransportStates.Connected:
try
{
if (this.OnTransportEvent != null)
this.OnTransportEvent(this, this.Transport, TransportEvents.Connected);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context);
}
SetState(ConnectionStates.Connected, null, this.defaultReconnect);
break;
case TransportStates.Failed:
if (this.State == ConnectionStates.Negotiating && !HTTPManager.IsQuitting)
{
try
{
if (this.OnTransportEvent != null)
this.OnTransportEvent(this, this.Transport, TransportEvents.FailedToConnect);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context);
}
this.triedoutTransports.Add(this.Transport.TransportType);
var nextTransport = GetNextTransportToTry();
if (nextTransport == null)
{
var reason = this.Transport.ErrorReason;
this.Transport = null;
SetState(ConnectionStates.Closed, reason, this.defaultReconnect);
}
else
ConnectImpl(nextTransport.Value);
}
else
{
try
{
if (this.OnTransportEvent != null)
this.OnTransportEvent(this, this.Transport, TransportEvents.ClosedWithError);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context);
}
var reason = this.Transport.ErrorReason;
this.Transport = null;
SetState(ConnectionStates.Closed, HTTPManager.IsQuitting ? null : reason, this.defaultReconnect);
}
break;
case TransportStates.Closed:
{
try
{
if (this.OnTransportEvent != null)
this.OnTransportEvent(this, this.Transport, TransportEvents.Closed);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnTransportEvent user code!", ex, this.Context);
}
// Check wheter we have any delayed message and a Close message among them. If there's one, delay the SetState(Close) too.
if (this.delayedMessages == null || this.delayedMessages.FindLast(dm => dm.type == MessageTypes.Close).type != MessageTypes.Close)
SetState(ConnectionStates.Closed, null, this.defaultReconnect);
}
break;
}
}
private TransportTypes? GetNextTransportToTry()
{
foreach (TransportTypes val in Enum.GetValues(typeof(TransportTypes)))
if (!this.triedoutTransports.Contains(val) && IsTransportSupported(val.ToString()))
return val;
return null;
}
bool defaultReconnect = true;
private void SetState(ConnectionStates state, string errorReason, bool allowReconnect)
{
HTTPManager.Logger.Information("HubConnection", string.Format("SetState - from State: '{0}' to State: '{1}', errorReason: '{2}', allowReconnect: {3}, isQuitting: {4}", this.State, state, errorReason, allowReconnect, HTTPManager.IsQuitting), this.Context);
if (this.State == state)
return;
var previousState = this.State;
this.State = state;
switch (state)
{
case ConnectionStates.Initial:
case ConnectionStates.Authenticating:
case ConnectionStates.Negotiating:
case ConnectionStates.CloseInitiated:
break;
case ConnectionStates.Reconnecting:
break;
case ConnectionStates.Connected:
// If reconnectStartTime isn't its default value we reconnected
if (this.reconnectStartTime != DateTime.MinValue)
{
try
{
if (this.OnReconnected != null)
this.OnReconnected(this);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "OnReconnected", ex, this.Context);
}
}
else
{
try
{
if (this.OnConnected != null)
this.OnConnected(this);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnConnected user code!", ex, this.Context);
}
}
this.lastMessageSentAt = DateTime.Now;
this.lastMessageReceivedAt = DateTime.Now;
// Clean up reconnect related fields
this.currentContext = new RetryContext();
this.reconnectStartTime = DateTime.MinValue;
this.reconnectAt = DateTime.MinValue;
HTTPUpdateDelegator.OnApplicationForegroundStateChanged -= this.OnApplicationForegroundStateChanged;
HTTPUpdateDelegator.OnApplicationForegroundStateChanged += this.OnApplicationForegroundStateChanged;
break;
case ConnectionStates.Closed:
// Go through all invocations and cancel them.
var error = new Message();
error.type = MessageTypes.Close;
error.error = errorReason;
foreach (var kvp in this.invocations)
{
try
{
kvp.Value.callback(error);
}
catch
{ }
}
this.invocations.Clear();
// No errorReason? It's an expected closure.
if (errorReason == null && (!allowReconnect || HTTPManager.IsQuitting))
{
if (this.OnClosed != null)
{
try
{
this.OnClosed(this);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnClosed user code!", ex, this.Context);
}
}
HTTPManager.Logger.Information("HubConnection", "Cleaning up", this.Context);
this.delayedMessages?.Clear();
HTTPManager.Heartbeats.Unsubscribe(this);
this.rwLock?.Dispose();
this.rwLock = null;
HTTPUpdateDelegator.OnApplicationForegroundStateChanged -= this.OnApplicationForegroundStateChanged;
}
else
{
// If possible, try to reconnect
if (allowReconnect && this.ReconnectPolicy != null && (previousState == ConnectionStates.Connected || this.reconnectStartTime != DateTime.MinValue))
{
// It's the first attempt after a successful connection
if (this.reconnectStartTime == DateTime.MinValue)
{
this.connectionStartedAt = this.reconnectStartTime = DateTime.Now;
try
{
if (this.OnReconnecting != null)
this.OnReconnecting(this, errorReason);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "SetState - ConnectionStates.Reconnecting", ex, this.Context);
}
}
RetryContext context = new RetryContext
{
ElapsedTime = DateTime.Now - this.reconnectStartTime,
PreviousRetryCount = this.currentContext.PreviousRetryCount,
RetryReason = errorReason
};
TimeSpan? nextAttempt = null;
try
{
nextAttempt = this.ReconnectPolicy.GetNextRetryDelay(context);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "ReconnectPolicy.GetNextRetryDelay", ex, this.Context);
}
// No more reconnect attempt, we are closing
if (nextAttempt == null)
{
HTTPManager.Logger.Warning("HubConnection", "No more reconnect attempt!", this.Context);
// Clean up everything
this.currentContext = new RetryContext();
this.reconnectStartTime = DateTime.MinValue;
this.reconnectAt = DateTime.MinValue;
}
else
{
HTTPManager.Logger.Information("HubConnection", "Next reconnect attempt after " + nextAttempt.Value.ToString(), this.Context);
this.currentContext = context;
this.currentContext.PreviousRetryCount += 1;
this.reconnectAt = DateTime.Now + nextAttempt.Value;
this.SetState(ConnectionStates.Reconnecting, null, this.defaultReconnect);
return;
}
}
if (this.OnError != null)
{
try
{
this.OnError(this, errorReason);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in OnError user code!", ex, this.Context);
}
}
}
break;
}
}
private void OnApplicationForegroundStateChanged(bool isPaused)
{
pausedInLastFrame = !isPaused;
HTTPManager.Logger.Information("HubConnection", $"OnApplicationForegroundStateChanged isPaused: {isPaused} pausedInLastFrame: {pausedInLastFrame}", this.Context);
}
void BestHTTP.Extensions.IHeartbeat.OnHeartbeatUpdate(TimeSpan dif)
{
switch (this.State)
{
case ConnectionStates.Negotiating:
case ConnectionStates.Authenticating:
case ConnectionStates.Redirected:
if (DateTime.Now >= this.connectionStartedAt + this.Options.ConnectTimeout)
{
if (this.AuthenticationProvider != null)
{
this.AuthenticationProvider.OnAuthenticationSucceded -= OnAuthenticationSucceded;
this.AuthenticationProvider.OnAuthenticationFailed -= OnAuthenticationFailed;
try
{
this.AuthenticationProvider.Cancel();
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("HubConnection", "Exception in AuthenticationProvider.Cancel !", ex, this.Context);
}
}
if (this.Transport != null)
{
this.Transport.OnStateChanged -= Transport_OnStateChanged;
this.Transport.StartClose();
}
SetState(ConnectionStates.Closed, string.Format("Couldn't connect in the given time({0})!", this.Options.ConnectTimeout), this.defaultReconnect);
}
break;
case ConnectionStates.Connected:
if (this.delayedMessages?.Count > 0)
{
pausedInLastFrame = false;
try
{
// if there's any Close message clear any other one.
int idx = this.delayedMessages.FindLastIndex(dm => dm.type == MessageTypes.Close);
if (idx > 0)
this.delayedMessages.RemoveRange(0, idx);
OnMessages(this.delayedMessages);
}
finally
{
this.delayedMessages.Clear();
}
}
// Still connected? Check pinging.
if (this.State == ConnectionStates.Connected)
{
if (this.Options.PingInterval != TimeSpan.Zero && DateTime.Now - this.lastMessageReceivedAt >= this.Options.PingTimeoutInterval)
{
// The transport itself can be in a failure state or in a completely valid one, so while we do not want to receive anything from it, we have to try to close it
if (this.Transport != null)
{
this.Transport.OnStateChanged -= Transport_OnStateChanged;
this.Transport.StartClose();
}
SetState(ConnectionStates.Closed,
string.Format("PingInterval set to '{0}' and no message is received since '{1}'. PingTimeoutInterval: '{2}'", this.Options.PingInterval, this.lastMessageReceivedAt, this.Options.PingTimeoutInterval),
this.defaultReconnect);
}
else if (this.Options.PingInterval != TimeSpan.Zero && DateTime.Now - this.lastMessageSentAt >= this.Options.PingInterval)
SendMessage(new Message() { type = MessageTypes.Ping });
}
break;
case ConnectionStates.Reconnecting:
if (this.reconnectAt != DateTime.MinValue && DateTime.Now >= this.reconnectAt)
{
this.delayedMessages?.Clear();
this.connectionStartedAt = DateTime.Now;
this.reconnectAt = DateTime.MinValue;
this.triedoutTransports.Clear();
this.StartConnect();
}
break;
}
}
}
}
#endif