General WebSocket improvements
This commit is contained in:
@@ -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);
|
_disconnectToken = new CancellationTokenSource();
|
||||||
_connectWaitOnLogin2 = new AutoResetEvent(false);
|
var cancelToken = _disconnectToken.Token;
|
||||||
_sendQueue = new ConcurrentQueue<byte[]>();
|
|
||||||
|
|
||||||
_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)
|
await Task.Factory.StartNew(SendAsync, cancelToken, TaskCreationOptions.LongRunning, TaskScheduler.Default))
|
||||||
).ContinueWith(x =>
|
.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();
|
_disconnectToken.Dispose();
|
||||||
_cancelToken = null;
|
_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
|
try
|
||||||
throw new Exception("No reply from Discord server");
|
{
|
||||||
_connectWaitOnLogin2.WaitOne(); //Post-Event
|
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);
|
QueueMessage(new WebSocketCommands.UpdateStatus());
|
||||||
SendMessage(new WebSocketCommands.KeepAlive(), cancelToken);
|
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))
|
||||||
{
|
await SendMessage(bytes, cancelToken);
|
||||||
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 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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user