From 03352354dc495e3714dbf1e6e738acf807cf4abc Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 30 Sep 2016 01:44:56 -0700 Subject: [PATCH] Initial commit --- .gitattributes | 52 +++++++ .gitignore | 33 +++++ LICENSE.md | 13 ++ NuGet.config | 7 + WebApplication95.sln | 32 +++++ global.json | 6 + src/WebApplication95/Bus.cs | 58 ++++++++ src/WebApplication95/Connection.cs | 52 +++++++ src/WebApplication95/ConnectionManager.cs | 102 ++++++++++++++ src/WebApplication95/ConnectionState.cs | 11 ++ src/WebApplication95/Dispatcher.cs | 143 ++++++++++++++++++++ src/WebApplication95/IDispatcher.cs | 9 ++ src/WebApplication95/LongPolling.cs | 138 +++++++++++++++++++ src/WebApplication95/Program.cs | 27 ++++ src/WebApplication95/ServerSentEvents.cs | 126 +++++++++++++++++ src/WebApplication95/Startup.cs | 36 +++++ src/WebApplication95/WebApplication95.xproj | 25 ++++ src/WebApplication95/WebSockets.cs | 86 ++++++++++++ src/WebApplication95/project.json | 50 +++++++ src/WebApplication95/web.config | 14 ++ src/WebApplication95/wwwroot/index.html | 55 ++++++++ src/WebApplication95/wwwroot/polling.html | 61 +++++++++ 22 files changed, 1136 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 NuGet.config create mode 100644 WebApplication95.sln create mode 100644 global.json create mode 100644 src/WebApplication95/Bus.cs create mode 100644 src/WebApplication95/Connection.cs create mode 100644 src/WebApplication95/ConnectionManager.cs create mode 100644 src/WebApplication95/ConnectionState.cs create mode 100644 src/WebApplication95/Dispatcher.cs create mode 100644 src/WebApplication95/IDispatcher.cs create mode 100644 src/WebApplication95/LongPolling.cs create mode 100644 src/WebApplication95/Program.cs create mode 100644 src/WebApplication95/ServerSentEvents.cs create mode 100644 src/WebApplication95/Startup.cs create mode 100644 src/WebApplication95/WebApplication95.xproj create mode 100644 src/WebApplication95/WebSockets.cs create mode 100644 src/WebApplication95/project.json create mode 100644 src/WebApplication95/web.config create mode 100644 src/WebApplication95/wwwroot/index.html create mode 100644 src/WebApplication95/wwwroot/polling.html diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..c2f0f84273 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,52 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf + +*.sh eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..9c43d9d9a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +*.sln.ide/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +.vs/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +project.lock.json +runtimes/ +.build/ +.testPublish/ +launchSettings.json +*.tmp diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..be67c6cd06 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,13 @@ +Copyright (c) David Fowler All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you +may not use this file except in compliance with the License. You may +obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions +and limitations under the License. diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000000..3ce9e7f223 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/WebApplication95.sln b/WebApplication95.sln new file mode 100644 index 0000000000..607b118e87 --- /dev/null +++ b/WebApplication95.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DA69F624-5398-4884-87E4-B816698CDE65}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{83B2C3EB-A3D8-4E6F-9A3C-A380B005EF31}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "WebApplication95", "src\WebApplication95\WebApplication95.xproj", "{52ED8B3A-2DBB-448A-A708-FAA0783B7917}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {52ED8B3A-2DBB-448A-A708-FAA0783B7917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52ED8B3A-2DBB-448A-A708-FAA0783B7917}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52ED8B3A-2DBB-448A-A708-FAA0783B7917}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52ED8B3A-2DBB-448A-A708-FAA0783B7917}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {52ED8B3A-2DBB-448A-A708-FAA0783B7917} = {DA69F624-5398-4884-87E4-B816698CDE65} + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000000..e793049cd5 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-preview2-003121" + } +} diff --git a/src/WebApplication95/Bus.cs b/src/WebApplication95/Bus.cs new file mode 100644 index 0000000000..8a03b6f5f2 --- /dev/null +++ b/src/WebApplication95/Bus.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace WebApplication95 +{ + public class Message + { + public string ContentType { get; set; } + public ArraySegment Payload { get; set; } + } + + public class Bus + { + private readonly ConcurrentDictionary>> _subscriptions = new ConcurrentDictionary>>(); + + public IDisposable Subscribe(string key, IObserver observer) + { + var connections = _subscriptions.GetOrAdd(key, _ => new List>()); + connections.Add(observer); + + return new DisposableAction(() => + { + connections.Remove(observer); + }); + } + + public void Publish(string key, Message message) + { + List> connections; + if (_subscriptions.TryGetValue(key, out connections)) + { + foreach (var c in connections) + { + c.OnNext(message); + } + } + } + + private class DisposableAction : IDisposable + { + private Action _action; + + public DisposableAction(Action action) + { + _action = action; + } + + public void Dispose() + { + Interlocked.Exchange(ref _action, () => { }).Invoke(); + } + } + } +} diff --git a/src/WebApplication95/Connection.cs b/src/WebApplication95/Connection.cs new file mode 100644 index 0000000000..0ab18ecd97 --- /dev/null +++ b/src/WebApplication95/Connection.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Channels; + +namespace WebApplication95 +{ + public enum TransportType + { + LongPolling, + WebSockets, + ServerSentEvents + } + + public class Connection : IChannel + { + public TransportType TransportType { get; set; } + + public string ConnectionId { get; set; } + + IReadableChannel IChannel.Input => Input; + + IWritableChannel IChannel.Output => Output; + + internal Channel Input { get; set; } + + internal Channel Output { get; set; } + + public Connection() + { + Stream = new ChannelStream(this); + } + + public Stream Stream { get; } + + public void Complete() + { + Input.CompleteReader(); + Input.CompleteWriter(); + + Output.CompleteReader(); + Output.CompleteWriter(); + } + + public void Dispose() + { + + } + } +} diff --git a/src/WebApplication95/ConnectionManager.cs b/src/WebApplication95/ConnectionManager.cs new file mode 100644 index 0000000000..7acf181a93 --- /dev/null +++ b/src/WebApplication95/ConnectionManager.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace WebApplication95 +{ + public class ConnectionManager + { + private ConcurrentDictionary _connections = new ConcurrentDictionary(); + private readonly ChannelFactory _channelFactory = new ChannelFactory(); + private Timer _timer; + + public ConnectionManager() + { + _timer = new Timer(Scan, this, 0, 1000); + } + + private static void Scan(object state) + { + ((ConnectionManager)state).Scan(); + } + + private void Scan() + { + foreach (var c in _connections) + { + if (!c.Value.Alive && (DateTimeOffset.UtcNow - c.Value.LastSeen).TotalSeconds > 30) + { + ConnectionState s; + if (_connections.TryRemove(c.Key, out s)) + { + s.Connection.Complete(); + } + else + { + + } + } + } + } + + public string GetConnectionId(HttpContext context) + { + // REVIEW: Only check the query string for longpolling + var id = context.Request.Query["id"]; + + if (!StringValues.IsNullOrEmpty(id)) + { + return id.ToString(); + } + + return Guid.NewGuid().ToString(); + } + + public bool TryGetConnection(string id, out ConnectionState state) + { + return _connections.TryGetValue(id, out state); + } + + public bool AddConnection(string id, out ConnectionState state) + { + state = _connections.GetOrAdd(id, connectionId => new ConnectionState()); + var isNew = state.Connection == null; + if (isNew) + { + state.Connection = new Connection + { + ConnectionId = id, + Input = _channelFactory.CreateChannel(), + Output = _channelFactory.CreateChannel() + }; + } + state.LastSeen = DateTimeOffset.UtcNow; + state.Alive = true; + return isNew; + } + + public void MarkConnectionDead(string id) + { + ConnectionState state; + if (_connections.TryGetValue(id, out state)) + { + state.Alive = false; + } + } + + public void RemoveConnection(string id) + { + ConnectionState state; + if (_connections.TryRemove(id, out state)) + { + + } + } + } +} diff --git a/src/WebApplication95/ConnectionState.cs b/src/WebApplication95/ConnectionState.cs new file mode 100644 index 0000000000..b615c798ff --- /dev/null +++ b/src/WebApplication95/ConnectionState.cs @@ -0,0 +1,11 @@ +using System; + +namespace WebApplication95 +{ + public class ConnectionState + { + public DateTimeOffset LastSeen { get; set; } + public bool Alive { get; set; } = true; + public Connection Connection { get; set; } + } +} diff --git a/src/WebApplication95/Dispatcher.cs b/src/WebApplication95/Dispatcher.cs new file mode 100644 index 0000000000..19dd8e4d9a --- /dev/null +++ b/src/WebApplication95/Dispatcher.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace WebApplication95 +{ + public class Dispatcher + { + private readonly ConnectionManager _manager = new ConnectionManager(); + private readonly EndPoint _endpoint = new EndPoint(); + + public async Task Execute(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/send")) + { + var connectionId = context.Request.Query["id"]; + + if (StringValues.IsNullOrEmpty(connectionId)) + { + throw new InvalidOperationException("Missing connection id"); + } + + ConnectionState state; + if (_manager.TryGetConnection(connectionId, out state)) + { + // Write the message length + await context.Request.Body.CopyToAsync(state.Connection.Input); + } + } + else + { + var connectionId = _manager.GetConnectionId(context); + + // Outgoing channels + if (context.Request.Path.StartsWithSegments("/sse")) + { + ConnectionState state; + _manager.AddConnection(connectionId, out state); + + var sse = new ServerSentEvents(state); + + var ignore = _endpoint.OnConnected(state.Connection); + + state.Connection.TransportType = TransportType.ServerSentEvents; + + await sse.ProcessRequest(context); + + state.Connection.Complete(); + + _manager.RemoveConnection(connectionId); + } + else if (context.Request.Path.StartsWithSegments("/ws")) + { + ConnectionState state; + _manager.AddConnection(connectionId, out state); + + var ws = new WebSockets(state); + + var ignore = _endpoint.OnConnected(state.Connection); + + state.Connection.TransportType = TransportType.WebSockets; + + await ws.ProcessRequest(context); + + state.Connection.Complete(); + + _manager.RemoveConnection(connectionId); + } + else if (context.Request.Path.StartsWithSegments("/poll")) + { + ConnectionState state; + bool newConnection = false; + if (_manager.AddConnection(connectionId, out state)) + { + newConnection = true; + var ignore = _endpoint.OnConnected(state.Connection); + state.Connection.TransportType = TransportType.LongPolling; + } + + var longPolling = new LongPolling(state); + + await longPolling.ProcessRequest(newConnection, context); + + _manager.MarkConnectionDead(connectionId); + } + + } + } + } + + public class EndPoint + { + private List _connections = new List(); + + public virtual async Task OnConnected(Connection connection) + { + lock (_connections) + { + _connections.Add(connection); + } + + // Echo server + while (true) + { + var input = await connection.Input.ReadAsync(); + try + { + if (input.IsEmpty && connection.Input.Reading.IsCompleted) + { + break; + } + + List connections = null; + lock (_connections) + { + connections = _connections; + } + + foreach (var c in connections) + { + var output = c.Output.Alloc(); + output.Append(ref input); + await output.FlushAsync(); + } + } + finally + { + connection.Input.Advance(input.End); + } + } + + lock (_connections) + { + _connections.Remove(connection); + } + } + } +} diff --git a/src/WebApplication95/IDispatcher.cs b/src/WebApplication95/IDispatcher.cs new file mode 100644 index 0000000000..2beada37bb --- /dev/null +++ b/src/WebApplication95/IDispatcher.cs @@ -0,0 +1,9 @@ +using System; + +namespace WebApplication95 +{ + public interface IDispatcher + { + void OnIncoming(ArraySegment data); + } +} \ No newline at end of file diff --git a/src/WebApplication95/LongPolling.cs b/src/WebApplication95/LongPolling.cs new file mode 100644 index 0000000000..7a6da27114 --- /dev/null +++ b/src/WebApplication95/LongPolling.cs @@ -0,0 +1,138 @@ +using System; +using System.Text; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; + +namespace WebApplication95 +{ + public class LongPolling + { + private Task _lastTask; + private object _lockObj = new object(); + private bool _completed; + private TaskCompletionSource _initTcs = new TaskCompletionSource(); + private TaskCompletionSource _lifetime = new TaskCompletionSource(); + private HttpContext _context; + private readonly ConnectionState _state; + + public LongPolling(ConnectionState state) + { + _lastTask = _initTcs.Task; + _state = state; + } + + private Task Post(Func work, object state) + { + if (_completed) + { + return _lastTask; + } + + lock (_lockObj) + { + _lastTask = _lastTask.ContinueWith((t, s1) => work(s1), state).Unwrap(); + } + + return _lastTask; + } + + public async Task ProcessRequest(bool newConnection, HttpContext context) + { + context.Response.ContentType = "application/json"; + + // End the connection if the client goes away + context.RequestAborted.Register(state => OnConnectionAborted(state), this); + + _context = context; + + _initTcs.TrySetResult(null); + + if (newConnection) + { + // Flush the connection id to the connection + var ignore = Send(default(ArraySegment)); + } + else + { + // Send queue messages to the connection + var ignore = ProcessMessages(context); + } + + + await _lifetime.Task; + + _completed = true; + } + + private async Task ProcessMessages(HttpContext context) + { + var buffer = await _state.Connection.Output.ReadAsync(); + + foreach (var memory in buffer) + { + ArraySegment data; + if (memory.TryGetArray(out data)) + { + await Send(data); + // Advance the buffer one block of memory + _state.Connection.Output.Advance(buffer.Slice(memory.Length).Start); + break; + } + } + } + + private static void OnConnectionAborted(object state) + { + ((LongPolling)state).CompleteRequest(); + } + + private void CompleteRequest() + { + Post(state => + { + ((TaskCompletionSource)state).TrySetResult(null); + return Task.CompletedTask; + }, + _lifetime); + } + + public async Task Send(ArraySegment value) + { + await Post(async state => + { + var data = ((ArraySegment)state); + // + 100 = laziness + var buffer = new byte[data.Count + _state.Connection.ConnectionId.Length + 100]; + var at = 0; + buffer[at++] = (byte)'{'; + buffer[at++] = (byte)'"'; + buffer[at++] = (byte)'c'; + buffer[at++] = (byte)'"'; + buffer[at++] = (byte)':'; + buffer[at++] = (byte)'"'; + int count = Encoding.UTF8.GetBytes(_state.Connection.ConnectionId, 0, _state.Connection.ConnectionId.Length, buffer, at); + at += count; + buffer[at++] = (byte)'"'; + if (data.Array != null) + { + buffer[at++] = (byte)','; + buffer[at++] = (byte)'"'; + buffer[at++] = (byte)'d'; + buffer[at++] = (byte)'"'; + buffer[at++] = (byte)':'; + //buffer[at++] = (byte)'"'; + Buffer.BlockCopy(data.Array, data.Offset, buffer, at, data.Count); + } + at += data.Count; + //buffer[at++] = (byte)'"'; + buffer[at++] = (byte)'}'; + _context.Response.ContentLength = at; + await _context.Response.Body.WriteAsync(buffer, 0, at); + }, + value); + + CompleteRequest(); + } + } +} diff --git a/src/WebApplication95/Program.cs b/src/WebApplication95/Program.cs new file mode 100644 index 0000000000..be7ba38402 --- /dev/null +++ b/src/WebApplication95/Program.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; + +namespace WebApplication95 +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel(options => + { + options.UseConnectionLogging(); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/WebApplication95/ServerSentEvents.cs b/src/WebApplication95/ServerSentEvents.cs new file mode 100644 index 0000000000..0aaf8287ee --- /dev/null +++ b/src/WebApplication95/ServerSentEvents.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; + +namespace WebApplication95 +{ + public class ServerSentEvents + { + private Task _lastTask; + private object _lockObj = new object(); + private bool _completed; + private TaskCompletionSource _initTcs = new TaskCompletionSource(); + private TaskCompletionSource _lifetime = new TaskCompletionSource(); + private HttpContext _context; + private readonly ConnectionState _state; + + public ServerSentEvents(ConnectionState state) + { + _state = state; + _lastTask = _initTcs.Task; + var ignore = StartSending(); + } + + private Task Post(Func work, object state) + { + if (_completed) + { + return _lastTask; + } + + lock (_lockObj) + { + _lastTask = _lastTask.ContinueWith((t, s1) => work(s1), state).Unwrap(); + } + + return _lastTask; + } + + public async Task ProcessRequest(HttpContext context) + { + context.Response.ContentType = "text/event-stream"; + + // End the connection if the client goes away + context.RequestAborted.Register(state => OnConnectionAborted(state), this); + + _context = context; + + await _context.Response.WriteAsync($"data: {_state.Connection.ConnectionId}\n\n"); + + // Set the initial TCS when everything is setup + _initTcs.TrySetResult(null); + + await _lifetime.Task; + + _completed = true; + } + + private static void OnConnectionAborted(object state) + { + ((ServerSentEvents)state).OnConnectedAborted(); + } + + private void OnConnectedAborted() + { + Post(state => + { + ((TaskCompletionSource)state).TrySetResult(null); + return Task.CompletedTask; + }, + _lifetime); + } + + private async Task StartSending() + { + await _initTcs.Task; + + while (true) + { + var buffer = await _state.Connection.Output.ReadAsync(); + + if (buffer.IsEmpty && _state.Connection.Output.Reading.IsCompleted) + { + break; + } + + foreach (var memory in buffer) + { + ArraySegment data; + if (memory.TryGetArray(out data)) + { + await Send(data); + } + } + + _state.Connection.Output.Advance(buffer.End); + } + + _state.Connection.Output.CompleteReader(); + } + + private Task Send(ArraySegment value) + { + return Post(async state => + { + var data = ((ArraySegment)state); + // TODO: Pooled buffers + // 8 = 6(data: ) + 2 (\n\n) + var buffer = new byte[8 + data.Count]; + var at = 0; + buffer[at++] = (byte)'d'; + buffer[at++] = (byte)'a'; + buffer[at++] = (byte)'t'; + buffer[at++] = (byte)'a'; + buffer[at++] = (byte)':'; + buffer[at++] = (byte)' '; + Buffer.BlockCopy(data.Array, data.Offset, buffer, at, data.Count); + at += data.Count; + buffer[at++] = (byte)'\n'; + buffer[at++] = (byte)'\n'; + await _context.Response.Body.WriteAsync(buffer, 0, at); + }, + value); + } + } +} diff --git a/src/WebApplication95/Startup.cs b/src/WebApplication95/Startup.cs new file mode 100644 index 0000000000..8ddf30b1fa --- /dev/null +++ b/src/WebApplication95/Startup.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace WebApplication95 +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(LogLevel.Debug); + + app.UseFileServer(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + var dispatcher = new Dispatcher(); + + app.Run(async (context) => + { + await dispatcher.Execute(context); + }); + } + } +} diff --git a/src/WebApplication95/WebApplication95.xproj b/src/WebApplication95/WebApplication95.xproj new file mode 100644 index 0000000000..85dbf4bbcb --- /dev/null +++ b/src/WebApplication95/WebApplication95.xproj @@ -0,0 +1,25 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 52ed8b3a-2dbb-448a-a708-faa0783b7917 + WebApplication95 + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + + + + + diff --git a/src/WebApplication95/WebSockets.cs b/src/WebApplication95/WebSockets.cs new file mode 100644 index 0000000000..a9ab616d96 --- /dev/null +++ b/src/WebApplication95/WebSockets.cs @@ -0,0 +1,86 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Http; + +namespace WebApplication95 +{ + public class WebSockets + { + private WebSocket _ws; + private ConnectionState _state; + private TaskCompletionSource _tcs = new TaskCompletionSource(); + + public WebSockets(ConnectionState state) + { + _state = state; + var ignore = StartSending(); + } + + private async Task StartSending() + { + await _tcs.Task; + + while (true) + { + var buffer = await _state.Connection.Output.ReadAsync(); + + if (buffer.IsEmpty && _state.Connection.Output.Reading.IsCompleted) + { + break; + } + + foreach (var memory in buffer) + { + ArraySegment data; + if (memory.TryGetArray(out data)) + { + await _ws.SendAsync(data, WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); + } + } + + _state.Connection.Output.Advance(buffer.End); + } + + _state.Connection.Output.CompleteReader(); + } + + public async Task ProcessRequest(HttpContext context) + { + if (!context.WebSockets.IsWebSocketRequest) + { + await Task.CompletedTask; + return; + } + + var ws = await context.WebSockets.AcceptWebSocketAsync(); + + _ws = ws; + + _tcs.TrySetResult(null); + + var buffer = new byte[2048]; + while (true) + { + var result = await ws.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + // TODO: Fragments + if (result.MessageType == WebSocketMessageType.Text) + { + await _state.Connection.Input.WriteAsync(new Span(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + await _state.Connection.Input.WriteAsync(new Span(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + break; + } + } + } + } +} diff --git a/src/WebApplication95/project.json b/src/WebApplication95/project.json new file mode 100644 index 0000000000..10ee59e745 --- /dev/null +++ b/src/WebApplication95/project.json @@ -0,0 +1,50 @@ +{ + "dependencies": { + "Channels": "0.2.0-beta-*", + "Microsoft.NETCore.App": { + "version": "1.0.0", + "type": "platform" + }, + "Microsoft.AspNetCore.Diagnostics": "1.0.0", + "Microsoft.AspNetCore.StaticFiles": "1.0.0", + + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.Extensions.Logging.Console": "1.0.0" + }, + + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" + }, + + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "dotnet5.6", + "portable-net45+win8" + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot", + "web.config" + ] + }, + + "scripts": { + "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + } +} diff --git a/src/WebApplication95/web.config b/src/WebApplication95/web.config new file mode 100644 index 0000000000..dc0514fca5 --- /dev/null +++ b/src/WebApplication95/web.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src/WebApplication95/wwwroot/index.html b/src/WebApplication95/wwwroot/index.html new file mode 100644 index 0000000000..a956cdfb66 --- /dev/null +++ b/src/WebApplication95/wwwroot/index.html @@ -0,0 +1,55 @@ + + + + + + + + +

Server Sent Events

+ + + +
    + +
+ + \ No newline at end of file diff --git a/src/WebApplication95/wwwroot/polling.html b/src/WebApplication95/wwwroot/polling.html new file mode 100644 index 0000000000..fc75370f12 --- /dev/null +++ b/src/WebApplication95/wwwroot/polling.html @@ -0,0 +1,61 @@ + + + + + + + + +

Long Polling

+ + + +
    + + \ No newline at end of file