1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-09 00:46:19 +00:00

Initial v2 changes

This commit is contained in:
Jan Korf 2018-11-22 12:45:36 +01:00
parent c558f25b6c
commit 5c63941f32
13 changed files with 618 additions and 240 deletions

View File

@ -174,7 +174,7 @@ namespace CryptoExchange.Net.UnitTests
TestImplementation client;
if (withOptions)
{
var options = new ExchangeOptions()
var options = new ClientOptions()
{
ApiCredentials = new ApiCredentials("Test", "Test2"),
LogVerbosity = verbosity
@ -219,7 +219,7 @@ namespace CryptoExchange.Net.UnitTests
factory.Setup(c => c.Create(It.IsAny<string>()))
.Returns(request.Object);
TestImplementation client = credentials ? new TestImplementation(new ExchangeOptions() { ApiCredentials = new ApiCredentials("Test", "Test2") }) : new TestImplementation();
TestImplementation client = credentials ? new TestImplementation(new ClientOptions() { ApiCredentials = new ApiCredentials("Test", "Test2") }) : new TestImplementation();
client.RequestFactory = factory.Object;
return client;
}

View File

@ -6,11 +6,11 @@ using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests
{
public class TestImplementation: ExchangeClient
public class TestImplementation: RestClient
{
public TestImplementation(): base(new ExchangeOptions(), null) { }
public TestImplementation(): base(new ClientOptions(), null) { }
public TestImplementation(ExchangeOptions exchangeOptions) : base(exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
public TestImplementation(ClientOptions exchangeOptions) : base(exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
{
}

View File

@ -0,0 +1,244 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CryptoExchange.Net
{
public abstract class BaseClient
{
protected string baseAddress;
protected Log log;
protected ApiProxy apiProxy;
protected AuthenticationProvider authProvider;
protected static int lastId;
protected static object idLock = new object();
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings()
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc
});
public BaseClient(ExchangeOptions options, AuthenticationProvider authenticationProvider)
{
log = new Log();
authProvider = authenticationProvider;
Configure(options);
}
/// <summary>
/// Configure the client using the provided options
/// </summary>
/// <param name="exchangeOptions">Options</param>
protected virtual void Configure(ExchangeOptions exchangeOptions)
{
log.UpdateWriters(exchangeOptions.LogWriters);
log.Level = exchangeOptions.LogVerbosity;
baseAddress = exchangeOptions.BaseAddress;
apiProxy = exchangeOptions.Proxy;
if (apiProxy != null)
log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}");
}
/// <summary>
/// Set the authentication provider
/// </summary>
/// <param name="authentictationProvider"></param>
protected void SetAuthenticationProvider(AuthenticationProvider authentictationProvider)
{
log.Write(LogVerbosity.Debug, "Setting api credentials");
authProvider = authentictationProvider;
}
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer serializer = null) where T : class
{
if (serializer == null)
serializer = defaultSerializer;
try
{
var obj = JToken.Parse(data);
if (checkObject && log.Level == LogVerbosity.Debug)
{
try
{
if (obj is JObject o)
{
CheckObject(typeof(T), o);
}
else
{
var ary = (JArray)obj;
if (ary.HasValues && ary[0] is JObject jObject)
CheckObject(typeof(T).GetElementType(), jObject);
}
}
catch (Exception e)
{
log.Write(LogVerbosity.Debug, "Failed to check response data: " + e.Message);
}
}
return new CallResult<T>(obj.ToObject<T>(serializer), null);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {data}";
log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {data}";
log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info));
}
catch (Exception ex)
{
var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {data}";
log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info));
}
}
private void CheckObject(Type type, JObject obj)
{
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
// If type has a custom JsonConverter we assume this will handle property mapping
return;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
return;
if (!obj.HasValues && type != typeof(object))
{
log.Write(LogVerbosity.Warning, $"Expected `{type.Name}`, but received object was empty");
return;
}
bool isDif = false;
var properties = new List<string>();
var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
if (ignore != null)
continue;
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName);
}
foreach (var token in obj)
{
var d = properties.SingleOrDefault(p => p == token.Key);
if (d == null)
{
d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower());
if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
{
log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
isDif = true;
continue;
}
}
properties.Remove(d);
var propType = GetProperty(d, props)?.PropertyType;
if (propType == null)
continue;
if (!IsSimple(propType) && propType != typeof(DateTime))
{
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
CheckObject(propType.GetElementType(), (JObject)token.Value[0]);
else if (token.Value is JObject)
CheckObject(propType, (JObject)token.Value);
}
}
foreach (var prop in properties)
{
var propInfo = props.First(p => p.Name == prop ||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
if (optional != null)
continue;
isDif = true;
log.Write(LogVerbosity.Warning, $"Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
}
if (isDif)
log.Write(LogVerbosity.Debug, "Returned data: " + obj);
}
private PropertyInfo GetProperty(string name, PropertyInfo[] props)
{
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
if (attr == null)
{
if (prop.Name.ToLower() == name.ToLower())
return prop;
}
else
{
if (((JsonPropertyAttribute)attr).PropertyName == name)
return prop;
}
}
return null;
}
private bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
protected int NextId()
{
lock (idLock)
{
lastId += 1;
return lastId;
}
}
protected static string FillPathParameter(string endpoint, params string[] values)
{
foreach (var value in values)
{
int index = endpoint.IndexOf("{}", StringComparison.Ordinal);
if (index >= 0)
{
endpoint = endpoint.Remove(index, 2);
endpoint = endpoint.Insert(index, value);
}
}
return endpoint;
}
public virtual void Dispose()
{
authProvider?.Credentials?.Dispose();
log.Write(LogVerbosity.Debug, "Disposing exchange client");
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Security;
@ -30,16 +31,16 @@ namespace CryptoExchange.Net
parameters.Add(key, value);
}
public static string CreateParamString(this Dictionary<string, object> parameters)
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues)
{
var uriString = "?";
var uriString = "";
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
foreach (var arrayEntry in arraysParameters)
{
uriString += $"{string.Join("&", ((object[])arrayEntry.Value).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? WebUtility.UrlEncode(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
}
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={s.Value}"))}";
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? WebUtility.UrlEncode(s.Value.ToString()) : s.Value)}"))}";
uriString = uriString.TrimEnd('&');
return uriString;
}

