563 lines
16 KiB
C#
563 lines
16 KiB
C#
|
|
|
|
using Quobject.EngineIoClientDotNet.ComponentEmitter;
|
|
using Quobject.EngineIoClientDotNet.Modules;
|
|
using Quobject.EngineIoClientDotNet.Thread;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
|
|
namespace Quobject.SocketIoClientDotNet.Client
|
|
{
|
|
public class Manager : Emitter
|
|
{
|
|
public enum ReadyStateEnum
|
|
{
|
|
OPENING,
|
|
OPEN,
|
|
CLOSED
|
|
}
|
|
|
|
public static readonly string EVENT_OPEN = "open";
|
|
public static readonly string EVENT_CLOSE = "close";
|
|
public static readonly string EVENT_PACKET = "packet";
|
|
public static readonly string EVENT_ERROR = "error";
|
|
public static readonly string EVENT_CONNECT_ERROR = "connect_error";
|
|
public static readonly string EVENT_CONNECT_TIMEOUT = "connect_timeout";
|
|
public static readonly string EVENT_RECONNECT = "reconnect";
|
|
public static readonly string EVENT_RECONNECT_ERROR = "reconnect_error";
|
|
public static readonly string EVENT_RECONNECT_FAILED = "reconnect_failed";
|
|
public static readonly string EVENT_RECONNECT_ATTEMPT = "reconnect_attempt";
|
|
public static readonly string EVENT_RECONNECTING = "reconnecting";
|
|
|
|
|
|
/*package*/
|
|
public ReadyStateEnum ReadyState = ReadyStateEnum.CLOSED;
|
|
|
|
private bool _reconnection;
|
|
private bool SkipReconnect;
|
|
private bool Reconnecting;
|
|
private bool Encoding;
|
|
private bool OpenReconnect;
|
|
private int _reconnectionAttempts;
|
|
private long _reconnectionDelay;
|
|
private long _reconnectionDelayMax;
|
|
private long _timeout;
|
|
private int Connected;
|
|
private int Attempts;
|
|
private Uri Uri;
|
|
private List<Parser.Packet> PacketBuffer;
|
|
private Queue<On.IHandle> Subs;
|
|
private Quobject.EngineIoClientDotNet.Client.Socket.Options Opts;
|
|
private bool AutoConnect;
|
|
/*package*/
|
|
|
|
public Quobject.EngineIoClientDotNet.Client.Socket EngineSocket;
|
|
private Parser.Parser.Encoder Encoder;
|
|
private Parser.Parser.Decoder Decoder;
|
|
|
|
/**
|
|
* This ConcurrentDictionary can be accessed from outside of EventThread.
|
|
*/
|
|
private System.Collections.Concurrent.ConcurrentDictionary<string, Socket> Nsps;
|
|
|
|
public Manager() : this(null, null)
|
|
{
|
|
|
|
}
|
|
|
|
public Manager(Uri uri) : this(uri, null)
|
|
{
|
|
|
|
}
|
|
|
|
public Manager(Options opts) : this(null, opts)
|
|
{
|
|
|
|
}
|
|
|
|
public Manager(Uri uri, Options opts)
|
|
{
|
|
if (opts == null)
|
|
{
|
|
opts = new Options();
|
|
}
|
|
if (opts.Path == null)
|
|
{
|
|
opts.Path = "/socket.io";
|
|
}
|
|
this.Opts = opts;
|
|
this.Nsps = new System.Collections.Concurrent.ConcurrentDictionary<string, Socket>();
|
|
this.Subs = new Queue<On.IHandle>();
|
|
this.Reconnection(opts.Reconnection);
|
|
this.ReconnectionAttempts(opts.ReconnectionAttempts != 0 ? opts.ReconnectionAttempts : int.MaxValue);
|
|
this.ReconnectionDelay(opts.ReconnectionDelay != 0 ? opts.ReconnectionDelay : 1000);
|
|
this.ReconnectionDelayMax(opts.ReconnectionDelayMax != 0 ? opts.ReconnectionDelayMax : 5000);
|
|
this.Timeout(opts.Timeout < 0 ? 20000 : opts.Timeout);
|
|
this.ReadyState = ReadyStateEnum.CLOSED;
|
|
this.Uri = uri;
|
|
this.Connected = 0;
|
|
this.Attempts = 0;
|
|
this.Encoding = false;
|
|
this.PacketBuffer = new List<Parser.Packet>();
|
|
this.Encoder = new Parser.Parser.Encoder();
|
|
this.Decoder = new Parser.Parser.Decoder();
|
|
this.AutoConnect = opts.AutoConnect;
|
|
if (AutoConnect)
|
|
{
|
|
Open();
|
|
}
|
|
}
|
|
|
|
private void EmitAll(string eventString, params object[] args)
|
|
{
|
|
Emit(eventString, args);
|
|
foreach (var socket in Nsps.Values)
|
|
{
|
|
socket.Emit(eventString, args);
|
|
}
|
|
}
|
|
|
|
public bool Reconnection()
|
|
{
|
|
return _reconnection;
|
|
}
|
|
|
|
private Manager Reconnection(bool v)
|
|
{
|
|
_reconnection = v;
|
|
return this;
|
|
}
|
|
|
|
|
|
public int ReconnectionAttempts()
|
|
{
|
|
return _reconnectionAttempts;
|
|
}
|
|
|
|
private Manager ReconnectionAttempts(int v)
|
|
{
|
|
_reconnectionAttempts = v;
|
|
return this;
|
|
}
|
|
|
|
public long ReconnectionDelay()
|
|
{
|
|
return _reconnectionDelay;
|
|
}
|
|
|
|
private Manager ReconnectionDelay(long v)
|
|
{
|
|
_reconnectionDelay = v;
|
|
return this;
|
|
}
|
|
|
|
public long ReconnectionDelayMax()
|
|
{
|
|
return _reconnectionDelayMax;
|
|
}
|
|
|
|
private Manager ReconnectionDelayMax(long v)
|
|
{
|
|
_reconnectionDelayMax = v;
|
|
return this;
|
|
}
|
|
|
|
public long Timeout()
|
|
{
|
|
return _timeout;
|
|
}
|
|
|
|
private Manager Timeout(long v)
|
|
{
|
|
_timeout = v;
|
|
return this;
|
|
}
|
|
|
|
private void MaybeReconnectOnOpen()
|
|
{
|
|
if (!this.OpenReconnect && !this.Reconnecting && this._reconnection)
|
|
{
|
|
this.OpenReconnect = true;
|
|
this.Reconnect();
|
|
}
|
|
}
|
|
|
|
public Manager Open()
|
|
{
|
|
return Open(null);
|
|
}
|
|
|
|
private Manager Open(IOpenCallback fn)
|
|
{
|
|
var log = LogManager.GetLogger(Global.CallerName());
|
|
log.Info(string.Format("readyState {0}", ReadyState));
|
|
if (ReadyState == ReadyStateEnum.OPEN)
|
|
{
|
|
return this;
|
|
}
|
|
|
|
log.Info(string.Format("opening {0}", Uri));
|
|
EngineSocket = new Engine(Uri, Opts);
|
|
Quobject.EngineIoClientDotNet.Client.Socket socket = EngineSocket;
|
|
|
|
ReadyState = ReadyStateEnum.OPENING;
|
|
|
|
var openSub = SocketIoClientDotNet.Client.On.Create(socket, Engine.EVENT_OPEN, new ListenerImpl(() =>
|
|
{
|
|
OnOpen();
|
|
if (fn != null)
|
|
{
|
|
fn.Call(null);
|
|
}
|
|
}));
|
|
|
|
var errorSub = Client.On.Create(socket, Engine.EVENT_ERROR, new ListenerImpl((data) =>
|
|
{
|
|
log.Info("connect_error");
|
|
Cleanup();
|
|
ReadyState = ReadyStateEnum.CLOSED;
|
|
EmitAll(EVENT_CONNECT_ERROR, data);
|
|
|
|
if (fn != null)
|
|
{
|
|
var err = new SocketIOException("Connection error", data is Exception ? (Exception) data : null);
|
|
fn.Call(err);
|
|
}
|
|
MaybeReconnectOnOpen();
|
|
}));
|
|
|
|
if (_timeout >= 0)
|
|
{
|
|
var timeout = (int) _timeout;
|
|
log.Info(string.Format("connection attempt will timeout after {0}", timeout));
|
|
var timer = EasyTimer.SetTimeout(() =>
|
|
{
|
|
var log2 = LogManager.GetLogger(Global.CallerName());
|
|
log2.Info("Manager Open start");
|
|
|
|
log2.Info(string.Format("connect attempt timed out after {0}", timeout));
|
|
openSub.Destroy();
|
|
socket.Close();
|
|
socket.Emit(Engine.EVENT_ERROR, new SocketIOException("timeout"));
|
|
EmitAll(EVENT_CONNECT_TIMEOUT, timeout);
|
|
log2.Info("Manager Open finish");
|
|
|
|
}, timeout);
|
|
lock (Subs)
|
|
{
|
|
Subs.Enqueue(new On.ActionHandleImpl(timer.Stop));
|
|
}
|
|
|
|
}
|
|
|
|
lock (Subs)
|
|
{
|
|
Subs.Enqueue(openSub);
|
|
Subs.Enqueue(errorSub);
|
|
}
|
|
EngineSocket.Open();
|
|
|
|
return this;
|
|
}
|
|
|
|
private void OnOpen()
|
|
{
|
|
var log = LogManager.GetLogger(Global.CallerName());
|
|
log.Info("open");
|
|
|
|
Cleanup();
|
|
|
|
ReadyState = ReadyStateEnum.OPEN;
|
|
Emit(EVENT_OPEN);
|
|
|
|
var socket = EngineSocket;
|
|
|
|
var sub = Client.On.Create(socket, Engine.EVENT_DATA, new ListenerImpl((data) =>
|
|
{
|
|
if (data is string)
|
|
{
|
|
OnData((string)data);
|
|
}
|
|
else if (data is byte[])
|
|
{
|
|
Ondata((byte[])data);
|
|
}
|
|
}));
|
|
lock (Subs)
|
|
{
|
|
Subs.Enqueue(sub);
|
|
}
|
|
|
|
sub = Client.On.Create(this.Decoder, Parser.Parser.Decoder.EVENT_DECODED, new ListenerImpl((data) =>
|
|
{
|
|
OnDecoded((Parser.Packet)data);
|
|
}));
|
|
lock (Subs)
|
|
{
|
|
Subs.Enqueue(sub);
|
|
}
|
|
|
|
sub = Client.On.Create(socket, Engine.EVENT_ERROR, new ListenerImpl((data) =>
|
|
{
|
|
OnError((Exception) data);
|
|
}));
|
|
lock (Subs)
|
|
{
|
|
Subs.Enqueue(sub);
|
|
}
|
|
|
|
sub = Client.On.Create(socket, Engine.EVENT_CLOSE, new ListenerImpl((data) =>
|
|
{
|
|
OnClose((string) data);
|
|
}));
|
|
|
|
lock (Subs)
|
|
{
|
|
Subs.Enqueue(sub);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private void OnData(string data)
|
|
{
|
|
this.Decoder.Add(data);
|
|
}
|
|
|
|
private void Ondata(byte[] data)
|
|
{
|
|
this.Decoder.Add(data);
|
|
}
|
|
|
|
private void OnDecoded(Parser.Packet packet)
|
|
{
|
|
this.Emit(EVENT_PACKET, packet);
|
|
}
|
|
|
|
private void OnError(Exception err)
|
|
{
|
|
var log = LogManager.GetLogger(Global.CallerName());
|
|
log.Error("error", err);
|
|
this.EmitAll(EVENT_ERROR, err);
|
|
}
|
|
|
|
public Socket Socket(string nsp)
|
|
{
|
|
if (Nsps.ContainsKey(nsp))
|
|
{
|
|
return Nsps[nsp];
|
|
}
|
|
|
|
var socket = new Socket(this,nsp);
|
|
Nsps.TryAdd(nsp, socket);
|
|
socket.On(Client.Socket.EVENT_CONNECT, new ListenerImpl(() =>
|
|
{
|
|
Connected++;
|
|
}));
|
|
return socket;
|
|
}
|
|
|
|
internal void Destroy(Socket socket)
|
|
{
|
|
--Connected;
|
|
if (Connected == 0)
|
|
{
|
|
Close();
|
|
}
|
|
}
|
|
|
|
|
|
internal void Packet(Parser.Packet packet)
|
|
{
|
|
var log = LogManager.GetLogger(Global.CallerName());
|
|
log.Info(string.Format("writing packet {0}", packet));
|
|
|
|
if (!Encoding)
|
|
{
|
|
Encoding = true;
|
|
Encoder.Encode(packet, new Parser.Parser.Encoder.CallbackImp((data) =>
|
|
{
|
|
var encodedPackets = (object[]) data;
|
|
foreach (var packet1 in encodedPackets)
|
|
{
|
|
if (packet1 is string)
|
|
{
|
|
EngineSocket.Write((string) packet1);
|
|
}
|
|
else if (packet1 is byte[])
|
|
{
|
|
EngineSocket.Write((byte[]) packet1);
|
|
}
|
|
}
|
|
Encoding = false;
|
|
ProcessPacketQueue();
|
|
}));
|
|
}
|
|
else
|
|
{
|
|
PacketBuffer.Add(packet);
|
|
}
|
|
|
|
}
|
|
|
|
private void ProcessPacketQueue()
|
|
{
|
|
if (this.PacketBuffer.Count > 0 && !this.Encoding)
|
|
{
|
|
Parser.Packet pack = this.PacketBuffer[0];
|
|
PacketBuffer.Remove(pack);
|
|
this.Packet(pack);
|
|
}
|
|
}
|
|
|
|
private void Cleanup()
|
|
{
|
|
lock (Subs)
|
|
{
|
|
foreach (var sub in Subs)
|
|
{
|
|
sub.Destroy();
|
|
}
|
|
Subs.Clear();
|
|
}
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
this.SkipReconnect = true;
|
|
this.EngineSocket.Close();
|
|
}
|
|
|
|
|
|
private void OnClose(string reason)
|
|
{
|
|
var log = LogManager.GetLogger(Global.CallerName());
|
|
log.Info("start");
|
|
Cleanup();
|
|
ReadyState = ReadyStateEnum.CLOSED;
|
|
Emit(EVENT_CLOSE, reason);
|
|
if (_reconnection && !SkipReconnect)
|
|
{
|
|
Reconnect();
|
|
}
|
|
}
|
|
|
|
|
|
private void Reconnect()
|
|
{
|
|
var log = LogManager.GetLogger(Global.CallerName());
|
|
|
|
if (Reconnecting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Attempts++;
|
|
|
|
if (Attempts > _reconnectionAttempts)
|
|
{
|
|
log.Info("reconnect failed");
|
|
EmitAll(EVENT_RECONNECT_FAILED);
|
|
Reconnecting = false;
|
|
}
|
|
else
|
|
{
|
|
var delay = Attempts*ReconnectionDelay();
|
|
delay = Math.Min(delay, ReconnectionDelayMax());
|
|
log.Info(string.Format("will wait {0}ms before reconnect attempt", delay));
|
|
|
|
Reconnecting = true;
|
|
var timer = EasyTimer.SetTimeout(() =>
|
|
{
|
|
var log2 = LogManager.GetLogger(Global.CallerName());
|
|
log2.Info("EasyTimer Reconnect start");
|
|
log2.Info(string.Format("attempting reconnect"));
|
|
EmitAll(EVENT_RECONNECT_ATTEMPT, Attempts);
|
|
EmitAll(EVENT_RECONNECTING, Attempts);
|
|
Open(new OpenCallbackImp((err) =>
|
|
{
|
|
if (err != null)
|
|
{
|
|
log.Error("reconnect attempt error", (Exception) err);
|
|
Reconnecting = false;
|
|
Reconnect();
|
|
EmitAll(EVENT_RECONNECT_ERROR, (Exception) err);
|
|
}
|
|
else
|
|
{
|
|
log.Info("reconnect success");
|
|
OnReconnect();
|
|
}
|
|
}));
|
|
log2.Info("EasyTimer Reconnect finish");
|
|
}, (int)delay);
|
|
|
|
lock (Subs){
|
|
Subs.Enqueue(new On.ActionHandleImpl(timer.Stop));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private void OnReconnect()
|
|
{
|
|
int attempts = this.Attempts;
|
|
this.Attempts = 0;
|
|
this.Reconnecting = false;
|
|
this.EmitAll(EVENT_RECONNECT, attempts);
|
|
}
|
|
|
|
|
|
public interface IOpenCallback
|
|
{
|
|
|
|
void Call(Exception err);
|
|
}
|
|
|
|
public class OpenCallbackImp : IOpenCallback
|
|
{
|
|
private Action<object> Fn;
|
|
|
|
public OpenCallbackImp(Action<object> fn)
|
|
{
|
|
Fn = fn;
|
|
}
|
|
|
|
public void Call(Exception err)
|
|
{
|
|
Fn(err);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class Engine : Quobject.EngineIoClientDotNet.Client.Socket
|
|
{
|
|
public Engine(Uri uri, Options opts) : base(uri, opts)
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public class Options : Quobject.EngineIoClientDotNet.Client.Socket.Options
|
|
{
|
|
|
|
public bool Reconnection = true;
|
|
public int ReconnectionAttempts;
|
|
public long ReconnectionDelay;
|
|
public long ReconnectionDelayMax;
|
|
public long Timeout = -1;
|
|
public bool AutoConnect = true;
|
|
public bool DisablePing = false;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|