[browser][wasm] Wasm websockets support (#37962)
authorKenneth Pouncey <kjpou@pt.lu>
Thu, 2 Jul 2020 21:57:45 +0000 (23:57 +0200)
committerGitHub <noreply@github.com>
Thu, 2 Jul 2020 21:57:45 +0000 (16:57 -0500)
* [browser][wasm] Initial addition of WebSockets support

* Clean up usings

* Create and use a WebSocketHandle.Browser.cs module

* Add conditional TARGETS_BROWSER so as not to throw Proxy PNSE during ClientWebSocket creation.

* Update WebSockets code

* Removing debug line

* Remove `ThrowIfReadOnly` method to address review comments

* Address review comments

- change accessor of _requestedSubProtocols to private
- condition more code in WebSocketHandle.Managed.cs to not access _requestedSubProtocols

* Address review comments

* Address review comments

* Change to be PlatformNotSupportedException

* Add ConnectAsync implementation to create a new instance of the BrowserWebSocket implementation.

* Address review comments

* Address subprotocols comments

* Remove internal custom class ActionQueue class in favor of using a Channel.

* Fix object leak.

- Lambdas are not being released automatically.

* Add doc for the constant that is being used.

- Address review comment

* Clean up SendAsync method.

- Validate the message type.
- Validate the array segment that is passed as the message.

* Add validation to ReceiveAsync for invalid buffer.

- remove unnecessary null checks from message buffering.

* Address review comments

* Address review comments about TCS

* Address camel case by using enum

* Address abort request

* Handle nullable

* Address review comment for removing cancellationtoken registration to the connect source.

* Use non-generic TaskCompletionSource

* Cleanup var usage

* Add string to .resx

* Inline dispose

* Fix WebSocket opening exception

* Remove the asynchronous completion from SendAsync.

* Fix object leak, exception on close and address review comments

* Handle race condition

* Address  TaskCompletionSource on connect review comments

* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs

Co-authored-by: campersau <buchholz.bastian@googlemail.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs

Co-authored-by: campersau <buchholz.bastian@googlemail.com>
* Fix Connect not completing task on error.

* Cleanup reference

* Remove redundant error check of memorystream buffer.

* cleanup

* Update src/libraries/System.Net.WebSockets.Client/src/System.Net.WebSockets.Client.csproj

Co-authored-by: Maxim Lipnin <mlipnin@gmail.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System.Net.WebSockets.Client.csproj

Co-authored-by: Maxim Lipnin <mlipnin@gmail.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System.Net.WebSockets.Client.csproj

Co-authored-by: Maxim Lipnin <mlipnin@gmail.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System.Net.WebSockets.Client.csproj

Co-authored-by: Maxim Lipnin <mlipnin@gmail.com>
* Fix typo

* clean up buffer code

* Modify callbacks to use lambda function.

- These are now released properly after reference counting went in.

* Fix object leak of delegate and clean up deprecated code.

* extract lambda into method

* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>
* Update src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs

Co-authored-by: Stephen Toub <stoub@microsoft.com>
* Review comments addressed

Co-authored-by: campersau <buchholz.bastian@googlemail.com>
Co-authored-by: Maxim Lipnin <mlipnin@gmail.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
src/libraries/System.Net.WebSockets.Client/src/Resources/Strings.resx
src/libraries/System.Net.WebSockets.Client/src/System.Net.WebSockets.Client.csproj
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs [new file with mode: 0644]
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ClientWebSocketOptions.cs [new file with mode: 0644]
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ReceivePayload.cs [new file with mode: 0644]
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Browser.cs

index 7a1231e..3259b86 100644 (file)
   <data name="net_WebSockets_RemoteValidationCallbackNotSupported" xml:space="preserve">
     <value>ClientWebSocketOptions.RemoteCertificateValidationCallback is not supported on this platform.</value>
   </data>
+  <data name="net_WebSockets_Connection_Aborted" xml:space="preserve">
+    <value>Connection was aborted.</value>
+  </data>  
+  <data name="net_WebSockets_Invalid_Binary_Type" xml:space="preserve">
+    <value>WebSocket binary type '{0}' not supported.</value>
+  </data>   
 </root>
index 1c432e3..61b1f5b 100644 (file)
@@ -1,12 +1,12 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
-    <TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
+    <TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-Browser</TargetFrameworks>
     <Nullable>enable</Nullable>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="System\Net\WebSockets\ClientWebSocket.cs" />
-    <Compile Include="System\Net\WebSockets\ClientWebSocketOptions.cs" />
+    <Compile Include="System\Net\WebSockets\ClientWebSocketOptions.cs" Condition="'$(TargetsBrowser)' != 'true'" />
     <Compile Include="System\Net\WebSockets\NetEventSource.WebSockets.cs" />
     <Compile Include="$(CommonPath)System\Net\UriScheme.cs" Link="Common\System\Net\UriScheme.cs" />
     <Compile Include="$(CommonPath)System\Net\WebSockets\WebSocketValidate.cs" Link="Common\System\Net\WebSockets\WebSocketValidate.cs" />
@@ -18,6 +18,9 @@
   </ItemGroup>
   <ItemGroup Condition=" '$(TargetsBrowser)' == 'true'">
     <Compile Include="System\Net\WebSockets\WebSocketHandle.Browser.cs" />
+    <Compile Include="System\Net\WebSockets\BrowserWebSockets\BrowserWebSocket.cs" />
+    <Compile Include="System\Net\WebSockets\BrowserWebSockets\ClientWebSocketOptions.cs" />
+    <Compile Include="System\Net\WebSockets\BrowserWebSockets\ReceivePayload.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="Microsoft.Win32.Primitives" />
@@ -35,5 +38,9 @@
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Security.Cryptography.Algorithms" />
     <Reference Include="System.Security.Cryptography.Primitives" />
+    <Reference Include="System.Threading.Channels" Condition="'$(TargetsBrowser)' == 'true'" />
   </ItemGroup>
+  <ItemGroup Condition="'$(TargetsBrowser)' == 'true'" >
+    <ProjectReference Include="..\..\System.Runtime.InteropServices.JavaScript\src\System.Runtime.InteropServices.JavaScript.csproj" />
+  </ItemGroup>  
 </Project>
diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs
new file mode 100644 (file)
index 0000000..68a374c
--- /dev/null
@@ -0,0 +1,479 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Threading.Channels;
+using System.Runtime.InteropServices.JavaScript;
+
+using JavaScript = System.Runtime.InteropServices.JavaScript;
+
+namespace System.Net.WebSockets
+{
+    // **Note** on `Task.ConfigureAwait(continueOnCapturedContext: true)` for the WebAssembly Browser.
+    // The current implementation of WebAssembly for the Browser does not have a SynchronizationContext nor a Scheduler
+    // thus forcing the callbacks to run on the main browser thread.  When threading is eventually implemented using
+    // emscripten's threading model of remote worker threads, via SharedArrayBuffer, any API calls will have to be
+    // remoted back to the main thread.  Most APIs only work on the main browser thread.
+    // During discussions the concensus has been that it will not matter right now which value is used for ConfigureAwait
+    // we should put this in place now.
+
+    /// <summary>
+    /// Provides a client for connecting to WebSocket services.
+    /// </summary>
+    internal sealed class BrowserWebSocket : WebSocket
+    {
+        private readonly Channel<ReceivePayload> _receiveMessageQueue = Channel.CreateUnbounded<ReceivePayload>(new UnboundedChannelOptions()
+        {
+            SingleReader = true,
+            SingleWriter = true,
+        });
+
+        private TaskCompletionSource? _tcsClose;
+        private WebSocketCloseStatus? _innerWebSocketCloseStatus;
+        private string? _innerWebSocketCloseStatusDescription;
+
+        private JSObject? _innerWebSocket;
+
+        private Action<JSObject>? _onOpen;
+        private Action<JSObject>? _onError;
+        private Action<JSObject>? _onClose;
+        private Action<JSObject>? _onMessage;
+
+        private MemoryStream? _writeBuffer;
+        private ReceivePayload? _bufferedPayload;
+        private readonly CancellationTokenSource _cts;
+
+        // Stages of this class.
+        private int _state;
+
+        private enum InternalState
+        {
+            Created = 0,
+            Connecting = 1,
+            Connected = 2,
+            Disposed = 3
+        }
+
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="System.Net.WebSockets.BrowserWebSocket"/> class.
+        /// </summary>
+        public BrowserWebSocket()
+        {
+            _cts = new CancellationTokenSource();
+        }
+
+        #region Properties
+
+        /// <summary>
+        /// Gets the WebSocket state of the <see cref="System.Net.WebSockets.BrowserWebSocket"/> instance.
+        /// </summary>
+        /// <value>The state.</value>
+        public override WebSocketState State
+        {
+            get
+            {
+                if (_innerWebSocket != null && !_innerWebSocket.IsDisposed)
+                {
+                    return ReadyStateToDotNetState((int)_innerWebSocket.GetObjectProperty("readyState"));
+                }
+                return (InternalState)_state switch
+                {
+                    InternalState.Created => WebSocketState.None,
+                    InternalState.Connecting => WebSocketState.Connecting,
+                    _ => WebSocketState.Closed
+                };
+            }
+        }
+
+        private static WebSocketState ReadyStateToDotNetState(int readyState) =>
+            // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
+            readyState switch
+            {
+                0 => WebSocketState.Connecting, // 0 (CONNECTING)
+                1 => WebSocketState.Open, // 1 (OPEN)
+                2 => WebSocketState.CloseSent, // 2 (CLOSING)
+                3 => WebSocketState.Closed, // 3 (CLOSED)
+                _ => WebSocketState.None
+            };
+
+        public override WebSocketCloseStatus? CloseStatus => _innerWebSocket == null ? null : _innerWebSocketCloseStatus;
+
+        public override string? CloseStatusDescription => _innerWebSocket == null ? null : _innerWebSocketCloseStatusDescription;
+
+        public override string? SubProtocol => _innerWebSocket != null && !_innerWebSocket.IsDisposed ? _innerWebSocket!.GetObjectProperty("protocol")?.ToString() : null;
+
+        #endregion Properties
+
+        internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellationToken, List<string>? requestedSubProtocols)
+        {
+            // Check that we have not started already
+            int priorState = Interlocked.CompareExchange(ref _state, (int)InternalState.Connecting, (int)InternalState.Created);
+            if (priorState == (int)InternalState.Disposed)
+            {
+                throw new ObjectDisposedException(GetType().FullName);
+            }
+            else if (priorState != (int)InternalState.Created)
+            {
+                throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted);
+            }
+
+            TaskCompletionSource tcsConnect = new TaskCompletionSource();
+
+            // For Abort/Dispose.  Calling Abort on the request at any point will close the connection.
+            _cts.Token.Register(s => ((BrowserWebSocket)s!).AbortRequest(), this);
+
+            try
+            {
+                if (requestedSubProtocols?.Count > 0)
+                {
+                    using (JavaScript.Array subProtocols = new JavaScript.Array())
+                    {
+                        foreach (string item in requestedSubProtocols)
+                        {
+                            subProtocols.Push(item);
+                        }
+                        _innerWebSocket = new HostObject("WebSocket", uri.ToString(), subProtocols);
+                    }
+                }
+                else
+                {
+                    _innerWebSocket = new HostObject("WebSocket", uri.ToString());
+                }
+                _innerWebSocket.SetObjectProperty("binaryType", "arraybuffer");
+
+                // Setup the onError callback
+                _onError = errorEvt => errorEvt.Dispose();
+
+                // Attach the onError callback
+                _innerWebSocket.SetObjectProperty("onerror", _onError);
+
+                // Setup the onClose callback
+                _onClose = (closeEvt) =>
+                {
+                    using (closeEvt)
+                    {
+                        _innerWebSocketCloseStatus = (WebSocketCloseStatus)closeEvt.GetObjectProperty("code");
+                        _innerWebSocketCloseStatusDescription = closeEvt.GetObjectProperty("reason")?.ToString();
+                        _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(Array.Empty<byte>(), WebSocketMessageType.Close));
+                        NativeCleanup();
+                        if ((InternalState)_state == InternalState.Connecting)
+                        {
+                            tcsConnect.TrySetException(new WebSocketException(WebSocketError.NativeError));
+                        }
+                        else
+                        {
+                            _tcsClose?.SetResult();
+                        }
+                    }
+                };
+
+                // Attach the onClose callback
+                _innerWebSocket.SetObjectProperty("onclose", _onClose);
+
+                // Setup the onOpen callback
+                _onOpen = (evt) =>
+                {
+                    using (evt)
+                    {
+                        if (!cancellationToken.IsCancellationRequested)
+                        {
+                            // Change internal _state to 'Connected' to enable the other methods
+                            if (Interlocked.CompareExchange(ref _state, (int)InternalState.Connected, (int)InternalState.Connecting) != (int)InternalState.Connecting)
+                            {
+                                // Aborted/Disposed during connect.
+                                tcsConnect.TrySetException(new ObjectDisposedException(GetType().FullName));
+                            }
+                            else
+                            {
+                                tcsConnect.SetResult();
+                            }
+                        }
+                        else
+                        {
+                            tcsConnect.SetCanceled(cancellationToken);
+                        }
+                    }
+                };
+
+                // Attach the onOpen callback
+                _innerWebSocket.SetObjectProperty("onopen", _onOpen);
+
+                // Setup the onMessage callback
+                _onMessage = (messageEvent) => onMessageCallback(messageEvent);
+
+                // Attach the onMessage callaback
+                _innerWebSocket.SetObjectProperty("onmessage", _onMessage);
+                await tcsConnect.Task.ConfigureAwait(continueOnCapturedContext: true);
+            }
+            catch (Exception wse)
+            {
+                Dispose();
+                WebSocketException wex = new WebSocketException(SR.net_webstatus_ConnectFailure, wse);
+                throw wex;
+            }
+        }
+
+        private void onMessageCallback(JSObject messageEvent)
+        {
+            // get the events "data"
+            using (messageEvent)
+            {
+                ThrowIfNotConnected();
+                // If the messageEvent's data property is marshalled as a JSObject then we are dealing with
+                // binary data
+                object eventData = messageEvent.GetObjectProperty("data");
+                switch (eventData)
+                {
+                    case ArrayBuffer buffer:
+                        using (buffer)
+                        {
+                            _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(buffer, WebSocketMessageType.Binary));
+                            break;
+                        }
+                    case JSObject blobData:
+                        using (blobData)
+                        {
+                            // Create a new "FileReader" object
+                            using (HostObject reader = new HostObject("FileReader"))
+                            {
+                                Action<JSObject> loadend = (loadEvent) =>
+                                {
+                                    using (loadEvent)
+                                    using (JSObject target = (JSObject)loadEvent.GetObjectProperty("target"))
+                                    {
+                                        // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readyState
+                                        if ((int)target.GetObjectProperty("readyState") == 2) // DONE - The operation is complete.
+                                        {
+                                            using (ArrayBuffer binResult = (ArrayBuffer)target.GetObjectProperty("result"))
+                                            {
+                                                _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(binResult, WebSocketMessageType.Binary));
+                                            }
+                                        }
+                                    }
+                                };
+                                reader.Invoke("addEventListener", "loadend", loadend);
+                                reader.Invoke("readAsArrayBuffer", blobData);
+                            }
+                            break;
+                        }
+                    case string message:
+                        {
+                            _receiveMessageQueue.Writer.TryWrite(new ReceivePayload(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text));
+                            break;
+                        }
+                    default:
+                        throw new NotImplementedException(SR.Format(SR.net_WebSockets_Invalid_Binary_Type, _innerWebSocket?.GetObjectProperty("binaryType").ToString()));
+                }
+            }
+        }
+
+        private void NativeCleanup()
+        {
+            // We need to clear the events on websocket as well or stray events
+            // are possible leading to crashes.
+            if (_onClose != null)
+            {
+                _innerWebSocket?.SetObjectProperty("onclose", "");
+                _onClose = null;
+            }
+            if (_onError != null)
+            {
+                _innerWebSocket?.SetObjectProperty("onerror", "");
+                _onError = null;
+            }
+            if (_onOpen != null)
+            {
+                _innerWebSocket?.SetObjectProperty("onopen", "");
+                _onOpen = null;
+            }
+            if (_onMessage != null)
+            {
+                _innerWebSocket?.SetObjectProperty("onmessage", "");
+                _onMessage = null;
+            }
+        }
+
+        public override void Dispose()
+        {
+            System.Diagnostics.Debug.WriteLine("BrowserWebSocket::Dispose");
+            int priorState = Interlocked.Exchange(ref _state, (int)InternalState.Disposed);
+            if (priorState == (int)InternalState.Disposed)
+            {
+                // No cleanup required.
+                return;
+            }
+
+            // registered by the CancellationTokenSource cts in the connect method
+            _cts.Cancel(false);
+            _cts.Dispose();
+
+            _writeBuffer?.Dispose();
+            _receiveMessageQueue.Writer.Complete();
+
+            NativeCleanup();
+
+            _innerWebSocket?.Dispose();
+        }
+
+        // This method is registered by the CancellationTokenSource cts in the connect method
+        // and called by Dispose or Abort so that any open websocket connection can be closed.
+        private async void AbortRequest()
+        {
+            if (State == WebSocketState.Open)
+            {
+                await CloseAsyncCore(WebSocketCloseStatus.NormalClosure, SR.net_WebSockets_Connection_Aborted, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: true);
+            }
+        }
+
+        /// <summary>
+        /// Send data on <see cref="System.Net.WebSockets.ClientWebSocket"/> as an asynchronous operation.
+        /// </summary>
+        /// <returns>The async.</returns>
+        /// <param name="buffer">Buffer.</param>
+        /// <param name="messageType">Message type.</param>
+        /// <param name="endOfMessage">If set to <c>true</c> end of message.</param>
+        /// <param name="cancellationToken">Cancellation token.</param>
+        public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
+        {
+            ThrowIfNotConnected();
+
+            if (messageType != WebSocketMessageType.Binary &&
+                    messageType != WebSocketMessageType.Text)
+            {
+                throw new ArgumentException(SR.Format(SR.net_WebSockets_Argument_InvalidMessageType,
+                    messageType,
+                    nameof(SendAsync),
+                    WebSocketMessageType.Binary,
+                    WebSocketMessageType.Text,
+                    nameof(CloseOutputAsync)),
+                    nameof(messageType));
+            }
+
+            WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer));
+
+            _writeBuffer ??= new MemoryStream();
+            _writeBuffer.Write(buffer.Array!, buffer.Offset, buffer.Count);
+
+            if (!endOfMessage)
+                return Task.CompletedTask;
+
+            MemoryStream writtenBuffer = _writeBuffer;
+            _writeBuffer = null;
+
+            try
+            {
+                switch (messageType)
+                {
+                    case WebSocketMessageType.Binary:
+                        using (Uint8Array uint8Buffer = Uint8Array.From(buffer))
+                        {
+                            _innerWebSocket!.Invoke("send", uint8Buffer);
+                        }
+                        break;
+                    default:
+                        string strBuffer = buffer.Array == null ? string.Empty : Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
+                        _innerWebSocket!.Invoke("send", strBuffer);
+                        break;
+                }
+            }
+            catch (Exception excb)
+            {
+                return Task.FromException(new WebSocketException(WebSocketError.NativeError, excb));
+            }
+            finally
+            {
+                writtenBuffer?.Dispose();
+            }
+            return Task.CompletedTask;
+        }
+
+        /// <summary>
+        /// Receives data on <see cref="System.Net.WebSockets.ClientWebSocket"/> as an asynchronous operation.
+        /// </summary>
+        /// <returns>The async.</returns>
+        /// <param name="buffer">Buffer.</param>
+        /// <param name="cancellationToken">Cancellation token.</param>
+        public override async Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)
+        {
+            WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer));
+
+            ThrowIfDisposed();
+            ThrowOnInvalidState(State, WebSocketState.Open, WebSocketState.CloseSent);
+            _bufferedPayload ??= await _receiveMessageQueue.Reader.ReadAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: true);
+
+            try
+            {
+                bool endOfMessage = _bufferedPayload.BufferPayload(buffer, out WebSocketReceiveResult receiveResult);
+                if (endOfMessage)
+                    _bufferedPayload = null;
+                return receiveResult;
+            }
+            catch (Exception exc)
+            {
+                throw new WebSocketException(WebSocketError.NativeError, exc);
+            }
+        }
+
+        /// <summary>
+        /// Aborts the connection and cancels any pending IO operations.
+        /// </summary>
+        public override void Abort()
+        {
+            if (_state == (int)InternalState.Disposed)
+            {
+                return;
+            }
+            _state = (int)WebSocketState.Aborted;
+            Dispose();
+        }
+
+        public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
+        {
+            _writeBuffer = null;
+            ThrowIfNotConnected();
+            await CloseAsyncCore(closeStatus, statusDescription, cancellationToken).ConfigureAwait(continueOnCapturedContext: true);
+        }
+
+        private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken)
+        {
+            ThrowOnInvalidState(State, WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent);
+
+            WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription);
+
+            _tcsClose = new TaskCompletionSource();
+            _innerWebSocketCloseStatus = closeStatus;
+            _innerWebSocketCloseStatusDescription = statusDescription;
+            _innerWebSocket!.Invoke("close", (int)closeStatus, statusDescription);
+            await _tcsClose.Task.ConfigureAwait(continueOnCapturedContext: true);
+        }
+
+        public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken) => throw new PlatformNotSupportedException();
+
+        private void ThrowIfNotConnected()
+        {
+            if (_state == (int)InternalState.Disposed)
+            {
+                throw new ObjectDisposedException(GetType().FullName);
+            }
+            else if (State != WebSocketState.Open)
+            {
+                throw new InvalidOperationException(SR.net_WebSockets_NotConnected);
+            }
+        }
+
+        private void ThrowIfDisposed()
+        {
+            if (_state == (int)InternalState.Disposed)
+            {
+                throw new ObjectDisposedException(GetType().FullName);
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ClientWebSocketOptions.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ClientWebSocketOptions.cs
new file mode 100644 (file)
index 0000000..eacb5e6
--- /dev/null
@@ -0,0 +1,117 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Security.Cryptography.X509Certificates;
+
+namespace System.Net.WebSockets
+{
+    public sealed class ClientWebSocketOptions
+    {
+        private bool _isReadOnly; // After ConnectAsync is called the options cannot be modified.
+        private List<string>? _requestedSubProtocols;
+
+        internal ClientWebSocketOptions()
+        { }
+
+        #region HTTP Settings
+
+        // Note that some headers are restricted like Host.
+        public void SetRequestHeader(string headerName, string headerValue)
+        {
+            throw new PlatformNotSupportedException();
+        }
+
+        public bool UseDefaultCredentials
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        public System.Net.ICredentials Credentials
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        public System.Net.IWebProxy Proxy
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        public X509CertificateCollection ClientCertificates
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        public System.Net.Security.RemoteCertificateValidationCallback RemoteCertificateValidationCallback
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        public System.Net.CookieContainer Cookies
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        #endregion HTTP Settings
+
+        #region WebSocket Settings
+
+        public void AddSubProtocol(string subProtocol)
+        {
+            if (_isReadOnly)
+            {
+                throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted);
+            }
+            WebSocketValidate.ValidateSubprotocol(subProtocol);
+
+            // Duplicates not allowed.
+            List<string> subprotocols = RequestedSubProtocols; // force initialization of the list
+            foreach (string item in subprotocols)
+            {
+                if (string.Equals(item, subProtocol, StringComparison.OrdinalIgnoreCase))
+                {
+                    throw new ArgumentException(SR.Format(SR.net_WebSockets_NoDuplicateProtocol, subProtocol), nameof(subProtocol));
+                }
+            }
+            subprotocols.Add(subProtocol);
+        }
+
+        internal List<string> RequestedSubProtocols => _requestedSubProtocols ??= new List<string>();
+
+        public TimeSpan KeepAliveInterval
+        {
+            get => throw new PlatformNotSupportedException();
+            set => throw new PlatformNotSupportedException();
+        }
+
+        public void SetBuffer(int receiveBufferSize, int sendBufferSize)
+        {
+            throw new PlatformNotSupportedException();
+        }
+
+        public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment<byte> buffer)
+        {
+            throw new PlatformNotSupportedException();
+        }
+
+        #endregion WebSocket settings
+
+        #region Helpers
+
+        internal void SetToReadOnly()
+        {
+            Debug.Assert(!_isReadOnly, "Already set");
+            _isReadOnly = true;
+        }
+
+        #endregion Helpers
+    }
+}
diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ReceivePayload.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/ReceivePayload.cs
new file mode 100644 (file)
index 0000000..04538ce
--- /dev/null
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices.JavaScript;
+
+namespace System.Net.WebSockets
+{
+    internal sealed class ReceivePayload
+    {
+        private readonly byte[] _dataMessageReceived;
+        private readonly WebSocketMessageType _messageType;
+        private int _unconsumedDataOffset;
+
+        public ReceivePayload(ArrayBuffer arrayBuffer, WebSocketMessageType messageType)
+        {
+            using (var bin = new Uint8Array(arrayBuffer))
+            {
+                _dataMessageReceived = bin.ToArray();
+                _messageType = messageType;
+            }
+        }
+
+        public ReceivePayload(ArraySegment<byte> payload, WebSocketMessageType messageType)
+        {
+            _dataMessageReceived = payload.Array ?? Array.Empty<byte>();
+            _messageType = messageType;
+        }
+
+        public bool BufferPayload(ArraySegment<byte> arraySegment, out WebSocketReceiveResult receiveResult)
+        {
+            int bytesTransferred = Math.Min(_dataMessageReceived.Length - _unconsumedDataOffset, arraySegment.Count);
+            bool endOfMessage = (_dataMessageReceived.Length - _unconsumedDataOffset) <= arraySegment.Count;
+            Buffer.BlockCopy(_dataMessageReceived, _unconsumedDataOffset, arraySegment.Array!, arraySegment.Offset, bytesTransferred);
+            _unconsumedDataOffset += arraySegment.Count;
+            receiveResult = new WebSocketReceiveResult(bytesTransferred, _messageType, endOfMessage);
+            return endOfMessage;
+        }
+    }
+
+}
index b7ff25e..c1d2244 100644 (file)
@@ -9,6 +9,7 @@ namespace System.Net.WebSockets
 {
     internal sealed class WebSocketHandle
     {
+        private readonly CancellationTokenSource _abortSource = new CancellationTokenSource();
         private WebSocketState _state = WebSocketState.Connecting;
 
         public WebSocket? WebSocket { get; private set; }
@@ -27,10 +28,46 @@ namespace System.Net.WebSockets
             WebSocket?.Abort();
         }
 
-        public Task ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
+        public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)
         {
-            // TODO: Implement this to connect and set WebSocket to a working instance.
-            return Task.FromException(new PlatformNotSupportedException());
+            try
+            {
+                CancellationTokenSource? linkedCancellation;
+                CancellationTokenSource externalAndAbortCancellation;
+                if (cancellationToken.CanBeCanceled) // avoid allocating linked source if external token is not cancelable
+                {
+                    linkedCancellation =
+                        externalAndAbortCancellation =
+                        CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _abortSource.Token);
+                }
+                else
+                {
+                    linkedCancellation = null;
+                    externalAndAbortCancellation = _abortSource;
+                }
+
+                using (linkedCancellation)
+                {
+                    WebSocket = new BrowserWebSocket();
+                    await ((BrowserWebSocket)WebSocket).ConnectAsyncJavaScript(uri, externalAndAbortCancellation.Token, options.RequestedSubProtocols).ConfigureAwait(continueOnCapturedContext: true);
+                    externalAndAbortCancellation.Token.ThrowIfCancellationRequested();
+                }
+            }
+            catch (Exception exc)
+            {
+                if (_state < WebSocketState.Closed)
+                {
+                    _state = WebSocketState.Closed;
+                }
+
+                Abort();
+
+                if (exc is WebSocketException)
+                {
+                    throw;
+                }
+                throw new WebSocketException(SR.net_webstatus_ConnectFailure, exc);
+            }
         }
     }
 }