diff --git a/aspnetcore/signalr/connection-handler.md b/aspnetcore/signalr/connection-handler.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/aspnetcore/signalr/connection-handler/sample/EchoConnectionHandler.cs b/aspnetcore/signalr/connection-handler/sample/EchoConnectionHandler.cs new file mode 100644 index 000000000000..45e92eceadf7 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/sample/EchoConnectionHandler.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Connections.Features; + +namespace SignalRConnectionHandlerSample +{ + public class EchoConnectionHandler : ConnectionHandler + { + public override async Task OnConnectedAsync(ConnectionContext connection) + { + var transportType = connection.Features.Get()?.TransportType; + var welcomeString = $"Welcome. A connection has been established with the '{transportType}' transport.\n\n"; + + Write(connection.Transport.Output, welcomeString, Encoding.UTF8); + await connection.Transport.Output.FlushAsync(); + + while (true) + { + var result = await connection.Transport.Input.ReadAsync(); + var buffer = result.Buffer; + + try + { + if (!buffer.IsEmpty) + { + if (buffer.IsSingleSegment) + { + await connection.Transport.Output.WriteAsync(buffer.First); + } + else + { + foreach (var segment in buffer) + { + await connection.Transport.Output.WriteAsync(segment); + } + } + } + + if (result.IsCompleted) + { + break; + } + } + finally + { + connection.Transport.Input.AdvanceTo(buffer.End); + } + } + } + + private static void Write(PipeWriter pipeWriter, string text, Encoding encoding) + { + var minimumByteSize = encoding.GetMaxByteCount(1); + var encodedLength = encoding.GetByteCount(text); + var destination = pipeWriter.GetSpan(minimumByteSize); + + if (encodedLength <= destination.Length) + { + // Just call Encoding.GetBytes if everything will fit into a single segment. + var bytesWritten = encoding.GetBytes(text, destination); + pipeWriter.Advance(bytesWritten); + } + else + { + WriteMultiSegmentEncoded(pipeWriter, text, encoding, destination, encodedLength, minimumByteSize); + } + } + + private static void WriteMultiSegmentEncoded(PipeWriter writer, string text, Encoding encoding, Span destination, int encodedLength, int minimumByteSize) + { + var encoder = encoding.GetEncoder(); + var source = text.AsSpan(); + var completed = false; + var totalBytesUsed = 0; + + // This may be a bug, but encoder.Convert returns completed = true for UTF7 too early. + // Therefore, we check encodedLength - totalBytesUsed too. + while (!completed || encodedLength - totalBytesUsed != 0) + { + encoder.Convert(source, destination, flush: source.Length == 0, out var charsUsed, out var bytesUsed, out completed); + totalBytesUsed += bytesUsed; + + writer.Advance(bytesUsed); + source = source.Slice(charsUsed); + + destination = writer.GetSpan(minimumByteSize); + } + } + } +} diff --git a/aspnetcore/signalr/connection-handler/sample/Program.cs b/aspnetcore/signalr/connection-handler/sample/Program.cs new file mode 100644 index 000000000000..547e8abf791d --- /dev/null +++ b/aspnetcore/signalr/connection-handler/sample/Program.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace SignalRConnectionHandlerSample +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/aspnetcore/signalr/connection-handler/sample/SignalRConnectionHandlerSample.csproj b/aspnetcore/signalr/connection-handler/sample/SignalRConnectionHandlerSample.csproj new file mode 100644 index 000000000000..bb9ffced1566 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/sample/SignalRConnectionHandlerSample.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.0 + + + diff --git a/aspnetcore/signalr/connection-handler/sample/Startup.cs b/aspnetcore/signalr/connection-handler/sample/Startup.cs new file mode 100644 index 000000000000..52c0c289b6b5 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/sample/Startup.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace SignalRConnectionHandlerSample +{ + 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 https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddConnections(); + services.AddSignalR(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseFileServer(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + app.UseEndpoints(endpoints => + { + endpoints.MapConnectionHandler("/echo"); + }); + }); + } + } +} diff --git a/aspnetcore/signalr/connection-handler/sample/appsettings.Development.json b/aspnetcore/signalr/connection-handler/sample/appsettings.Development.json new file mode 100644 index 000000000000..e203e9407e74 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/sample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/aspnetcore/signalr/connection-handler/sample/appsettings.json b/aspnetcore/signalr/connection-handler/sample/appsettings.json new file mode 100644 index 000000000000..d9d9a9bff6fd --- /dev/null +++ b/aspnetcore/signalr/connection-handler/sample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore/signalr/connection-handler/wip/.gitignore b/aspnetcore/signalr/connection-handler/wip/.gitignore new file mode 100644 index 000000000000..fbfccf63cc31 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/.gitignore @@ -0,0 +1 @@ +wwwroot/lib/ \ No newline at end of file diff --git a/aspnetcore/signalr/connection-handler/wip/MessagesConnectionHandler.cs b/aspnetcore/signalr/connection-handler/wip/MessagesConnectionHandler.cs new file mode 100644 index 000000000000..46be30d6e322 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/MessagesConnectionHandler.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Buffers; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.AspNetCore.Http.Connections.Features; + +namespace SignalRSamples.ConnectionHandlers +{ + public class MessagesConnectionHandler : ConnectionHandler + { + private ConnectionList Connections { get; } = new ConnectionList(); + + public override async Task OnConnectedAsync(ConnectionContext connection) + { + Connections.Add(connection); + + var transportType = connection.Features.Get()?.TransportType; + + await Broadcast($"{connection.ConnectionId} connected ({transportType})"); + + try + { + while (true) + { + var result = await connection.Transport.Input.ReadAsync(); + var buffer = result.Buffer; + + try + { + if (!buffer.IsEmpty) + { + // We can avoid the copy here but we'll deal with that later + var text = Encoding.UTF8.GetString(buffer.ToArray()); + text = $"{connection.ConnectionId}: {text}"; + await Broadcast(Encoding.UTF8.GetBytes(text)); + } + else if (result.IsCompleted) + { + break; + } + } + finally + { + connection.Transport.Input.AdvanceTo(buffer.End); + } + } + } + finally + { + Connections.Remove(connection); + + await Broadcast($"{connection.ConnectionId} disconnected ({transportType})"); + } + } + + private Task Broadcast(string text) + { + return Broadcast(Encoding.UTF8.GetBytes(text)); + } + + private Task Broadcast(byte[] payload) + { + var tasks = new List(Connections.Count); + foreach (var c in Connections) + { + tasks.Add(c.Transport.Output.WriteAsync(payload).AsTask()); + } + + return Task.WhenAll(tasks); + } + } +} diff --git a/aspnetcore/signalr/connection-handler/wip/Program.cs b/aspnetcore/signalr/connection-handler/wip/Program.cs new file mode 100644 index 000000000000..2f932a18a48f --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/Program.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SignalRSamples.Hubs; + +namespace SignalRSamples +{ + public class Program + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .Build(); + + var host = new WebHostBuilder() + .UseConfiguration(config) + .UseSetting(WebHostDefaults.PreventHostingStartupKey, "true") + .ConfigureLogging(factory => + { + factory.AddConsole(); + }) + .UseKestrel(options => + { + // Default port + options.ListenLocalhost(5000); + + // Hub bound to TCP end point + options.Listen(IPAddress.Any, 9001, builder => + { + builder.UseHub(); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/aspnetcore/signalr/connection-handler/wip/SignalRConnectionHandlerSample.csproj b/aspnetcore/signalr/connection-handler/wip/SignalRConnectionHandlerSample.csproj new file mode 100644 index 000000000000..97bf72df88bc --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/SignalRConnectionHandlerSample.csproj @@ -0,0 +1,40 @@ + + + + $(DefaultNetCoreTargetFramework) + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnetcore/signalr/connection-handler/wip/Startup.cs b/aspnetcore/signalr/connection-handler/wip/Startup.cs new file mode 100644 index 000000000000..a114b36b3cc0 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/Startup.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Text.Json; +using System.Text.Json.Serialization; +using SignalRSamples.ConnectionHandlers; +using SignalRSamples.Hubs; + +namespace SignalRSamples +{ + public class Startup + { + + private readonly JsonWriterOptions _jsonWriterOptions = new JsonWriterOptions { Indented = true }; + + // 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) + { + services.AddConnections(); + + services.AddSignalR(options => + { + // Faster pings for testing + options.KeepAliveInterval = TimeSpan.FromSeconds(5); + }) + .AddMessagePackProtocol(); + //.AddStackExchangeRedis(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseFileServer(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapConnectionHandler("/chat"); + }); + } + } +} diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/channelParameters.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/channelParameters.html new file mode 100644 index 000000000000..159cc48bda6f --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/channelParameters.html @@ -0,0 +1,121 @@ + + + + + + + + + +

Streaming Parameters

+

Unknown Transport

+ +

Controls

+
+ + + +
+ +
+ + +
+ +

Results

+
    + +
      + + + + + + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/hubs.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/hubs.html new file mode 100644 index 000000000000..014f753744f4 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/hubs.html @@ -0,0 +1,259 @@ + + + + + + + + + +

      SignalR Hubs Sample

      +
      + + + + + + + + + + + + Enable automatic reconnects +
      + + +

      To Everybody

      +
      +
      + + + +
      +
      + +

      To Connection

      +
      +
      + + + +
      +
      + +

      To Me

      +
      +
      + + + +
      +
      + +

      Group Actions

      +
      +
      + + + + + + + +
      +
      + +
        + + + + + + + + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/index.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/index.html new file mode 100644 index 000000000000..afffefdd598c --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/index.html @@ -0,0 +1,32 @@ + + + + + + + +

        ASP.NET Core Connections

        + +

        ASP.NET Core SignalR (Hubs)

        + +

        ASP.NET Core SignalR (Streaming)

        + +

        ASP.NET Core SignalR (Parameter Streaming via Channels)

        + +
      • Upload streaming via Long polling
      • +
      • Upload streaming via SSE
      • +
      • Upload streaming via Websockets
      • +
        + + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/sockets.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/sockets.html new file mode 100644 index 000000000000..52189b2a066d --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/sockets.html @@ -0,0 +1,50 @@ + + + + + + + + + +

        Unknown Transport

        + +
        + + +
        + +
          + + + + + + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/streaming.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/streaming.html new file mode 100644 index 000000000000..5cc0b3fd6745 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/streaming.html @@ -0,0 +1,116 @@ + + + + + + + + + +

          Unknown Transport

          + +

          Controls

          +
          + + + +
          + +
          + + + +
          + +

          Results

          +
            + +
              + + + + + + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/utils.js b/aspnetcore/signalr/connection-handler/wip/wwwroot/utils.js new file mode 100644 index 000000000000..3572d8188678 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/utils.js @@ -0,0 +1,28 @@ +function getParameterByName(name, url) { + if (!url) { + url = window.location.href; + } + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, " ")); +} + +function click(id, callback) { + document.getElementById(id).addEventListener('click', function (event) { + callback(event); + event.preventDefault(); + }); +} + +function addLine(listId, line, color) { + var child = document.createElement('li'); + if (color) { + child.style.color = color; + } + child.innerText = line; + document.getElementById(listId).appendChild(child); +} + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/worker.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/worker.html new file mode 100644 index 000000000000..afe8b7f7488e --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/worker.html @@ -0,0 +1,54 @@ + + + + + + + +

              SignalR Web Worker Sample

              + +
              +
              + + +
              +
              + +
                + + + + diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/worker.js b/aspnetcore/signalr/connection-handler/wip/wwwroot/worker.js new file mode 100644 index 000000000000..61a9b0ae9f4c --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/worker.js @@ -0,0 +1,23 @@ +importScripts('lib/signalr-webworker/signalr.js'); + +var connection = null; + +onmessage = function (e) { + if (connection === null) { + connection = new signalR.HubConnectionBuilder() + .withUrl(e.data + '/default') + .build(); + + connection.on('send', function (message) { + postMessage('Received message: ' + message); + }); + + connection.start().then(function () { + postMessage('connected'); + }); + } else if (connection.connectionState == signalR.HubConnectionState.Connected) { + connection.invoke('send', e.data); + } else { + postMessage('Attempted to send message while disconnected.') + } +}; diff --git a/aspnetcore/signalr/connection-handler/wip/wwwroot/ws.html b/aspnetcore/signalr/connection-handler/wip/wwwroot/ws.html new file mode 100644 index 000000000000..e56f3233ed99 --- /dev/null +++ b/aspnetcore/signalr/connection-handler/wip/wwwroot/ws.html @@ -0,0 +1,44 @@ + + + + + + + + +

                WebSockets

                + +
                + + +
                + +
                  + +
                + +