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