View File

@ -7,13 +7,15 @@ namespace CryptoExchange.Net.Interfaces
{
public interface IWebsocket: IDisposable
{
void SetEnabledSslProtocols(SslProtocols protocols);
event Action OnClose;
event Action<string> OnMessage;
event Action<Exception> OnError;
event Action OnOpen;
int Id { get; }
bool ShouldReconnect { get; set; }
Func<byte[], string> DataInterpreter { get; set; }
DateTime? DisconnectTime { get; set; }
string Url { get; }
WebSocketState SocketState { get; }
bool IsClosed { get; }
@ -21,6 +23,7 @@ namespace CryptoExchange.Net.Interfaces
bool PingConnection { get; set; }
TimeSpan PingInterval { get; set; }
void SetEnabledSslProtocols(SslProtocols protocols);
Task<bool> Connect();
void Send(string data);
Task Close();

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects
{
public class ByteOrderComparer : IComparer<byte[]>
{
public int Compare(byte[] x, byte[] y)
{
// Shortcuts: If both are null, they are the same.
if (x == null && y == null) return 0;
// If one is null and the other isn't, then the
// one that is null is "lesser".
if (x == null && y != null) return -1;
if (x != null && y == null) return 1;
// Both arrays are non-null. Find the shorter
// of the two lengths.
int bytesToCompare = Math.Min(x.Length, y.Length);
// Compare the bytes.
for (int index = 0; index < bytesToCompare; ++index)
{
// The x and y bytes.
byte xByte = x[index];
byte yByte = y[index];
// Compare result.
int compareResult = Comparer<byte>.Default.Compare(xByte, yByte);
// If not the same, then return the result of the
// comparison of the bytes, as they were the same
// up until now.
if (compareResult != 0) return compareResult;
// They are the same, continue.
}
// The first n bytes are the same. Compare lengths.
// If the lengths are the same, the arrays
// are the same.
if (x.Length == y.Length) return 0;
// Compare lengths.
return x.Length < y.Length ? -1 : 1;
}
}
}

View File

@ -0,0 +1,13 @@
namespace CryptoExchange.Net.Objects
{
public class Constants
{
public const string GetMethod = "GET";
public const string PostMethod = "POST";
public const string DeleteMethod = "DELETE";
public const string PutMethod = "PUT";
public const string JsonContentHeader = "application/json";
public const string FormContentHeader = "application/x-www-form-urlencoded";
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
@ -35,8 +36,11 @@ namespace CryptoExchange.Net.Objects
/// <summary>
/// The log writers
/// </summary>
public List<TextWriter> LogWriters { get; set; } = new List<TextWriter>() {new DebugTextWriter()};
public List<TextWriter> LogWriters { get; set; } = new List<TextWriter>() {new DebugTextWriter()};
}
public class ClientOptions: ExchangeOptions
{
/// <summary>
/// List of ratelimiters to use
/// </summary>
@ -47,4 +51,12 @@ namespace CryptoExchange.Net.Objects
/// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
}
public class SocketClientOptions: ExchangeOptions
{
/// <summary>
/// Time to wait between reconnect attempts
/// </summary>
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(2);
}
}

View File

@ -6,11 +6,9 @@ using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
@ -18,23 +16,18 @@ using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiter;
using CryptoExchange.Net.Requests;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
public abstract class ExchangeClient : IDisposable
public abstract class RestClient: BaseClient
{
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
protected string baseAddress;
protected Log log;
protected ApiProxy apiProxy;
protected RateLimitingBehaviour rateLimitBehaviour;
protected PostParameters postParametersPosition = PostParameters.InBody;
protected RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
protected AuthenticationProvider authProvider;
private List<IRateLimiter> rateLimiters;
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings()
@ -42,10 +35,8 @@ namespace CryptoExchange.Net
DateTimeZoneHandling = DateTimeZoneHandling.Utc
});
protected ExchangeClient(ExchangeOptions exchangeOptions, AuthenticationProvider authenticationProvider)
protected RestClient(ClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider)
{
log = new Log();
authProvider = authenticationProvider;
Configure(exchangeOptions);
}
@ -53,16 +44,8 @@ namespace CryptoExchange.Net
/// Configure the client using the provided options
/// </summary>
/// <param name="exchangeOptions">Options</param>
protected void Configure(ExchangeOptions exchangeOptions)
protected void Configure(ClientOptions exchangeOptions)
{
log.UpdateWriters(exchangeOptions.LogWriters);
log.Level = exchangeOptions.LogVerbosity;
baseAddress = exchangeOptions.BaseAddress;
apiProxy = exchangeOptions.Proxy;
if (apiProxy != null)
log.Write(LogVerbosity.Info, $"Setting api proxy to {exchangeOptions.Proxy.Host}:{exchangeOptions.Proxy.Port}");
rateLimitBehaviour = exchangeOptions.RateLimitingBehaviour;
rateLimiters = new List<IRateLimiter>();
foreach (var rateLimiter in exchangeOptions.RateLimiters)
@ -86,16 +69,6 @@ namespace CryptoExchange.Net
rateLimiters.Clear();
}
/// <summary>
/// Set the authentication provider
/// </summary>
/// <param name="authentictationProvider"></param>
protected void SetAuthenticationProvider(AuthenticationProvider authentictationProvider)
{
log.Write(LogVerbosity.Debug, "Setting api credentials");
authProvider = authentictationProvider;
}
/// <summary>
/// Ping to see if the server is reachable
/// </summary>
@ -130,7 +103,7 @@ namespace CryptoExchange.Net
return new CallResult<long>(0, new CantConnectError() { Message = "Ping failed: " + reply.Status });
}
protected virtual async Task<CallResult<T>> ExecuteRequest<T>(Uri uri, string method = "GET", Dictionary<string, object> parameters = null, bool signed = false) where T : class
protected virtual async Task<CallResult<T>> ExecuteRequest<T>(Uri uri, string method = Constants.GetMethod, Dictionary<string, object> parameters = null, bool signed = false) where T : class
{
log.Write(LogVerbosity.Debug, $"Creating request for " + uri);
if (signed && authProvider == null)
@ -171,7 +144,7 @@ namespace CryptoExchange.Net
paramString = paramString.Trim(',');
}
log.Write(LogVerbosity.Debug, $"Sending {method} {(signed ? "signed" : "")} request to {request.Uri} {(paramString ?? "")}");
log.Write(LogVerbosity.Debug, $"Sending {method}{(signed ? " signed" : "")} request to {request.Uri} {(paramString ?? "")}");
var result = await ExecuteRequest(request).ConfigureAwait(false);
return result.Error != null ? new CallResult<T>(null, result.Error) : Deserialize<T>(result.Data);
}
@ -185,12 +158,12 @@ namespace CryptoExchange.Net
if(authProvider != null)
parameters = authProvider.AddAuthenticationToParameters(uriString, method, parameters, signed);
if((method == "GET" || method == "DELETE" || ((method == "POST" || method == "PUT") && postParametersPosition == PostParameters.InUri)) && parameters?.Any() == true)
uriString += parameters.CreateParamString();
if((method == Constants.GetMethod || method == Constants.DeleteMethod || (postParametersPosition == PostParameters.InUri)) && parameters?.Any() == true)
uriString += "?" + parameters.CreateParamString(true);
var request = RequestFactory.Create(uriString);
request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? "application/json": "application/x-www-form-urlencoded";
request.Accept = "application/json";
request.ContentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
request.Accept = Constants.JsonContentHeader;
request.Method = method;
var headers = new Dictionary<string, string>();
@ -200,7 +173,7 @@ namespace CryptoExchange.Net
foreach (var header in headers)
request.Headers.Add(header.Key, header.Value);
if ((method == "POST" || method == "PUT") && postParametersPosition != PostParameters.InUri)
if ((method == Constants.PostMethod || method == Constants.PutMethod) && postParametersPosition != PostParameters.InUri)
{
if(parameters?.Any() == true)
WriteParamBody(request, parameters);
@ -289,164 +262,5 @@ namespace CryptoExchange.Net
{
return new ServerError(error);
}
protected CallResult<T> Deserialize<T>(string data, bool checkObject = true, JsonSerializer serializer = null) where T : class
{
if (serializer == null)
serializer = defaultSerializer;
try
{
var obj = JToken.Parse(data);
if (checkObject && log.Level == LogVerbosity.Debug)
{
try
{
if (obj is JObject o)
{
CheckObject(typeof(T), o);
}
else
{
var ary = (JArray)obj;
if (ary.HasValues && ary[0] is JObject jObject)
CheckObject(typeof(T).GetElementType(), jObject);
}
}
catch (Exception e)
{
log.Write(LogVerbosity.Debug, "Failed to check response data: " + e.Message);
}
}
return new CallResult<T>(obj.ToObject<T>(serializer), null);
}
catch (JsonReaderException jre)
{
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Received data: {data}";
log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info));
}
catch (JsonSerializationException jse)
{
var info = $"Deserialize JsonSerializationException: {jse.Message}. Received data: {data}";
log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info));
}
catch (Exception ex)
{
var info = $"Deserialize Unknown Exception: {ex.Message}. Received data: {data}";
log.Write(LogVerbosity.Error, info);
return new CallResult<T>(null, new DeserializeError(info));
}
}
private void CheckObject(Type type, JObject obj)
{
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
// If type has a custom JsonConverter we assume this will handle property mapping
return;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
return;
if (!obj.HasValues && type != typeof(object))
{
log.Write(LogVerbosity.Warning, $"Expected `{type.Name}`, but received object was empty");
return;
}
bool isDif = false;
var properties = new List<string>();
var props = type.GetProperties();
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
if (ignore != null)
continue;
properties.Add(attr == null ? prop.Name : ((JsonPropertyAttribute)attr).PropertyName);
}
foreach (var token in obj)
{
var d = properties.SingleOrDefault(p => p == token.Key);
if (d == null)
{
d = properties.SingleOrDefault(p => p.ToLower() == token.Key.ToLower());
if (d == null && !(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
{
log.Write(LogVerbosity.Warning, $"Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
isDif = true;
continue;
}
}
properties.Remove(d);
var propType = GetProperty(d, props)?.PropertyType;
if (propType == null)
continue;
if (!IsSimple(propType) && propType != typeof(DateTime))
{
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
CheckObject(propType.GetElementType(), (JObject)token.Value[0]);
else if (token.Value is JObject)
CheckObject(propType, (JObject)token.Value);
}
}
foreach (var prop in properties)
{
var propInfo = props.First(p => p.Name == prop ||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
if (optional != null)
continue;
isDif = true;
log.Write(LogVerbosity.Warning, $"Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
}
if (isDif)
log.Write(LogVerbosity.Debug, "Returned data: " + obj);
}
private PropertyInfo GetProperty(string name, PropertyInfo[] props)
{
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
if (attr == null)
{
if (prop.Name.ToLower() == name.ToLower())
return prop;
}
else
{
if (((JsonPropertyAttribute)attr).PropertyName == name)
return prop;
}
}
return null;
}
private bool IsSimple(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
// nullable type, check if the nested type is simple.
return IsSimple(type.GetGenericArguments()[0]);
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(decimal);
}
public virtual void Dispose()
{
authProvider?.Credentials?.Dispose();
log.Write(LogVerbosity.Debug, "Disposing exchange client");
}
}
}

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
public abstract class SocketClient: BaseClient
{
#region fields
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
private const SslProtocols protocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
protected List<SocketSubscription> sockets = new List<SocketSubscription>();
protected TimeSpan reconnectInterval;
protected Func<byte[], string> dataInterpreter;
#endregion
protected SocketClient(SocketClientOptions exchangeOptions, AuthenticationProvider authenticationProvider): base(exchangeOptions, authenticationProvider)
{
Configure(exchangeOptions);
}
/// <summary>
/// Configure the client using the provided options
/// </summary>
/// <param name="exchangeOptions">Options</param>
protected void Configure(SocketClientOptions exchangeOptions)
{
reconnectInterval = exchangeOptions.ReconnectInterval;
}
protected void SetDataInterpreter(Func<byte[], string> handler)
{
dataInterpreter = handler;
}
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(log, address);
log.Write(LogVerbosity.Debug, "Created new socket for " + address);
if (apiProxy != null)
socket.SetProxy(apiProxy.Host, apiProxy.Port);
socket.SetEnabledSslProtocols(protocols);
socket.DataInterpreter = dataInterpreter;
socket.OnClose += () =>
{
socket.DisconnectTime = DateTime.UtcNow;
SocketOnClose(socket);
};
socket.OnError += (e) =>
{
log.Write(LogVerbosity.Warning, $"Socket {socket.Id} error: " + e.ToString());
SocketError(socket, e);
};
socket.OnOpen += () =>
{
socket.ShouldReconnect = true;
SocketOpened(socket);
};
return socket;
}
protected abstract void SocketOpened(IWebsocket socket);
protected abstract void SocketClosed(IWebsocket socket);
protected abstract void SocketError(IWebsocket socket, Exception ex);
protected abstract bool SocketReconnect(SocketSubscription socket, TimeSpan disconnectedTime);
protected virtual CallResult<SocketSubscription> ConnectSocket(IWebsocket socket)
{
if (socket.Connect().Result)
{
var subscription = new SocketSubscription(socket);
lock (sockets)
sockets.Add(subscription);
return new CallResult<SocketSubscription>(subscription, null);
}
socket.Dispose();
return new CallResult<SocketSubscription>(null, new CantConnectError());
}
protected virtual void SocketOnClose(IWebsocket socket)
{
if (socket.ShouldReconnect)
{
log.Write(LogVerbosity.Info, $"Socket {socket.Id} Connection lost, going to try to reconnect");
Task.Run(() =>
{
Thread.Sleep(reconnectInterval);
if (!socket.Connect().Result)
{
log.Write(LogVerbosity.Debug, $"Socket {socket.Id} failed to reconnect");
return; // Connect() should result in a SocketClosed event so we end up here again
}
log.Write(LogVerbosity.Info, $"Socket {socket.Id} reconnected after {DateTime.UtcNow - socket.DisconnectTime.Value}");
SocketSubscription subscription;
lock(sockets)
subscription = sockets.Single(s => s.Socket == socket);
SocketReconnected(subscription, DateTime.UtcNow - socket.DisconnectTime.Value);
});
}
else
{
socket.Dispose();
lock (sockets)
{
var subscription = sockets.SingleOrDefault(s => s.Socket == socket);
if(subscription != null)
sockets.Remove(subscription);
}
}
}
protected virtual void Send<T>(IWebsocket socket, T obj, NullValueHandling nullValueHandling = NullValueHandling.Ignore)
{
Send(socket, JsonConvert.SerializeObject(obj, Formatting.None, new JsonSerializerSettings { NullValueHandling = nullValueHandling }));
}
protected virtual void Send(IWebsocket socket, string data)
{
log.Write(LogVerbosity.Debug, $"Socket {socket.Id} sending data: {data}");
socket.Send(data);
}
protected virtual async Task<CallResult<string>> SendAndWait<T>(IWebsocket socket, T obj, Func<JToken, bool> waitingFor, int timeout=5000)
{
return await Task.Run(() =>
{
var data = JsonConvert.SerializeObject(obj);
ManualResetEvent evnt = new ManualResetEvent(false);
string result = null;
var onMessageAction = new Action<string>((msg) =>
{
if (!waitingFor(JToken.Parse(msg)))
return;
log.Write(LogVerbosity.Debug, "Socket received query response: " + msg);
result = msg;
evnt?.Set();
});
socket.OnMessage += onMessageAction;
Send(socket, data);
evnt.WaitOne(timeout);
socket.OnMessage -= onMessageAction;
evnt.Dispose();
evnt = null;
if (result == null)
return new CallResult<string>(null, new ServerError("No response from server"));
return new CallResult<string>(result, null);
}).ConfigureAwait(false);
}
public override void Dispose()
{
lock(sockets)
foreach (var socket in sockets)
socket.Socket.Dispose();
sockets.Clear();
}
}
}

