Early version of the .NET Client
This commit is contained in:
parent
df9057a6f7
commit
e1d9aa2dd4
86
SignalR.sln
86
SignalR.sln
|
|
@ -17,8 +17,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SocketsSample", "samples\So
|
|||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Sockets", "src\Microsoft.AspNetCore.Sockets\Microsoft.AspNetCore.Sockets.xproj", "{1715EA8D-8E13-4ACF-8BCA-57D048E55ED8}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ClientSample", "samples\ClientSample\ClientSample.xproj", "{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6A35B453-52EC-48AF-89CA-D4A69800F131}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Sockets.Tests", "test\Microsoft.AspNetCore.Sockets.Tests\Microsoft.AspNetCore.Sockets.Tests.xproj", "{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}"
|
||||
|
|
@ -45,6 +43,30 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Signal
|
|||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.SignalR.Tests", "test\Microsoft.AspNetCore.SignalR.Tests\Microsoft.AspNetCore.SignalR.Tests.xproj", "{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ClientSample", "samples\ClientSample\ClientSample.xproj", "{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "WebSocketSample", "samples\WebSocketSample\WebSocketSample.xproj", "{EE790D50-C632-46B9-A430-06FA2F2FDCD7}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Sockets.Client", "src\Microsoft.AspNetCore.Sockets.Client\Microsoft.AspNetCore.Sockets.Client.xproj", "{623FD372-36DE-41A9-A564-F6040D570DBD}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Sockets.Client.Tests", "test\Microsoft.AspNetCore.Sockets.Client.Tests\Microsoft.AspNetCore.Sockets.Client.Tests.xproj", "{B19C15A5-F5EA-4CA7-936B-1166ABEE35C4}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.SignalR.Common", "src\Microsoft.AspNetCore.SignalR.Common\Microsoft.AspNetCore.SignalR.Common.xproj", "{E37324FF-6BAF-4243-BA80-7C024CF5F29D}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.SignalR.Client", "src\Microsoft.AspNetCore.SignalR.Client\Microsoft.AspNetCore.SignalR.Client.xproj", "{354335AB-CEE9-4434-A641-78058F6EFE56}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.SignalR.Client.FunctionalTests", "test\Microsoft.AspNetCore.SignalR.Client.FunctionalTests\Microsoft.AspNetCore.SignalR.Client.FunctionalTests.xproj", "{455B68D2-C5B6-4BF4-A685-964B07AFAAF8}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Server.IntegrationTesting", "..\Hosting\src\Microsoft.AspNetCore.Server.IntegrationTesting\Microsoft.AspNetCore.Server.IntegrationTesting.xproj", "{3DA89347-6731-4366-80C4-548F24E8607B}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Hosting.Abstractions", "..\Hosting\src\Microsoft.AspNetCore.Hosting.Abstractions\Microsoft.AspNetCore.Hosting.Abstractions.xproj", "{BB780FBB-7842-4759-8DE7-96FA2E5571C1}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Hosting.Server.Abstractions", "..\Hosting\src\Microsoft.AspNetCore.Hosting.Server.Abstractions\Microsoft.AspNetCore.Hosting.Server.Abstractions.xproj", "{FDBBA081-5248-4FC0-9E08-B46BEF3FA438}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Hosting", "..\Hosting\src\Microsoft.AspNetCore.Hosting\Microsoft.AspNetCore.Hosting.xproj", "{3944F036-7E75-47E8-AA52-C4B89A64EC3A}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.TestHost", "..\Hosting\src\Microsoft.AspNetCore.TestHost\Microsoft.AspNetCore.TestHost.xproj", "{1A415A3F-1081-45DB-809B-EE19CEA02DC0}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -59,10 +81,6 @@ Global
|
|||
{1715EA8D-8E13-4ACF-8BCA-57D048E55ED8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1715EA8D-8E13-4ACF-8BCA-57D048E55ED8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1715EA8D-8E13-4ACF-8BCA-57D048E55ED8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
|
@ -111,6 +129,54 @@ Global
|
|||
{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE790D50-C632-46B9-A430-06FA2F2FDCD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE790D50-C632-46B9-A430-06FA2F2FDCD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE790D50-C632-46B9-A430-06FA2F2FDCD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE790D50-C632-46B9-A430-06FA2F2FDCD7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{623FD372-36DE-41A9-A564-F6040D570DBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{623FD372-36DE-41A9-A564-F6040D570DBD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{623FD372-36DE-41A9-A564-F6040D570DBD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{623FD372-36DE-41A9-A564-F6040D570DBD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B19C15A5-F5EA-4CA7-936B-1166ABEE35C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B19C15A5-F5EA-4CA7-936B-1166ABEE35C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B19C15A5-F5EA-4CA7-936B-1166ABEE35C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B19C15A5-F5EA-4CA7-936B-1166ABEE35C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E37324FF-6BAF-4243-BA80-7C024CF5F29D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E37324FF-6BAF-4243-BA80-7C024CF5F29D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E37324FF-6BAF-4243-BA80-7C024CF5F29D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E37324FF-6BAF-4243-BA80-7C024CF5F29D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{354335AB-CEE9-4434-A641-78058F6EFE56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{354335AB-CEE9-4434-A641-78058F6EFE56}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{354335AB-CEE9-4434-A641-78058F6EFE56}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{354335AB-CEE9-4434-A641-78058F6EFE56}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{455B68D2-C5B6-4BF4-A685-964B07AFAAF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{455B68D2-C5B6-4BF4-A685-964B07AFAAF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{455B68D2-C5B6-4BF4-A685-964B07AFAAF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{455B68D2-C5B6-4BF4-A685-964B07AFAAF8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3DA89347-6731-4366-80C4-548F24E8607B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3DA89347-6731-4366-80C4-548F24E8607B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3DA89347-6731-4366-80C4-548F24E8607B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3DA89347-6731-4366-80C4-548F24E8607B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BB780FBB-7842-4759-8DE7-96FA2E5571C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BB780FBB-7842-4759-8DE7-96FA2E5571C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BB780FBB-7842-4759-8DE7-96FA2E5571C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BB780FBB-7842-4759-8DE7-96FA2E5571C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FDBBA081-5248-4FC0-9E08-B46BEF3FA438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FDBBA081-5248-4FC0-9E08-B46BEF3FA438}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FDBBA081-5248-4FC0-9E08-B46BEF3FA438}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FDBBA081-5248-4FC0-9E08-B46BEF3FA438}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3944F036-7E75-47E8-AA52-C4B89A64EC3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3944F036-7E75-47E8-AA52-C4B89A64EC3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3944F036-7E75-47E8-AA52-C4B89A64EC3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3944F036-7E75-47E8-AA52-C4B89A64EC3A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1A415A3F-1081-45DB-809B-EE19CEA02DC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1A415A3F-1081-45DB-809B-EE19CEA02DC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1A415A3F-1081-45DB-809B-EE19CEA02DC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1A415A3F-1081-45DB-809B-EE19CEA02DC0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -118,7 +184,6 @@ Global
|
|||
GlobalSection(NestedProjects) = preSolution
|
||||
{C4AEAB04-F341-4539-B6C0-52368FB4BF9E} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||
{1715EA8D-8E13-4ACF-8BCA-57D048E55ED8} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||
{AAD719D5-5E31-4ED1-A60F-6EB92EFA66D9} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{5D9DA986-2EAB-4C6D-BF15-9A4BDD4DE775} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{A7050BAE-3DB9-4FB3-A49D-303201415B13} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
|
|
@ -131,5 +196,12 @@ Global
|
|||
{8D789F94-CB74-45FD-ACE7-92AF6E55042E} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||
{A0BF246B-FE7D-4E12-99BF-FFDC131B85D8} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{1CE2B3BE-056C-41E3-A5F5-6A1EF1D288BA} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{BA99C2A1-48F9-4FA5-B95A-9687A73B7CC9} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||
{EE790D50-C632-46B9-A430-06FA2F2FDCD7} = {C4BC9889-B49F-41B6-806B-F84941B2549B}
|
||||
{623FD372-36DE-41A9-A564-F6040D570DBD} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{B19C15A5-F5EA-4CA7-936B-1166ABEE35C4} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
{E37324FF-6BAF-4243-BA80-7C024CF5F29D} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{354335AB-CEE9-4434-A641-78058F6EFE56} = {DA69F624-5398-4884-87E4-B816698CDE65}
|
||||
{455B68D2-C5B6-4BF4-A685-964B07AFAAF8} = {6A35B453-52EC-48AF-89CA-D4A69800F131}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
// 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.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClientSample
|
||||
{
|
||||
internal class HubSample
|
||||
{
|
||||
public static async Task MainAsync(string[] args)
|
||||
{
|
||||
var baseUrl = "http://localhost:5000/hubs";
|
||||
if (args.Length > 0)
|
||||
{
|
||||
baseUrl = args[0];
|
||||
}
|
||||
|
||||
var loggerFactory = new LoggerFactory();
|
||||
loggerFactory.AddConsole(LogLevel.Debug);
|
||||
var logger = loggerFactory.CreateLogger<Program>();
|
||||
|
||||
using (var httpClient = new HttpClient(new LoggingMessageHandler(loggerFactory, new HttpClientHandler())))
|
||||
using (var pipelineFactory = new PipelineFactory())
|
||||
{
|
||||
logger.LogInformation("Connecting to {0}", baseUrl);
|
||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||
using (var connection = await HubConnection.ConnectAsync(new Uri(baseUrl), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
||||
{
|
||||
logger.LogInformation("Connected to {0}", baseUrl);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (sender, a) =>
|
||||
{
|
||||
a.Cancel = true;
|
||||
logger.LogInformation("Stopping loops...");
|
||||
cts.Cancel();
|
||||
};
|
||||
|
||||
// Set up handler
|
||||
connection.On("Send", new[] { typeof(string) }, a =>
|
||||
{
|
||||
var message = (string)a[0];
|
||||
Console.WriteLine("RECEIVED: " + message);
|
||||
});
|
||||
|
||||
while (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var line = Console.ReadLine();
|
||||
logger.LogInformation("Sending: {0}", line);
|
||||
|
||||
await connection.Invoke<object>("Send", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// 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.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClientSample
|
||||
{
|
||||
internal class LoggingMessageHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ILogger<LoggingMessageHandler> _logger;
|
||||
|
||||
public LoggingMessageHandler(ILoggerFactory loggerFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<LoggingMessageHandler>();
|
||||
}
|
||||
|
||||
public LoggingMessageHandler(ILoggerFactory loggerFactory, HttpMessageHandler innerHandler) : base(innerHandler)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<LoggingMessageHandler>();
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Send: {0} {1}", request.Method, request.RequestUri);
|
||||
var result = await base.SendAsync(request, cancellationToken);
|
||||
_logger.LogDebug("Recv: {0} {1}", (int)result.StatusCode, request.RequestUri);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,68 +2,20 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.WebSockets;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClientSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
RunWebSockets().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static async Task RunWebSockets()
|
||||
{
|
||||
var ws = new ClientWebSocket();
|
||||
await ws.ConnectAsync(new Uri("ws://localhost:5000/chat/ws"), CancellationToken.None);
|
||||
|
||||
Console.WriteLine("Connected");
|
||||
|
||||
var sending = Task.Run(async () =>
|
||||
{
|
||||
string line;
|
||||
while ((line = Console.ReadLine()) != null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(line);
|
||||
await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None);
|
||||
}
|
||||
|
||||
await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
});
|
||||
|
||||
var receiving = Receiving(ws);
|
||||
|
||||
await Task.WhenAll(sending, receiving);
|
||||
}
|
||||
|
||||
private static async Task Receiving(ClientWebSocket ws)
|
||||
{
|
||||
var buffer = new byte[2048];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Binary)
|
||||
{
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
//public static void Main(string[] args) => HubSample.MainAsync(args).Wait();
|
||||
public static void Main(string[] args) => RawSample.MainAsync(args).Wait();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
// 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.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClientSample
|
||||
{
|
||||
internal class RawSample
|
||||
{
|
||||
public static async Task MainAsync(string[] args)
|
||||
{
|
||||
var baseUrl = "http://localhost:5000/chat";
|
||||
if (args.Length > 0)
|
||||
{
|
||||
baseUrl = args[0];
|
||||
}
|
||||
|
||||
var loggerFactory = new LoggerFactory();
|
||||
loggerFactory.AddConsole(LogLevel.Debug);
|
||||
var logger = loggerFactory.CreateLogger<Program>();
|
||||
|
||||
using (var httpClient = new HttpClient(new LoggingMessageHandler(loggerFactory, new HttpClientHandler())))
|
||||
using (var pipelineFactory = new PipelineFactory())
|
||||
{
|
||||
logger.LogInformation("Connecting to {0}", baseUrl);
|
||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||
using (var connection = await Connection.ConnectAsync(new Uri(baseUrl), transport, httpClient, pipelineFactory, loggerFactory))
|
||||
{
|
||||
logger.LogInformation("Connected to {0}", baseUrl);
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (sender, a) =>
|
||||
{
|
||||
a.Cancel = true;
|
||||
logger.LogInformation("Stopping loops...");
|
||||
cts.Cancel();
|
||||
};
|
||||
|
||||
// Ready to start the loops
|
||||
var receive = StartReceiving(loggerFactory.CreateLogger("ReceiveLoop"), connection, cts.Token);
|
||||
var send = StartSending(loggerFactory.CreateLogger("SendLoop"), connection, cts.Token);
|
||||
|
||||
await Task.WhenAll(receive, send);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task StartSending(ILogger logger, Connection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Send loop starting");
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var line = Console.ReadLine();
|
||||
logger.LogInformation("Sending: {0}", line);
|
||||
|
||||
await connection.Output.WriteAsync(Encoding.UTF8.GetBytes(line));
|
||||
}
|
||||
logger.LogInformation("Send loop terminated");
|
||||
}
|
||||
|
||||
private static async Task StartReceiving(ILogger logger, Connection connection, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Receive loop starting");
|
||||
using (cancellationToken.Register(() => connection.Input.Complete()))
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = await connection.Input.ReadAsync();
|
||||
var buffer = result.Buffer;
|
||||
try
|
||||
{
|
||||
if (!buffer.IsEmpty)
|
||||
{
|
||||
var message = Encoding.UTF8.GetString(buffer.ToArray());
|
||||
logger.LogInformation("Received: {0}", message);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connection.Input.Advance(buffer.End);
|
||||
}
|
||||
if (result.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.LogInformation("Receive loop terminated");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,9 @@
|
|||
"version": "1.1.0-*",
|
||||
"type": "platform"
|
||||
},
|
||||
"System.Net.WebSockets.Client": "4.3.0-*"
|
||||
"Microsoft.Extensions.Logging.Console": "1.2.0-*",
|
||||
"Microsoft.Extensions.Logging": "1.2.0-*",
|
||||
"Microsoft.AspNetCore.SignalR.Client": "1.0.0-*"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
|
|
@ -11,7 +12,7 @@ namespace SocketsSample
|
|||
{
|
||||
public class LineInvocationAdapter : IInvocationAdapter
|
||||
{
|
||||
public async Task<InvocationDescriptor> ReadInvocationDescriptorAsync(Stream stream, Func<string, Type[]> getParams)
|
||||
public async Task<InvocationMessage> ReadMessageAsync(Stream stream, IInvocationBinder binder, CancellationToken cancellationToken)
|
||||
{
|
||||
var streamReader = new StreamReader(stream);
|
||||
var line = await streamReader.ReadLineAsync();
|
||||
|
|
@ -22,31 +23,61 @@ namespace SocketsSample
|
|||
|
||||
var values = line.Split(',');
|
||||
|
||||
var method = values[1].Substring(1);
|
||||
var type = values[0].Substring(0, 2);
|
||||
var id = values[0].Substring(2);
|
||||
|
||||
return new InvocationDescriptor
|
||||
if (type.Equals("RI"))
|
||||
{
|
||||
Id = values[0].Substring(2),
|
||||
Method = method,
|
||||
Arguments = values.Skip(2).Zip(getParams(method), (v, t) => Convert.ChangeType(v, t)).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task WriteInvocationDescriptorAsync(InvocationDescriptor invocationDescriptor, Stream stream)
|
||||
{
|
||||
var msg = $"CI{invocationDescriptor.Id},M{invocationDescriptor.Method},{string.Join(",", invocationDescriptor.Arguments.Select(a => a.ToString()))}\n";
|
||||
await WriteAsync(msg, stream);
|
||||
}
|
||||
|
||||
public async Task WriteInvocationResultAsync(InvocationResultDescriptor resultDescriptor, Stream stream)
|
||||
{
|
||||
if (string.IsNullOrEmpty(resultDescriptor.Error))
|
||||
{
|
||||
await WriteAsync($"RI{resultDescriptor.Id},E{resultDescriptor.Error}\n", stream);
|
||||
var resultType = values[1].Substring(0, 1);
|
||||
var result = values[1].Substring(1);
|
||||
return new InvocationResultDescriptor()
|
||||
{
|
||||
Id = id,
|
||||
Result = resultType.Equals("E") ? null : result,
|
||||
Error = resultType.Equals("E") ? result : null,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
await WriteAsync($"RI{resultDescriptor.Id},R{(resultDescriptor.Result != null ? resultDescriptor.Result.ToString() : string.Empty)}\n", stream);
|
||||
var method = values[1].Substring(1);
|
||||
|
||||
return new InvocationDescriptor
|
||||
{
|
||||
Id = id,
|
||||
Method = method,
|
||||
Arguments = values.Skip(2).Zip(binder.GetParameterTypes(method), (v, t) => Convert.ChangeType(v, t)).ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Task WriteMessageAsync(InvocationMessage message, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var invocationDescriptor = message as InvocationDescriptor;
|
||||
if (invocationDescriptor != null)
|
||||
{
|
||||
return WriteInvocationDescriptorAsync(invocationDescriptor, stream);
|
||||
}
|
||||
else
|
||||
{
|
||||
return WriteInvocationResultAsync((InvocationResultDescriptor)message, stream);
|
||||
}
|
||||
}
|
||||
|
||||
private Task WriteInvocationDescriptorAsync(InvocationDescriptor invocationDescriptor, Stream stream)
|
||||
{
|
||||
var msg = $"CI{invocationDescriptor.Id},M{invocationDescriptor.Method},{string.Join(",", invocationDescriptor.Arguments.Select(a => a.ToString()))}\n";
|
||||
return WriteAsync(msg, stream);
|
||||
}
|
||||
|
||||
private Task WriteInvocationResultAsync(InvocationResultDescriptor resultDescriptor, Stream stream)
|
||||
{
|
||||
if (string.IsNullOrEmpty(resultDescriptor.Error))
|
||||
{
|
||||
return WriteAsync($"RI{resultDescriptor.Id},E{resultDescriptor.Error}\n", stream);
|
||||
}
|
||||
else
|
||||
{
|
||||
return WriteAsync($"RI{resultDescriptor.Id},R{(resultDescriptor.Result != null ? resultDescriptor.Result.ToString() : string.Empty)}\n", stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
|
@ -19,17 +20,41 @@ namespace SocketsSample.Protobuf
|
|||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<InvocationDescriptor> ReadInvocationDescriptorAsync(Stream stream, Func<string, Type[]> getParams)
|
||||
public Task<InvocationMessage> ReadMessageAsync(Stream stream, IInvocationBinder binder, CancellationToken cancellationToken)
|
||||
{
|
||||
return await Task.Run(() => CreateInvocationDescriptorInt(stream, getParams));
|
||||
return Task.Run(() => CreateInvocationMessageInt(stream, binder));
|
||||
}
|
||||
|
||||
private Task<InvocationDescriptor> CreateInvocationDescriptorInt(Stream stream, Func<string, Type[]> getParams)
|
||||
public Task WriteMessageAsync(InvocationMessage message, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private Task<InvocationMessage> CreateInvocationMessageInt(Stream stream, IInvocationBinder binder)
|
||||
{
|
||||
var inputStream = new CodedInputStream(stream, leaveOpen: true);
|
||||
var messageKind = new RpcMessageKind();
|
||||
inputStream.ReadMessage(messageKind);
|
||||
if(messageKind.MessageKind == RpcMessageKind.Types.Kind.Invocation)
|
||||
{
|
||||
return CreateInvocationDescriptorInt(inputStream, binder);
|
||||
}
|
||||
else
|
||||
{
|
||||
return CreateInvocationResultDescriptorInt(inputStream, binder);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<InvocationMessage> CreateInvocationResultDescriptorInt(CodedInputStream inputStream, IInvocationBinder binder)
|
||||
{
|
||||
throw new NotImplementedException("Not yet implemented for Protobuf");
|
||||
}
|
||||
|
||||
private Task<InvocationMessage> CreateInvocationDescriptorInt(CodedInputStream inputStream, IInvocationBinder binder)
|
||||
{
|
||||
var invocationHeader = new RpcInvocationHeader();
|
||||
inputStream.ReadMessage(invocationHeader);
|
||||
var argumentTypes = getParams(invocationHeader.Name);
|
||||
var argumentTypes = binder.GetParameterTypes(invocationHeader.Name);
|
||||
|
||||
var invocationDescriptor = new InvocationDescriptor();
|
||||
invocationDescriptor.Method = invocationHeader.Name;
|
||||
|
|
@ -59,7 +84,7 @@ namespace SocketsSample.Protobuf
|
|||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(invocationDescriptor);
|
||||
return Task.FromResult<InvocationMessage>(invocationDescriptor);
|
||||
}
|
||||
|
||||
public async Task WriteInvocationResultAsync(InvocationResultDescriptor resultDescriptor, Stream stream)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
// 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.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WebSocketSample
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
RunWebSockets().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static async Task RunWebSockets()
|
||||
{
|
||||
var ws = new ClientWebSocket();
|
||||
await ws.ConnectAsync(new Uri("ws://localhost:5000/chat/ws"), CancellationToken.None);
|
||||
|
||||
Console.WriteLine("Connected");
|
||||
|
||||
var sending = Task.Run(async () =>
|
||||
{
|
||||
string line;
|
||||
while ((line = Console.ReadLine()) != null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(line);
|
||||
await ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None);
|
||||
}
|
||||
|
||||
await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
});
|
||||
|
||||
var receiving = Receiving(ws);
|
||||
|
||||
await Task.WhenAll(sending, receiving);
|
||||
}
|
||||
|
||||
private static async Task Receiving(ClientWebSocket ws)
|
||||
{
|
||||
var buffer = new byte[2048];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, result.Count));
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Binary)
|
||||
{
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("ClientSample")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("ba99c2a1-48f9-4fa5-b95a-9687a73b7cc9")]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?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>ee790d50-c632-46b9-a430-06fa2f2fdcd7</ProjectGuid>
|
||||
<RootNamespace>ClientSample</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"buildOptions": {
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.1.0-*",
|
||||
"type": "platform"
|
||||
},
|
||||
"System.Net.WebSockets.Client": "4.3.0-*"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
// 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.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client
|
||||
{
|
||||
public class HubConnection : IDisposable
|
||||
{
|
||||
private readonly Task _reader;
|
||||
private readonly Stream _stream;
|
||||
private readonly ILogger _logger;
|
||||
private readonly Connection _connection;
|
||||
private readonly IInvocationAdapter _adapter;
|
||||
private readonly HubBinder _binder;
|
||||
|
||||
private readonly CancellationTokenSource _readerCts = new CancellationTokenSource();
|
||||
private readonly ConcurrentDictionary<string, InvocationRequest> _pendingCalls = new ConcurrentDictionary<string, InvocationRequest>();
|
||||
private readonly ConcurrentDictionary<string, InvocationHandler> _handlers = new ConcurrentDictionary<string, InvocationHandler>();
|
||||
|
||||
private int _nextId = 0;
|
||||
|
||||
private HubConnection(Connection connection, IInvocationAdapter adapter, ILogger logger)
|
||||
{
|
||||
_binder = new HubBinder(this);
|
||||
_connection = connection;
|
||||
_stream = connection.GetStream();
|
||||
_adapter = adapter;
|
||||
_logger = logger;
|
||||
|
||||
_reader = ReceiveMessages(_readerCts.Token);
|
||||
_connection.Output.Writing.ContinueWith(
|
||||
t => CompletePendingCalls(t.IsFaulted ? t.Exception : null));
|
||||
}
|
||||
|
||||
// TODO: Client return values/tasks?
|
||||
// TODO: Overloads for void hub methods
|
||||
// TODO: Overloads that use type parameters (like On<T1>, On<T1, T2>, etc.)
|
||||
public void On(string methodName, Type[] parameterTypes, Action<object[]> handler)
|
||||
{
|
||||
var invocationHandler = new InvocationHandler(parameterTypes, handler);
|
||||
_handlers.AddOrUpdate(methodName, invocationHandler, (_, __) => invocationHandler);
|
||||
}
|
||||
|
||||
public Task<T> Invoke<T>(string methodName, params object[] args) => Invoke<T>(methodName, CancellationToken.None, args);
|
||||
public async Task<T> Invoke<T>(string methodName, CancellationToken cancellationToken, params object[] args) => ((T)(await Invoke(methodName, typeof(T), cancellationToken, args)));
|
||||
|
||||
public Task<object> Invoke(string methodName, Type returnType, params object[] args) => Invoke(methodName, returnType, CancellationToken.None, args);
|
||||
public async Task<object> Invoke(string methodName, Type returnType, CancellationToken cancellationToken, params object[] args)
|
||||
{
|
||||
// TODO: we should reject calls to here after the connection is "done" (e.g. sending an invocation failed)
|
||||
|
||||
_logger.LogTrace("Preparing invocation of '{0}', with return type '{1}' and {2} args", methodName, returnType.AssemblyQualifiedName, args.Length);
|
||||
|
||||
// Create an invocation descriptor.
|
||||
var descriptor = new InvocationDescriptor
|
||||
{
|
||||
Id = GetNextId(),
|
||||
Method = methodName,
|
||||
Arguments = args
|
||||
};
|
||||
|
||||
// I just want an excuse to use 'irq' as a variable name...
|
||||
_logger.LogDebug("Registering Invocation ID '{0}' for tracking", descriptor.Id);
|
||||
var irq = new InvocationRequest(cancellationToken, returnType);
|
||||
var addedSuccessfully = _pendingCalls.TryAdd(descriptor.Id, irq);
|
||||
|
||||
// This should always be true since we monotonically increase ids.
|
||||
Debug.Assert(addedSuccessfully, "Id already in use?");
|
||||
|
||||
// Trace the invocation, but only if that logging level is enabled (because building the args list is a bit slow)
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
var argsList = string.Join(", ", args.Select(a => a.GetType().FullName));
|
||||
_logger.LogTrace("Invocation #{0}: {1} {2}({3})", descriptor.Id, returnType.FullName, methodName, argsList);
|
||||
}
|
||||
|
||||
// Write the invocation to the stream
|
||||
_logger.LogInformation("Sending Invocation #{0}", descriptor.Id);
|
||||
await _adapter.WriteMessageAsync(descriptor, _stream, cancellationToken);
|
||||
_logger.LogInformation("Sending Invocation #{0} complete", descriptor.Id);
|
||||
|
||||
// Return the completion task. It will be completed by ReceiveMessages when the response is received.
|
||||
return await irq.Completion.Task;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_readerCts.Cancel();
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
// TODO: Clean up the API here. Negotiation of format would be better than providing an adapter instance. Similarly, we should not require a logger factory
|
||||
public static Task<HubConnection> ConnectAsync(Uri url, IInvocationAdapter adapter, ITransport transport, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory) => ConnectAsync(url, adapter, transport, new HttpClient(), pipelineFactory, loggerFactory);
|
||||
|
||||
public static async Task<HubConnection> ConnectAsync(Uri url, IInvocationAdapter adapter, ITransport transport, HttpClient httpClient, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory)
|
||||
{
|
||||
// Connect the underlying connection
|
||||
var connection = await Connection.ConnectAsync(url, transport, httpClient, pipelineFactory, loggerFactory);
|
||||
|
||||
// Create the RPC connection wrapper
|
||||
return new HubConnection(connection, adapter, loggerFactory.CreateLogger<HubConnection>());
|
||||
}
|
||||
|
||||
private async Task ReceiveMessages(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
_logger.LogTrace("Beginning receive loop");
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// This is a little odd... we want to remove the InvocationRequest once and only once so we pull it out in the callback,
|
||||
// and stash it here because we know the callback will have finished before the end of the await.
|
||||
var message = await _adapter.ReadMessageAsync(_stream, _binder, cancellationToken);
|
||||
|
||||
var invocationDescriptor = message as InvocationDescriptor;
|
||||
if (invocationDescriptor != null)
|
||||
{
|
||||
DispatchInvocation(invocationDescriptor, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var invocationResultDescriptor = message as InvocationResultDescriptor;
|
||||
if (invocationResultDescriptor != null)
|
||||
{
|
||||
DispatchInvocationResult(invocationResultDescriptor, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger.LogTrace("Ending receive loop");
|
||||
}
|
||||
|
||||
private void CompletePendingCalls(Exception e)
|
||||
{
|
||||
_logger.LogTrace("Completing pending calls");
|
||||
|
||||
foreach (var call in _pendingCalls.Values)
|
||||
{
|
||||
if (e == null)
|
||||
{
|
||||
call.Completion.TrySetCanceled();
|
||||
}
|
||||
else
|
||||
{
|
||||
call.Completion.TrySetException(e);
|
||||
}
|
||||
}
|
||||
_pendingCalls.Clear();
|
||||
}
|
||||
|
||||
private void DispatchInvocation(InvocationDescriptor invocationDescriptor, CancellationToken cancellationToken)
|
||||
{
|
||||
// Find the handler
|
||||
InvocationHandler handler;
|
||||
if (!_handlers.TryGetValue(invocationDescriptor.Method, out handler))
|
||||
{
|
||||
_logger.LogWarning("Failed to find handler for '{0}' method", invocationDescriptor.Method);
|
||||
}
|
||||
|
||||
// TODO: Return values
|
||||
// TODO: Dispatch to a sync context to ensure we aren't blocking this loop.
|
||||
handler.Handler(invocationDescriptor.Arguments);
|
||||
}
|
||||
|
||||
private void DispatchInvocationResult(InvocationResultDescriptor result, CancellationToken cancellationToken)
|
||||
{
|
||||
InvocationRequest irq;
|
||||
var successfullyRemoved = _pendingCalls.TryRemove(result.Id, out irq);
|
||||
Debug.Assert(successfullyRemoved, $"Invocation request {result.Id} was removed from the pending calls dictionary!");
|
||||
|
||||
_logger.LogInformation("Received Result for Invocation #{0}", result.Id);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(irq.Completion != null, "Didn't properly capture InvocationRequest in callback for ReadInvocationResultDescriptorAsync");
|
||||
|
||||
// If the invocation hasn't been cancelled, dispatch the result
|
||||
if (!irq.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
irq.Registration.Dispose();
|
||||
|
||||
// Complete the request based on the result
|
||||
// TODO: the TrySetXYZ methods will cause continuations attached to the Task to run, so we should dispatch to a sync context or thread pool.
|
||||
if (!string.IsNullOrEmpty(result.Error))
|
||||
{
|
||||
_logger.LogInformation("Completing Invocation #{0} with error: {1}", result.Id, result.Error);
|
||||
irq.Completion.TrySetException(new Exception(result.Error));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Completing Invocation #{0} with result of type: {1}", result.Id, result.Result?.GetType()?.FullName ?? "<<void>>");
|
||||
irq.Completion.TrySetResult(result.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNextId() => Interlocked.Increment(ref _nextId).ToString();
|
||||
|
||||
private class HubBinder : IInvocationBinder
|
||||
{
|
||||
private HubConnection _connection;
|
||||
|
||||
public HubBinder(HubConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
public Type GetReturnType(string invocationId)
|
||||
{
|
||||
InvocationRequest irq;
|
||||
if (!_connection._pendingCalls.TryGetValue(invocationId, out irq))
|
||||
{
|
||||
_connection._logger.LogError("Unsolicited response received for invocation '{0}'", invocationId);
|
||||
return null;
|
||||
}
|
||||
return irq.ResultType;
|
||||
}
|
||||
|
||||
public Type[] GetParameterTypes(string methodName)
|
||||
{
|
||||
InvocationHandler handler;
|
||||
if (!_connection._handlers.TryGetValue(methodName, out handler))
|
||||
{
|
||||
_connection._logger.LogWarning("Failed to find handler for '{0}' method", methodName);
|
||||
return Type.EmptyTypes;
|
||||
}
|
||||
return handler.ParameterTypes;
|
||||
}
|
||||
}
|
||||
|
||||
private struct InvocationHandler
|
||||
{
|
||||
public Action<object[]> Handler { get; }
|
||||
public Type[] ParameterTypes { get; }
|
||||
|
||||
public InvocationHandler(Type[] parameterTypes, Action<object[]> handler)
|
||||
{
|
||||
Handler = handler;
|
||||
ParameterTypes = parameterTypes;
|
||||
}
|
||||
}
|
||||
|
||||
private struct InvocationRequest
|
||||
{
|
||||
public Type ResultType { get; }
|
||||
public CancellationToken CancellationToken { get; }
|
||||
public CancellationTokenRegistration Registration { get; }
|
||||
public TaskCompletionSource<object> Completion { get; }
|
||||
|
||||
public InvocationRequest(CancellationToken cancellationToken, Type resultType)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
Completion = tcs;
|
||||
CancellationToken = cancellationToken;
|
||||
Registration = cancellationToken.Register(() => tcs.TrySetCanceled());
|
||||
ResultType = resultType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?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>354335ab-cee9-4434-a641-78058f6efe56</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNetCore.SignalR.Client</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// 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.Reflection;
|
||||
using System.Resources;
|
||||
|
||||
[assembly: AssemblyMetadata("Serviceable", "True")]
|
||||
[assembly: NeutralResourcesLanguage("en-us")]
|
||||
[assembly: AssemblyCompany("Microsoft Corporation.")]
|
||||
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
|
||||
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"description": "Client for ASP.NET Core SignalR",
|
||||
|
||||
"packOptions": {
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/aspnet/signalr"
|
||||
},
|
||||
"tags": [
|
||||
"aspnetcore",
|
||||
"signalr"
|
||||
]
|
||||
},
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"nowarn": [
|
||||
"CS1591"
|
||||
],
|
||||
"xmlDoc": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.SignalR.Common": "1.0.0-*",
|
||||
"Microsoft.AspNetCore.Sockets.Client": "1.0.0-*",
|
||||
"NETStandard.Library": "1.6.1-*"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netstandard1.3": {},
|
||||
"net451": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public interface IInvocationAdapter
|
||||
{
|
||||
Task<InvocationMessage> ReadMessageAsync(Stream stream, IInvocationBinder binder, CancellationToken cancellationToken);
|
||||
|
||||
Task WriteMessageAsync(InvocationMessage message, Stream stream, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public interface IInvocationBinder
|
||||
{
|
||||
Type GetReturnType(string invocationId);
|
||||
Type[] GetParameterTypes(string methodName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public static class InvocationAdapterExtensions
|
||||
{
|
||||
public static Task<InvocationMessage> ReadMessageAsync(this IInvocationAdapter self, Stream stream, IInvocationBinder binder) => self.ReadMessageAsync(stream, binder, CancellationToken.None);
|
||||
|
||||
public static Task WriteMessageAsync(this IInvocationAdapter self, InvocationMessage message, Stream stream) => self.WriteMessageAsync(message, stream, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,8 @@ using System;
|
|||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public class InvocationDescriptor
|
||||
public class InvocationDescriptor : InvocationMessage
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Method { get; set; }
|
||||
|
||||
public object[] Arguments { get; set; }
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public abstract class InvocationMessage
|
||||
{
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,8 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public class InvocationResultDescriptor
|
||||
public class InvocationResultDescriptor : InvocationMessage
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public object Result { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public class JsonNetInvocationAdapter : IInvocationAdapter
|
||||
{
|
||||
private JsonSerializer _serializer = new JsonSerializer();
|
||||
|
||||
public JsonNetInvocationAdapter()
|
||||
{
|
||||
}
|
||||
|
||||
public Task<InvocationMessage> ReadMessageAsync(Stream stream, IInvocationBinder binder, CancellationToken cancellationToken)
|
||||
{
|
||||
var reader = new JsonTextReader(new StreamReader(stream));
|
||||
// REVIEW: Task.Run()
|
||||
return Task.Run<InvocationMessage>(() =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var json = _serializer.Deserialize<JObject>(reader);
|
||||
if (json == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine the type of the message
|
||||
if (json["Result"] != null)
|
||||
{
|
||||
// It's a result
|
||||
return BindInvocationResultDescriptor(json, binder, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BindInvocationDescriptor(json, binder, cancellationToken);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public Task WriteMessageAsync(InvocationMessage message, Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
var writer = new JsonTextWriter(new StreamWriter(stream));
|
||||
_serializer.Serialize(writer, message);
|
||||
writer.Flush();
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
private InvocationDescriptor BindInvocationDescriptor(JObject json, IInvocationBinder binder, CancellationToken cancellationToken)
|
||||
{
|
||||
var invocation = new InvocationDescriptor
|
||||
{
|
||||
Id = json.Value<string>("Id"),
|
||||
Method = json.Value<string>("Method"),
|
||||
};
|
||||
|
||||
var paramTypes = binder.GetParameterTypes(invocation.Method);
|
||||
invocation.Arguments = new object[paramTypes.Length];
|
||||
|
||||
var args = json.Value<JArray>("Arguments");
|
||||
for (var i = 0; i < paramTypes.Length; i++)
|
||||
{
|
||||
var paramType = paramTypes[i];
|
||||
invocation.Arguments[i] = args[i].ToObject(paramType, _serializer);
|
||||
}
|
||||
|
||||
return invocation;
|
||||
}
|
||||
|
||||
private InvocationResultDescriptor BindInvocationResultDescriptor(JObject json, IInvocationBinder binder, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = json.Value<string>("Id");
|
||||
var returnType = binder.GetReturnType(id);
|
||||
var result = new InvocationResultDescriptor()
|
||||
{
|
||||
Id = id,
|
||||
Result = returnType == null ? null : json["Result"].ToObject(returnType, _serializer),
|
||||
Error = json.Value<string>("Error")
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?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>e37324ff-6baf-4243-ba80-7c024cf5f29d</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNetCore.SignalR</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// 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.Reflection;
|
||||
using System.Resources;
|
||||
|
||||
[assembly: AssemblyMetadata("Serviceable", "True")]
|
||||
[assembly: NeutralResourcesLanguage("en-us")]
|
||||
[assembly: AssemblyCompany("Microsoft Corporation.")]
|
||||
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
|
||||
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"description": "Common serialiation primitives for SignalR Clients Servers",
|
||||
|
||||
"packOptions": {
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/aspnet/signalr"
|
||||
},
|
||||
"tags": [
|
||||
"aspnetcore",
|
||||
"signalr"
|
||||
]
|
||||
},
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"nowarn": [
|
||||
"CS1591"
|
||||
],
|
||||
"xmlDoc": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"NETStandard.Library": "1.6.1-*",
|
||||
"Microsoft.Extensions.TaskCache.Sources": {
|
||||
"version": "1.2.0-*",
|
||||
"type": "build"
|
||||
},
|
||||
"Newtonsoft.Json": "9.0.1"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netstandard1.3": {},
|
||||
"net451": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.SignalR.Redis
|
|||
// BAD
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
await invocationAdapter.WriteInvocationDescriptorAsync(message, ms);
|
||||
await invocationAdapter.WriteMessageAsync(message, ms);
|
||||
|
||||
await _bus.PublishAsync(channel, ms.ToArray());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
|
||||
var invocationAdapter = _registry.GetInvocationAdapter(connection.Metadata.Get<string>("formatType"));
|
||||
|
||||
tasks.Add(invocationAdapter.WriteInvocationDescriptorAsync(message, connection.Channel.GetStream()));
|
||||
tasks.Add(invocationAdapter.WriteMessageAsync(message, connection.Channel.GetStream()));
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
|
|
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
Arguments = args
|
||||
};
|
||||
|
||||
return invocationAdapter.WriteInvocationDescriptorAsync(message, connection.Channel.GetStream());
|
||||
return invocationAdapter.WriteMessageAsync(message, connection.Channel.GetStream());
|
||||
}
|
||||
|
||||
public override Task InvokeGroupAsync(string groupName, string methodName, object[] args)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
}
|
||||
}
|
||||
|
||||
public class HubEndPoint<THub, TClient> : EndPoint where THub : Hub<TClient>
|
||||
public class HubEndPoint<THub, TClient> : EndPoint, IInvocationBinder where THub : Hub<TClient>
|
||||
{
|
||||
private readonly Dictionary<string, Func<Connection, InvocationDescriptor, Task<InvocationResultDescriptor>>> _callbacks
|
||||
= new Dictionary<string, Func<Connection, InvocationDescriptor, Task<InvocationResultDescriptor>>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
|
@ -102,14 +102,8 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
|
||||
while (true)
|
||||
{
|
||||
var invocationDescriptor =
|
||||
await invocationAdapter.ReadInvocationDescriptorAsync(
|
||||
stream, methodName =>
|
||||
{
|
||||
Type[] types;
|
||||
// TODO: null or throw?
|
||||
return _paramTypes.TryGetValue(methodName, out types) ? types : null;
|
||||
});
|
||||
// TODO: Handle receiving InvocationResultDescriptor
|
||||
var invocationDescriptor = await invocationAdapter.ReadMessageAsync(stream, this) as InvocationDescriptor;
|
||||
|
||||
// Is there a better way of detecting that a connection was closed?
|
||||
if (invocationDescriptor == null)
|
||||
|
|
@ -140,7 +134,7 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
_logger.LogError("Unknown hub method '{method}'", invocationDescriptor.Method);
|
||||
}
|
||||
|
||||
await invocationAdapter.WriteInvocationResultAsync(result, stream);
|
||||
await invocationAdapter.WriteMessageAsync(result, stream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -240,5 +234,17 @@ namespace Microsoft.AspNetCore.SignalR
|
|||
// TODO: Add more checks
|
||||
return m.IsPublic;
|
||||
}
|
||||
|
||||
Type IInvocationBinder.GetReturnType(string invocationId)
|
||||
{
|
||||
return typeof(object);
|
||||
}
|
||||
|
||||
Type[] IInvocationBinder.GetParameterTypes(string methodName)
|
||||
{
|
||||
Type[] types;
|
||||
// TODO: null or throw?
|
||||
return _paramTypes.TryGetValue(methodName, out types) ? types : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
// 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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public interface IInvocationAdapter
|
||||
{
|
||||
Task<InvocationDescriptor> ReadInvocationDescriptorAsync(Stream stream, Func<string, Type[]> getParams);
|
||||
|
||||
Task WriteInvocationResultAsync(InvocationResultDescriptor resultDescriptor, Stream stream);
|
||||
|
||||
Task WriteInvocationDescriptorAsync(InvocationDescriptor invocationDescriptor, Stream stream);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
// 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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR
|
||||
{
|
||||
public class JsonNetInvocationAdapter : IInvocationAdapter
|
||||
{
|
||||
private JsonSerializer _serializer = new JsonSerializer();
|
||||
|
||||
public JsonNetInvocationAdapter()
|
||||
{
|
||||
}
|
||||
|
||||
public Task<InvocationDescriptor> ReadInvocationDescriptorAsync(Stream stream, Func<string, Type[]> getParams)
|
||||
{
|
||||
var reader = new JsonTextReader(new StreamReader(stream));
|
||||
// REVIEW: Task.Run()
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var jsonInvocation = _serializer.Deserialize<JsonNetInvocationDescriptor>(reader);
|
||||
if (jsonInvocation == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var invocation = new InvocationDescriptor
|
||||
{
|
||||
Id = jsonInvocation.Id,
|
||||
Method = jsonInvocation.Method,
|
||||
};
|
||||
|
||||
var paramTypes = getParams(jsonInvocation.Method);
|
||||
invocation.Arguments = new object[paramTypes.Length];
|
||||
|
||||
for (int i = 0; i < paramTypes.Length; i++)
|
||||
{
|
||||
var paramType = paramTypes[i];
|
||||
invocation.Arguments[i] = jsonInvocation.Arguments[i].ToObject(paramType, _serializer);
|
||||
}
|
||||
|
||||
return invocation;
|
||||
});
|
||||
}
|
||||
|
||||
public Task WriteInvocationResultAsync(InvocationResultDescriptor resultDescriptor, Stream stream)
|
||||
{
|
||||
Write(resultDescriptor, stream);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task WriteInvocationDescriptorAsync(InvocationDescriptor invocationDescriptor, Stream stream)
|
||||
{
|
||||
Write(invocationDescriptor, stream);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private void Write(object value, Stream stream)
|
||||
{
|
||||
var writer = new JsonTextWriter(new StreamWriter(stream));
|
||||
_serializer.Serialize(writer, value);
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
private class JsonNetInvocationDescriptor
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public string Method { get; set; }
|
||||
|
||||
public JArray Arguments { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.AddSingleton<IConfigureOptions<SignalROptions>, SignalROptionsSetup>();
|
||||
services.AddSingleton<JsonNetInvocationAdapter>();
|
||||
services.AddSingleton<InvocationAdapterRegistry>();
|
||||
services.AddRouting();
|
||||
|
||||
return new SignalRBuilder(services);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@
|
|||
"Microsoft.AspNetCore.Sockets": {
|
||||
"target": "project"
|
||||
},
|
||||
"Microsoft.AspNetCore.SignalR.Common": "1.0.0-*",
|
||||
"Microsoft.Extensions.TaskCache.Sources": {
|
||||
"version": "1.2.0-*",
|
||||
"type": "build"
|
||||
},
|
||||
"NETStandard.Library": "1.6.1-*",
|
||||
"Newtonsoft.Json": "9.0.1"
|
||||
"NETStandard.Library": "1.6.1-*"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
// 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.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
internal class ReadableBufferContent : HttpContent
|
||||
{
|
||||
private ReadableBuffer _buffer;
|
||||
|
||||
public ReadableBufferContent(ReadableBuffer buffer)
|
||||
{
|
||||
_buffer = buffer;
|
||||
}
|
||||
|
||||
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => _buffer.CopyToAsync(stream);
|
||||
|
||||
protected override bool TryComputeLength(out long length)
|
||||
{
|
||||
length = _buffer.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// 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.Pipelines;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
public class Connection : IPipelineConnection
|
||||
{
|
||||
private IPipelineConnection _consumerPipe;
|
||||
private ITransport _transport;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public Uri Url { get; }
|
||||
|
||||
// TODO: Review. This is really only designed to be used from ConnectAsync
|
||||
private Connection(Uri url, ITransport transport, IPipelineConnection consumerPipe, ILogger logger)
|
||||
{
|
||||
Url = url;
|
||||
|
||||
_logger = logger;
|
||||
_transport = transport;
|
||||
_consumerPipe = consumerPipe;
|
||||
|
||||
_consumerPipe.Output.Writing.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
_consumerPipe.Input.Complete(t.Exception);
|
||||
}
|
||||
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
public IPipelineReader Input => _consumerPipe.Input;
|
||||
public IPipelineWriter Output => _consumerPipe.Output;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_consumerPipe.Dispose();
|
||||
_transport.Dispose();
|
||||
}
|
||||
|
||||
// TODO: More overloads. PipelineFactory should be optional but someone needs to dispose the pool, if we're OK with it being the GC, then this is easy.
|
||||
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, PipelineFactory pipelineFactory) => ConnectAsync(url, transport, new HttpClient(), pipelineFactory, NullLoggerFactory.Instance);
|
||||
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory) => ConnectAsync(url, transport, new HttpClient(), pipelineFactory, loggerFactory);
|
||||
public static Task<Connection> ConnectAsync(Uri url, ITransport transport, HttpClient httpClient, PipelineFactory pipelineFactory) => ConnectAsync(url, transport, httpClient, pipelineFactory, NullLoggerFactory.Instance);
|
||||
|
||||
public static async Task<Connection> ConnectAsync(Uri url, ITransport transport, HttpClient httpClient, PipelineFactory pipelineFactory, ILoggerFactory loggerFactory)
|
||||
{
|
||||
if (url == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(url));
|
||||
}
|
||||
|
||||
if (transport == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(transport));
|
||||
}
|
||||
|
||||
if (httpClient == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
if (pipelineFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pipelineFactory));
|
||||
}
|
||||
|
||||
if (loggerFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
}
|
||||
|
||||
var logger = loggerFactory.CreateLogger<Connection>();
|
||||
var getIdUrl = Utils.AppendPath(url, "getid");
|
||||
|
||||
string connectionId;
|
||||
try
|
||||
{
|
||||
// Get a connection ID from the server
|
||||
logger.LogDebug("Reserving Connection Id from: {0}", getIdUrl);
|
||||
connectionId = await httpClient.GetStringAsync(getIdUrl);
|
||||
logger.LogDebug("Reserved Connection Id: {0}", connectionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError("Failed to start connection. Error getting connection id from '{0}': {1}", getIdUrl, ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
var connectedUrl = Utils.AppendQueryString(url, "id=" + connectionId);
|
||||
|
||||
var pair = pipelineFactory.CreatePipelinePair();
|
||||
|
||||
// Start the transport, giving it one end of the pipeline
|
||||
try
|
||||
{
|
||||
await transport.StartAsync(connectedUrl, pair.Item1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError("Failed to start connection. Error starting transport '{0}': {1}", transport.GetType().Name, ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Create the connection, giving it the other end of the pipeline
|
||||
return new Connection(url, transport, pair.Item2, logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
public interface ITransport : IDisposable
|
||||
{
|
||||
Task StartAsync(Uri url, IPipelineConnection pipeline);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
public class LongPollingTransport : ITransport
|
||||
{
|
||||
private static readonly string DefaultUserAgent = "Microsoft.AspNetCore.SignalR.Client/0.0.0";
|
||||
private static readonly ProductInfoHeaderValue DefaultUserAgentHeader = ProductInfoHeaderValue.Parse(DefaultUserAgent);
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private readonly CancellationTokenSource _senderCts = new CancellationTokenSource();
|
||||
private readonly CancellationTokenSource _pollCts = new CancellationTokenSource();
|
||||
|
||||
private IPipelineConnection _pipeline;
|
||||
private Task _sender;
|
||||
private Task _poller;
|
||||
|
||||
public Task Running { get; private set; }
|
||||
|
||||
public LongPollingTransport(HttpClient httpClient, ILoggerFactory loggerFactory)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = loggerFactory.CreateLogger<LongPollingTransport>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_senderCts.Cancel();
|
||||
_pollCts.Cancel();
|
||||
_pipeline?.Dispose();
|
||||
}
|
||||
|
||||
public Task StartAsync(Uri url, IPipelineConnection pipeline)
|
||||
{
|
||||
_pipeline = pipeline;
|
||||
|
||||
// Schedule shutdown of the poller when the output is closed
|
||||
pipeline.Output.Writing.ContinueWith(_ =>
|
||||
{
|
||||
_pollCts.Cancel();
|
||||
return TaskCache.CompletedTask;
|
||||
});
|
||||
|
||||
// Start sending and polling
|
||||
_sender = SendMessages(Utils.AppendPath(url, "send"), _senderCts.Token);
|
||||
_poller = Poll(Utils.AppendPath(url, "poll"), _pollCts.Token);
|
||||
Running = Task.WhenAll(_sender, _poller);
|
||||
|
||||
return TaskCache.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Poll(Uri pollUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, pollUrl);
|
||||
request.Headers.UserAgent.Add(DefaultUserAgentHeader);
|
||||
|
||||
var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NoContent || cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Transport closed or polling stopped, we're done
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write the data to the output
|
||||
var buffer = _pipeline.Output.Alloc();
|
||||
var stream = new WriteableBufferStream(buffer);
|
||||
await response.Content.CopyToAsync(stream);
|
||||
await buffer.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// Polling complete
|
||||
_pipeline.Output.Complete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Shut down the output pipeline and log
|
||||
_logger.LogError("Error while polling '{0}': {1}", pollUrl, ex);
|
||||
_pipeline.Output.Complete(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessages(Uri sendUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
using (cancellationToken.Register(() => _pipeline.Input.Complete()))
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var result = await _pipeline.Input.ReadAsync();
|
||||
var buffer = result.Buffer;
|
||||
if (buffer.IsEmpty || result.IsCompleted)
|
||||
{
|
||||
// No more data to send
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a message to send
|
||||
var message = new HttpRequestMessage(HttpMethod.Post, sendUrl);
|
||||
message.Headers.UserAgent.Add(DefaultUserAgentHeader);
|
||||
message.Content = new ReadableBufferContent(buffer);
|
||||
|
||||
// Send it
|
||||
var response = await _httpClient.SendAsync(message);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
_pipeline.Input.Advance(buffer.End);
|
||||
}
|
||||
|
||||
// Sending complete
|
||||
_pipeline.Input.Complete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Shut down the input pipeline and log
|
||||
_logger.LogError("Error while sending to '{0}': {1}", sendUrl, ex);
|
||||
_pipeline.Input.Complete(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?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>623fd372-36de-41a9-a564-f6040d570dbd</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNetCore.SignalR.Client</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
internal class NullLoggerFactory : ILoggerFactory
|
||||
{
|
||||
public static readonly ILoggerFactory Instance = new NullLoggerFactory();
|
||||
|
||||
private NullLoggerFactory()
|
||||
{
|
||||
}
|
||||
|
||||
public void AddProvider(ILoggerProvider provider)
|
||||
{
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return NullLogger.Instance;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using System.IO.Pipelines;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
internal class PipelineConnection : IPipelineConnection
|
||||
{
|
||||
public IPipelineReader Input { get; }
|
||||
public IPipelineWriter Output { get; }
|
||||
|
||||
public PipelineConnection(IPipelineReader input, IPipelineWriter output)
|
||||
{
|
||||
Input = input;
|
||||
Output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Input.Complete();
|
||||
Output.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using System.IO.Pipelines;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
// TODO: Move to System.IO.Pipelines
|
||||
public static class PipelineFactoryExtensions
|
||||
{
|
||||
// TODO: Use a named tuple? Though there aren't really good names for these ... client/server? left/right?
|
||||
public static Tuple<IPipelineConnection, IPipelineConnection> CreatePipelinePair(this PipelineFactory self)
|
||||
{
|
||||
// Create a pair of pipelines for "Server" and "Client"
|
||||
var clientToServer = self.Create();
|
||||
var serverToClient = self.Create();
|
||||
|
||||
// "Server" reads from clientToServer and writes to serverToClient
|
||||
var server = new PipelineConnection(
|
||||
input: clientToServer,
|
||||
output: serverToClient);
|
||||
|
||||
// "Client" reads from serverToClient and writes to clientToServer
|
||||
var client = new PipelineConnection(
|
||||
input: serverToClient,
|
||||
output: clientToServer);
|
||||
|
||||
return Tuple.Create((IPipelineConnection)server, (IPipelineConnection)client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
// 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.Reflection;
|
||||
using System.Resources;
|
||||
|
||||
[assembly: AssemblyMetadata("Serviceable", "True")]
|
||||
[assembly: NeutralResourcesLanguage("en-us")]
|
||||
[assembly: AssemblyCompany("Microsoft Corporation.")]
|
||||
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
|
||||
[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Sockets.Client
|
||||
{
|
||||
internal static class Utils
|
||||
{
|
||||
public static Uri AppendPath(Uri url, string path)
|
||||
{
|
||||
var builder = new UriBuilder(url);
|
||||
if (!builder.Path.EndsWith("/"))
|
||||
{
|
||||
builder.Path += "/";
|
||||
}
|
||||
builder.Path += path;
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
internal static Uri AppendQueryString(Uri url, string qs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(qs))
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
var builder = new UriBuilder(url);
|
||||
if (!string.IsNullOrEmpty(builder.Query))
|
||||
{
|
||||
builder.Query += "&";
|
||||
}
|
||||
builder.Query += qs;
|
||||
return builder.Uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"description": "Client for ASP.NET Core SignalR",
|
||||
|
||||
"packOptions": {
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/aspnet/signalr"
|
||||
},
|
||||
"tags": [
|
||||
"aspnetcore",
|
||||
"signalr"
|
||||
]
|
||||
},
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"nowarn": [
|
||||
"CS1591"
|
||||
],
|
||||
"xmlDoc": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"NETStandard.Library": "1.6.1-*",
|
||||
"System.IO.Pipelines": "0.1.0-*",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "1.2.0-*",
|
||||
"Microsoft.Extensions.TaskCache.Sources": {
|
||||
"type": "build",
|
||||
"version": "1.2.0-*"
|
||||
}
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"netstandard1.3": {},
|
||||
"net451": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Threading;
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ namespace Microsoft.Extensions.WebSockets.Internal
|
|||
buffer.WriteBigEndian((ushort)Status);
|
||||
if (!string.IsNullOrEmpty(Description))
|
||||
{
|
||||
buffer.Append(Description, TextEncoding.Utf8);
|
||||
buffer.Append(Description, EncodingData.InvariantUtf8.TextEncoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
// 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.Pipelines;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Sockets.Client;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||
{
|
||||
public class HubConnectionTests : IDisposable
|
||||
{
|
||||
private readonly TestServer _testServer;
|
||||
|
||||
public HubConnectionTests()
|
||||
{
|
||||
var webHostBuilder = new WebHostBuilder().
|
||||
ConfigureServices(services =>
|
||||
{
|
||||
services.AddSignalR();
|
||||
})
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseSignalR(routes =>
|
||||
{
|
||||
routes.MapHub<TestHub>("/hubs");
|
||||
});
|
||||
});
|
||||
_testServer = new TestServer(webHostBuilder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckFixedMessage()
|
||||
{
|
||||
var loggerFactory = new LoggerFactory();
|
||||
|
||||
using (var httpClient = _testServer.CreateClient())
|
||||
using (var pipelineFactory = new PipelineFactory())
|
||||
{
|
||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
||||
{
|
||||
//TODO: Get rid of this. This is to prevent "No channel" failures due to sends occuring before the first poll.
|
||||
await Task.Delay(500);
|
||||
var result = await connection.Invoke<string>("HelloWorld");
|
||||
|
||||
Assert.Equal("Hello World!", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanSendAndReceiveMessage()
|
||||
{
|
||||
var loggerFactory = new LoggerFactory();
|
||||
const string originalMessage = "SignalR";
|
||||
|
||||
using (var httpClient = _testServer.CreateClient())
|
||||
using (var pipelineFactory = new PipelineFactory())
|
||||
{
|
||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
||||
{
|
||||
//TODO: Get rid of this. This is to prevent "No channel" failures due to sends occuring before the first poll.
|
||||
await Task.Delay(500);
|
||||
var result = await connection.Invoke<string>("Echo", originalMessage);
|
||||
|
||||
Assert.Equal(originalMessage, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanInvokeClientMethodFromServer()
|
||||
{
|
||||
var loggerFactory = new LoggerFactory();
|
||||
const string originalMessage = "SignalR";
|
||||
|
||||
using (var httpClient = _testServer.CreateClient())
|
||||
using (var pipelineFactory = new PipelineFactory())
|
||||
{
|
||||
var transport = new LongPollingTransport(httpClient, loggerFactory);
|
||||
using (var connection = await HubConnection.ConnectAsync(new Uri("http://test/hubs"), new JsonNetInvocationAdapter(), transport, httpClient, pipelineFactory, loggerFactory))
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
connection.On("Echo", new[] { typeof(string) }, a =>
|
||||
{
|
||||
tcs.TrySetResult((string)a[0]);
|
||||
});
|
||||
|
||||
//TODO: Get rid of this. This is to prevent "No channel" failures due to sends occuring before the first poll.
|
||||
await Task.Delay(500);
|
||||
await connection.Invoke<Task>("CallEcho", originalMessage);
|
||||
var completed = await Task.WhenAny(Task.Delay(2000), tcs.Task);
|
||||
Assert.True(completed == tcs.Task, "Receive timed out!");
|
||||
Assert.Equal(originalMessage, tcs.Task.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_testServer.Dispose();
|
||||
}
|
||||
|
||||
public class TestHub : Hub
|
||||
{
|
||||
public string HelloWorld()
|
||||
{
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
public string Echo(string message)
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
public async Task CallEcho(string message)
|
||||
{
|
||||
await Clients.Client(Context.ConnectionId).InvokeAsync("Echo", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?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)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>455b68d2-c5b6-4bf4-a685-964b07afaaf8</ProjectGuid>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Microsoft.AspNetCore.SignalR.Client.FunctionalTests")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("455b68d2-c5b6-4bf4-a685-964b07afaaf8")]
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"dotnet-test-xunit": "2.2.0-*",
|
||||
"Microsoft.AspNetCore.Http": "1.2.0-*",
|
||||
"Microsoft.AspNetCore.Sockets": "0.1.0-*",
|
||||
"Microsoft.AspNetCore.Hosting": "1.2.0-*",
|
||||
"Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*",
|
||||
"Microsoft.AspNetCore.SignalR": "1.0.0-*",
|
||||
"Microsoft.AspNetCore.SignalR.Client": "1.0.0-*",
|
||||
"xunit": "2.2.0-*",
|
||||
"Microsoft.AspNetCore.TestHost": "1.2.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.1.0-*",
|
||||
"type": "platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net451": {}
|
||||
},
|
||||
"testRunner": "xunit"
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>b19c15a5-f5ea-4ca7-936b-1166abee35c4</ProjectGuid>
|
||||
<RootNamespace>Microsoft.AspNetCore.Sockets.Client.Tests</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Microsoft.AspNetCore.SignalR.Client.Tests")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("40f805d4-068c-4def-aa51-77419b9c70ac")]
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"dotnet-test-xunit": "2.2.0-*",
|
||||
"xunit": "2.2.0-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"version": "1.1.0-*",
|
||||
"type": "platform"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net451": {}
|
||||
},
|
||||
"testRunner": "xunit"
|
||||
}
|
||||
Loading…
Reference in New Issue