├── .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
--------------------------------------------------------------------------------