View File

@ -11,22 +11,30 @@ using SuperSocket.ClientEngine;
using SuperSocket.ClientEngine.Proxy;
using WebSocket4Net;
namespace CryptoExchange.Net.Implementation
namespace CryptoExchange.Net.Sockets
{
public class BaseSocket: IWebsocket
{
internal static int lastStreamId;
private static readonly object streamIdLock = new object();
protected WebSocket socket;
protected Log log;
protected object socketLock = new object();
protected DateTime? lostTime = null;
protected readonly List<Action<Exception>> errorhandlers = new List<Action<Exception>>();
protected readonly List<Action> openhandlers = new List<Action>();
protected readonly List<Action> closehandlers = new List<Action>();
protected readonly List<Action<string>> messagehandlers = new List<Action<string>>();
protected readonly List<Action<Exception>> errorHandlers = new List<Action<Exception>>();
protected readonly List<Action> openHandlers = new List<Action>();
protected readonly List<Action> closeHandlers = new List<Action>();
protected readonly List<Action<string>> messageHandlers = new List<Action<string>>();
public int Id { get; }
public DateTime? DisconnectTime { get; set; }
public bool ShouldReconnect { get; set; }
public string Url { get; }
public bool IsClosed => socket.State == WebSocketState.Closed;
public bool IsOpen => socket.State == WebSocketState.Open;
public Func<byte[], string> DataInterpreter { get; set; }
public bool PingConnection
{
@ -56,6 +64,7 @@ namespace CryptoExchange.Net.Implementation
public BaseSocket(Log log, string url, IDictionary<string, string> cookies, IDictionary<string, string> headers)
{
Id = NextStreamId();
this.log = log;
Url = url;
socket = new WebSocket(url, cookies: cookies.ToList(), customHeaderItems: headers.ToList())
@ -63,33 +72,38 @@ namespace CryptoExchange.Net.Implementation
EnableAutoSendPing = true,
AutoSendPingInterval = 10
};
socket.Opened += (o, s) => Handle(openhandlers);
socket.Closed += (o, s) => Handle(closehandlers);
socket.Error += (o, s) => Handle(errorhandlers, s.Exception);
socket.MessageReceived += (o, s) => Handle(messagehandlers, s.Message);
socket.EnableAutoSendPing = true;
socket.AutoSendPingInterval = 10;
socket.Opened += (o, s) => Handle(openHandlers);
socket.Closed += (o, s) => Handle(closeHandlers);
socket.Error += (o, s) => Handle(errorHandlers, s.Exception);
socket.MessageReceived += (o, s) => Handle(messageHandlers, s.Message);
socket.DataReceived += (o, s) => HandleByteData(s.Data);
}
private void HandleByteData(byte[] data)
{
var message = DataInterpreter(data);
Handle(messageHandlers, message);
}
public event Action OnClose
{
add => closehandlers.Add(value);
remove => closehandlers.Remove(value);
add => closeHandlers.Add(value);
remove => closeHandlers.Remove(value);
}
public event Action<string> OnMessage
{
add => messagehandlers.Add(value);
remove => messagehandlers.Remove(value);
add => messageHandlers.Add(value);
remove => messageHandlers.Remove(value);
}
public event Action<Exception> OnError
{
add => errorhandlers.Add(value);
remove => errorhandlers.Remove(value);
add => errorHandlers.Add(value);
remove => errorHandlers.Remove(value);
}
public event Action OnOpen
{
add => openhandlers.Add(value);
remove => openhandlers.Remove(value);
add => openHandlers.Add(value);
remove => openHandlers.Remove(value);
}
protected static void Handle(List<Action> handlers)
@ -112,12 +126,12 @@ namespace CryptoExchange.Net.Implementation
{
if (socket == null || IsClosed)
{
log.Write(LogVerbosity.Debug, "Socket was already closed/disposed");
log.Write(LogVerbosity.Debug, $"Socket {Id} was already closed/disposed");
return;
}
var waitLock = new object();
log.Write(LogVerbosity.Debug, "Closing websocket");
log.Write(LogVerbosity.Debug, $"Socket {Id} closing");
ManualResetEvent evnt = new ManualResetEvent(false);
var handler = new EventHandler((o, a) =>
{
@ -133,7 +147,7 @@ namespace CryptoExchange.Net.Implementation
evnt.Dispose();
evnt = null;
}
log.Write(LogVerbosity.Debug, "Websocket closed");
log.Write(LogVerbosity.Debug, $"Socket {Id} closed");
}
}).ConfigureAwait(false);
}
@ -150,7 +164,7 @@ namespace CryptoExchange.Net.Implementation
bool connected;
lock (socketLock)
{
log.Write(LogVerbosity.Debug, "Connecting websocket");
log.Write(LogVerbosity.Debug, $"Socket {Id} connecting");
var waitLock = new object();
ManualResetEvent evnt = new ManualResetEvent(false);
var handler = new EventHandler((o, a) =>
@ -178,9 +192,9 @@ namespace CryptoExchange.Net.Implementation
}
connected = socket.State == WebSocketState.Open;
if (connected)
log.Write(LogVerbosity.Debug, "Websocket connected");
log.Write(LogVerbosity.Debug, $"Socket {Id} connected");
else
log.Write(LogVerbosity.Debug, "Websocket connection failed, state: " + socket.State);
log.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State);
}
if (socket.State == WebSocketState.Connecting)
@ -208,15 +222,24 @@ namespace CryptoExchange.Net.Implementation
lock (socketLock)
{
if (socket != null)
log.Write(LogVerbosity.Debug, "Disposing websocket");
log.Write(LogVerbosity.Debug, $"Socket {Id} sisposing websocket");
socket?.Dispose();
socket = null;
errorhandlers.Clear();
openhandlers.Clear();
closehandlers.Clear();
messagehandlers.Clear();
errorHandlers.Clear();
openHandlers.Clear();
closeHandlers.Clear();
messageHandlers.Clear();
}
}
private int NextStreamId()
{
lock (streamIdLock)
{
lastStreamId++;
return lastStreamId;
}
}
}

View File

@ -0,0 +1,39 @@
using CryptoExchange.Net.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Sockets
{
public class SocketSubscription
{
public event Action ConnectionLost;
public event Action ConnectionRestored;
public IWebsocket Socket { get; set; }
public object Request { get; set; }
private bool lostTriggered;
public SocketSubscription(IWebsocket socket)
{
Socket = socket;
Socket.OnClose += () =>
{
if (lostTriggered)
return;
lostTriggered = true;
if (Socket.ShouldReconnect)
ConnectionLost?.Invoke();
};
Socket.OnOpen += () =>
{
lostTriggered = false;
if (Socket.DisconnectTime != null)
ConnectionRestored?.Invoke();
};
}
}
}

View File

@ -1,7 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Logging;
namespace CryptoExchange.Net.Implementation
namespace CryptoExchange.Net.Sockets
{
public class WebsocketFactory : IWebsocketFactory
{