#if !BESTHTTP_DISABLE_SIGNALR
using System;
using System.Collections.Generic;
using BestHTTP.SignalR.Messages;
using BestHTTP.SignalR.JsonEncoders;
namespace BestHTTP.SignalR.Transports
{
public delegate void OnTransportStateChangedDelegate(TransportBase transport, TransportStates oldState, TransportStates newState);
public abstract class TransportBase
{
private const int MaxRetryCount = 5;
#region Public Properties
///
/// Name of the transport.
///
public string Name { get; protected set; }
///
/// True if the manager has to check the last message received time and reconnect if too much time passes.
///
public abstract bool SupportsKeepAlive { get; }
///
/// Type of the transport. Used mainly by the manager in its BuildUri function.
///
public abstract TransportTypes Type { get; }
///
/// Reference to the manager.
///
public IConnection Connection { get; protected set; }
///
/// The current state of the transport.
///
public TransportStates State
{
get { return _state; }
protected set
{
TransportStates old = _state;
_state = value;
if (OnStateChanged != null)
OnStateChanged(this, old, _state);
}
}
public TransportStates _state;
///
/// Thi event called when the transport's State set to a new value.
///
public event OnTransportStateChangedDelegate OnStateChanged;
#endregion
public TransportBase(string name, Connection connection)
{
this.Name = name;
this.Connection = connection;
this.State = TransportStates.Initial;
}
#region Abstract functions
///
/// Start to connect to the server
///
public abstract void Connect();
///
/// Stop the connection
///
public abstract void Stop();
///
/// The transport specific implementation to send the given json string to the server.
///
protected abstract void SendImpl(string json);
///
/// Called when the Start request finished successfully, or after a reconnect.
/// Manager.TransportOpened(); called from the TransportBase after this call
///
protected abstract void Started();
///
/// Called when the abort request finished successfully.
///
protected abstract void Aborted();
#endregion
///
/// Called after a succesful connect/reconnect. The transport implementations have to call this function.
///
protected void OnConnected()
{
if (this.State != TransportStates.Reconnecting)
{
// Send the Start request
Start();
}
else
{
Connection.TransportReconnected();
Started();
this.State = TransportStates.Started;
}
}
#region Start Request Sending
///
/// Sends out the /start request to the server.
///
protected void Start()
{
HTTPManager.Logger.Information("Transport - " + this.Name, "Sending Start Request");
this.State = TransportStates.Starting;
if (this.Connection.Protocol > ProtocolVersions.Protocol_2_0)
{
var request = new HTTPRequest(Connection.BuildUri(RequestTypes.Start, this), HTTPMethods.Get, true, true, OnStartRequestFinished);
request.Tag = 0;
request.DisableRetry = true;
request.Timeout = Connection.NegotiationResult.ConnectionTimeout + TimeSpan.FromSeconds(10);
Connection.PrepareRequest(request, RequestTypes.Start);
request.Send();
}
else
{
// The transport and the signalr protocol now started
this.State = TransportStates.Started;
Started();
Connection.TransportStarted();
}
}
private void OnStartRequestFinished(HTTPRequest req, HTTPResponse resp)
{
switch (req.State)
{
case HTTPRequestStates.Finished:
if (resp.IsSuccess)
{
HTTPManager.Logger.Information("Transport - " + this.Name, "Start - Returned: " + resp.DataAsText);
string response = Connection.ParseResponse(resp.DataAsText);
if (response != "started")
{
Connection.Error(string.Format("Expected 'started' response, but '{0}' found!", response));
return;
}
// The transport and the signalr protocol now started
this.State = TransportStates.Started;
Started();
Connection.TransportStarted();
return;
}
else
HTTPManager.Logger.Warning("Transport - " + this.Name, string.Format("Start - request finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2} Uri: {3}",
resp.StatusCode,
resp.Message,
resp.DataAsText,
req.CurrentUri));
goto default;
default:
HTTPManager.Logger.Information("Transport - " + this.Name, "Start request state: " + req.State.ToString());
// The request may not reached the server. Try it again.
int retryCount = (int)req.Tag;
if (retryCount++ < MaxRetryCount)
{
req.Tag = retryCount;
req.Send();
}
else
Connection.Error("Failed to send Start request.");
break;
}
}
#endregion
#region Abort Implementation
///
/// Will abort the transport. In SignalR 'Abort'ing is a graceful process, while 'Close'ing is a hard-abortion...
///
public virtual void Abort()
{
if (this.State != TransportStates.Started)
return;
this.State = TransportStates.Closing;
var request = new HTTPRequest(Connection.BuildUri(RequestTypes.Abort, this), HTTPMethods.Get, true, true, OnAbortRequestFinished);
// Retry counter
request.Tag = 0;
request.DisableRetry = true;
Connection.PrepareRequest(request, RequestTypes.Abort);
request.Send();
}
protected void AbortFinished()
{
this.State = TransportStates.Closed;
Connection.TransportAborted();
this.Aborted();
}
private void OnAbortRequestFinished(HTTPRequest req, HTTPResponse resp)
{
switch (req.State)
{
case HTTPRequestStates.Finished:
if (resp.IsSuccess)
{
HTTPManager.Logger.Information("Transport - " + this.Name, "Abort - Returned: " + resp.DataAsText);
if (this.State == TransportStates.Closing)
AbortFinished();
}
else
{
HTTPManager.Logger.Warning("Transport - " + this.Name, string.Format("Abort - Handshake request finished Successfully, but the server sent an error. Status Code: {0}-{1} Message: {2} Uri: {3}",
resp.StatusCode,
resp.Message,
resp.DataAsText,
req.CurrentUri));
// try again
goto default;
}
break;
default:
HTTPManager.Logger.Information("Transport - " + this.Name, "Abort request state: " + req.State.ToString());
// The request may not reached the server. Try it again.
int retryCount = (int)req.Tag;
if (retryCount++ < MaxRetryCount)
{
req.Tag = retryCount;
req.Send();
}
else
Connection.Error("Failed to send Abort request!");
break;
}
}
#endregion
#region Send Implementation
///
/// Sends the given json string to the wire.
///
///
public void Send(string jsonStr)
{
try
{
HTTPManager.Logger.Information("Transport - " + this.Name, "Sending: " + jsonStr);
SendImpl(jsonStr);
}
catch (Exception ex)
{
HTTPManager.Logger.Exception("Transport - " + this.Name, "Send", ex);
}
}
#endregion
#region Helper Functions
///
/// Start the reconnect process
///
public void Reconnect()
{
HTTPManager.Logger.Information("Transport - " + this.Name, "Reconnecting");
Stop();
this.State = TransportStates.Reconnecting;
Connect();
}
///
/// When the json string is successfully parsed will return with an IServerMessage implementation.
///
public static IServerMessage Parse(IJsonEncoder encoder, string json)
{
// Nothing to parse?
if (string.IsNullOrEmpty(json))
{
HTTPManager.Logger.Error("MessageFactory", "Parse - called with empty or null string!");
return null;
}
// We don't have to do further decoding, if it's an empty json object, then it's a KeepAlive message from the server
if (json.Length == 2 && json == "{}")
return new KeepAliveMessage();
IDictionary msg = null;
try
{
// try to decode the json message with the encoder
msg = encoder.DecodeMessage(json);
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("MessageFactory", "Parse - encoder.DecodeMessage", ex);
return null;
}
if (msg == null)
{
HTTPManager.Logger.Error("MessageFactory", "Parse - Json Decode failed for json string: \"" + json + "\"");
return null;
}
// "C" is for message id
IServerMessage result = null;
if (!msg.ContainsKey("C"))
{
// If there are no ErrorMessage in the object, then it was a success
if (!msg.ContainsKey("E"))
result = new ResultMessage();
else
result = new FailureMessage();
}
else
result = new MultiMessage();
try
{
result.Parse(msg);
}
catch
{
HTTPManager.Logger.Error("MessageFactory", "Can't parse msg: " + json);
throw;
}
return result;
}
#endregion
}
}
#endif