General WebSocket improvements
This commit is contained in:
@@ -15,51 +15,59 @@ namespace Discord
|
||||
{
|
||||
private const int ReceiveChunkSize = 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 CancellationTokenSource _cancelToken;
|
||||
private volatile CancellationTokenSource _disconnectToken;
|
||||
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;
|
||||
_cancelToken.Dispose();
|
||||
_cancelToken = null;
|
||||
_disconnectToken.Dispose();
|
||||
_disconnectToken = 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, _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;
|
||||
RaiseConnected();
|
||||
@@ -87,14 +107,14 @@ namespace Discord
|
||||
{
|
||||
if (_tasks != null)
|
||||
{
|
||||
_cancelToken.Cancel();
|
||||
_disconnectToken.Cancel();
|
||||
await _tasks;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveAsync()
|
||||
{
|
||||
var cancelToken = _cancelToken.Token;
|
||||
var cancelToken = _disconnectToken.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), _cancelToken.Token);
|
||||
result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _disconnectToken.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 { _cancelToken.Cancel(); }
|
||||
finally { _disconnectToken.Cancel(); }
|
||||
}
|
||||
|
||||
private async Task SendAsync()
|
||||
{
|
||||
var cancelToken = _cancelToken.Token;
|
||||
var cancelToken = _disconnectToken.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 { _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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user