From ed32790cf54bed752fd442aeeb563dda98bc83b5 Mon Sep 17 00:00:00 2001 From: Jarl Gullberg Date: Mon, 18 Dec 2017 20:28:45 +0100 Subject: [PATCH] Add new mono-compatible websocket provider. --- Discord.Net.sln | 17 +- ...iscord.Net.Providers.WebSocketSharp.csproj | 15 ++ .../WebSocketSharpClient.cs | 223 ++++++++++++++++++ .../WebSocketSharpProvider.cs | 9 + 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 src/Discord.Net.Providers.WebSocketSharp/Discord.Net.Providers.WebSocketSharp.csproj create mode 100644 src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpClient.cs create mode 100644 src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpProvider.cs diff --git a/Discord.Net.sln b/Discord.Net.sln index 58bfcad86..38b3509f7 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.12 MinimumVisualStudioVersion = 10.0.40219.1 @@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Providers.WebSocketSharp", "src\Discord.Net.Providers.WebSocketSharp\Discord.Net.Providers.WebSocketSharp.csproj", "{0748E83F-87A1-4C07-B458-6E438841925B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,6 +132,18 @@ Global {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Debug|x64.ActiveCfg = Debug|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Debug|x64.Build.0 = Debug|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Debug|x86.ActiveCfg = Debug|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Debug|x86.Build.0 = Debug|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Release|Any CPU.Build.0 = Release|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Release|x64.ActiveCfg = Release|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Release|x64.Build.0 = Release|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Release|x86.ActiveCfg = Release|Any CPU + {0748E83F-87A1-4C07-B458-6E438841925B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -141,6 +155,7 @@ Global {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} {9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A} + {0748E83F-87A1-4C07-B458-6E438841925B} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495} diff --git a/src/Discord.Net.Providers.WebSocketSharp/Discord.Net.Providers.WebSocketSharp.csproj b/src/Discord.Net.Providers.WebSocketSharp/Discord.Net.Providers.WebSocketSharp.csproj new file mode 100644 index 000000000..50db25efd --- /dev/null +++ b/src/Discord.Net.Providers.WebSocketSharp/Discord.Net.Providers.WebSocketSharp.csproj @@ -0,0 +1,15 @@ + + + + Discord.Net.Providers.WebSocketSharp + Discord.Providers.WebSocketSharp + An optional WebSocket client provider for Discord.Net using websocket-sharp + netstandard2.0 + + + + + + + + \ No newline at end of file diff --git a/src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpClient.cs b/src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpClient.cs new file mode 100644 index 000000000..40aeed644 --- /dev/null +++ b/src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpClient.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Discord.Net; +using Discord.Net.WebSockets; +using WebSocketSharp; + +namespace Discord.Providers.WebSocketSharp +{ + /// + /// WebSocket provider using websocket-sharp. + /// + internal class WebSocketSharpClient : IWebSocketClient, IDisposable + { + /// + public event Func BinaryMessage; + + /// + public event Func TextMessage; + + /// + public event Func Closed; + + private readonly SemaphoreSlim _lock; + private readonly Dictionary _headers; + private readonly ManualResetEventSlim _waitUntilConnect; + + private WebSocket _client; + private CancellationTokenSource _cancelTokenSource; + private CancellationToken _cancelToken; + private CancellationToken _parentToken; + + private bool _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + public WebSocketSharpClient() + { + _headers = new Dictionary(); + _lock = new SemaphoreSlim(1, 1); + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationToken.None; + _parentToken = CancellationToken.None; + _waitUntilConnect = new ManualResetEventSlim(); + } + + /// + public void SetHeader(string key, string value) + { + _headers[key] = value; + } + + /// + public void SetCancelToken(CancellationToken cancelToken) + { + _parentToken = cancelToken; + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource + ( + _parentToken, + _cancelTokenSource.Token + ) + .Token; + } + + /// + public async Task ConnectAsync(string host) + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternalAsync(host).ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + + private async Task ConnectInternalAsync(string host) + { + await DisconnectInternalAsync().ConfigureAwait(false); + + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = CancellationTokenSource.CreateLinkedTokenSource + ( + _parentToken, + _cancelTokenSource.Token + ) + .Token; + + _client = new WebSocket(host) + { + CustomHeaders = _headers.ToList() + }; + _client.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12; + + _client.OnMessage += OnMessage; + _client.OnOpen += OnConnected; + _client.OnClose += OnClosed; + + _client.Connect(); + _waitUntilConnect.Wait(_cancelToken); + } + + /// + public async Task DisconnectAsync() + { + await _lock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternalAsync().ConfigureAwait(false); + } + finally + { + _lock.Release(); + } + } + + private Task DisconnectInternalAsync() + { + _cancelTokenSource.Cancel(); + if (_client is null) + { + return Task.CompletedTask; + } + + if (_client.ReadyState == WebSocketState.Open) + { + _client.Close(); + } + + _client.OnMessage -= OnMessage; + _client.OnOpen -= OnConnected; + _client.OnClose -= OnClosed; + + _client = null; + _waitUntilConnect.Reset(); + + return Task.CompletedTask; + } + + private void OnMessage(object sender, MessageEventArgs messageEventArgs) + { + if (messageEventArgs.IsBinary) + { + OnBinaryMessage(messageEventArgs); + } + else if (messageEventArgs.IsText) + { + OnTextMessage(messageEventArgs); + } + } + + /// + public async Task SendAsync(byte[] data, int index, int count, bool isText) + { + await _lock.WaitAsync(_cancelToken).ConfigureAwait(false); + try + { + if (isText) + { + _client.Send(Encoding.UTF8.GetString(data, index, count)); + } + else + { + _client.Send(data.Skip(index).Take(count).ToArray()); + } + } + finally + { + _lock.Release(); + } + } + + private void OnTextMessage(MessageEventArgs e) + { + TextMessage?.Invoke(e.Data).GetAwaiter().GetResult(); + } + + private void OnBinaryMessage(MessageEventArgs e) + { + BinaryMessage?.Invoke(e.RawData, 0, e.RawData.Length).GetAwaiter().GetResult(); + } + + private void OnConnected(object sender, EventArgs e) + { + _waitUntilConnect.Set(); + } + + private void OnClosed(object sender, CloseEventArgs e) + { + if (e.WasClean) + { + Closed?.Invoke(null).GetAwaiter().GetResult(); + return; + } + + var ex = new WebSocketClosedException(e.Code, e.Reason); + Closed?.Invoke(ex).GetAwaiter().GetResult(); + } + + /// + public void Dispose() + { + if (_isDisposed) + { + return; + } + + DisconnectInternalAsync().GetAwaiter().GetResult(); + + ((IDisposable)_client)?.Dispose(); + _client = null; + + _isDisposed = true; + } + } +} diff --git a/src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpProvider.cs b/src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpProvider.cs new file mode 100644 index 000000000..e200d3c0c --- /dev/null +++ b/src/Discord.Net.Providers.WebSocketSharp/WebSocketSharpProvider.cs @@ -0,0 +1,9 @@ +using Discord.Net.WebSockets; + +namespace Discord.Providers.WebSocketSharp +{ + public static class WebSocketSharpProvider + { + public static readonly WebSocketProvider Instance = () => new WebSocketSharpClient(); + } +}