using System;
using System.Collections.Generic;
using UnityEngine;
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
using BestHTTP.Caching;
#endif
using BestHTTP.Extensions;
using BestHTTP.Logger;
using BestHTTP.Statistics;
namespace BestHTTP
{
///
///
///
public static class HTTPManager
{
// Static constructor. Setup default values
static HTTPManager()
{
MaxConnectionPerServer = 4;
KeepAliveDefaultValue = true;
MaxPathLength = 255;
MaxConnectionIdleTime = TimeSpan.FromSeconds(20);
#if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR)
IsCookiesEnabled = true;
#endif
CookieJarSize = 10 * 1024 * 1024;
EnablePrivateBrowsing = false;
ConnectTimeout = TimeSpan.FromSeconds(20);
RequestTimeout = TimeSpan.FromSeconds(60);
// Set the default logger mechanism
logger = new BestHTTP.Logger.DefaultLogger();
#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR)
DefaultCertificateVerifyer = null;
UseAlternateSSLDefaultValue = true;
#endif
}
#region Global Options
///
/// The maximum active TCP connections that the client will maintain to a server. Default value is 4. Minimum value is 1.
///
public static byte MaxConnectionPerServer
{
get{ return maxConnectionPerServer; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException("MaxConnectionPerServer must be greater than 0!");
maxConnectionPerServer = value;
}
}
private static byte maxConnectionPerServer;
///
/// Default value of a HTTP request's IsKeepAlive value. Default value is true. If you make rare request to the server it should be changed to false.
///
public static bool KeepAliveDefaultValue { get; set; }
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
///
/// Set to true, if caching is prohibited.
///
public static bool IsCachingDisabled { get; set; }
#endif
///
/// How many time must be passed to destroy that connection after a connection finished its last request. Its default value is 20 seconds.
///
public static TimeSpan MaxConnectionIdleTime { get; set; }
#if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR)
///
/// Set to false to disable all Cookie. It's default value is true.
///
public static bool IsCookiesEnabled { get; set; }
#endif
///
/// Size of the Cookie Jar in bytes. It's default value is 10485760 (10 MB).
///
public static uint CookieJarSize { get; set; }
///
/// If this property is set to true, then new cookies treated as session cookies and these cookies are not saved to disk. Its default value is false;
///
public static bool EnablePrivateBrowsing { get; set; }
///
/// Global, default value of the HTTPRequest's ConnectTimeout property. If set to TimeSpan.Zero or lower, no connect timeout logic is executed. Default value is 20 seconds.
///
public static TimeSpan ConnectTimeout { get; set; }
///
/// Global, default value of the HTTPRequest's Timeout property. Default value is 60 seconds.
///
public static TimeSpan RequestTimeout { get; set; }
#if !((BESTHTTP_DISABLE_CACHING && BESTHTTP_DISABLE_COOKIES) || (UNITY_WEBGL && !UNITY_EDITOR))
///
/// By default the plugin will save all cache and cookie data under the path returned by Application.persistentDataPath.
/// You can assign a function to this delegate to return a custom root path to define a new path.
/// This delegate will be called on a non Unity thread!
///
public static System.Func RootCacheFolderProvider { get; set; }
#endif
#if !BESTHTTP_DISABLE_PROXY
///
/// The global, default proxy for all HTTPRequests. The HTTPRequest's Proxy still can be changed per-request. Default value is null.
///
public static HTTPProxy Proxy { get; set; }
#endif
///
/// Heartbeat manager to use less threads in the plugin. The heartbeat updates are called from the OnUpdate function.
///
public static HeartbeatManager Heartbeats
{
get
{
if (heartbeats == null)
heartbeats = new HeartbeatManager();
return heartbeats;
}
}
private static HeartbeatManager heartbeats;
///
/// A basic BestHTTP.Logger.ILogger implementation to be able to log intelligently additional informations about the plugin's internal mechanism.
///
public static BestHTTP.Logger.ILogger Logger
{
get
{
// Make sure that it has a valid logger instance.
if (logger == null)
{
logger = new DefaultLogger();
logger.Level = Loglevels.None;
}
return logger;
}
set { logger = value; }
}
private static BestHTTP.Logger.ILogger logger;
#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR)
///
/// The default ICertificateVerifyer implementation that the plugin will use when the request's UseAlternateSSL property is set to true.
///
public static Org.BouncyCastle.Crypto.Tls.ICertificateVerifyer DefaultCertificateVerifyer { get; set; }
///
/// The default IClientCredentialsProvider implementation that the plugin will use when the request's UseAlternateSSL property is set to true.
///
public static Org.BouncyCastle.Crypto.Tls.IClientCredentialsProvider DefaultClientCredentialsProvider { get; set; }
///
/// The default value for the HTTPRequest's UseAlternateSSL property.
///
public static bool UseAlternateSSLDefaultValue { get; set; }
#endif
#if !NETFX_CORE && !UNITY_WP8
public static Func DefaultCertificationValidator { get; set; }
#endif
///
/// Setting this option to true, the processing connection will set the TCP NoDelay option to send out data as soon as it can.
///
public static bool TryToMinimizeTCPLatency = false;
public static int SendBufferSize = 65 * 1024;
public static int ReceiveBufferSize = 65 * 1024;
///
/// On most systems the maximum length of a path is around 255 character. If a cache entity's path is longer than this value it doesn't get cached. There no platform independent API to query the exact value on the current system, but it's
/// exposed here and can be overridden. It's default value is 255.
///
internal static int MaxPathLength { get; set; }
#endregion
#region Manager variables
///
/// All connection has a reference in this Dictionary until it's removed completely.
///
private static Dictionary> Connections = new Dictionary>();
///
/// Active connections. These connections all has a request to process.
///
private static List ActiveConnections = new List();
///
/// Free connections. They can be removed completely after a specified time.
///
private static List FreeConnections = new List();
///
/// Connections that recycled in the Update loop. If they are not used in the same loop to process a request, they will be transferred to the FreeConnections list.
///
private static List RecycledConnections = new List();
///
/// List of request that have to wait until there is a free connection to the server.
///
private static List RequestQueue = new List();
private static bool IsCallingCallbacks;
internal static System.Object Locker = new System.Object();
internal static bool IsQuitting { get; private set; }
#endregion
#region Public Interface
public static void Setup()
{
HTTPUpdateDelegator.CheckInstance();
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
HTTPCacheService.CheckSetup();
#endif
#if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR)
Cookies.CookieJar.SetupFolder();
#endif
}
public static HTTPRequest SendRequest(string url, OnRequestFinishedDelegate callback)
{
return SendRequest(new HTTPRequest(new Uri(url), HTTPMethods.Get, callback));
}
public static HTTPRequest SendRequest(string url, HTTPMethods methodType, OnRequestFinishedDelegate callback)
{
return SendRequest(new HTTPRequest(new Uri(url), methodType, callback));
}
public static HTTPRequest SendRequest(string url, HTTPMethods methodType, bool isKeepAlive, OnRequestFinishedDelegate callback)
{
return SendRequest(new HTTPRequest(new Uri(url), methodType, isKeepAlive, callback));
}
public static HTTPRequest SendRequest(string url, HTTPMethods methodType, bool isKeepAlive, bool disableCache, OnRequestFinishedDelegate callback)
{
return SendRequest(new HTTPRequest(new Uri(url), methodType, isKeepAlive, disableCache, callback));
}
public static HTTPRequest SendRequest(HTTPRequest request)
{
lock (Locker)
{
Setup();
if (IsCallingCallbacks)
{
request.State = HTTPRequestStates.Queued;
RequestQueue.Add(request);
}
else
SendRequestImpl(request);
return request;
}
}
public static GeneralStatistics GetGeneralStatistics(StatisticsQueryFlags queryFlags)
{
GeneralStatistics stat = new GeneralStatistics();
stat.QueryFlags = queryFlags;
if ((queryFlags & StatisticsQueryFlags.Connections) != 0)
{
int connections = 0;
foreach(var conn in HTTPManager.Connections)
{
if (conn.Value != null)
connections += conn.Value.Count;
}
#if !BESTHTTP_DISABLE_WEBSOCKET && UNITY_WEBGL && !UNITY_EDITOR
connections += WebSocket.WebSocket.WebSockets.Count;
#endif
stat.Connections = connections;
stat.ActiveConnections = ActiveConnections.Count
#if !BESTHTTP_DISABLE_WEBSOCKET && UNITY_WEBGL && !UNITY_EDITOR
+ WebSocket.WebSocket.WebSockets.Count
#endif
;
stat.FreeConnections = FreeConnections.Count;
stat.RecycledConnections = RecycledConnections.Count;
stat.RequestsInQueue = RequestQueue.Count;
}
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
if ((queryFlags & StatisticsQueryFlags.Cache) != 0)
{
stat.CacheEntityCount = HTTPCacheService.GetCacheEntityCount();
stat.CacheSize = HTTPCacheService.GetCacheSize();
}
#endif
#if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR)
if ((queryFlags & StatisticsQueryFlags.Cookies) != 0)
{
List cookies = Cookies.CookieJar.GetAll();
stat.CookieCount = cookies.Count;
uint cookiesSize = 0;
for (int i = 0; i < cookies.Count; ++i)
cookiesSize += cookies[i].GuessSize();
stat.CookieJarSize = cookiesSize;
}
#endif
return stat;
}
#endregion
#region Private Functions
private static void SendRequestImpl(HTTPRequest request)
{
ConnectionBase conn = FindOrCreateFreeConnection(request);
if (conn != null)
{
// found a free connection: put it in the ActiveConnection list(they will be checked periodically in the OnUpdate call)
if (ActiveConnections.Find((c) => c == conn) == null)
ActiveConnections.Add(conn);
FreeConnections.Remove(conn);
request.State = HTTPRequestStates.Processing;
request.Prepare();
// then start process the request
conn.Process(request);
}
else
{
// If no free connection found and creation prohibited, we will put back to the queue
request.State = HTTPRequestStates.Queued;
RequestQueue.Add(request);
}
}
private static string GetKeyForRequest(HTTPRequest request)
{
if (request.CurrentUri.IsFile)
return request.CurrentUri.ToString();
// proxyUri + requestUri
// HTTP and HTTPS needs different connections.
return
#if !BESTHTTP_DISABLE_PROXY
(request.Proxy != null ? new UriBuilder(request.Proxy.Address.Scheme, request.Proxy.Address.Host, request.Proxy.Address.Port).Uri.ToString() : string.Empty) +
#endif
new UriBuilder(request.CurrentUri.Scheme, request.CurrentUri.Host, request.CurrentUri.Port).Uri.ToString();
}
///
/// Factory method to create a concrete connection object.
///
private static ConnectionBase CreateConnection(HTTPRequest request, string serverUrl)
{
if (request.CurrentUri.IsFile && Application.platform != RuntimePlatform.WebGLPlayer)
return new FileConnection(serverUrl);
#if UNITY_WEBGL && !UNITY_EDITOR
return new WebGLConnection(serverUrl);
#else
return new HTTPConnection(serverUrl);
#endif
}
private static ConnectionBase FindOrCreateFreeConnection(HTTPRequest request)
{
ConnectionBase conn = null;
List connections;
string serverUrl = GetKeyForRequest(request);
if (Connections.TryGetValue(serverUrl, out connections))
{
// count active connections
int activeConnections = 0;
for (int i = 0; i < connections.Count; ++i)
if (connections[i].IsActive)
activeConnections++;
if (activeConnections <= MaxConnectionPerServer)
// search for a Free connection
for (int i = 0; i < connections.Count && conn == null; ++i)
{
var tmpConn = connections[i];
if (tmpConn != null &&
tmpConn.IsFree &&
(
#if !BESTHTTP_DISABLE_PROXY
!tmpConn.HasProxy ||
#endif
tmpConn.LastProcessedUri == null ||
tmpConn.LastProcessedUri.Host.Equals(request.CurrentUri.Host, StringComparison.OrdinalIgnoreCase)))
conn = tmpConn;
}
}
else
Connections.Add(serverUrl, connections = new List(MaxConnectionPerServer));
// No free connection found?
if (conn == null)
{
// Max connection reached?
if (connections.Count >= MaxConnectionPerServer)
return null;
// if no, create a new one
connections.Add(conn = CreateConnection(request, serverUrl));
}
return conn;
}
///
/// Will return with true when there at least one request that can be processed from the RequestQueue.
///
private static bool CanProcessFromQueue()
{
for (int i = 0; i < RequestQueue.Count; ++i)
if (FindOrCreateFreeConnection(RequestQueue[i]) != null)
return true;
return false;
}
private static void RecycleConnection(ConnectionBase conn)
{
conn.Recycle(OnConnectionRecylced);
}
private static void OnConnectionRecylced(ConnectionBase conn)
{
lock (RecycledConnections)
{
RecycledConnections.Add(conn);
}
}
#endregion
#region Internal Helper Functions
///
/// Will return the ConnectionBase object that processing the given request.
///
internal static ConnectionBase GetConnectionWith(HTTPRequest request)
{
lock (Locker)
{
for (int i = 0; i < ActiveConnections.Count; ++i)
{
var connection = ActiveConnections[i];
if (connection.CurrentRequest == request)
return connection;
}
return null;
}
}
internal static bool RemoveFromQueue(HTTPRequest request)
{
return RequestQueue.Remove(request);
}
#if !((BESTHTTP_DISABLE_CACHING && BESTHTTP_DISABLE_COOKIES) || (UNITY_WEBGL && !UNITY_EDITOR))
///
/// Will return where the various caches should be saved.
///
internal static string GetRootCacheFolder()
{
try
{
if (RootCacheFolderProvider != null)
return RootCacheFolderProvider();
}
catch(Exception ex)
{
HTTPManager.Logger.Exception("HTTPManager", "GetRootCacheFolder", ex);
}
#if NETFX_CORE
return Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#else
return Application.persistentDataPath;
#endif
}
#endif
#endregion
#region MonoBehaviour Events (Called from HTTPUpdateDelegator)
///
/// Update function that should be called regularly from a Unity event(Update, LateUpdate). Callbacks are dispatched from this function.
///
public static void OnUpdate()
{
// We will try to acquire a lock. If it fails, we will skip this frame without calling any callback.
if (System.Threading.Monitor.TryEnter(Locker))
{
try
{
IsCallingCallbacks = true;
try
{
for (int i = 0; i < ActiveConnections.Count; ++i)
{
ConnectionBase conn = ActiveConnections[i];
switch (conn.State)
{
case HTTPConnectionStates.Processing:
conn.HandleProgressCallback();
if (conn.CurrentRequest.UseStreaming && conn.CurrentRequest.Response != null && conn.CurrentRequest.Response.HasStreamedFragments())
conn.HandleCallback();
try
{
if (((!conn.CurrentRequest.UseStreaming && conn.CurrentRequest.UploadStream == null) || conn.CurrentRequest.EnableTimoutForStreaming) &&
DateTime.UtcNow - conn.StartTime > conn.CurrentRequest.Timeout)
conn.Abort(HTTPConnectionStates.TimedOut);
}
catch (OverflowException)
{
HTTPManager.Logger.Warning("HTTPManager", "TimeSpan overflow");
}
break;
case HTTPConnectionStates.TimedOut:
// The connection is still in TimedOut state, and if we waited enough time, we will dispatch the
// callback and recycle the connection
try
{
if (DateTime.UtcNow - conn.TimedOutStart > TimeSpan.FromMilliseconds(500))
{
HTTPManager.Logger.Information("HTTPManager", "Hard aborting connection because of a long waiting TimedOut state");
conn.CurrentRequest.Response = null;
conn.CurrentRequest.State = HTTPRequestStates.TimedOut;
conn.HandleCallback();
// this will set the connection's CurrentRequest to null
RecycleConnection(conn);
}
}
catch(OverflowException)
{
HTTPManager.Logger.Warning("HTTPManager", "TimeSpan overflow");
}
break;
case HTTPConnectionStates.Redirected:
// If the server redirected us, we need to find or create a connection to the new server and send out the request again.
SendRequest(conn.CurrentRequest);
RecycleConnection(conn);
break;
case HTTPConnectionStates.WaitForRecycle:
// If it's a streamed request, it's finished now
conn.CurrentRequest.FinishStreaming();
// Call the callback
conn.HandleCallback();
// Then recycle the connection
RecycleConnection(conn);
break;
case HTTPConnectionStates.Upgraded:
// The connection upgraded to an other protocol
conn.HandleCallback();
break;
case HTTPConnectionStates.WaitForProtocolShutdown:
var ws = conn.CurrentRequest.Response as IProtocol;
if (ws != null)
ws.HandleEvents();
if (ws == null || ws.IsClosed)
{
conn.HandleCallback();
// After both sending and receiving a Close message, an endpoint considers the WebSocket connection closed and MUST close the underlying TCP connection.
conn.Dispose();
RecycleConnection(conn);
}
break;
case HTTPConnectionStates.AbortRequested:
// Corner case: we aborted a WebSocket connection
{
ws = conn.CurrentRequest.Response as IProtocol;
if (ws != null)
{
ws.HandleEvents();
if (ws.IsClosed)
{
conn.HandleCallback();
conn.Dispose();
RecycleConnection(conn);
}
}
}
break;
case HTTPConnectionStates.Closed:
// If it's a streamed request, it's finished now
conn.CurrentRequest.FinishStreaming();
// Call the callback
conn.HandleCallback();
// It will remove from the ActiveConnections
RecycleConnection(conn);
break;
case HTTPConnectionStates.Free:
RecycleConnection(conn);
break;
}
}
}
finally
{
IsCallingCallbacks = false;
}
// Just try to grab the lock, if we can't have it we can wait another turn because it isn't
// critical to do it right now.
if (System.Threading.Monitor.TryEnter(RecycledConnections))
try
{
if (RecycledConnections.Count > 0)
{
for (int i = 0; i < RecycledConnections.Count; ++i)
{
var connection = RecycledConnections[i];
// If in a callback made a request that acquired this connection, then we will not remove it from the
// active connections.
if (connection.IsFree)
{
ActiveConnections.Remove(connection);
FreeConnections.Add(connection);
}
}
RecycledConnections.Clear();
}
}
finally
{
System.Threading.Monitor.Exit(RecycledConnections);
}
if (FreeConnections.Count > 0)
for (int i = 0; i < FreeConnections.Count; i++)
{
var connection = FreeConnections[i];
if (connection.IsRemovable)
{
// Remove the connection from the connection reference table
List connections = null;
if (Connections.TryGetValue(connection.ServerAddress, out connections))
connections.Remove(connection);
// Dispose the connection
connection.Dispose();
FreeConnections.RemoveAt(i);
i--;
}
}
if (CanProcessFromQueue())
{
// Sort the queue by priority, only if we have to
if (RequestQueue.Find((req) => req.Priority != 0) != null)
RequestQueue.Sort((req1, req2) => req1.Priority - req2.Priority);
// Create an array from the queue and clear it. When we call the SendRequest while still no room for new connections, the same queue will be rebuilt.
var queue = RequestQueue.ToArray();
RequestQueue.Clear();
for (int i = 0; i < queue.Length; ++i)
SendRequest(queue[i]);
}
}
finally
{
System.Threading.Monitor.Exit(Locker);
}
}
if (heartbeats != null)
heartbeats.Update();
}
public static void OnQuit()
{
lock (Locker)
{
IsQuitting = true;
#if !BESTHTTP_DISABLE_CACHING && (!UNITY_WEBGL || UNITY_EDITOR)
Caching.HTTPCacheService.SaveLibrary();
#endif
#if !BESTHTTP_DISABLE_COOKIES && (!UNITY_WEBGL || UNITY_EDITOR)
Cookies.CookieJar.Persist();
#endif
AbortAll(true);
OnUpdate();
}
}
public static void AbortAll(bool allowCallbacks = false)
{
lock (Locker)
{
var queue = RequestQueue.ToArray();
RequestQueue.Clear();
foreach (var req in queue)
{
// Swallow any exceptions, we are quitting anyway.
try
{
if (!allowCallbacks)
req.Callback = null;
req.Abort();
}
catch { }
}
// Close all TCP connections when the application is terminating.
foreach (var kvp in Connections)
{
foreach (var conn in kvp.Value)
{
// Swallow any exceptions, we are quitting anyway.
try
{
if (conn.CurrentRequest != null)
{
if (!allowCallbacks)
conn.CurrentRequest.Callback = null;
conn.CurrentRequest.State = HTTPRequestStates.Aborted;
}
conn.Abort(HTTPConnectionStates.Closed);
conn.Dispose();
}
catch { }
}
kvp.Value.Clear();
}
Connections.Clear();
}
}
#endregion
}
}