@@ -15,51 +15,59 @@ namespace Discord
{
private const int ReceiveChunkSize = 4096;
private const int SendChunkSize = 4096;
private const int ReadyTimeout = 50 00; //Max time in milliseconds between connecting to Discord and receiving a READY event
private const int ReadyTimeout = 2 500; //Max time in milliseconds between connecting to Discord and receiving a READY event
private volatile ClientWebSocket _webSocket;
private volatile CancellationTokenSource _cancel Token;
private volatile CancellationTokenSource _disconnect Token;
private volatile Task _tasks;
private ConcurrentQueue<byte[]> _sendQueue;
private int _heartbeatInterval;
private DateTime _lastHeartbeat;
private AutoResetEvent _connectWaitOnLogin, _connectWaitOnLogin2;
private ManualResetEventSlim _connectWaitOnLogin, _connectWaitOnLogin2;
private bool _isConnected;
public DiscordWebSocket()
{
_connectWaitOnLogin = new ManualResetEventSlim(false);
_connectWaitOnLogin2 = new ManualResetEventSlim(false);
_sendQueue = new ConcurrentQueue<byte[]>();
}
public async Task ConnectAsync(string url, bool autoLogin)
{
await DisconnectAsync();
_connectWaitOnLogin = new AutoResetEvent(false);
_connectWaitOnLogin2 = new AutoResetEvent(false);
_sendQueue = new ConcurrentQueue<byte[]>();
_disconnectToken = new CancellationTokenSource();
var cancelToken = _disconnectToken.Token;
_webSocket = new ClientWebSocket();
_webSocket.Options.KeepAliveInterval = TimeSpan.Zero;
_cancelToken = new CancellationTokenSource();
var cancelToken = _cancelToken.Token;
await _webSocket.ConnectAsync(new Uri(url), cancelToken);
_tasks = Task.WhenAll(
await Task.Factory.StartNew(ReceiveAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default),
await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default)
) .ContinueWith(x =>
await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default))
.ContinueWith(x =>
{
//Do not clean up until both tasks have ended
_heartbeatInterval = 0;
_lastHeartbeat = DateTime.MinValue;
_webSocket.Dispose();
_webSocket = null;
_cancel Token.Dispose();
_cancel Token = null;
_disconnect Token.Dispose();
_disconnect Token = null;
_tasks = null;
//Clear send queue
byte[] ignored;
while (_sendQueue.TryDequeue(out ignored)) { }
if (_isConnected)
{
_isConnected = false;
RaiseDisconnected();
}
}
});
if (autoLogin)
@@ -67,6 +75,11 @@ namespace Discord
}
public void Login()
{
var cancelToken = _disconnectToken.Token;
_connectWaitOnLogin.Reset();
_connectWaitOnLogin2.Reset();
WebSocketCommands.Login msg = new WebSocketCommands.Login();
msg.Payload.Token = Http.Token;
msg.Payload.Properties["$os"] = "";
@@ -74,11 +87,18 @@ namespace Discord
msg.Payload.Properties["$device"] = "Discord.Net";
msg.Payload.Properties["$referrer"] = "";
msg.Payload.Properties["$referring_domain"] = "";
SendMessage(msg, _cancel Token.Token);
SendMessage(msg, _disconnect Token.Token);
if (!_connectWaitOnLogin.WaitOne(ReadyTimeout)) //Pre-Event
throw new Exception("No reply from Discord server");
_connectWaitOnLogin2.WaitOne(); //Post-Event
try
{
if (!_connectWaitOnLogin.Wait(ReadyTimeout, cancelToken)) //Waiting on READY message
throw new Exception("No reply from Discord server");
}
catch (OperationCanceledException)
{
throw new InvalidOperationException("Bad Token");
}
_connectWaitOnLogin2.Wait(cancelToken); //Waiting on READY handler
_isConnected = true;
RaiseConnected();
@@ -87,14 +107,14 @@ namespace Discord
{
if (_tasks != null)
{
_cancel Token.Cancel();
_disconnect Token.Cancel();
await _tasks;
}
}
private async Task ReceiveAsync()
{
var cancelToken = _cancel Token.Token;
var cancelToken = _disconnect Token.Token;
var buffer = new byte[ReceiveChunkSize];
var builder = new StringBuilder();
@@ -105,7 +125,7 @@ namespace Discord
WebSocketReceiveResult result;
do
{
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cancel Token.Token);
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _disconnect Token.Token);
if (result.MessageType == WebSocketMessageType.Close)
{
@@ -126,8 +146,8 @@ namespace Discord
{
var payload = (msg.Payload as JToken).ToObject<WebSocketEvents.Ready>();
_heartbeatInterval = payload.HeartbeatInterval;
SendMessage(new WebSocketCommands.UpdateStatus(), cancelToken );
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken );
QueueMessage(new WebSocketCommands.UpdateStatus() );
QueueMessage(new WebSocketCommands.KeepAlive() );
_connectWaitOnLogin.Set(); //Pre-Event
}
RaiseGotEvent(msg.Type, msg.Payload as JToken);
@@ -143,12 +163,12 @@ namespace Discord
}
}
catch { }
finally { _cancel Token.Cancel(); }
finally { _disconnect Token.Cancel(); }
}
private async Task SendAsync()
{
var cancelToken = _cancel Token.Token;
var cancelToken = _disconnect Token.Token;
try
{
byte[] bytes;
@@ -159,41 +179,46 @@ namespace Discord
DateTime now = DateTime.UtcNow;
if ((now - _lastHeartbeat).TotalMilliseconds > _heartbeatInterval)
{
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken);
await SendMessage(new WebSocketCommands.KeepAlive(), cancelToken);
_lastHeartbeat = now;
}
}
while (_sendQueue.TryDequeue(out bytes))
{
var frameCount = (int)Math.Ceiling((double)bytes.Length / SendChunkSize);
int offset = 0;
for (var i = 0; i < frameCount; i++, offset += SendChunkSize)
{
bool isLast = i == (frameCount - 1);
int count;
if (isLast)
count = bytes.Length - (i * SendChunkSize);
else
count = SendChunkSize;
await _webSocket.SendAsync(new ArraySegment<byte>(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken);
}
}
await SendMessage(bytes, cancelToken);
await Task.Delay(100);
}
}
catch { }
finally { _cancel Token.Cancel(); }
finally { _disconnectToken.Cancel(); }
}
private void SendMessage(object frame, CancellationToken token )
private void QueueMessage(object message)
{
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(fra me));
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message));
_sendQueue.Enqueue(bytes);
}
private Task SendMessage(object message, CancellationToken cancelToken)
=> SendMessage(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)), cancelToken);
private async Task SendMessage(byte[] message, CancellationToken cancelToken)
{
var frameCount = (int)Math.Ceiling((double)message.Length / SendChunkSize);
int offset = 0;
for (var i = 0; i < frameCount; i++, offset += SendChunkSize)
{
bool isLast = i == (frameCount - 1);
int count;
if (isLast)
count = message.Length - (i * SendChunkSize);
else
count = SendChunkSize;
await _webSocket.SendAsync(new ArraySegment<byte>(message, offset, count), WebSocketMessageType.Text, isLast, cancelToken);
}
}
#region IDisposable Support
private bool _isDisposed = false;