diff --git a/samples/SocketsSample/EndPoints/Echo.cs b/samples/SocketsSample/EndPoints/Echo.cs new file mode 100644 index 0000000000..e8711ad7d2 --- /dev/null +++ b/samples/SocketsSample/EndPoints/Echo.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SocketsSample +{ + public class Echo + { + public string Send(string message) + { + return message; + } + } +} diff --git a/samples/SocketsSample/EndPoints/JsonRpcEndpoint.cs b/samples/SocketsSample/EndPoints/JsonRpcEndpoint.cs new file mode 100644 index 0000000000..1450182084 --- /dev/null +++ b/samples/SocketsSample/EndPoints/JsonRpcEndpoint.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Channels; +using Microsoft.AspNetCore.Sockets; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace SocketsSample +{ + // This end point implementation is used for framing JSON objects from the stream + public class JsonRpcEndpoint : EndPoint + { + private readonly Dictionary> _callbacks = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public JsonRpcEndpoint(ILogger logger, IServiceProvider serviceProvider) + { + // TODO: Discover end points + _logger = logger; + _serviceProvider = serviceProvider; + + RegisterJsonRPCEndPoint(typeof(Echo)); + } + + public override async Task OnConnected(Connection connection) + { + // TODO: Dispatch from the caller + await Task.Yield(); + + // DO real async reads + var stream = connection.Channel.GetStream(); + var reader = new JsonTextReader(new StreamReader(stream)); + reader.SupportMultipleContent = true; + + while (true) + { + while (!reader.Read()) + { + break; + } + + JObject request; + try + { + request = JObject.Load(reader); + } + catch (Exception) + { + if (connection.Channel.Input.Reading.IsCompleted) + { + break; + } + + throw; + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Received JSON RPC request: {request}", request); + } + + JObject response = null; + + Func callback; + if (_callbacks.TryGetValue(request.Value("method"), out callback)) + { + response = callback(request); + } + else + { + // If there's no method then return a failed response for this request + response = new JObject(); + response["id"] = request["id"]; + response["error"] = string.Format("Unknown method '{0}'", request.Value("method")); + } + + _logger.LogDebug("Sending JSON RPC response: {data}", response); + + var writer = new JsonTextWriter(new StreamWriter(stream)); + response.WriteTo(writer); + writer.Flush(); + } + } + + private void RegisterJsonRPCEndPoint(Type type) + { + var methods = new List(); + + foreach (var m in type.GetTypeInfo().DeclaredMethods.Where(m => m.IsPublic)) + { + var methodName = type.FullName + "." + m.Name; + + methods.Add(methodName); + + var parameters = m.GetParameters(); + + if (_callbacks.ContainsKey(methodName)) + { + throw new NotSupportedException(String.Format("Duplicate definitions of {0}. Overloading is not supported.", m.Name)); + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("RPC method '{methodName}' is bound", methodName); + } + + _callbacks[methodName] = request => + { + var response = new JObject(); + response["id"] = request["id"]; + + var scopeFactory = _serviceProvider.GetRequiredService(); + + // Scope per call so that deps injected get disposed + using (var scope = scopeFactory.CreateScope()) + { + object value = scope.ServiceProvider.GetService(type) ?? Activator.CreateInstance(type); + + try + { + var args = request.Value("params").Zip(parameters, (a, p) => a.ToObject(p.ParameterType)) + .ToArray(); + + var result = m.Invoke(value, args); + + if (result != null) + { + response["result"] = JToken.FromObject(result); + } + } + catch (TargetInvocationException ex) + { + response["error"] = ex.InnerException.Message; + } + catch (Exception ex) + { + response["error"] = ex.Message; + } + } + + return response; + }; + }; + } + } +} diff --git a/samples/SocketsSample/Startup.cs b/samples/SocketsSample/Startup.cs index c2ea82a2ce..7865200ea2 100644 --- a/samples/SocketsSample/Startup.cs +++ b/samples/SocketsSample/Startup.cs @@ -14,6 +14,7 @@ namespace SocketsSample { services.AddRouting(); + services.AddSingleton(); services.AddSingleton(); } @@ -32,6 +33,7 @@ namespace SocketsSample app.UseSockets(d => { d.MapSocketEndpoint("/chat"); + d.MapSocketEndpoint("/jsonrpc"); }); } } diff --git a/samples/SocketsSample/project.json b/samples/SocketsSample/project.json index cc582adfa6..ecfbf94513 100644 --- a/samples/SocketsSample/project.json +++ b/samples/SocketsSample/project.json @@ -7,6 +7,7 @@ "version": "1.0.0", "type": "platform" }, + "Newtonsoft.Json": "9.0.1", "Microsoft.AspNetCore.Diagnostics": "1.1.0-*", "Microsoft.AspNetCore.StaticFiles": "1.1.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", diff --git a/samples/SocketsSample/wwwroot/index.html b/samples/SocketsSample/wwwroot/index.html index 1c8fa1008b..e410c4c5c9 100644 --- a/samples/SocketsSample/wwwroot/index.html +++ b/samples/SocketsSample/wwwroot/index.html @@ -5,11 +5,14 @@ -

Chat sample

+

ASP.NET Sockets

+

Chat sample

+

JSON RPC

+ JSON RPC \ No newline at end of file diff --git a/samples/SocketsSample/wwwroot/rpc.html b/samples/SocketsSample/wwwroot/rpc.html new file mode 100644 index 0000000000..5884277e9b --- /dev/null +++ b/samples/SocketsSample/wwwroot/rpc.html @@ -0,0 +1,74 @@ + + + + + + + + +

WebSockets

+ + + +
    + + \ No newline at end of file