From 2cbca7d596399cabc1f56da29f5f6141bcb179d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Darko=20Juri=C4=87?= Date: Fri, 29 Dec 2017 01:39:24 +0100 Subject: [PATCH] Enhancing samples, restorign raw messaging support, bug fixing. --- Samples/AspRpc/Program.cs | 2 - ...{ServerClientJs.csproj => ClientJs.csproj} | 0 Samples/ClientJs/Program.cs | 14 ++--- Samples/ClientJs/Site/Index.html | 3 +- Samples/MultiService/Program.cs | 6 +-- Samples/README.md | 40 +++++++++++--- Samples/RawMsgJs/Program.cs | 52 +++++++++++++++++++ Samples/RawMsgJs/RawMsgJs.csproj | 29 +++++++++++ Samples/RawMsgJs/Site/Index.html | 52 +++++++++++++++++++ Samples/Serialization/Program.cs | 11 ++-- Samples/ServerClientSample/Client/Program.cs | 2 +- Samples/ServerClientSample/Server/Program.cs | 2 +- Source/WebSocketRPC.Base/RPC.cs | 16 +++++- Source/WebSocketRPC.Base/RPCSettings.cs | 2 +- .../Components/JsCallerGenerator.cs | 1 - .../Resources/ClientAPIBase.js | 10 ++-- .../ClientServer/Server.cs | 23 ++++++-- WebsocketRPC.sln | 9 +++- 18 files changed, 235 insertions(+), 39 deletions(-) rename Samples/ClientJs/{ServerClientJs.csproj => ClientJs.csproj} (100%) create mode 100644 Samples/RawMsgJs/Program.cs create mode 100644 Samples/RawMsgJs/RawMsgJs.csproj create mode 100644 Samples/RawMsgJs/Site/Index.html diff --git a/Samples/AspRpc/Program.cs b/Samples/AspRpc/Program.cs index 416e4e2..fc671e8 100644 --- a/Samples/AspRpc/Program.cs +++ b/Samples/AspRpc/Program.cs @@ -4,9 +4,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using System; using System.IO; -using System.Net.WebSockets; using WebSocketRPC; namespace AspRpc diff --git a/Samples/ClientJs/ServerClientJs.csproj b/Samples/ClientJs/ClientJs.csproj similarity index 100% rename from Samples/ClientJs/ServerClientJs.csproj rename to Samples/ClientJs/ClientJs.csproj diff --git a/Samples/ClientJs/Program.cs b/Samples/ClientJs/Program.cs index c899fd5..102fdc0 100644 --- a/Samples/ClientJs/Program.cs +++ b/Samples/ClientJs/Program.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using WebSocketRPC; -namespace TestClientJs +namespace RawMsgJs { /// /// Remote API. @@ -43,22 +43,24 @@ public async Task LongRunningTask(int a, int b) class Program { - //if access denied execute: "netsh http delete urlacl url=http://+:8001/" + //if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'ocalhost', add for public address) //open Index.html to run the client static void Main(string[] args) { //generate js code File.WriteAllText($"./Site/{nameof(LocalAPI)}.js", RPCJs.GenerateCallerWithDoc()); - + //start server and bind its local and remote API var cts = new CancellationTokenSource(); - var s = Server.ListenAsync("http://localhost:8005/", cts.Token, (c, ws) => + var s = Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => { c.Bind(new LocalAPI()); - c.BindTimeout(TimeSpan.FromSeconds(1)); //close connection if no response after X seconds + //c.BindTimeout(TimeSpan.FromSeconds(1)); //close connection if there is no incommming message after X seconds + + c.OnOpen += async () => await c.SendAsync("Hello from server using WebSocketRPC", RPCSettings.Encoding); }); - Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(TestClientJs)); + Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(RawMsgJs)); Console.ReadLine(); cts.Cancel(); s.Wait(); diff --git a/Samples/ClientJs/Site/Index.html b/Samples/ClientJs/Site/Index.html index 410d90b..57868d6 100644 --- a/Samples/ClientJs/Site/Index.html +++ b/Samples/ClientJs/Site/Index.html @@ -20,7 +20,7 @@ } //init API - var api = new LocalAPI("ws://localhost:8005"); + var api = new LocalAPI("ws://localhost:8001"); //implement the interface api.writeProgress = function (p) @@ -34,6 +34,7 @@ document.getElementById("result").innerHTML = "Result: " + JSON.stringify(r, null, "\t"); } + api.onOtherMessage = writeMsg; api.connect(() => execAPI(api), err => writeMsg(err, 'red'), msg => writeMsg(msg)); diff --git a/Samples/MultiService/Program.cs b/Samples/MultiService/Program.cs index 2ef2a8d..53f8c60 100644 --- a/Samples/MultiService/Program.cs +++ b/Samples/MultiService/Program.cs @@ -3,7 +3,7 @@ using System.Threading; using WebSocketRPC; -namespace ClientJsMultiService +namespace MultiService { /// /// Numeric service providing operations on numbers. @@ -41,7 +41,7 @@ public string Add(string a, string b) class Program { - //if access denied execute: "netsh http delete urlacl url=http://+:8001/" + //if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'ocalhost', add for public address) //open Index.html to run the client static void Main(string[] args) { @@ -65,7 +65,7 @@ static void Main(string[] args) }) .Wait(0); - Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(ClientJsMultiService)); + Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(MultiService)); Console.ReadLine(); cts.Cancel(); } diff --git a/Samples/README.md b/Samples/README.md index d1e3dc7..ee2ffbe 100644 --- a/Samples/README.md +++ b/Samples/README.md @@ -3,13 +3,37 @@ Samples The recommendation is to run samples in the following order (from the more simple ones to more complex ones): -1. ServerClientSample -2. ServerClientJs -3. MultiService -4. Serialization -5. AspRpc +1. **RawMsgJs** + Javascript client and C# server. + Sending/receiving raw text mesages. + *Server is receiving messages form a client and sends back altered messages.* -**Remarks** -+ If a sample contains JavaScript client, the additional step is to open the included Index.html. -+ Samples that contain multiple executables have 'Run.bat' for one-click run. +2. **ServerClientSample/++** + C# client and server. + Two way RPC: .NET <-> .NET. + *Server is executing long running task on the client's request and sending progress notifications to to client.* + +3. **ClientJs** + Javascript client and C# server. + Two way RPC: .NET <-> Javascript. + *Server is executing long running task on the client's request and sending progress notifications to to client. + Client's code is autogenerated.* + +4. **MultiService** + Javascript client(s) and C# server. + Introduction to multi-services RPC. + *Server has two services: numeric and text services / classes. Two Javascript files for each of the services are autogenerated.* +5. **Serialization** + Javascript client and C# server. + Image serialization example. + *Server has image-processing service which receives an image url and gives processed image back to a Javascript client.* + +6. **AspRpc** + Javascript client and C# server. + ASP.NET library integration. + *Server has reporting service initiatted and stopped on the client's request.* + +**Remarks** ++ If a sample contains JavaScript client, an additional step requires opening the included *'Index.html'*. ++ Samples that contain multiple executables have *'Run.bat'* for one-click run. diff --git a/Samples/RawMsgJs/Program.cs b/Samples/RawMsgJs/Program.cs new file mode 100644 index 0000000..f159ad5 --- /dev/null +++ b/Samples/RawMsgJs/Program.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; +using System.Threading; +using WebSocketRPC; + +namespace RawMsgJs +{ + //Empty class (would contains methods if RPC was used). + class MessagingAPI + { } + + class Program + { + //if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'ocalhost', add for public address) + //open Index.html to run the client + static void Main(string[] args) + { + //set message limit + RPCSettings.MaxMessageSize = RPCSettings.Encoding.GetMaxByteCount(40); + + //generate js code + File.WriteAllText($"./Site/{nameof(MessagingAPI)}.js", RPCJs.GenerateCaller()); + + //start server + var cts = new CancellationTokenSource(); + var s = Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => + { + //set idle timeout + c.BindTimeout(TimeSpan.FromSeconds(30)); + + c.OnOpen += async () => await c.SendAsync("Hello from server using WebSocketRPC", RPCSettings.Encoding); + c.OnClose += () => Console.WriteLine("Connection closed."); + c.OnError += e => Console.WriteLine("Error: " + e.Message); + + c.OnReceive += async (msg, isText) => + { + var txt = msg.ToString(RPCSettings.Encoding); + Console.WriteLine("Received: " + txt); + await c.SendAsync("Server received: " + txt, RPCSettings.Encoding); + + if (txt.ToLower() == "close") + await c.CloseAsync(statusDescription: "Close requested by user."); + }; + }); + + Console.WriteLine("Running: '{0}'. Press [Enter] to exit.", nameof(RawMsgJs)); + Console.ReadLine(); + cts.Cancel(); + s.Wait(); + } + } +} diff --git a/Samples/RawMsgJs/RawMsgJs.csproj b/Samples/RawMsgJs/RawMsgJs.csproj new file mode 100644 index 0000000..db4f0f9 --- /dev/null +++ b/Samples/RawMsgJs/RawMsgJs.csproj @@ -0,0 +1,29 @@ + + + netcoreapp2.0;net47 + + + + bin\ + + Exe + + + bin\$(TargetFramework)\ServerClientJs.xml + + + + + + + + + + + + + + Never + + + \ No newline at end of file diff --git a/Samples/RawMsgJs/Site/Index.html b/Samples/RawMsgJs/Site/Index.html new file mode 100644 index 0000000..8ee5d17 --- /dev/null +++ b/Samples/RawMsgJs/Site/Index.html @@ -0,0 +1,52 @@ + + + + + + +

Send/receive messages. Message is sent when the text box loses the focus.

+
+ + + +

Responses:

+
+ + + + + \ No newline at end of file diff --git a/Samples/Serialization/Program.cs b/Samples/Serialization/Program.cs index 21e626d..688663f 100644 --- a/Samples/Serialization/Program.cs +++ b/Samples/Serialization/Program.cs @@ -7,7 +7,7 @@ using WebSocketRPC; using System.Runtime.CompilerServices; -namespace ServerClientJsSerialization +namespace Serialization { class JpgBase64Converter : JsonConverter { @@ -55,7 +55,7 @@ class ImageProcessingAPI Bgr[,] image = null; try { image = imgUrl.GetBytes().DecodeAsColorImage(); } - catch(Exception ex) { throw new Exception("The specified url does not point to a valid image."); } + catch(Exception ex) { throw new Exception("The specified url does not point to a valid image.", ex); } image.Apply(c => swapChannels(c, order), inPlace: true); return image; @@ -71,7 +71,7 @@ unsafe Bgr swapChannels(Bgr c, int[] order) class Program { - //if access denied execute: "cmd" + //if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'ocalhost', add for public address) //open Index.html to run the client static void Main(string[] args) { @@ -86,8 +86,9 @@ static void Main(string[] args) //start server and bind its local and remote API var cts = new CancellationTokenSource(); Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) => c.Bind(new ImageProcessingAPI())).Wait(); - - Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(ServerClientJsSerialization)); + + + Console.Write("Running: '{0}'. Press [Enter] to exit.", nameof(Serialization)); Console.ReadLine(); cts.Cancel(); } diff --git a/Samples/ServerClientSample/Client/Program.cs b/Samples/ServerClientSample/Client/Program.cs index 8ca4a47..7e069ad 100644 --- a/Samples/ServerClientSample/Client/Program.cs +++ b/Samples/ServerClientSample/Client/Program.cs @@ -21,7 +21,7 @@ public void WriteProgress(float progress) public class Program { - //if access denied execute: "netsh http delete urlacl url=http://+:8001/" + //if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'ocalhost', add for public address) static void Main(string[] args) { //Debug.Listeners.Add(new TextWriterTraceListener(Console.Out)); diff --git a/Samples/ServerClientSample/Server/Program.cs b/Samples/ServerClientSample/Server/Program.cs index 7099c2f..6b62287 100644 --- a/Samples/ServerClientSample/Server/Program.cs +++ b/Samples/ServerClientSample/Server/Program.cs @@ -26,7 +26,7 @@ public async Task LongRunningTask(int a, int b) public class Program { - //if access denied execute: "netsh http delete urlacl url=http://+:8001/" + //if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'ocalhost', add for public address) static void Main(string[] args) { Console.ForegroundColor = ConsoleColor.Red; diff --git a/Source/WebSocketRPC.Base/RPC.cs b/Source/WebSocketRPC.Base/RPC.cs index 3e20c13..4d1a692 100644 --- a/Source/WebSocketRPC.Base/RPC.cs +++ b/Source/WebSocketRPC.Base/RPC.cs @@ -27,7 +27,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Text; using System.Threading.Tasks; namespace WebSocketRPC @@ -235,8 +234,12 @@ public static async Task CallAsync(this IEnumera #endregion + #region Misc + /// + /// Gets the connection count. + /// public static int ConnectionCount { get @@ -248,6 +251,17 @@ public static int ConnectionCount } } + /// + /// Gets whether the data contain RPC message or not. + /// + /// Received data. + /// True if the data contain RPC message, false otherwise. + public static bool IsRpcMessage(this ArraySegment data) + { + var str = data.ToString(RPCSettings.Encoding); + return !Request.FromJson(str).IsEmpty || !Response.FromJson(str).IsEmpty; + } + #endregion } } diff --git a/Source/WebSocketRPC.Base/RPCSettings.cs b/Source/WebSocketRPC.Base/RPCSettings.cs index 26a7857..6bc9c99 100644 --- a/Source/WebSocketRPC.Base/RPCSettings.cs +++ b/Source/WebSocketRPC.Base/RPCSettings.cs @@ -62,7 +62,7 @@ public static void AddConverter(JsonConverter converter) } /// - /// Gets or sets the maximum message size [1..inf]. + /// Gets or sets the maximum message size in bytes [1..Int32.MaxValue]. /// public static int MaxMessageSize { diff --git a/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs b/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs index aca460c..5db2aeb 100644 --- a/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs +++ b/Source/WebSocketRPC.JS/Components/JsCallerGenerator.cs @@ -53,7 +53,6 @@ public static (string, MethodInfo[]) GetMethods(params Expression>[ var methodList = objType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); var overloadedMethodNames = methodList.GroupBy(x => x.Name) - .DefaultIfEmpty() .Where(x => x.Count() > 1) .Select(x => x.Key); diff --git a/Source/WebSocketRPC.JS/Resources/ClientAPIBase.js b/Source/WebSocketRPC.JS/Resources/ClientAPIBase.js index 1b62751..e9d3dc5 100644 --- a/Source/WebSocketRPC.JS/Resources/ClientAPIBase.js +++ b/Source/WebSocketRPC.JS/Resources/ClientAPIBase.js @@ -84,13 +84,13 @@ function onMessage(msg, onError) this.connect = function (onOpen, onError, onClose) { if (!!onOpen === false || typeof onOpen !== 'function') - throw 'OnOpen function callback is missing.'; + throw 'onOpen function callback is missing.'; if (!!onError === false || typeof onError !== 'function') - throw 'OnError function callback is missing.'; + throw 'onError function callback is missing.'; if (!!onClose === false || typeof onClose !== 'function') - throw 'OnClose function callback is missing.'; + throw 'onClose function callback is missing.'; if (!!window.WebSocket === false) @@ -125,10 +125,10 @@ this.connect = function (onOpen, onError, onClose) onClose({ id: evt.code, closeReason: evt.reason || "abnormal", summary: "Websocket connection was closed abnormally." }); break; case 1008: - onClose({ id: evt.code, closeReason: evt.reason || "policy violation", summary: "Websocket connection was closed due policy violation." }); + onClose({ id: evt.code, closeReason: evt.reason || "policy violation", summary: "Websocket connection was closed due to policy violation." }); break; case 1009: - onClose({ id: evt.code, closeReason: evt.reason ||"message too big", summary: "Websocket connection was closed due too large message." }); + onClose({ id: evt.code, closeReason: evt.reason ||"message too big", summary: "Websocket connection was closed due to too large message." }); break; case 3001: break; //nothing diff --git a/Source/WebsocketRPC.Standalone/ClientServer/Server.cs b/Source/WebsocketRPC.Standalone/ClientServer/Server.cs index b2c48c3..6d54f82 100644 --- a/Source/WebsocketRPC.Standalone/ClientServer/Server.cs +++ b/Source/WebsocketRPC.Standalone/ClientServer/Server.cs @@ -49,6 +49,9 @@ public static class Server /// Server task. public static async Task ListenAsync(int port, CancellationToken token, Action onConnect, bool useHttps = false) { + if (port < 0 || port > UInt16.MaxValue) + throw new NotSupportedException($"The provided port value must in the range: [0..{UInt16.MaxValue}"); + var s = useHttps ? "s" : String.Empty; await ListenAsync($"http{s}://+:{port}/", token, onConnect); } @@ -65,6 +68,9 @@ public static async Task ListenAsync(int port, CancellationToken token, ActionServer task. public static async Task ListenAsync(int port, CancellationToken token, Action onConnect, Func onHttpRequestAsync, bool useHttps = false) { + if (port < 0 || port > UInt16.MaxValue) + throw new NotSupportedException($"The provided port value must in the range: [0..{UInt16.MaxValue}"); + var s = useHttps ? "s" : String.Empty; await ListenAsync($"http{s}://+:{port}/", token, onConnect, onHttpRequestAsync); } @@ -98,8 +104,19 @@ await ListenAsync(httpListenerPrefix, token, onConnect, (rq, rp) => /// Server task. public static async Task ListenAsync(string httpListenerPrefix, CancellationToken token, Action onConnect, Func onHttpRequestAsync) { + if (token == null) + throw new ArgumentNullException(nameof(token), "The provided token must not be null."); + + if (onConnect == null) + throw new ArgumentNullException(nameof(onConnect), "The provided connection action must not be null."); + + if (onHttpRequestAsync == null) + throw new ArgumentNullException(nameof(onHttpRequestAsync), "The provided HTTP request/response action must not be null."); + + var listener = new HttpListener(); - listener.Prefixes.Add(httpListenerPrefix); + try { listener.Prefixes.Add(httpListenerPrefix); } + catch (Exception ex) { throw new ArgumentException("The provided prefix is not supported. Prefixes have the format: 'http(s)://+:(port)/'", ex); } try { listener.Start(); } catch (Exception ex) when ((ex as HttpListenerException)?.ErrorCode == 5) @@ -147,7 +164,7 @@ static void closeListener(HttpListener listener) for (int i = 0; i < connections.Count; i++) wsCloseTasks[i] = connections[i].CloseAsync(); - Task.WaitAll(wsCloseTasks.Where(t => t != null).ToArray()); + Task.WaitAll(wsCloseTasks.Where(t => t != null).ToArray()); //tasks will be null if 'CloseAsync' fails listener.Stop(); connections.Clear(); } @@ -173,7 +190,7 @@ static async Task listenAsync(HttpListenerContext ctx, CancellationToken token, } catch (Exception) { - ctx.Response.StatusCode = 500; + ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError; ctx.Response.Close(); return; } diff --git a/WebsocketRPC.sln b/WebsocketRPC.sln index 115fb3e..0e35e23 100644 --- a/WebsocketRPC.sln +++ b/WebsocketRPC.sln @@ -19,7 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServerClientSample", "Serve Samples\ServerClientSample\Run.bat = Samples\ServerClientSample\Run.bat EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerClientJs", "Samples\ClientJs\ServerClientJs.csproj", "{C0449FA5-C667-4B5C-878A-04773903B130}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientJs", "Samples\ClientJs\ClientJs.csproj", "{C0449FA5-C667-4B5C-878A-04773903B130}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "Samples\ServerClientSample\Client\Client.csproj", "{E0DBB446-3B2B-4BC2-871F-925AB60917E3}" EndProject @@ -61,6 +61,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspRpc", "Samples\AspRpc\As EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{01049849-1A9A-4C3A-BD56-EA6B628F9F36}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RawMsgJs", "Samples\RawMsgJs\RawMsgJs.csproj", "{76FDC1DC-D80A-42E8-8A5D-2FE3462B9E3F}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution Source\WebSocketRPC.Base\WebSocketRPC.Base.projitems*{eaebaa06-d5bb-4106-8694-c022832175d6}*SharedItemsImports = 13 @@ -106,6 +108,10 @@ Global {CFC334A5-2B52-42A0-87AE-4B3F84B09530}.Debug|Any CPU.Build.0 = Debug|Any CPU {CFC334A5-2B52-42A0-87AE-4B3F84B09530}.Release|Any CPU.ActiveCfg = Release|Any CPU {CFC334A5-2B52-42A0-87AE-4B3F84B09530}.Release|Any CPU.Build.0 = Release|Any CPU + {76FDC1DC-D80A-42E8-8A5D-2FE3462B9E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76FDC1DC-D80A-42E8-8A5D-2FE3462B9E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76FDC1DC-D80A-42E8-8A5D-2FE3462B9E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76FDC1DC-D80A-42E8-8A5D-2FE3462B9E3F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -124,6 +130,7 @@ Global {EAEBAA06-D5BB-4106-8694-C022832175D6} = {3CE373E0-6D9A-48C4-808D-510E6FB69D5C} {CE4BAD88-2358-4B3D-8A34-90A74C73E521} = {3CE373E0-6D9A-48C4-808D-510E6FB69D5C} {CFC334A5-2B52-42A0-87AE-4B3F84B09530} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE} + {76FDC1DC-D80A-42E8-8A5D-2FE3462B9E3F} = {E5509F73-1E5E-45B4-AED7-4A38F8DF1DDE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {591A6475-8DF2-42DA-AFF1-8EF88BCF6EE4}