├── .gitignore ├── LICENSE ├── NuGet.config ├── README.md ├── src ├── Pipelines.WebSockets.sln ├── Pipelines.WebSockets │ ├── ByteArrayMessageWriter.cs │ ├── ConnectionType.cs │ ├── ExtensionMethods.cs │ ├── HttpRequest.cs │ ├── HttpRequestHeaders.cs │ ├── IMessageWriter.cs │ ├── Message.cs │ ├── MessageWriter.cs │ ├── Pipelines.WebSockets.csproj │ ├── StringMessageWriter.cs │ ├── TaskResult.cs │ ├── WebSocketChannel.cs │ ├── WebSocketConnection.cs │ ├── WebSocketServer.cs │ ├── WebSocketsFrame.cs │ └── WebSocketsProtocol.cs └── SampleServer │ ├── EchoServer.cs │ ├── Program.cs │ ├── SampleServer.xproj │ ├── TrivialClient.cs │ └── project.json └── tools └── Key.snk /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | project.lock.json 4 | .vs 5 | bin/ 6 | obj/ 7 | release/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Stack Overflow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pipelines.WebSockets 2 | .NET WebSocket (RFC 6455) implementation using the `System.IO.Pipelines` API 3 | 4 | `System.IO.Pipelines` is the [experimental corefxlab API](https://github.com/dotnet/corefxlab/tree/master/src/System.IO.Pipelines) offering efficient zero-copy async-IO mechanisms for .NET [using "spans"](http://blog.marcgravell.com/2017/04/spans-and-ref-part-2-spans.html); in a former life it was known as [Channels](http://blog.marcgravell.com/2016/09/channelling-my-inner-geek.html). 5 | 6 | [What is web-sockets?](https://tools.ietf.org/html/rfc6455) 7 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pipelines.WebSockets", "Pipelines.WebSockets\Pipelines.WebSockets.csproj", "{994F5612-B132-43BC-B37C-128EAD305243}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {994F5612-B132-43BC-B37C-128EAD305243}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {994F5612-B132-43BC-B37C-128EAD305243}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {994F5612-B132-43BC-B37C-128EAD305243}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {994F5612-B132-43BC-B37C-128EAD305243}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/ByteArrayMessageWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipelines; 3 | 4 | namespace Pipelines.WebSockets 5 | { 6 | internal struct ByteArrayMessageWriter : IMessageWriter 7 | { 8 | private byte[] value; 9 | private int offset, count; 10 | public ByteArrayMessageWriter(byte[] value, int offset, int count) 11 | { 12 | if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); 13 | if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); 14 | if (offset + count > (value?.Length ?? 0)) throw new ArgumentOutOfRangeException(nameof(count)); 15 | 16 | this.value = value; 17 | this.offset = offset; 18 | this.count = count; 19 | } 20 | 21 | void IMessageWriter.WritePayload(WritableBuffer buffer) 22 | { 23 | if (count != 0) buffer.Write(new Span(value, offset, count)); 24 | } 25 | 26 | int IMessageWriter.GetPayloadLength() => count; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/ConnectionType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Pipelines.WebSockets 7 | { 8 | public enum ConnectionType 9 | { 10 | Client, 11 | Server 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.IO.Pipelines; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Pipelines.WebSockets 8 | { 9 | internal static class ExtensionMethods 10 | { 11 | public static void WriteAsciiString(this WritableBuffer buffer, string value) 12 | { 13 | if (value == null || value.Length == 0) return; 14 | 15 | WriteAsciiString(ref buffer, value.Slice()); 16 | } 17 | private static void WriteAsciiString(ref WritableBuffer buffer, ReadOnlySpan value) 18 | { 19 | if (value == null || value.Length == 0) return; 20 | 21 | while (value.Length != 0) 22 | { 23 | buffer.Ensure(); 24 | 25 | var span = buffer.Buffer.Span; 26 | int bytesToWrite = Math.Min(value.Length, span.Length); 27 | 28 | // todo: Vector.Narrow 29 | 30 | for (int i = 0; i < bytesToWrite; i++) 31 | { 32 | span[i] = (byte)value[i]; 33 | } 34 | 35 | buffer.Advance(bytesToWrite); 36 | buffer.Commit(); 37 | value = value.Slice(bytesToWrite); 38 | } 39 | } 40 | 41 | public static void WriteUtf8String(this WritableBuffer buffer, string value) 42 | { 43 | if (value == null || value.Length == 0) return; 44 | 45 | WriteUtf8String(ref buffer, value.Slice()); 46 | } 47 | private static void WriteUtf8String(ref WritableBuffer buffer, ReadOnlySpan value) 48 | { 49 | if (value == null || value.Length == 0) return; 50 | 51 | var encoder = TextEncoder.Utf8; 52 | while (value.Length != 0) 53 | { 54 | buffer.Ensure(4); // be able to write at least one character (worst case) 55 | 56 | var span = buffer.Buffer.Span; 57 | encoder.TryEncode(value, span, out int charsConsumed, out int bytesWritten); 58 | buffer.Advance(bytesWritten); 59 | buffer.Commit(); 60 | value = value.Slice(charsConsumed); 61 | } 62 | } 63 | 64 | public static Task AsTask(this WritableBufferAwaitable flush) 65 | { 66 | async Task Awaited(WritableBufferAwaitable result) 67 | { 68 | if ((await result).IsCancelled) throw new ObjectDisposedException("Flush cancelled"); 69 | } 70 | if (!flush.IsCompleted) return Awaited(flush); 71 | if (flush.GetResult().IsCancelled) throw new ObjectDisposedException("Flush cancelled"); 72 | return Task.CompletedTask; // all sync? who knew! 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/HttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Pipelines; 4 | 5 | namespace Pipelines.WebSockets 6 | { 7 | internal struct HttpRequest : IDisposable 8 | { 9 | public void Dispose() 10 | { 11 | Method.Dispose(); 12 | Path.Dispose(); 13 | HttpVersion.Dispose(); 14 | Headers.Dispose(); 15 | Method = Path = HttpVersion = default(PreservedBuffer); 16 | Headers = default(HttpRequestHeaders); 17 | } 18 | public PreservedBuffer Method { get; private set; } 19 | public PreservedBuffer Path { get; private set; } 20 | public PreservedBuffer HttpVersion { get; private set; } 21 | 22 | public HttpRequestHeaders Headers; // yes, naked field - internal type, so not too exposed; allows for "ref" without copy 23 | 24 | public HttpRequest(PreservedBuffer method, PreservedBuffer path, PreservedBuffer httpVersion, Dictionary headers) 25 | { 26 | Method = method; 27 | Path = path; 28 | HttpVersion = httpVersion; 29 | Headers = new HttpRequestHeaders(headers); 30 | } 31 | } 32 | internal struct HttpResponse : IDisposable 33 | { 34 | private HttpRequest request; 35 | public void Dispose() 36 | { 37 | request.Dispose(); 38 | request = default(HttpRequest); 39 | } 40 | internal HttpResponse(HttpRequest request) 41 | { 42 | this.request = request; 43 | } 44 | public HttpRequestHeaders Headers => request.Headers; 45 | // looks similar, but different order 46 | public PreservedBuffer HttpVersion => request.Method; 47 | public PreservedBuffer StatusCode => request.Path; 48 | public PreservedBuffer StatusText => request.HttpVersion; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/HttpRequestHeaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO.Pipelines; 5 | using System.IO.Pipelines.Text.Primitives; 6 | 7 | namespace Pipelines.WebSockets 8 | { 9 | public struct HttpRequestHeaders : IEnumerable>, IDisposable 10 | { 11 | private Dictionary headers; 12 | public void Dispose() 13 | { 14 | if (headers != null) 15 | { 16 | foreach (var pair in headers) 17 | pair.Value.Dispose(); 18 | } 19 | headers = null; 20 | } 21 | internal HttpRequestHeaders(Dictionary headers) 22 | { 23 | this.headers = headers; 24 | } 25 | public bool ContainsKey(string key) => headers.ContainsKey(key); 26 | IEnumerator> IEnumerable>.GetEnumerator() => ((IEnumerable>)headers).GetEnumerator(); 27 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)headers).GetEnumerator(); 28 | public Dictionary.Enumerator GetEnumerator() => headers.GetEnumerator(); 29 | 30 | public string GetAsciiString(string key) 31 | { 32 | PreservedBuffer buffer; 33 | if (headers.TryGetValue(key, out buffer)) return buffer.Buffer.GetAsciiString(); 34 | return null; 35 | } 36 | internal PreservedBuffer GetRaw(string key) 37 | { 38 | PreservedBuffer buffer; 39 | if (headers.TryGetValue(key, out buffer)) return buffer; 40 | return default(PreservedBuffer); 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/IMessageWriter.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | 3 | namespace Pipelines.WebSockets 4 | { 5 | internal interface IMessageWriter 6 | { 7 | void WritePayload(WritableBuffer buffer); 8 | int GetPayloadLength(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/Message.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Pipelines; 4 | using System.Text; 5 | using System.Threading; 6 | using System.IO.Pipelines.Text.Primitives; 7 | 8 | namespace Pipelines.WebSockets 9 | { 10 | public struct Message : IMessageWriter 11 | { 12 | private ReadableBuffer _buffer; 13 | private List _buffers; 14 | private int _mask; 15 | private string _text; 16 | public bool IsFinal { get; } 17 | internal Message(ReadableBuffer buffer, int mask, bool isFinal) 18 | { 19 | this._buffer = buffer; 20 | this._mask = mask; 21 | _text = null; 22 | IsFinal = isFinal; 23 | _buffers = null; 24 | } 25 | 26 | internal WritableBufferAwaitable WriteAsync(IPipeWriter output) 27 | { 28 | var write = output.Alloc(); 29 | if(_buffers != null) 30 | { 31 | foreach (var buffer in _buffers) 32 | { 33 | write.Append(buffer.Buffer); 34 | } 35 | } 36 | else 37 | { 38 | ApplyMask(); 39 | write.Append(_buffer); 40 | } 41 | return write.FlushAsync(); 42 | 43 | } 44 | 45 | internal Message(List buffers) 46 | { 47 | _mask = 0; 48 | _text = null; 49 | IsFinal = true; 50 | //if (buffers.Count == 1) // can simplify 51 | //{ 52 | // _buffer = buffers[0]; 53 | // this._buffers = null; 54 | //} 55 | //else 56 | //{ 57 | _buffer = default(ReadableBuffer); 58 | this._buffers = buffers; 59 | //} 60 | } 61 | private void ApplyMask() 62 | { 63 | if (_mask != 0) 64 | { 65 | WebSocketsFrame.ApplyMask(ref _buffer, _mask); 66 | _mask = 0; 67 | } 68 | } 69 | public override string ToString() => GetText(); 70 | public string GetText() 71 | { 72 | if (_text != null) return _text; 73 | 74 | var buffers = this._buffers; 75 | if (buffers == null) 76 | { 77 | if (_buffer.Length == 0) return _text = ""; 78 | 79 | ApplyMask(); 80 | return _text = _buffer.GetUtf8String(); 81 | } 82 | return _text = GetText(buffers); 83 | } 84 | 85 | private static readonly Encoding Utf8Encoding = Encoding.UTF8; 86 | private static Decoder Utf8Decoder; 87 | 88 | private static string GetText(List buffers) 89 | { 90 | // try to re-use a shared decoder; note that in heavy usage, we might need to allocate another 91 | var decoder = (Decoder)Interlocked.Exchange(ref Utf8Decoder, null); 92 | if (decoder == null) decoder = Utf8Encoding.GetDecoder(); 93 | else decoder.Reset(); 94 | 95 | var length = 0; 96 | foreach (var buffer in buffers) length += buffer.Buffer.Length; 97 | 98 | var capacity = length; // worst case is 1 byte per char 99 | var chars = new char[capacity]; 100 | var charIndex = 0; 101 | 102 | int bytesUsed = 0; 103 | int charsUsed = 0; 104 | bool completed; 105 | foreach (var buffer in buffers) 106 | { 107 | foreach (var span in buffer.Buffer) 108 | { 109 | ArraySegment segment; 110 | if(!span.TryGetArray(out segment)) 111 | { 112 | throw new InvalidOperationException("Array not available for span"); 113 | } 114 | decoder.Convert( 115 | segment.Array, 116 | segment.Offset, 117 | segment.Count, 118 | chars, 119 | charIndex, 120 | capacity, 121 | false, // a single character could span two spans 122 | out bytesUsed, 123 | out charsUsed, 124 | out completed); 125 | 126 | charIndex += charsUsed; 127 | capacity -= charsUsed; 128 | } 129 | } 130 | // make the decoder available for re-use 131 | Interlocked.CompareExchange(ref Utf8Decoder, decoder, null); 132 | return new string(chars, 0, charIndex); 133 | } 134 | private static readonly byte[] NilBytes = new byte[0]; 135 | public byte[] GetBytes() 136 | { 137 | int len = GetPayloadLength(); 138 | if (len == 0) return NilBytes; 139 | 140 | ApplyMask(); 141 | return _buffer.ToArray(); 142 | } 143 | public int GetPayloadLength() 144 | { 145 | var buffers = this._buffers; 146 | if (buffers == null) return _buffer.Length; 147 | int count = 0; 148 | foreach (var buffer in buffers) count += buffer.Buffer.Length; 149 | return count; 150 | } 151 | 152 | void IMessageWriter.WritePayload(WritableBuffer destination) 153 | { 154 | var buffers = this._buffers; 155 | if (buffers == null) 156 | { 157 | ApplyMask(); 158 | destination.Append(_buffer); 159 | } 160 | else 161 | { 162 | foreach(var buffer in buffers) 163 | { 164 | destination.Append(buffer.Buffer); 165 | } 166 | } 167 | 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/MessageWriter.cs: -------------------------------------------------------------------------------- 1 | namespace Pipelines.WebSockets 2 | { 3 | internal static class MessageWriter 4 | { 5 | internal static ByteArrayMessageWriter Create(byte[] message) 6 | { 7 | return (message == null || message.Length == 0) ? default(ByteArrayMessageWriter) : new ByteArrayMessageWriter(message, 0, message.Length); 8 | } 9 | internal static StringMessageWriter Create(string message, bool computeLength = false) 10 | { 11 | return string.IsNullOrEmpty(message) ? default(StringMessageWriter) : new StringMessageWriter(message, computeLength); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/Pipelines.WebSockets.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.3 5 | $(DefineConstants);LOGGING 6 | true 7 | Pipelines.WebSockets 8 | ../../tools/Key.snk 9 | true 10 | true 11 | Pipelines.WebSockets 12 | 1.6.1 13 | 4.3.0 14 | 0.1.0-e170425-2 15 | 4.4.0-preview1-25219-04 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/StringMessageWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.IO.Pipelines; 3 | 4 | namespace Pipelines.WebSockets 5 | { 6 | internal struct StringMessageWriter : IMessageWriter 7 | { 8 | private string value; 9 | private int totalBytes; 10 | public StringMessageWriter(string value, bool preComputeLength) 11 | { 12 | this.value = value; 13 | this.totalBytes = value.Length == 0 ? 0 : -1; 14 | if (preComputeLength) GetPayloadLength(); 15 | } 16 | void IMessageWriter.WritePayload(WritableBuffer buffer) 17 | => buffer.WriteUtf8String(value); 18 | 19 | public int GetPayloadLength() 20 | { 21 | // lazily calculate 22 | return totalBytes >= 0 ? totalBytes : (totalBytes = encoding.GetByteCount(value)); 23 | } 24 | static readonly Encoding encoding = Encoding.UTF8; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/TaskResult.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Pipelines.WebSockets 4 | { 5 | //internal static class TaskResult 6 | //{ 7 | // public static readonly Task Zero = Task.FromResult(0); 8 | //} 9 | } 10 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/WebSocketChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipelines; 3 | using System.Threading.Tasks; 4 | 5 | namespace Pipelines.WebSockets 6 | { 7 | public class WebSocketChannel : IPipeConnection 8 | { 9 | private IPipe _input, _output; 10 | private WebSocketConnection _webSocket; 11 | 12 | public WebSocketChannel(WebSocketConnection webSocket, PipeFactory pipeFactory) 13 | { 14 | 15 | _webSocket = webSocket; 16 | _input = pipeFactory.Create(); 17 | _output = pipeFactory.Create(); 18 | 19 | webSocket.BufferFragments = false; 20 | webSocket.BinaryAsync += msg => msg.WriteAsync(_input.Writer).AsTask(); 21 | webSocket.Closed += () => _input.Writer.Complete(); 22 | SendCloseWhenOutputCompleted(); 23 | PushFromOutputToWebSocket(); 24 | } 25 | 26 | 27 | 28 | private async void SendCloseWhenOutputCompleted() 29 | { 30 | await _output.Reading; 31 | await _webSocket.CloseAsync(); 32 | } 33 | 34 | private async void PushFromOutputToWebSocket() 35 | { 36 | while (true) 37 | { 38 | var read = await _output.Reader.ReadAsync(); 39 | if (read.IsCompleted) break; 40 | var data = read.Buffer; 41 | if(data.IsEmpty) 42 | { 43 | _output.Reader.Advance(data.End); 44 | } 45 | else 46 | { 47 | // note we don't need to create a mask here - is created internally 48 | var message = new Message(data, mask: 0, isFinal: true); 49 | var send = _webSocket.SendAsync(WebSocketsFrame.OpCodes.Binary, ref message); 50 | // can free up one lock on the data *before* we await... 51 | _output.Reader.Advance(data.End); 52 | await send; 53 | } 54 | } 55 | } 56 | 57 | public IPipeReader Input => _input.Reader; 58 | public IPipeWriter Output => _output.Writer; 59 | 60 | public void Dispose() => _webSocket.Dispose(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/WebSocketConnection.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines.Networking.Sockets; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.IO.Pipelines; 8 | 9 | namespace Pipelines.WebSockets 10 | { 11 | public class WebSocketConnection : IDisposable 12 | { 13 | 14 | private readonly ConnectionType connectionType; 15 | 16 | public ConnectionType ConnectionType => connectionType; 17 | public static async Task ConnectAsync( 18 | string location, string protocol = null, string origin = null, 19 | Action addHeaders = null, 20 | PipeFactory pipeFactory = null) 21 | { 22 | WebSocketServer.WriteStatus(ConnectionType.Client, $"Connecting to {location}..."); 23 | Uri uri; 24 | if (!Uri.TryCreate(location, UriKind.Absolute, out uri) 25 | || uri.Scheme != "ws") 26 | { 27 | throw new ArgumentException(nameof(location)); 28 | } 29 | IPAddress ip; 30 | if (!IPAddress.TryParse(uri.Host, out ip)) 31 | { 32 | throw new NotImplementedException("host must be an IP address at the moment, sorry"); 33 | } 34 | WebSocketServer.WriteStatus(ConnectionType.Client, $"Opening socket to {ip}:{uri.Port}..."); 35 | var socket = await SocketConnection.ConnectAsync(new IPEndPoint(ip, uri.Port), pipeFactory); 36 | 37 | return await WebSocketProtocol.ClientHandshake(socket, uri, origin, protocol); 38 | } 39 | 40 | 41 | private IPipeConnection connection; 42 | internal IPipeConnection Connection => connection; 43 | 44 | internal WebSocketConnection(IPipeConnection connection, ConnectionType connectionType) 45 | { 46 | this.connection = connection; 47 | this.connectionType = connectionType; 48 | } 49 | 50 | public string Host { get; internal set; } 51 | public string Origin { get; internal set; } 52 | public string Protocol { get; internal set; } 53 | public string RequestLine { get; internal set; } 54 | 55 | public object UserState { get; set; } 56 | internal async Task ProcessIncomingFramesAsync(WebSocketServer server) 57 | { 58 | while (true) 59 | { 60 | var read = await connection.Input.ReadAsync(); 61 | 62 | if(read.IsCompleted) 63 | { 64 | break; // that's all, folks 65 | } 66 | WebSocketsFrame frame; 67 | var buffer = read.Buffer; 68 | try 69 | { 70 | if (WebSocketProtocol.TryReadFrameHeader(ref buffer, out frame)) 71 | { 72 | int payloadLength = frame.PayloadLength; 73 | // buffer now points to the payload 74 | 75 | if (connectionType == ConnectionType.Server) 76 | { 77 | if (!frame.IsMasked) 78 | { 79 | throw new InvalidOperationException("Client-to-server frames should be masked"); 80 | } 81 | } 82 | else 83 | { 84 | if (frame.IsMasked) 85 | { 86 | throw new InvalidOperationException("Server-to-client frames should not be masked"); 87 | } 88 | } 89 | 90 | if (frame.IsControlFrame && !frame.IsFinal) 91 | { 92 | throw new InvalidOperationException("Control frames cannot be fragmented"); 93 | } 94 | await OnFrameReceivedAsync(ref frame, ref buffer, server); 95 | // and finally, progress past the frame 96 | if (payloadLength != 0) buffer = buffer.Slice(payloadLength); 97 | } 98 | } 99 | finally 100 | { 101 | connection.Input.Advance(buffer.Start, buffer.End); 102 | } 103 | } 104 | } 105 | internal Task OnFrameReceivedAsync(ref WebSocketsFrame frame, ref ReadableBuffer buffer, WebSocketServer server) 106 | { 107 | WebSocketServer.WriteStatus(ConnectionType, frame.ToString()); 108 | 109 | 110 | // note that this call updates the connection state; must be called, even 111 | // if we don't get as far as the 'switch' 112 | var opCode = GetEffectiveOpCode(ref frame); 113 | 114 | Message msg; 115 | if (!frame.IsControlFrame) 116 | { 117 | if (frame.IsFinal) 118 | { 119 | if (this.HasBacklog) 120 | { 121 | try 122 | { 123 | // add our data to the existing backlog 124 | this.AddBacklog(ref buffer, ref frame); 125 | 126 | // use the backlog buffer to execute the method; note that 127 | // we un-masked *while adding*; don't need mask here 128 | msg = new Message(this.GetBacklog()); 129 | if (server != null) 130 | { 131 | return OnServerAndConnectionFrameReceivedImplAsync(opCode, msg, server); 132 | } 133 | else 134 | { 135 | return OnConectionFrameReceivedImplAsync(opCode, ref msg); 136 | } 137 | } 138 | finally 139 | { 140 | // and release the backlog 141 | this.ClearBacklog(); 142 | } 143 | } 144 | } 145 | else if (BufferFragments) 146 | { 147 | // need to buffer this data against the connection 148 | this.AddBacklog(ref buffer, ref frame); 149 | return Task.CompletedTask; 150 | } 151 | } 152 | msg = new Message(buffer.Slice(0, frame.PayloadLength), frame.MaskLittleEndian, frame.IsFinal); 153 | if (server != null) 154 | { 155 | return OnServerAndConnectionFrameReceivedImplAsync(opCode, msg, server); 156 | } 157 | else 158 | { 159 | return OnConectionFrameReceivedImplAsync(opCode, ref msg); 160 | } 161 | 162 | } 163 | private async Task OnServerAndConnectionFrameReceivedImplAsync(WebSocketsFrame.OpCodes opCode, Message message, WebSocketServer server) 164 | { 165 | await OnConectionFrameReceivedImplAsync(opCode, ref message); 166 | await OnServerFrameReceivedImplAsync(opCode, ref message, server); 167 | } 168 | 169 | private Task OnServerFrameReceivedImplAsync(WebSocketsFrame.OpCodes opCode, ref Message message, WebSocketServer server) 170 | { 171 | switch (opCode) 172 | { 173 | case WebSocketsFrame.OpCodes.Binary: 174 | return server.OnBinaryAsync(this, ref message); 175 | case WebSocketsFrame.OpCodes.Text: 176 | return server.OnTextAsync(this, ref message); 177 | } 178 | return Task.CompletedTask; 179 | } 180 | private Task OnConectionFrameReceivedImplAsync(WebSocketsFrame.OpCodes opCode, ref Message message) 181 | { 182 | WebSocketServer.WriteStatus(connectionType, $"Processing {opCode}, {message.GetPayloadLength()} bytes..."); 183 | switch (opCode) 184 | { 185 | case WebSocketsFrame.OpCodes.Binary: 186 | return OnBinaryAsync(ref message); 187 | case WebSocketsFrame.OpCodes.Text: 188 | return OnTextAsync(ref message); 189 | case WebSocketsFrame.OpCodes.Close: 190 | connection.Input.Complete(); 191 | // respond to a close in-kind (2-handed close) 192 | try { Closed?.Invoke(); } catch { } 193 | 194 | if (connection.Output.Writing.IsCompleted) return Task.CompletedTask; // already closed 195 | 196 | return SendAsync(WebSocketsFrame.OpCodes.Close, ref message); 197 | case WebSocketsFrame.OpCodes.Ping: 198 | // by default, respond to a ping with a matching pong 199 | return SendAsync(WebSocketsFrame.OpCodes.Pong, ref message); // right back at you 200 | case WebSocketsFrame.OpCodes.Pong: 201 | return Task.CompletedTask; 202 | default: 203 | return Task.CompletedTask; 204 | } 205 | } 206 | WebSocketsFrame.OpCodes lastOpCode; 207 | internal WebSocketsFrame.OpCodes GetEffectiveOpCode(ref WebSocketsFrame frame) 208 | { 209 | if (frame.IsControlFrame) 210 | { 211 | // doesn't change state 212 | return frame.OpCode; 213 | } 214 | 215 | var frameOpCode = frame.OpCode; 216 | 217 | // re-use the previous opcode if we are a continuation 218 | var result = frameOpCode == WebSocketsFrame.OpCodes.Continuation ? lastOpCode : frameOpCode; 219 | 220 | // if final, clear the opcode; otherwise: use what we thought of 221 | lastOpCode = frame.IsFinal ? WebSocketsFrame.OpCodes.Continuation : result; 222 | return result; 223 | } 224 | 225 | 226 | volatile List backlog; 227 | // TODO: not sure how to do this; basically I want to lease a writable, expandable area 228 | // for a duration, and be able to access it for reading, and release 229 | internal void AddBacklog(ref ReadableBuffer buffer, ref WebSocketsFrame frame) 230 | { 231 | var length = frame.PayloadLength; 232 | if (length == 0) return; // nothing to store! 233 | 234 | var slicedBuffer = buffer.Slice(0, length); 235 | // unscramble the data 236 | if (frame.MaskLittleEndian != 0) WebSocketsFrame.ApplyMask(ref slicedBuffer, frame.MaskLittleEndian); 237 | 238 | var backlog = this.backlog; 239 | if (backlog == null) 240 | { 241 | var newBacklog = new List(); 242 | backlog = Interlocked.CompareExchange(ref this.backlog, newBacklog, null) ?? newBacklog; 243 | } 244 | backlog.Add(slicedBuffer.Preserve()); 245 | } 246 | internal void ClearBacklog() 247 | { 248 | var backlog = this.backlog; 249 | if (backlog != null) 250 | { 251 | foreach (var buffer in backlog) 252 | buffer.Dispose(); 253 | backlog.Clear(); 254 | } 255 | } 256 | 257 | internal async void StartProcessingIncomingFrames() 258 | { 259 | WebSocketServer.WriteStatus(ConnectionType.Client, "Processing incoming frames..."); 260 | try 261 | { 262 | await ProcessIncomingFramesAsync(null); 263 | } 264 | catch (Exception ex) 265 | { 266 | WebSocketServer.WriteStatus(ConnectionType, ex.Message); 267 | } 268 | } 269 | 270 | public bool HasBacklog => (backlog?.Count ?? 0) != 0; 271 | 272 | internal List GetBacklog() => backlog; 273 | 274 | public Task SendAsync(string message, WebSocketsFrame.FrameFlags flags = WebSocketsFrame.FrameFlags.IsFinal) 275 | { 276 | if (message == null) throw new ArgumentNullException(nameof(message)); 277 | var msg = MessageWriter.Create(message); 278 | return SendAsync(WebSocketsFrame.OpCodes.Text, ref msg, flags); 279 | } 280 | public Task SendAsync(byte[] message, WebSocketsFrame.FrameFlags flags = WebSocketsFrame.FrameFlags.IsFinal) 281 | { 282 | if (message == null) throw new ArgumentNullException(nameof(message)); 283 | var msg = MessageWriter.Create(message); 284 | return SendAsync(WebSocketsFrame.OpCodes.Binary, ref msg, flags); 285 | } 286 | public Task PingAsync(string message = null) 287 | { 288 | var msg = MessageWriter.Create(message); 289 | return SendAsync(WebSocketsFrame.OpCodes.Ping, ref msg); 290 | } 291 | public Task CloseAsync(string message = null) 292 | { 293 | if (connection.Output.Writing.IsCompleted) return Task.CompletedTask; 294 | 295 | var msg = MessageWriter.Create(message); 296 | var task = SendAsync(WebSocketsFrame.OpCodes.Close, ref msg); 297 | return task; 298 | } 299 | internal Task SendAsync(WebSocketsFrame.OpCodes opCode, ref Message message, WebSocketsFrame.FrameFlags flags = WebSocketsFrame.FrameFlags.IsFinal) 300 | { 301 | if(connection.Output.IsCompleted) 302 | { 303 | throw new InvalidOperationException("Connection has been closed"); 304 | } 305 | return SendAsyncImpl(opCode, message, flags); 306 | } 307 | 308 | public bool BufferFragments { get; set; } // TODO: flags bool 309 | public bool IsClosed => connection.Output.Writing.IsCompleted || connection.Input.Reading.IsCompleted; 310 | 311 | public event Action Closed; 312 | 313 | internal Task SendAsync(WebSocketsFrame.OpCodes opCode, ref T message, WebSocketsFrame.FrameFlags flags = WebSocketsFrame.FrameFlags.IsFinal) where T : struct, IMessageWriter 314 | { 315 | return SendAsyncImpl(opCode, message, flags); 316 | } 317 | private async Task SendAsyncImpl(WebSocketsFrame.OpCodes opCode, T message, WebSocketsFrame.FrameFlags flags) where T : struct, IMessageWriter 318 | { 319 | //TODO: come up with a better way of getting ordered access to the socket 320 | var writeLock = GetWriteSemaphore(); 321 | bool haveLock = writeLock.Wait(0); 322 | 323 | if (!haveLock) 324 | { // try to acquire asynchronously instead, then 325 | await writeLock.WaitAsync(); 326 | } 327 | try 328 | { 329 | WebSocketServer.WriteStatus(ConnectionType, $"Writing {opCode} message ({message.GetPayloadLength()} bytes)..."); 330 | await WebSocketProtocol.WriteAsync(this, opCode, flags, ref message); 331 | if (opCode == WebSocketsFrame.OpCodes.Close) connection.Output.Complete(); 332 | } 333 | finally 334 | { 335 | writeLock.Release(); 336 | } 337 | } 338 | private SemaphoreSlim GetWriteSemaphore() 339 | { 340 | var tmp = writeLock; 341 | if (tmp != null) return (SemaphoreSlim)tmp; // simple path 342 | 343 | IDisposable newSemaphore = null; 344 | try 345 | { 346 | #if VOLATILE_READ 347 | while ((tmp = Thread.VolatileRead(ref writeLock)) == null) 348 | #else 349 | while ((tmp = Interlocked.CompareExchange(ref writeLock, null, null)) == null) 350 | #endif 351 | { 352 | if (newSemaphore == null) newSemaphore = new SemaphoreSlim(1, 1); 353 | if (Interlocked.CompareExchange(ref writeLock, newSemaphore, null) == null) 354 | { 355 | tmp = newSemaphore; // success, we swapped it in 356 | newSemaphore = null; // to avoid it being disposed 357 | break; // no need to read again 358 | } 359 | } 360 | } 361 | finally 362 | { 363 | newSemaphore?.Dispose(); 364 | } 365 | return (SemaphoreSlim)tmp; 366 | } 367 | 368 | public event Func BinaryAsync, TextAsync; 369 | 370 | 371 | internal Task OnBinaryAsync(ref Message message) => OnMessage(BinaryAsync, ref message); 372 | 373 | internal Task OnTextAsync(ref Message message) => OnMessage(TextAsync, ref message); 374 | 375 | static async Task Awaited(Task task) => await task; 376 | static Task OnMessage(Func handler, ref Message message) 377 | { 378 | if (handler == null) return Task.CompletedTask; 379 | var task = handler(message); 380 | return task.IsCompleted ? task : Awaited(task); 381 | } 382 | 383 | // lazily instantiated SemaphoreSlim 384 | object writeLock; 385 | 386 | 387 | public void Dispose() 388 | { 389 | BinaryAsync = TextAsync = null; 390 | ((IDisposable)writeLock)?.Dispose(); 391 | ClearBacklog(); 392 | connection.Input.Complete(); 393 | connection.Output.Complete(); 394 | connection.Dispose(); 395 | } 396 | } 397 | 398 | 399 | } 400 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/WebSocketServer.cs: -------------------------------------------------------------------------------- 1 | using System.IO.Pipelines; 2 | using System.IO.Pipelines.Networking.Libuv; 3 | using System.IO.Pipelines.Networking.Sockets; 4 | using System.IO.Pipelines.Text.Primitives; 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Net; 12 | using System.Threading.Tasks; 13 | 14 | namespace Pipelines.WebSockets 15 | { 16 | public abstract class WebSocketServer : IDisposable 17 | { 18 | private UvTcpListener uvListener; 19 | private UvThread uvThread; 20 | private SocketListener socketListener; 21 | 22 | private IPAddress ip; 23 | private int port; 24 | public int Port => port; 25 | public IPAddress IP => ip; 26 | 27 | public bool BufferFragments { get; set; } 28 | public bool AllowClientsMissingConnectionHeaders { get; set; } = true; // stoopid browsers 29 | 30 | public WebSocketServer() 31 | { 32 | if (!BitConverter.IsLittleEndian) 33 | { 34 | throw new NotSupportedException("This code has not been tested on big-endian architectures"); 35 | } 36 | } 37 | public void Dispose() => Dispose(true); 38 | ~WebSocketServer() { Dispose(false); } 39 | protected virtual void Dispose(bool disposing) 40 | { 41 | if (disposing) 42 | { 43 | GC.SuppressFinalize(this); 44 | Stop(); 45 | } 46 | } 47 | public int ConnectionCount => connections.Count; 48 | 49 | public ValueTask CloseAllAsync(string message = null, Func predicate = null) 50 | { 51 | if (connections.IsEmpty) return new ValueTask(0); // avoid any processing 52 | return BroadcastAsync(WebSocketsFrame.OpCodes.Close, MessageWriter.Create(message, true), predicate); 53 | } 54 | public ValueTask BroadcastAsync(string message, Func predicate = null) 55 | { 56 | if (message == null) throw new ArgumentNullException(nameof(message)); 57 | if (connections.IsEmpty) return new ValueTask(0); // avoid any processing 58 | return BroadcastAsync(WebSocketsFrame.OpCodes.Text, MessageWriter.Create(message, true), predicate); 59 | } 60 | public ValueTask PingAsync(string message = null, Func predicate = null) 61 | { 62 | if (connections.IsEmpty) return new ValueTask(0); // avoid any processing 63 | return BroadcastAsync(WebSocketsFrame.OpCodes.Ping, MessageWriter.Create(message, true), predicate); 64 | } 65 | public ValueTask BroadcastAsync(byte[] message, Func predicate = null) 66 | { 67 | if (message == null) throw new ArgumentNullException(nameof(message)); 68 | if (connections.IsEmpty) return new ValueTask(0); // avoid any processing 69 | return BroadcastAsync(WebSocketsFrame.OpCodes.Binary, MessageWriter.Create(message), predicate); 70 | } 71 | private async ValueTask BroadcastAsync(WebSocketsFrame.OpCodes opCode, T message, Func predicate) 72 | where T : struct, IMessageWriter 73 | { 74 | int count = 0; 75 | foreach (var pair in connections) 76 | { 77 | var conn = pair.Key; 78 | if (!conn.IsClosed && (predicate == null || predicate(conn))) 79 | { 80 | try 81 | { 82 | var task = conn.SendAsync(opCode, ref message); 83 | if (task.Status != TaskStatus.RanToCompletion) 84 | { 85 | await task; 86 | } 87 | count++; 88 | } 89 | catch { } // not really all that bothered - they just won't get counted 90 | } 91 | } 92 | return count; 93 | } 94 | // todo: pick a more appropriate container for connection management; this insane choice is just arbitrary 95 | private ConcurrentDictionary connections = new ConcurrentDictionary(); 96 | 97 | public Task StartLibuvAsync(IPAddress ip, int port) 98 | { 99 | if (uvListener == null && socketListener == null) 100 | { 101 | uvThread = new UvThread(); 102 | uvListener = new UvTcpListener(uvThread, new IPEndPoint(ip, port)); 103 | uvListener.OnConnection(OnConnection); 104 | return uvListener.StartAsync(); 105 | } 106 | return Task.CompletedTask; 107 | } 108 | public void StartManagedSockets(IPAddress ip, int port, PipeFactory pipeFactory = null) 109 | { 110 | if (uvListener == null && socketListener == null) 111 | { 112 | socketListener = new SocketListener(pipeFactory); 113 | socketListener.OnConnection(OnConnection); 114 | socketListener.Start(new IPEndPoint(ip, port)); 115 | } 116 | } 117 | 118 | private async Task OnConnection(IPipeConnection connection) 119 | { 120 | using (connection) 121 | { 122 | WebSocketConnection socket = null; 123 | try 124 | { 125 | WriteStatus(ConnectionType.Server, "Connected"); 126 | 127 | WriteStatus(ConnectionType.Server, "Parsing http request..."); 128 | var request = await ParseHttpRequest(connection.Input); 129 | try 130 | { 131 | WriteStatus(ConnectionType.Server, "Identifying protocol..."); 132 | socket = GetProtocol(connection, ref request); 133 | WriteStatus(ConnectionType.Server, $"Protocol: {WebSocketProtocol.Name}"); 134 | WriteStatus(ConnectionType.Server, "Authenticating..."); 135 | if (!await OnAuthenticateAsync(socket, ref request.Headers)) throw new InvalidOperationException("Authentication refused"); 136 | WriteStatus(ConnectionType.Server, "Completing handshake..."); 137 | await WebSocketProtocol.CompleteServerHandshakeAsync(ref request, socket); 138 | } 139 | finally 140 | { 141 | request.Dispose(); // can't use "ref request" or "ref headers" otherwise 142 | } 143 | WriteStatus(ConnectionType.Server, "Handshake complete hook..."); 144 | await OnHandshakeCompleteAsync(socket); 145 | 146 | connections.TryAdd(socket, socket); 147 | WriteStatus(ConnectionType.Server, "Processing incoming frames..."); 148 | await socket.ProcessIncomingFramesAsync(this); 149 | WriteStatus(ConnectionType.Server, "Exiting..."); 150 | await socket.CloseAsync(); 151 | } 152 | catch (Exception ex) 153 | {// meh, bye bye broken connection 154 | try { socket?.Dispose(); } catch { } 155 | WriteStatus(ConnectionType.Server, ex.StackTrace); 156 | WriteStatus(ConnectionType.Server, ex.GetType().Name); 157 | WriteStatus(ConnectionType.Server, ex.Message); 158 | } 159 | finally 160 | { 161 | WebSocketConnection tmp; 162 | if (socket != null) connections.TryRemove(socket, out tmp); 163 | try { connection.Output.Complete(); } catch { } 164 | try { connection.Input.Complete(); } catch { } 165 | } 166 | } 167 | } 168 | 169 | [Conditional("LOGGING")] 170 | internal static void WriteStatus(ConnectionType type, string message) 171 | { 172 | #if LOGGING 173 | Console.WriteLine($"[{type}:{Environment.CurrentManagedThreadId}]: {message}"); 174 | #endif 175 | } 176 | 177 | protected internal virtual Task OnBinaryAsync(WebSocketConnection connection, ref Message message) => Task.CompletedTask; 178 | protected internal virtual Task OnTextAsync(WebSocketConnection connection, ref Message message) => Task.CompletedTask; 179 | 180 | static readonly char[] Comma = { ',' }; 181 | 182 | protected virtual ValueTask OnAuthenticateAsync(WebSocketConnection connection, ref HttpRequestHeaders headers) => new ValueTask(true); 183 | protected virtual Task OnHandshakeCompleteAsync(WebSocketConnection connection) => Task.CompletedTask; 184 | 185 | private WebSocketConnection GetProtocol(IPipeConnection connection, ref HttpRequest request) 186 | { 187 | var headers = request.Headers; 188 | string host = headers.GetAsciiString("Host"); 189 | if (string.IsNullOrEmpty(host)) 190 | { 191 | //4. The request MUST contain a |Host| header field whose value 192 | //contains /host/ plus optionally ":" followed by /port/ (when not 193 | //using the default port). 194 | throw new InvalidOperationException("host required"); 195 | } 196 | 197 | bool looksGoodEnough = false; 198 | // mozilla sends "keep-alive, Upgrade"; let's make it more forgiving 199 | var connectionParts = new HashSet(StringComparer.OrdinalIgnoreCase); 200 | if (headers.ContainsKey("Connection")) 201 | { 202 | // so for mozilla, this will be the set {"keep-alive", "Upgrade"} 203 | var parts = headers.GetAsciiString("Connection").Split(Comma); 204 | foreach (var part in parts) connectionParts.Add(part.Trim()); 205 | } 206 | if (connectionParts.Contains("Upgrade") && IsCaseInsensitiveAsciiMatch(headers.GetRaw("Upgrade").Buffer, "websocket")) 207 | { 208 | //5. The request MUST contain an |Upgrade| header field whose value 209 | //MUST include the "websocket" keyword. 210 | //6. The request MUST contain a |Connection| header field whose value 211 | //MUST include the "Upgrade" token. 212 | looksGoodEnough = true; 213 | } 214 | 215 | if (!looksGoodEnough && AllowClientsMissingConnectionHeaders) 216 | { 217 | if ((headers.ContainsKey("Sec-WebSocket-Version") && headers.ContainsKey("Sec-WebSocket-Key")) 218 | || (headers.ContainsKey("Sec-WebSocket-Key1") && headers.ContainsKey("Sec-WebSocket-Key2"))) 219 | { 220 | looksGoodEnough = true; 221 | } 222 | } 223 | 224 | if (looksGoodEnough) 225 | { 226 | //9. The request MUST include a header field with the name 227 | //|Sec-WebSocket-Version|. The value of this header field MUST be 228 | 229 | if (!headers.ContainsKey("Sec-WebSocket-Version")) 230 | { 231 | throw new NotSupportedException(); 232 | } 233 | else 234 | { 235 | var version = headers.GetRaw("Sec-WebSocket-Version").Buffer.GetUInt32(); 236 | switch (version) 237 | { 238 | 239 | case 4: 240 | case 5: 241 | case 6: 242 | case 7: 243 | case 8: // these are all early drafts 244 | case 13: // this is later drafts and RFC6455 245 | break; // looks ok 246 | default: 247 | // should issues a 400 "upgrade required" and specify Sec-WebSocket-Version - see 4.4 248 | throw new InvalidOperationException(string.Format("Sec-WebSocket-Version {0} is not supported", version)); 249 | } 250 | } 251 | } 252 | else 253 | { 254 | throw new InvalidOperationException("Request was not a web-socket upgrade request"); 255 | } 256 | //The "Request-URI" of the GET method [RFC2616] is used to identify the 257 | //endpoint of the WebSocket connection, both to allow multiple domains 258 | //to be served from one IP address and to allow multiple WebSocket 259 | //endpoints to be served by a single server. 260 | var socket = new WebSocketConnection(connection, ConnectionType.Server); 261 | socket.Host = host; 262 | socket.BufferFragments = BufferFragments; 263 | // Some early drafts used the latter, so we'll allow it as a fallback 264 | // in particular, two drafts of version "8" used (separately) **both**, 265 | // so we can't rely on the version for this (hybi-10 vs hybi-11). 266 | // To make it even worse, hybi-00 used Origin, so it is all over the place! 267 | socket.Origin = headers.GetAsciiString("Origin") ?? headers.GetAsciiString("Sec-WebSocket-Origin"); 268 | socket.Protocol = headers.GetAsciiString("Sec-WebSocket-Protocol"); 269 | socket.RequestLine = request.Path.Buffer.GetAsciiString(); 270 | return socket; 271 | } 272 | 273 | 274 | 275 | 276 | 277 | private enum ParsingState 278 | { 279 | StartLine, 280 | Headers 281 | } 282 | internal static async Task ParseHttpResponse(IPipeReader input) 283 | { 284 | return new HttpResponse(await ParseHttpRequest(input)); 285 | } 286 | static int Peek(ref ReadableBuffer buffer) => buffer.Length == 0 ? -1 : buffer.First.Span[0]; 287 | internal static async Task ParseHttpRequest(IPipeReader input) 288 | { 289 | PreservedBuffer Method = default(PreservedBuffer), Path = default(PreservedBuffer), HttpVersion = default(PreservedBuffer); 290 | Dictionary Headers = new Dictionary(); 291 | try 292 | { 293 | ParsingState _state = ParsingState.StartLine; 294 | bool needMoreData = true; 295 | while (needMoreData) 296 | { 297 | var read = await input.ReadAsync(); 298 | var buffer = read.Buffer; 299 | 300 | var consumed = buffer.Start; 301 | needMoreData = true; 302 | 303 | try 304 | { 305 | if (buffer.IsEmpty && read.IsCompleted) 306 | { 307 | throw new EndOfStreamException(); 308 | } 309 | 310 | if (_state == ParsingState.StartLine) 311 | { 312 | // Find \n 313 | ReadCursor delim; 314 | ReadableBuffer startLine; 315 | if (!buffer.TrySliceTo((byte)'\r', (byte)'\n', out startLine, out delim)) 316 | { 317 | continue; 318 | } 319 | 320 | 321 | // Move the buffer to the rest 322 | buffer = buffer.Slice(delim).Slice(2); 323 | 324 | ReadableBuffer method; 325 | if (!startLine.TrySliceTo((byte)' ', out method, out delim)) 326 | { 327 | throw new Exception(); 328 | } 329 | 330 | Method = method.Preserve(); 331 | 332 | // Skip ' ' 333 | startLine = startLine.Slice(delim).Slice(1); 334 | 335 | ReadableBuffer path; 336 | if (!startLine.TrySliceTo((byte)' ', out path, out delim)) 337 | { 338 | throw new Exception(); 339 | } 340 | 341 | Path = path.Preserve(); 342 | 343 | // Skip ' ' 344 | startLine = startLine.Slice(delim).Slice(1); 345 | 346 | var httpVersion = startLine; 347 | if (httpVersion.IsEmpty) 348 | { 349 | throw new Exception(); 350 | } 351 | 352 | HttpVersion = httpVersion.Preserve(); 353 | 354 | _state = ParsingState.Headers; 355 | consumed = buffer.Start; 356 | } 357 | 358 | // Parse headers 359 | // key: value\r\n 360 | 361 | while (!buffer.IsEmpty) 362 | { 363 | var ch = Peek(ref buffer); 364 | 365 | if (ch == -1) 366 | { 367 | break; 368 | } 369 | 370 | if (ch == '\r') 371 | { 372 | // Check for final CRLF. 373 | buffer = buffer.Slice(1); 374 | ch = Peek(ref buffer); 375 | buffer = buffer.Slice(1); 376 | 377 | if (ch == -1) 378 | { 379 | break; 380 | } 381 | else if (ch == '\n') 382 | { 383 | consumed = buffer.Start; 384 | needMoreData = false; 385 | break; 386 | } 387 | 388 | // Headers don't end in CRLF line. 389 | throw new Exception(); 390 | } 391 | 392 | var headerName = default(ReadableBuffer); 393 | var headerValue = default(ReadableBuffer); 394 | 395 | // End of the header 396 | // \n 397 | ReadCursor delim; 398 | ReadableBuffer headerPair; 399 | if (!buffer.TrySliceTo((byte)'\n', out headerPair, out delim)) 400 | { 401 | break; 402 | } 403 | 404 | buffer = buffer.Slice(delim).Slice(1); 405 | 406 | // : 407 | if (!headerPair.TrySliceTo((byte)':', out headerName, out delim)) 408 | { 409 | throw new Exception(); 410 | } 411 | 412 | headerName = headerName.TrimStart(); 413 | headerPair = headerPair.Slice(delim).Slice(1); 414 | 415 | // \r 416 | if (!headerPair.TrySliceTo((byte)'\r', out headerValue, out delim)) 417 | { 418 | // Bad request 419 | throw new Exception(); 420 | } 421 | 422 | headerValue = headerValue.TrimStart(); 423 | Headers[ToHeaderKey(ref headerName)] = headerValue.Preserve(); 424 | 425 | // Move the consumed 426 | consumed = buffer.Start; 427 | } 428 | } 429 | finally 430 | { 431 | input.Advance(consumed); 432 | } 433 | } 434 | var result = new HttpRequest(Method, Path, HttpVersion, Headers); 435 | Method = Path = HttpVersion = default(PreservedBuffer); 436 | Headers = null; 437 | return result; 438 | } 439 | finally 440 | { 441 | Method.Dispose(); 442 | Path.Dispose(); 443 | HttpVersion.Dispose(); 444 | if (Headers != null) 445 | { 446 | foreach (var pair in Headers) 447 | pair.Value.Dispose(); 448 | } 449 | } 450 | } 451 | 452 | static readonly string[] CommonHeaders = new string[] 453 | { 454 | "Accept", 455 | "Accept-Encoding", 456 | "Accept-Language", 457 | "Cache-Control", 458 | "Connection", 459 | "Cookie", 460 | "Host", 461 | "Origin", 462 | "Pragma", 463 | "Sec-WebSocket-Extensions", 464 | "Sec-WebSocket-Key", 465 | "Sec-WebSocket-Key1", 466 | "Sec-WebSocket-Key2", 467 | "Sec-WebSocket-Accept", 468 | "Sec-WebSocket-Origin", 469 | "Sec-WebSocket-Protocol", 470 | "Sec-WebSocket-Version", 471 | "Upgrade", 472 | "Upgrade-Insecure-Requests", 473 | "User-Agent" 474 | }, CommonHeadersLowerCaseInvariant = CommonHeaders.Select(s => s.ToLowerInvariant()).ToArray(); 475 | 476 | private static string ToHeaderKey(ref ReadableBuffer headerName) 477 | { 478 | var lowerCaseHeaders = CommonHeadersLowerCaseInvariant; 479 | for (int i = 0; i < lowerCaseHeaders.Length; i++) 480 | { 481 | if (IsCaseInsensitiveAsciiMatch(headerName, lowerCaseHeaders[i])) return CommonHeaders[i]; 482 | } 483 | 484 | return headerName.GetAsciiString(); 485 | } 486 | 487 | private static bool IsCaseInsensitiveAsciiMatch(ReadableBuffer bufferUnknownCase, string valueLowerCase) 488 | { 489 | if (bufferUnknownCase.Length != valueLowerCase.Length) return false; 490 | int charIndex = 0; 491 | foreach (var memory in bufferUnknownCase) 492 | { 493 | var span = memory.Span; 494 | for (int spanIndex = 0; spanIndex < span.Length; spanIndex++) 495 | { 496 | char x = (char)span[spanIndex], y = valueLowerCase[charIndex++]; 497 | if (x != y && char.ToLowerInvariant(x) != y) return false; 498 | } 499 | } 500 | return true; 501 | } 502 | 503 | public void Stop() 504 | { 505 | uvThread?.Dispose(); 506 | uvListener = null; 507 | uvThread = null; 508 | 509 | socketListener?.Stop(); 510 | socketListener?.Dispose(); 511 | socketListener = null; 512 | } 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/WebSocketsFrame.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Pipelines; 3 | using System.Numerics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Pipelines.WebSockets 7 | { 8 | public struct WebSocketsFrame 9 | { 10 | public override string ToString() 11 | { 12 | return OpCode.ToString() + ": " + PayloadLength.ToString() + " bytes (" + Flags.ToString() + ")"; 13 | } 14 | private readonly byte header; 15 | private readonly byte header2; 16 | [Flags] 17 | public enum FrameFlags : byte 18 | { 19 | IsFinal = 128, 20 | Reserved1 = 64, 21 | Reserved2 = 32, 22 | Reserved3 = 16, 23 | None = 0 24 | } 25 | public enum OpCodes : byte 26 | { 27 | Continuation = 0, 28 | Text = 1, 29 | Binary = 2, 30 | // 3-7 reserved for non-control op-codes 31 | Close = 8, 32 | Ping = 9, 33 | Pong = 10, 34 | // 11-15 reserved for control op-codes 35 | } 36 | public WebSocketsFrame(byte header, bool isMasked, int maskLittleEndian, int payloadLength) 37 | { 38 | this.header = header; 39 | header2 = (byte)(isMasked ? 1 : 0); 40 | PayloadLength = payloadLength; 41 | MaskLittleEndian = isMasked ? maskLittleEndian : 0; 42 | } 43 | public bool IsMasked => (header2 & 1) != 0; 44 | private bool HasFlag(FrameFlags flag) => (header & (byte)flag) != 0; 45 | 46 | 47 | //private static readonly int vectorWidth = Vector.Count; 48 | //private static readonly int vectorShift = (int)Math.Log(vectorWidth, 2); 49 | //private static readonly int vectorOverflow = ~(~0 << vectorShift); 50 | //private static readonly bool isHardwareAccelerated = Vector.IsHardwareAccelerated; 51 | 52 | private static unsafe uint ApplyMask(Span span, uint mask) 53 | { 54 | // Vector widths 55 | if (Vector.IsHardwareAccelerated) 56 | { 57 | var vectors = span.NonPortableCast>(); 58 | var vectorMask = new Vector(mask); 59 | for(int i = 0; i < vectors.Length; i++) 60 | { 61 | vectors[i] ^= vectorMask; 62 | } 63 | span = span.Slice(vectors.Length * Vector.Count); 64 | } 65 | 66 | // qword widths 67 | if((span.Length & ~7) != 0) 68 | { 69 | ulong mask8 = mask; 70 | mask8 = (mask8 << 32) | mask8; 71 | var ulongs = span.NonPortableCast(); 72 | for (int i = 0; i < ulongs.Length; i++) 73 | { 74 | ulongs[i] ^= mask8; 75 | } 76 | span = span.Slice(ulongs.Length << 3); 77 | } 78 | 79 | // Now there can be at most 7 bytes left; loop 80 | for(int i = 0; i < span.Length; i++) 81 | { 82 | var b = mask & 0xFF; 83 | mask = (mask >> 8) | (b << 24); 84 | span[i] ^= (byte)b; 85 | } 86 | return mask; 87 | } 88 | 89 | internal unsafe static void ApplyMask(ref ReadableBuffer buffer, int maskLittleEndian) 90 | { 91 | if (maskLittleEndian == 0) return; 92 | 93 | uint m = (uint)maskLittleEndian; 94 | foreach(var span in buffer) 95 | { 96 | // note: this is an optimized xor implementation using Vector, qword hacks, etc; unsafe access 97 | // to the span is warranted 98 | // note: in this case, we know we're talking about memory from the pool, so we know it is already pinned 99 | m = ApplyMask(span.Span, m); 100 | } 101 | } 102 | 103 | 104 | public bool IsControlFrame { get { return (header & (byte)OpCodes.Close) != 0; } } 105 | public int MaskLittleEndian { get; } 106 | public OpCodes OpCode => (OpCodes)(header & 15); 107 | public FrameFlags Flags => (FrameFlags)(header & ~15); 108 | public bool IsFinal { get { return HasFlag(FrameFlags.IsFinal); } } 109 | public bool Reserved1 { get { return HasFlag(FrameFlags.Reserved1); } } 110 | public bool Reserved2 { get { return HasFlag(FrameFlags.Reserved2); } } 111 | public bool Reserved3 { get { return HasFlag(FrameFlags.Reserved3); } } 112 | 113 | public int PayloadLength { get; } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Pipelines.WebSockets/WebSocketsProtocol.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Binary; 3 | using System.Runtime.CompilerServices; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.IO.Pipelines.Networking.Sockets; 8 | using System.IO.Pipelines; 9 | using System.IO.Pipelines.Text.Primitives; 10 | using System.Buffers; 11 | using System.Numerics; 12 | 13 | namespace Pipelines.WebSockets 14 | { 15 | internal static class WebSocketProtocol 16 | { 17 | public static string Name => "RFC6455"; 18 | static readonly byte[] 19 | StandardPrefixBytes = Encoding.ASCII.GetBytes("HTTP/1.1 101 Switching Protocols\r\n" 20 | + "Upgrade: websocket\r\n" 21 | + "Connection: Upgrade\r\n" 22 | + "Sec-WebSocket-Accept: "), 23 | StandardPostfixBytes = Encoding.ASCII.GetBytes("\r\n\r\n"); 24 | internal static Task CompleteServerHandshakeAsync(ref HttpRequest request, WebSocketConnection socket) 25 | { 26 | var key = request.Headers.GetRaw("Sec-WebSocket-Key"); 27 | 28 | var connection = socket.Connection; 29 | 30 | var buffer = connection.Output.Alloc(StandardPrefixBytes.Length + 31 | SecResponseLength + StandardPostfixBytes.Length); 32 | 33 | buffer.Write(StandardPrefixBytes); 34 | // RFC6455 logic to prove that we know how to web-socket 35 | buffer.Ensure(SecResponseLength); 36 | int bytes = ComputeReply(key.Buffer, buffer.Buffer.Span); 37 | if (bytes != SecResponseLength) 38 | { 39 | throw new InvalidOperationException($"Incorrect response token length; expected {SecResponseLength}, got {bytes}"); 40 | } 41 | buffer.Advance(SecResponseLength); 42 | buffer.Write(StandardPostfixBytes); 43 | 44 | return buffer.FlushAsync().AsTask(); 45 | } 46 | 47 | private static readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); 48 | private static readonly Random cheapRandom = new Random(); 49 | public static int CreateMask() 50 | { 51 | int mask = cheapRandom.Next(); 52 | if (mask == 0) mask = 0x2211BBFF; // just something non zero 53 | return mask; 54 | } 55 | static readonly byte[] maskBytesBuffer = new byte[4]; 56 | public static void GetRandomBytes(byte[] buffer) 57 | { 58 | lock (rng) // thread safety not explicit 59 | { 60 | rng.GetBytes(buffer); 61 | } 62 | } 63 | 64 | internal static async Task ClientHandshake(IPipeConnection connection, Uri uri, string origin, string protocol) 65 | { 66 | WebSocketServer.WriteStatus(ConnectionType.Client, "Writing client handshake..."); 67 | // write the outbound request portion of the handshake 68 | var output = connection.Output.Alloc(); 69 | output.Write(GET); 70 | output.WriteAsciiString(uri.PathAndQuery); 71 | output.Write(HTTP_Host); 72 | output.WriteAsciiString(uri.Host); 73 | output.Write(UpgradeConnectionKey); 74 | 75 | byte[] challengeKey = new byte[WebSocketProtocol.SecResponseLength]; 76 | 77 | GetRandomBytes(challengeKey); // we only want the first 16 for entropy, but... meh 78 | output.Ensure(WebSocketProtocol.SecRequestLength); 79 | int bytesWritten = Base64.Encode(new ReadOnlySpan(challengeKey, 0, 16), output.Buffer.Span); 80 | // now cheekily use that data we just wrote the the output buffer 81 | // as a source to compute the expected bytes, and store them back 82 | // into challengeKey; sneaky! 83 | WebSocketProtocol.ComputeReply( 84 | output.Buffer.Slice(0, WebSocketProtocol.SecRequestLength).Span, 85 | challengeKey); 86 | output.Advance(bytesWritten); 87 | output.Write(CRLF); 88 | 89 | if (!string.IsNullOrWhiteSpace(origin)) 90 | { 91 | output.WriteAsciiString("Origin: "); 92 | output.WriteAsciiString(origin); 93 | output.Write(CRLF); 94 | } 95 | if (!string.IsNullOrWhiteSpace(protocol)) 96 | { 97 | output.WriteAsciiString("Sec-WebSocket-Protocol: "); 98 | output.WriteAsciiString(protocol); 99 | output.Write(CRLF); 100 | } 101 | output.Write(WebSocketVersion); 102 | output.Write(CRLF); // final CRLF to end the HTTP request 103 | await output.FlushAsync(); 104 | 105 | WebSocketServer.WriteStatus(ConnectionType.Client, "Parsing response to client handshake..."); 106 | using (var resp = await WebSocketServer.ParseHttpResponse(connection.Input)) 107 | { 108 | if (!resp.HttpVersion.Equals(ExpectedHttpVersion) 109 | || !resp.StatusCode.Equals(ExpectedStatusCode) 110 | || !resp.StatusText.Equals(ExpectedStatusText) 111 | || !resp.Headers.GetRaw("Upgrade").Equals(ExpectedUpgrade) 112 | || !resp.Headers.GetRaw("Connection").Equals(ExpectedConnection)) 113 | { 114 | throw new InvalidOperationException("Not a web-socket server"); 115 | } 116 | 117 | var accept = resp.Headers.GetRaw("Sec-WebSocket-Accept"); 118 | if (!accept.Equals(challengeKey)) 119 | { 120 | throw new InvalidOperationException("Sec-WebSocket-Accept mismatch"); 121 | } 122 | 123 | protocol = resp.Headers.GetAsciiString("Sec-WebSocket-Protocol"); 124 | } 125 | 126 | var webSocket = new WebSocketConnection(connection, ConnectionType.Client) 127 | { 128 | Host = uri.Host, 129 | RequestLine = uri.OriginalString, 130 | Origin = origin, 131 | Protocol = protocol 132 | }; 133 | webSocket.StartProcessingIncomingFrames(); 134 | return webSocket; 135 | } 136 | static readonly byte[] GET = Encoding.ASCII.GetBytes("GET "), 137 | HTTP_Host = Encoding.ASCII.GetBytes(" HTTP/1.1\r\nHost: "), 138 | UpgradeConnectionKey = Encoding.ASCII.GetBytes("\r\nUpgrade: websocket\r\n" 139 | + "Connection: Upgrade\r\n" 140 | + "Sec-WebSocket-Key: "), 141 | WebSocketVersion = Encoding.ASCII.GetBytes("Sec-WebSocket-Version: 13\r\n"), 142 | CRLF = { (byte)'\r', (byte)'\n' }; 143 | 144 | static readonly byte[] 145 | ExpectedHttpVersion = Encoding.ASCII.GetBytes("HTTP/1.1"), 146 | ExpectedStatusCode = Encoding.ASCII.GetBytes("101"), 147 | ExpectedStatusText = Encoding.ASCII.GetBytes("Switching Protocols"), 148 | ExpectedUpgrade = Encoding.ASCII.GetBytes("websocket"), 149 | ExpectedConnection = Encoding.ASCII.GetBytes("Upgrade"); 150 | 151 | 152 | static readonly byte[] WebSocketKeySuffixBytes = Encoding.ASCII.GetBytes("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); 153 | 154 | internal const int SecRequestLength = 24; 155 | internal const int SecResponseLength = 28; 156 | internal unsafe static int ComputeReply(ReadableBuffer key, Span destination) 157 | { 158 | if(key.IsSingleSpan) 159 | { 160 | return ComputeReply(key.First.Span, destination); 161 | } 162 | if (key.Length != SecRequestLength) throw new ArgumentException("Invalid key length", nameof(key)); 163 | byte* ptr = stackalloc byte[SecRequestLength]; 164 | var span = new Span(ptr, SecRequestLength); 165 | key.CopyTo(span); 166 | return ComputeReply(destination, destination); 167 | } 168 | internal static int ComputeReply(ReadOnlySpan key, Span destination) 169 | { 170 | //To prove that the handshake was received, the server has to take two 171 | //pieces of information and combine them to form a response. The first 172 | //piece of information comes from the |Sec-WebSocket-Key| header field 173 | //in the client handshake: 174 | 175 | // Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 176 | 177 | //For this header field, the server has to take the value (as present 178 | //in the header field, e.g., the base64-encoded [RFC4648] version minus 179 | //any leading and trailing whitespace) and concatenate this with the 180 | //Globally Unique Identifier (GUID, [RFC4122]) "258EAFA5-E914-47DA- 181 | //95CA-C5AB0DC85B11" in string form, which is unlikely to be used by 182 | //network endpoints that do not understand the WebSocket Protocol. A 183 | //SHA-1 hash (160 bits) [FIPS.180-3], base64-encoded (see Section 4 of 184 | //[RFC4648]), of this concatenation is then returned in the server's 185 | //handshake. 186 | 187 | if (key.Length != SecRequestLength) throw new ArgumentException("Invalid key length", nameof(key)); 188 | 189 | byte[] arr = new byte[SecRequestLength + WebSocketKeySuffixBytes.Length]; 190 | key.TryCopyTo(arr); 191 | Buffer.BlockCopy( // append the magic number from RFC6455 192 | WebSocketKeySuffixBytes, 0, 193 | arr, SecRequestLength, 194 | WebSocketKeySuffixBytes.Length); 195 | 196 | // compute the hash 197 | using (var sha = SHA1.Create()) 198 | { 199 | var hash = sha.ComputeHash(arr, 0, 200 | SecRequestLength + WebSocketKeySuffixBytes.Length); 201 | 202 | return Base64.Encode(hash, destination); 203 | } 204 | } 205 | 206 | 207 | internal static void WriteFrameHeader(ref WritableBuffer output, WebSocketsFrame.FrameFlags flags, WebSocketsFrame.OpCodes opCode, int payloadLength, int mask) 208 | { 209 | output.Ensure(MaxHeaderLength); 210 | 211 | int index = 0; 212 | var span = output.Buffer.Span; 213 | 214 | span[index++] = (byte)(((int)flags & 240) | ((int)opCode & 15)); 215 | if (payloadLength > ushort.MaxValue) 216 | { // write as a 64-bit length 217 | span[index++] = (byte)((mask != 0 ? 128 : 0) | 127); 218 | span.Slice(index).Write((uint)0); 219 | span.Slice(index + 4).Write(ToNetworkByteOrder((uint)payloadLength)); 220 | index += 8; 221 | } 222 | else if (payloadLength > 125) 223 | { // write as a 16-bit length 224 | span[index++] = (byte)((mask != 0 ? 128 : 0) | 126); 225 | span.Slice(index).Write(ToNetworkByteOrder((ushort)payloadLength)); 226 | index += 2; 227 | } 228 | else 229 | { // write in the header 230 | span[index++] = (byte)((mask != 0 ? 128 : 0) | payloadLength); 231 | } 232 | if (mask != 0) 233 | { 234 | span.Slice(index).Write(mask); 235 | index += 4; 236 | } 237 | output.Advance(index); 238 | } 239 | 240 | // note assumes little endian architecture 241 | private static ulong ToNetworkByteOrder(uint value) 242 | => (value >> 24) 243 | | ((value >> 8) & 0xFF00) 244 | | ((value << 8) & 0xFF0000) 245 | | ((value << 24) & 0xFF000000); 246 | 247 | private static ushort ToNetworkByteOrder(ushort value) 248 | => (ushort)(value >> 8 | value << 8); 249 | 250 | internal const int MaxHeaderLength = 14; 251 | // the `unsafe` here is so that in the "multiple spans, header crosses spans", we can use stackalloc to 252 | // collate the header bytes in one place, and pass that down for analysis 253 | internal unsafe static bool TryReadFrameHeader(ref ReadableBuffer buffer, out WebSocketsFrame frame) 254 | { 255 | int bytesAvailable = buffer.Length; 256 | if (bytesAvailable < 2) 257 | { 258 | frame = default(WebSocketsFrame); 259 | return false; // can't read that; frame takes at minimum two bytes 260 | } 261 | 262 | var firstSpan = buffer.First; 263 | if (buffer.IsSingleSpan || firstSpan.Length >= MaxHeaderLength) 264 | { 265 | return TryReadFrameHeader(firstSpan.Length, firstSpan.Span, ref buffer, out frame); 266 | } 267 | else 268 | { 269 | // header is at most 14 bytes; can afford the stack for that - but note that if we aim for 16 bytes instead, 270 | // we will usually benefit from using 2 qword copies 271 | byte* header = stackalloc byte[16]; 272 | var slice = buffer.Slice(0, Math.Min(16, bytesAvailable)); 273 | var headerSpan = new Span(header, slice.Length); 274 | slice.CopyTo(headerSpan); 275 | 276 | // note that we're using the "slice" above to preview the header, but we 277 | // still want to pass the *original* buffer down below, so that we can 278 | // check the overall length (payload etc) 279 | return TryReadFrameHeader(slice.Length, headerSpan, ref buffer, out frame); 280 | } 281 | } 282 | internal static bool TryReadFrameHeader(int inspectableBytes, Span header, ref ReadableBuffer buffer, out WebSocketsFrame frame) 283 | { 284 | bool masked = (header[1] & 128) != 0; 285 | int tmp = header[1] & 127; 286 | int headerLength, maskOffset, payloadLength; 287 | switch (tmp) 288 | { 289 | case 126: 290 | headerLength = masked ? 8 : 4; 291 | if (inspectableBytes < headerLength) 292 | { 293 | frame = default(WebSocketsFrame); 294 | return false; 295 | } 296 | payloadLength = (header[2] << 8) | header[3]; 297 | maskOffset = 4; 298 | break; 299 | case 127: 300 | headerLength = masked ? 14 : 10; 301 | if (inspectableBytes < headerLength) 302 | { 303 | frame = default(WebSocketsFrame); 304 | return false; 305 | } 306 | int big = header.Slice(2).ReadBigEndian(), little = header.Slice(6).ReadBigEndian(); 307 | if (big != 0 || little < 0) throw new ArgumentOutOfRangeException(); // seriously, we're not going > 2GB 308 | payloadLength = little; 309 | maskOffset = 10; 310 | break; 311 | default: 312 | headerLength = masked ? 6 : 2; 313 | if (inspectableBytes < headerLength) 314 | { 315 | frame = default(WebSocketsFrame); 316 | return false; 317 | } 318 | payloadLength = tmp; 319 | maskOffset = 2; 320 | break; 321 | } 322 | if (buffer.Length < headerLength + payloadLength) 323 | { 324 | frame = default(WebSocketsFrame); 325 | return false; // frame (header+body) isn't intact 326 | } 327 | 328 | 329 | frame = new WebSocketsFrame(header[0], masked, 330 | masked ? header.Slice(maskOffset).ReadLittleEndian() : 0, 331 | payloadLength); 332 | buffer = buffer.Slice(headerLength); // header is fully consumed now 333 | return true; 334 | } 335 | internal static Task WriteAsync(WebSocketConnection connection, 336 | WebSocketsFrame.OpCodes opCode, 337 | WebSocketsFrame.FrameFlags flags, ref T message) 338 | where T : struct, IMessageWriter 339 | { 340 | int payloadLength = message.GetPayloadLength(); 341 | var buffer = connection.Connection.Output.Alloc(MaxHeaderLength + payloadLength); 342 | int mask = connection.ConnectionType == ConnectionType.Client ? CreateMask() : 0; 343 | WriteFrameHeader(ref buffer, WebSocketsFrame.FrameFlags.IsFinal, opCode, payloadLength, mask); 344 | if (payloadLength != 0) 345 | { 346 | if (mask == 0) { message.WritePayload(buffer); } 347 | else 348 | { 349 | var payloadStart = buffer.AsReadableBuffer().End; 350 | message.WritePayload(buffer); 351 | var payload = buffer.AsReadableBuffer().Slice(payloadStart); // note that this is a different AsReadableBuffer; call twice is good 352 | WebSocketsFrame.ApplyMask(ref payload, mask); 353 | } 354 | } 355 | return buffer.FlushAsync().AsTask(); 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/SampleServer/EchoServer.cs: -------------------------------------------------------------------------------- 1 | using Channels; 2 | using Channels.Networking.Libuv; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net; 6 | 7 | namespace SampleServer 8 | { 9 | public class EchoServer : IDisposable 10 | { 11 | private UvTcpListener listener; 12 | private UvThread thread; 13 | public void Start(IPEndPoint endpoint) 14 | { 15 | if (listener == null) 16 | { 17 | thread = new UvThread(); 18 | listener = new UvTcpListener(thread, endpoint); 19 | listener.OnConnection(OnConnection); 20 | listener.Start(); 21 | } 22 | } 23 | 24 | private List connections = new List(); 25 | 26 | public int CloseAllConnections(Exception error = null) 27 | { 28 | UvTcpConnection[] arr; 29 | lock(connections) 30 | { 31 | arr = connections.ToArray(); // lazy 32 | } 33 | int count = 0; 34 | foreach (var conn in arr) 35 | { 36 | Close(conn, error); 37 | count++; 38 | } 39 | return count; 40 | } 41 | private async void OnConnection(UvTcpConnection connection) 42 | { 43 | try 44 | { 45 | Console.WriteLine("[server] OnConnection entered"); 46 | lock(connections) 47 | { 48 | connections.Add(connection); 49 | } 50 | while(true) 51 | { 52 | ReadableBuffer request; 53 | 54 | Console.WriteLine("[server] awaiting input..."); 55 | try 56 | { 57 | request = await connection.Input.ReadAsync(); 58 | } 59 | finally 60 | { 61 | Console.WriteLine("[server] await completed"); 62 | } 63 | if (request.IsEmpty && connection.Input.Reading.IsCompleted) break; 64 | 65 | int len = request.Length; 66 | Console.WriteLine($"[server] echoing {len} bytes..."); 67 | var response = connection.Output.Alloc(); 68 | response.Append(ref request); 69 | await response.FlushAsync(); 70 | Console.WriteLine($"[server] echoed"); 71 | connection.Input.Advance(request.End); 72 | } 73 | Close(connection); 74 | } 75 | catch(Exception ex) 76 | { 77 | Program.WriteError(ex); 78 | } 79 | finally 80 | { 81 | lock(connections) 82 | { 83 | connections.Remove(connection); 84 | } 85 | Console.WriteLine("[server] OnConnection exited"); 86 | } 87 | } 88 | 89 | private void Close(UvTcpConnection connection, Exception error = null) 90 | { 91 | Console.WriteLine("[server] closing connection..."); 92 | connection.Output.Complete(error); 93 | connection.Input.Complete(error); 94 | Console.WriteLine("[server] connection closed"); 95 | } 96 | 97 | public void Stop() 98 | { 99 | CloseAllConnections(); 100 | listener?.Stop(); 101 | thread?.Dispose(); 102 | listener = null; 103 | thread = null; 104 | } 105 | public void Dispose() => Dispose(true); 106 | private void Dispose(bool disposing) 107 | { 108 | if (disposing) Stop(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/SampleServer/Program.cs: -------------------------------------------------------------------------------- 1 | using Channels; 2 | using Channels.Networking.Libuv; 3 | using Channels.Networking.Sockets; 4 | using Channels.Text.Primitives; 5 | using Channels.WebSockets; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Diagnostics; 9 | using System.Net; 10 | using System.Net.WebSockets; 11 | using System.Reflection; 12 | using System.Text; 13 | using System.Text.RegularExpressions; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | 17 | namespace SampleServer 18 | { 19 | public static class Program 20 | { 21 | class EchoWebSocketServer : WebSocketServer 22 | { 23 | protected override Task OnTextAsync(WebSocketConnection connection, ref Message message) 24 | { 25 | if (logging) 26 | { 27 | Console.WriteLine($"Received {message.GetPayloadLength()} bytes: {message.GetText()} (final: {message.IsFinal})"); 28 | } 29 | return base.OnTextAsync(connection, ref message); 30 | } 31 | } 32 | static bool logging = true; 33 | 34 | private async static void Echo(IChannel channel) 35 | { 36 | try 37 | { 38 | while (true) 39 | { 40 | ReadableBuffer request = await channel.Input.ReadAsync(); 41 | if (request.IsEmpty && channel.Input.Reading.IsCompleted) 42 | { 43 | channel.Input.Advance(request.End); 44 | break; 45 | } 46 | 47 | int len = request.Length; 48 | var response = channel.Output.Alloc(); 49 | response.Append(ref request); 50 | await response.FlushAsync(); 51 | channel.Input.Advance(request.End); 52 | } 53 | channel.Input.Complete(); 54 | channel.Output.Complete(); 55 | } 56 | catch (Exception ex) 57 | { 58 | if (!(channel.Input?.Reading?.IsCompleted ?? true)) channel.Input.Complete(ex); 59 | if (!(channel.Output?.Writing?.IsCompleted ?? true)) channel.Output.Complete(ex); 60 | } 61 | finally 62 | { 63 | channel?.Dispose(); 64 | } 65 | } 66 | 67 | class WrappedEchoWebSocketServer : WebSocketServer 68 | { 69 | private ChannelFactory _factory; 70 | public WrappedEchoWebSocketServer(ChannelFactory factory) 71 | { 72 | _factory = factory; 73 | } 74 | protected override Task OnHandshakeCompleteAsync(WebSocketConnection connection) 75 | { 76 | WriteStatus("[server] Wrapping as channel..."); 77 | var channel = new WebSocketChannel(connection, _factory); 78 | WriteStatus("[server] Initiating channel-based echo..."); 79 | Echo(channel); 80 | return base.OnHandshakeCompleteAsync(connection); 81 | } 82 | 83 | } 84 | static void Main2() 85 | { 86 | using (var factory = new ChannelFactory()) 87 | using (var server = new WrappedEchoWebSocketServer(factory)) 88 | { 89 | Console.WriteLine("Starting server..."); 90 | server.StartManagedSockets(IPAddress.Loopback, 6080, factory); 91 | RunClientAsync(factory); 92 | 93 | Console.WriteLine("Press any key"); 94 | Console.ReadKey(); 95 | } 96 | } 97 | static async void RunClientAsync(ChannelFactory channelFactory) 98 | { 99 | Console.WriteLine("Creating client..."); 100 | using (var client = await WebSocketConnection.ConnectAsync("ws://127.0.0.1:6080/", channelFactory: channelFactory)) 101 | { 102 | Console.WriteLine("Wrapping client as channel..."); 103 | var channel = new WebSocketChannel(client, channelFactory); 104 | 105 | string message = "Hello world!"; 106 | Console.WriteLine($"Sending '{message}' via a wrapped channel..."); 107 | string reply = await SendMessageAndWaitForReply(channel, message); 108 | Console.WriteLine($"Got back: '{reply}'"); 109 | } 110 | } 111 | private static async Task SendMessageAndWaitForReply(IChannel channel, string message) 112 | { 113 | var output = channel.Output.Alloc(); 114 | output.WriteUtf8String(message); 115 | await output.FlushAsync(); 116 | channel.Output.Complete(); 117 | 118 | while (true) 119 | { 120 | var input = await channel.Input.ReadAsync(); 121 | // wait for the end of the data before processing anything 122 | if (channel.Input.Reading.IsCompleted) 123 | { 124 | string reply = input.GetUtf8String(); 125 | channel.Input.Advance(input.End); 126 | return reply; 127 | } 128 | else 129 | { 130 | channel.Input.Advance(input.Start, input.End); 131 | } 132 | } 133 | } 134 | 135 | static int Main() 136 | { 137 | try 138 | { 139 | TaskScheduler.UnobservedTaskException += (sender, args) => 140 | { 141 | Console.WriteLine($"{nameof(TaskScheduler)}.{nameof(TaskScheduler.UnobservedTaskException)}"); 142 | args.SetObserved(); 143 | WriteError(args.Exception); 144 | }; 145 | #if NET451 146 | AppDomain.CurrentDomain.UnhandledException += (sender, args) => 147 | { 148 | Console.WriteLine($"{nameof(AppDomain)}.{nameof(AppDomain.UnhandledException)}"); 149 | WriteError(args.ExceptionObject as Exception); 150 | }; 151 | #endif 152 | WriteAssemblyVersion(typeof(ReadableBuffer)); 153 | WriteAssemblyVersion(typeof(UvTcpListener)); 154 | WriteAssemblyVersion(typeof(SocketListener)); 155 | WriteAssemblyVersion(typeof(ReadableBufferExtensions)); 156 | 157 | // TestOpenAndCloseListener(); 158 | // RunBasicEchoServer(); 159 | // RunWebSocketServer(ChannelProvider.Libuv); 160 | RunWebSocketServer(ChannelProvider.ManagedSockets); 161 | CollectGarbage(); 162 | return 0; 163 | } catch(Exception ex) 164 | { 165 | WriteError(ex); 166 | return -1; 167 | } 168 | } 169 | 170 | private static void TestOpenAndCloseListener() 171 | { 172 | var thread = new UvThread(); 173 | var ep = new IPEndPoint(IPAddress.Loopback, 5003); 174 | var listener = new UvTcpListener(thread, ep); 175 | listener.OnConnection(_ => Console.WriteLine("Hi and bye")); 176 | listener.Start(); 177 | Console.WriteLine("Listening..."); 178 | Thread.Sleep(1000); 179 | Console.WriteLine("Stopping listener..."); 180 | listener.Stop(); 181 | Thread.Sleep(1000); 182 | Console.WriteLine("Disposing thread..."); 183 | thread.Dispose(); 184 | } 185 | 186 | private static void RunBasicEchoServer() 187 | { 188 | using (var server = new EchoWebSocketServer()) 189 | using (var client = new TrivialClient()) 190 | { 191 | var endpoint = new IPEndPoint(IPAddress.Loopback, 5002); 192 | Console.WriteLine($"Starting server on {endpoint}..."); 193 | server.StartManagedSockets(IPAddress.Loopback, 5002); 194 | Console.WriteLine($"Server running"); 195 | 196 | Thread.Sleep(1000); // let things spin up 197 | 198 | Console.WriteLine($"Opening client to {endpoint}..."); 199 | client.ConnectAsync(endpoint).FireOrForget(); 200 | Console.WriteLine("Client connected"); 201 | 202 | Console.WriteLine("Write data to echo, or 'quit' to exit,"); 203 | Console.WriteLine("'killc' to kill from the client, 'kills' to kill from the server, 'kill' for both"); 204 | while (true) 205 | { 206 | var line = Console.ReadLine(); 207 | if (line == null || line == "quit") break; 208 | switch(line) 209 | { 210 | case "kill": 211 | server.CloseAllAsync(); 212 | client.Close(); 213 | break; 214 | case "killc": 215 | client.Close(); 216 | break; 217 | case "kills": 218 | server.CloseAllAsync(); 219 | break; 220 | default: 221 | client.SendAsync(line).FireOrForget(); 222 | break; 223 | } 224 | } 225 | server.Stop(); 226 | } 227 | } 228 | 229 | private static void WriteAssemblyVersion(Type type) 230 | { 231 | #if NET451 232 | var assembly = type.Assembly; 233 | var assemblyName = assembly.GetName(); 234 | var attrib = (AssemblyInformationalVersionAttribute)Attribute.GetCustomAttribute(assembly, typeof(AssemblyInformationalVersionAttribute)); 235 | if(attrib != null) 236 | { 237 | Console.WriteLine($"{assemblyName.Name}: {attrib.InformationalVersion}"); 238 | } 239 | else 240 | { 241 | 242 | Console.WriteLine($"{assemblyName.Name}: {assemblyName.Version}"); 243 | } 244 | 245 | #endif 246 | } 247 | 248 | private static void CollectGarbage() 249 | { 250 | for (int i = 0; i < 5; i++) // try to force any finalizer bugs 251 | { 252 | GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); 253 | GC.WaitForPendingFinalizers(); 254 | } 255 | } 256 | public enum ChannelProvider 257 | { 258 | Libuv, 259 | ManagedSockets 260 | } 261 | public static void RunWebSocketServer(ChannelProvider provider) 262 | { 263 | using (var server = new EchoWebSocketServer()) 264 | { 265 | switch(provider) 266 | { 267 | case ChannelProvider.Libuv: 268 | server.StartLibuv(IPAddress.Loopback, 5001); 269 | break; 270 | case ChannelProvider.ManagedSockets: 271 | server.StartManagedSockets(IPAddress.Loopback, 5001); 272 | break; 273 | } 274 | 275 | Console.WriteLine($"Running on {server.IP}:{server.Port}..."); 276 | CancellationTokenSource cancel = new CancellationTokenSource(); 277 | 278 | bool keepGoing = true, writeLegend = true, writeStatus = true; 279 | while (keepGoing) 280 | { 281 | if (writeLegend) 282 | { 283 | Console.WriteLine("c: start client"); 284 | Console.WriteLine("c ###: start ### clients"); 285 | Console.WriteLine("b: broadbast from server"); 286 | Console.WriteLine("b ###: broadcast ### bytes from server"); 287 | Console.WriteLine("s: send from clients"); 288 | Console.WriteLine("s ###: send ### bytes from clients"); 289 | Console.WriteLine("p: ping"); 290 | Console.WriteLine("l: toggle logging"); 291 | Console.WriteLine("cls: clear console"); 292 | Console.WriteLine("xc: close at client"); 293 | Console.WriteLine("xs: close at server"); 294 | Console.WriteLine($"bf: toggle {nameof(server.BufferFragments)}"); 295 | Console.WriteLine("frag: send fragmented message from clients"); 296 | Console.WriteLine("q: quit"); 297 | Console.WriteLine("stat: write status"); 298 | Console.WriteLine("?: help"); 299 | writeLegend = false; 300 | } 301 | if(writeStatus) 302 | { 303 | Console.WriteLine($"clients: {ClientCount}; server connections: {server.ConnectionCount}"); 304 | writeStatus = false; 305 | } 306 | 307 | var line = Console.ReadLine(); 308 | switch (line) 309 | { 310 | case null: 311 | case "cls": 312 | Console.Clear(); 313 | break; 314 | case "q": 315 | keepGoing = false; 316 | break; 317 | case "stat": 318 | writeStatus = true; 319 | break; 320 | case "?": 321 | writeLegend = true; 322 | break; 323 | case "l": 324 | logging = !logging; 325 | Console.WriteLine("logging is now " + (logging ? "on" : "off")); 326 | break; 327 | case "bf": 328 | server.BufferFragments = !server.BufferFragments; 329 | Console.WriteLine($"{nameof(server.BufferFragments)} is now " + (server.BufferFragments ? "on" : "off")); 330 | break; 331 | case "xc": 332 | CloseAllClients(cancel.Token).ContinueWith(t => 333 | { 334 | try 335 | { 336 | Console.WriteLine($"Closed {t.Result} clients"); 337 | } 338 | catch (Exception e) 339 | { 340 | WriteError(e); 341 | } 342 | }); 343 | break; 344 | case "xs": 345 | server.CloseAllAsync("nuked from orbit").ContinueWith(t => 346 | { 347 | try 348 | { 349 | Console.WriteLine($"Closed {t.Result} connections at the server"); 350 | } 351 | catch (Exception e) 352 | { 353 | WriteError(e); 354 | } 355 | }); 356 | break; 357 | case "b": 358 | server.BroadcastAsync("hello to all clients").ContinueWith(t => 359 | { 360 | try 361 | { 362 | Console.WriteLine($"Broadcast to {t.Result} clients"); 363 | } 364 | catch (Exception e) 365 | { 366 | WriteError(e); 367 | } 368 | }); 369 | break; 370 | case "s": 371 | SendFromClients(cancel.Token).ContinueWith(t => 372 | { 373 | try 374 | { 375 | Console.WriteLine($"Sent from {t.Result} clients"); 376 | } 377 | catch (Exception e) 378 | { 379 | WriteError(e); 380 | } 381 | }); 382 | break; 383 | case "frag": 384 | SendFragmentedFromClients(cancel.Token).ContinueWith(t => 385 | { 386 | try 387 | { 388 | Console.WriteLine($"Sent fragmented from {t.Result} clients"); 389 | } 390 | catch (Exception e) 391 | { 392 | WriteError(e); 393 | } 394 | }); 395 | break; 396 | case "cc": 397 | StartChannelClients(); 398 | break; 399 | case "c": 400 | StartClients(cancel.Token); 401 | break; 402 | case "p": 403 | server.PingAsync("ping!").ContinueWith(t => 404 | { 405 | try 406 | { 407 | Console.WriteLine($"Pinged {t.Result} clients"); 408 | } 409 | catch (Exception e) 410 | { 411 | WriteError(e); 412 | } 413 | }); 414 | break; 415 | default: 416 | Match match; 417 | int i; 418 | if ((match = Regex.Match(line, "c ([0-9]+)")).Success && int.TryParse(match.Groups[1].Value, out i) && i.ToString() == match.Groups[1].Value && i >= 1) 419 | { 420 | StartClients(cancel.Token, i); 421 | } 422 | else if ((match = Regex.Match(line, "s ([0-9]+)")).Success && int.TryParse(match.Groups[1].Value, out i) && i.ToString() == match.Groups[1].Value && i >= 1) 423 | { 424 | SendFromClients(cancel.Token, new string('#', i)).FireOrForget(); 425 | } 426 | else if ((match = Regex.Match(line, "b ([0-9]+)")).Success && int.TryParse(match.Groups[1].Value, out i) && i.ToString() == match.Groups[1].Value && i >= 1) 427 | { 428 | server.BroadcastAsync(new string('#', i)).ContinueWith(t => 429 | { 430 | try 431 | { 432 | Console.WriteLine($"Broadcast to {t.Result} clients"); 433 | } 434 | catch (Exception e) 435 | { 436 | WriteError(e); 437 | } 438 | }); 439 | } 440 | else 441 | { 442 | writeLegend = true; 443 | } 444 | break; 445 | } 446 | } 447 | Console.WriteLine("Shutting down..."); 448 | cancel.Cancel(); 449 | } 450 | } 451 | 452 | private static void StartChannelClients(int count = 1) 453 | { 454 | if (count <= 0) return; 455 | int countBefore = ClientCount; 456 | for (int i = 0; i < count; i++) Task.Run(() => ExecuteChannel(true)); 457 | // not thread-pool so probably aren't there yet 458 | Console.WriteLine($"{count} client(s) started; expected: {countBefore + count}"); 459 | } 460 | 461 | private static void StartClients(CancellationToken cancel, int count = 1) 462 | { 463 | if (count <= 0) return; 464 | int countBefore = ClientCount; 465 | for (int i = 0; i < count; i++) Task.Run(() => Execute(true, cancel)); 466 | // not thread-pool so probably aren't there yet 467 | Console.WriteLine($"{count} client(s) started; expected: {countBefore + count}"); 468 | } 469 | 470 | internal static void WriteError(Exception e) 471 | { 472 | while (e is AggregateException && e.InnerException != null) 473 | { 474 | e = e.InnerException; 475 | } 476 | if (e != null) 477 | { 478 | Console.WriteLine($"{e.GetType().Name}: {e.Message}"); 479 | Console.WriteLine(e.StackTrace); 480 | } 481 | } 482 | 483 | internal static void FireOrForget(this Task task) => task.ContinueWith(t => GC.KeepAlive(t.Exception), TaskContinuationOptions.OnlyOnFaulted); 484 | 485 | [Conditional("LOGGING")] 486 | private static void WriteStatus(string message) 487 | { 488 | #if LOGGING 489 | Console.WriteLine($"[Client:{Environment.CurrentManagedThreadId}]: {message}"); 490 | #endif 491 | } 492 | static int clientNumber; 493 | // lazy client-side connection manager 494 | static readonly List clients = new List(); 495 | public static int ClientCount 496 | { 497 | get 498 | { 499 | lock (clients) { return clients.Count; } 500 | } 501 | } 502 | private static readonly Encoding encoding = Encoding.UTF8; 503 | private static async Task SendFromClients(CancellationToken cancel, string message = null) 504 | { 505 | ClientWebSocketWithIdentity[] arr; 506 | lock (clients) 507 | { 508 | arr = clients.ToArray(); 509 | } 510 | int count = 0; 511 | foreach (var client in arr) 512 | { 513 | try 514 | { 515 | await client.SendAsync(message ?? $"Hello from client {client.Id}", cancel); 516 | count++; 517 | } 518 | catch { } 519 | 520 | 521 | } 522 | return count; 523 | } 524 | 525 | private static async Task SendFragmentedFromClients(CancellationToken cancel) 526 | { 527 | ClientWebSocketWithIdentity[] arr; 528 | lock (clients) 529 | { 530 | arr = clients.ToArray(); 531 | } 532 | int count = 0; 533 | foreach (var client in arr) 534 | { 535 | try 536 | { 537 | await client.SendAsync($"Hello ", cancel, false); 538 | await client.SendAsync($"from client {client.Id}", cancel, true); 539 | count++; 540 | } 541 | catch { } 542 | } 543 | return count; 544 | } 545 | private static async Task CloseAllClients(CancellationToken cancel) 546 | { 547 | ClientWebSocketWithIdentity[] arr; 548 | lock (clients) 549 | { 550 | arr = clients.ToArray(); 551 | } 552 | int count = 0; 553 | foreach (var client in arr) 554 | { 555 | var msg = encoding.GetBytes($"Hello from client {client.Id}"); 556 | try 557 | { 558 | await client.CloseAsync(cancel); 559 | count++; 560 | } 561 | catch { } 562 | } 563 | return count; 564 | } 565 | struct ClientWebSocketWithIdentity : IEquatable 566 | { 567 | public readonly object Socket; 568 | public readonly int Id; 569 | public ClientWebSocketWithIdentity(WebSocketConnection socket, int id) 570 | { 571 | Socket = socket; 572 | Id = id; 573 | } 574 | public ClientWebSocketWithIdentity(ClientWebSocket socket, int id) 575 | { 576 | Socket = socket; 577 | Id = id; 578 | } 579 | public override bool Equals(object obj) => obj is ClientWebSocketWithIdentity && Equals((ClientWebSocketWithIdentity)obj); 580 | public bool Equals(ClientWebSocketWithIdentity obj) => obj.Id == this.Id && obj.Socket == this.Socket; 581 | public override int GetHashCode() => Id; 582 | public override string ToString() => $"{Id}: {Socket}"; 583 | 584 | internal Task SendAsync(string message, CancellationToken cancel, bool final = true) 585 | { 586 | if (Socket is ClientWebSocket) 587 | { 588 | var msg = encoding.GetBytes(message); 589 | return ((ClientWebSocket)Socket).SendAsync(new ArraySegment(msg, 0, msg.Length), WebSocketMessageType.Text, final, cancel); 590 | } 591 | else if (Socket is WebSocketConnection) 592 | { 593 | return ((WebSocketConnection)Socket).SendAsync(message, 594 | final ? WebSocketsFrame.FrameFlags.IsFinal : 0); 595 | } 596 | else return null; 597 | } 598 | 599 | internal Task CloseAsync(CancellationToken cancel) 600 | { 601 | if (Socket is ClientWebSocket) 602 | { 603 | return ((ClientWebSocket)Socket).CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", cancel); 604 | } 605 | else if (Socket is WebSocketConnection) 606 | { 607 | return ((WebSocketConnection)Socket).CloseAsync("bye"); 608 | } 609 | else return null; 610 | } 611 | } 612 | private static async Task ExecuteChannel(bool listen) 613 | { 614 | try 615 | { 616 | const string uri = "ws://127.0.0.1:5001"; 617 | WriteStatus($"connecting to {uri}..."); 618 | 619 | var socket = await WebSocketConnection.ConnectAsync(uri); 620 | 621 | WriteStatus("connected"); 622 | int clientNumber = Interlocked.Increment(ref Program.clientNumber); 623 | var named = new ClientWebSocketWithIdentity(socket, clientNumber); 624 | lock (clients) 625 | { 626 | clients.Add(named); 627 | } 628 | ClientChannelReceiveMessages(named); 629 | } 630 | catch (Exception ex) 631 | { 632 | WriteStatus(ex.GetType().Name); 633 | WriteStatus(ex.Message); 634 | } 635 | } 636 | 637 | private static async Task Execute(bool listen, CancellationToken token) 638 | { 639 | try 640 | { 641 | using (var socket = new ClientWebSocket()) 642 | { 643 | var uri = new Uri("ws://127.0.0.1:5001"); 644 | WriteStatus($"connecting to {uri}..."); 645 | await socket.ConnectAsync(uri, token); 646 | 647 | WriteStatus("connected"); 648 | int clientNumber = Interlocked.Increment(ref Program.clientNumber); 649 | var named = new ClientWebSocketWithIdentity(socket, clientNumber); 650 | lock (clients) 651 | { 652 | clients.Add(named); 653 | } 654 | try 655 | { 656 | await ClientReceiveLoop(named, token); 657 | } 658 | finally 659 | { 660 | lock (clients) 661 | { 662 | clients.Remove(named); 663 | } 664 | } 665 | } 666 | } 667 | catch (Exception ex) 668 | { 669 | WriteStatus(ex.GetType().Name); 670 | WriteStatus(ex.Message); 671 | } 672 | } 673 | 674 | private static void ClientChannelReceiveMessages(ClientWebSocketWithIdentity named) 675 | { 676 | var socket = (WebSocketConnection)named.Socket; 677 | socket.TextAsync += msg => 678 | { 679 | if (logging) 680 | { 681 | var message = msg.GetText(); 682 | Console.WriteLine($"client {named.Id} received text, {msg.GetPayloadLength()} bytes, final: {msg.IsFinal}: {message}"); 683 | } 684 | return null; 685 | }; 686 | } 687 | private static async Task ClientReceiveLoop(ClientWebSocketWithIdentity named, CancellationToken token) 688 | { 689 | var socket = (ClientWebSocket)named.Socket; 690 | var buffer = new byte[2048]; 691 | while (!token.IsCancellationRequested) 692 | { 693 | var result = await socket.ReceiveAsync(new ArraySegment(buffer, 0, buffer.Length), token); 694 | if (logging) 695 | { 696 | var message = encoding.GetString(buffer, 0, result.Count); 697 | Console.WriteLine($"client {named.Id} received {result.MessageType}, {result.Count} bytes, final: {result.EndOfMessage}: {message}"); 698 | } 699 | } 700 | 701 | } 702 | } 703 | } 704 | -------------------------------------------------------------------------------- /src/SampleServer/SampleServer.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 7def38b6-284d-4060-8595-7123d8e56bbc 10 | SampleServer 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 16 | 2.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/SampleServer/TrivialClient.cs: -------------------------------------------------------------------------------- 1 | using Channels.Networking.Libuv; 2 | using Channels.Text.Primitives; 3 | using System; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | 7 | namespace SampleServer 8 | { 9 | public class TrivialClient : IDisposable 10 | { 11 | UvTcpClient client; 12 | UvThread thread; 13 | UvTcpConnection connection; 14 | 15 | internal Task SendAsync(string line) 16 | { 17 | 18 | try 19 | { 20 | if (connection == null) 21 | { 22 | Console.WriteLine($"[client] (no connection; cannot send)"); 23 | return done; 24 | } 25 | else if (string.IsNullOrEmpty(line)) 26 | { 27 | return done; 28 | } 29 | else 30 | { 31 | var buffer = connection.Output.Alloc(); 32 | Console.WriteLine($"[client] sending {line.Length} bytes..."); 33 | buffer.WriteAsciiString(line); 34 | return buffer.FlushAsync(); 35 | } 36 | } 37 | catch (Exception ex) 38 | { 39 | Program.WriteError(ex); 40 | return done; 41 | } 42 | } 43 | static readonly Task done = Task.FromResult(0); 44 | 45 | internal async Task ConnectAsync(IPEndPoint endpoint) 46 | { 47 | thread = new UvThread(); 48 | client = new UvTcpClient(thread, endpoint); 49 | connection = await client.ConnectAsync(); 50 | ReadLoop(); // will hand over to libuv thread 51 | } 52 | internal async void ReadLoop() 53 | { 54 | Console.WriteLine("[client] read loop started"); 55 | try 56 | { 57 | while (true) 58 | { 59 | var buffer = await connection.Input.ReadAsync(); 60 | if (buffer.IsEmpty && (connection == null || connection.Input.Reading.IsCompleted)) 61 | { 62 | Console.WriteLine("[client] input ended"); 63 | break; 64 | } 65 | 66 | var s = buffer.GetAsciiString(); 67 | connection.Input.Advance(buffer.End, buffer.End); 68 | 69 | Console.Write("[client] received: "); 70 | Console.WriteLine(s); 71 | } 72 | } 73 | catch(Exception ex) 74 | { 75 | Console.Write("[client] read loop exploded"); 76 | Program.WriteError(ex); 77 | } 78 | finally 79 | { 80 | Console.WriteLine("[client] read loop ended"); 81 | } 82 | } 83 | public void Close() 84 | { 85 | if (connection != null) Close(connection); 86 | connection = null; 87 | // client.Dispose(); // 88 | thread?.Dispose(); 89 | thread = null; 90 | } 91 | public void Dispose() => Dispose(true); 92 | private void Dispose(bool disposing) 93 | { 94 | if (disposing) Close(); 95 | } 96 | 97 | private void Close(UvTcpConnection connection, Exception error = null) 98 | { 99 | Console.WriteLine("[client] closing connection..."); 100 | connection.Input.Complete(error); 101 | connection.Output.Complete(error); 102 | Console.WriteLine("[client] connection closed"); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/SampleServer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "debugType": "portable", 5 | "emitEntryPoint": true, 6 | "allowUnsafe": true 7 | }, 8 | "dependencies": { 9 | "Channels": "0.2.0-beta-*", 10 | "Channels.Networking.Libuv": "0.2.0-beta-*", 11 | "Channels.Networking.Sockets": "0.2.0-beta-*", 12 | "Channels.Text.Primitives": "0.2.0-beta-*", 13 | "Channels.WebSockets": { 14 | "target": "project", 15 | "version": "1.0.0-*" 16 | } 17 | }, 18 | "frameworks": { 19 | //"net451": {}, 20 | "netcoreapp1.0": { 21 | "dependencies": { 22 | "Microsoft.NETCore.App": { 23 | "type": "platform", 24 | "version": "1.0.0" 25 | }, 26 | "System.Net.WebSockets.Client": "4.0.0" 27 | }, 28 | "imports": "dnxcore50" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tools/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgravell/Pipelines.WebSockets/831532f284306b9c894bbac55b5a48310fef90f7/tools/Key.snk --------------------------------------------------------------------------------