Initial commit
This commit is contained in:
commit
03352354dc
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="Channels" value="https://www.myget.org/F/channels/api/v3/index.json" />
|
||||
<add key="dotnet-corefxlab" value="https://dotnet.myget.org/F/dotnet-corefxlab/api/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"projects": [ "src", "test" ],
|
||||
"sdk": {
|
||||
"version": "1.0.0-preview2-003121"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> Payload { get; set; }
|
||||
}
|
||||
|
||||
public class Bus
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, List<IObserver<Message>>> _subscriptions = new ConcurrentDictionary<string, List<IObserver<Message>>>();
|
||||
|
||||
public IDisposable Subscribe(string key, IObserver<Message> observer)
|
||||
{
|
||||
var connections = _subscriptions.GetOrAdd(key, _ => new List<IObserver<Message>>());
|
||||
connections.Add(observer);
|
||||
|
||||
return new DisposableAction(() =>
|
||||
{
|
||||
connections.Remove(observer);
|
||||
});
|
||||
}
|
||||
|
||||
public void Publish(string key, Message message)
|
||||
{
|
||||
List<IObserver<Message>> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, ConnectionState> _connections = new ConcurrentDictionary<string, ConnectionState>();
|
||||
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))
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Connection> _connections = new List<Connection>();
|
||||
|
||||
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<Connection> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace WebApplication95
|
||||
{
|
||||
public interface IDispatcher
|
||||
{
|
||||
void OnIncoming(ArraySegment<byte> data);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<object> _initTcs = new TaskCompletionSource<object>();
|
||||
private TaskCompletionSource<object> _lifetime = new TaskCompletionSource<object>();
|
||||
private HttpContext _context;
|
||||
private readonly ConnectionState _state;
|
||||
|
||||
public LongPolling(ConnectionState state)
|
||||
{
|
||||
_lastTask = _initTcs.Task;
|
||||
_state = state;
|
||||
}
|
||||
|
||||
private Task Post(Func<object, Task> 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<byte>));
|
||||
}
|
||||
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<byte> 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<object>)state).TrySetResult(null);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
_lifetime);
|
||||
}
|
||||
|
||||
public async Task Send(ArraySegment<byte> value)
|
||||
{
|
||||
await Post(async state =>
|
||||
{
|
||||
var data = ((ArraySegment<byte>)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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Startup>()
|
||||
.Build();
|
||||
|
||||
host.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<object> _initTcs = new TaskCompletionSource<object>();
|
||||
private TaskCompletionSource<object> _lifetime = new TaskCompletionSource<object>();
|
||||
private HttpContext _context;
|
||||
private readonly ConnectionState _state;
|
||||
|
||||
public ServerSentEvents(ConnectionState state)
|
||||
{
|
||||
_state = state;
|
||||
_lastTask = _initTcs.Task;
|
||||
var ignore = StartSending();
|
||||
}
|
||||
|
||||
private Task Post(Func<object, Task> 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<object>)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<byte> data;
|
||||
if (memory.TryGetArray(out data))
|
||||
{
|
||||
await Send(data);
|
||||
}
|
||||
}
|
||||
|
||||
_state.Connection.Output.Advance(buffer.End);
|
||||
}
|
||||
|
||||
_state.Connection.Output.CompleteReader();
|
||||
}
|
||||
|
||||
private Task Send(ArraySegment<byte> value)
|
||||
{
|
||||
return Post(async state =>
|
||||
{
|
||||
var data = ((ArraySegment<byte>)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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>52ed8b3a-2dbb-448a-a708-faa0783b7917</ProjectGuid>
|
||||
<RootNamespace>WebApplication95</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<DnxInvisibleContent Include="bower.json" />
|
||||
<DnxInvisibleContent Include=".bowerrc" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -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<object> _tcs = new TaskCompletionSource<object>();
|
||||
|
||||
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<byte> 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<byte>(buffer), CancellationToken.None);
|
||||
|
||||
// TODO: Fragments
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
await _state.Connection.Input.WriteAsync(new Span<byte>(buffer, 0, result.Count));
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Binary)
|
||||
{
|
||||
await _state.Connection.Input.WriteAsync(new Span<byte>(buffer, 0, result.Count));
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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%" ]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
|
||||
<!--
|
||||
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380
|
||||
-->
|
||||
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
|
||||
</handlers>
|
||||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<script>
|
||||
|
||||
var connectionId;
|
||||
|
||||
function send() {
|
||||
var body = document.getElementById('data').value;
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
var url = '/send?id=' + connectionId;
|
||||
xhr.open("POST", url, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/json');
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||
}
|
||||
}
|
||||
var data = JSON.stringify(body);
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
var source = new EventSource('/sse');
|
||||
|
||||
source.onopen = function () {
|
||||
console.log('Opened!');
|
||||
};
|
||||
|
||||
source.onerror = function (err) {
|
||||
console.log('Error: ' + err.type);
|
||||
};
|
||||
|
||||
source.onmessage = function (data) {
|
||||
if (!connectionId) {
|
||||
connectionId = data.data;
|
||||
return;
|
||||
}
|
||||
var child = document.createElement('li');
|
||||
child.innerText = data.data;
|
||||
document.getElementById('messages').appendChild(child);
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Server Sent Events</h1>
|
||||
<input type="text" id="data" />
|
||||
<input type="button" value="Send" onclick="send()" />
|
||||
|
||||
<ul id="messages">
|
||||
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title></title>
|
||||
<script>
|
||||
|
||||
var connectionId;
|
||||
|
||||
function send() {
|
||||
var body = document.getElementById('data').value;
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
var url = '/send?id=' + connectionId;
|
||||
xhr.open("POST", url, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/json');
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||
}
|
||||
}
|
||||
var data = JSON.stringify(body);
|
||||
xhr.send(data);
|
||||
}
|
||||
|
||||
|
||||
function poll(id) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
var url = '/poll' + (id == null ? '' : '?id=' + id);
|
||||
xhr.open("POST", url, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState == 4 && xhr.status == 200) {
|
||||
var json = JSON.parse(xhr.responseText);
|
||||
var id = json.c;
|
||||
var data = json.d;
|
||||
if (data) {
|
||||
var child = document.createElement('li');
|
||||
child.innerText = data;
|
||||
document.getElementById('messages').appendChild(child);
|
||||
}
|
||||
|
||||
if (!connectionId) {
|
||||
connectionId = id;
|
||||
}
|
||||
|
||||
poll(id);
|
||||
}
|
||||
}
|
||||
xhr.send(null);
|
||||
}
|
||||
|
||||
poll();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Long Polling</h1>
|
||||
<input type="text" id="data" />
|
||||
<input type="button" value="Send" onclick="send()" />
|
||||
|
||||
<ul id="messages"></ul>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue