Early version of the .NET Client

This commit is contained in:
Mikael Mengistu 2016-12-14 10:04:48 -08:00 committed by GitHub
parent df9057a6f7
commit e1d9aa2dd4
53 changed files with 1809 additions and 212 deletions

View File

@ -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

View File

@ -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);
}
}
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -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");
}
}
}

View File

@ -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": {

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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;
}
}
}
}
}

View File

@ -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")]

View File

@ -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>

View File

@ -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": {}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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": {}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Microsoft.AspNetCore.SignalR
{
public interface IInvocationBinder
{
Type GetReturnType(string invocationId);
Type[] GetParameterTypes(string methodName);
}
}

View File

@ -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);
}
}

View File

@ -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; }

View File

@ -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; }
}
}

View File

@ -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; }

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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": {}
}
}

View File

@ -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());
}

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}
}

View File

@ -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);
}

View File

@ -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": {

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}
}

View File

@ -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>

View File

@ -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()
{
}
}
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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")]

View File

@ -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;
}
}
}

View File

@ -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": {}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Threading;

View File

@ -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);
}
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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"
}

View File

@ -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>

View File

@ -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")]

View File

@ -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"
}