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