Skip to content
This repository has been archived by the owner on Dec 20, 2019. It is now read-only.

Commit

Permalink
Refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
dajuric committed Dec 30, 2017
1 parent 219fbc8 commit b819694
Show file tree
Hide file tree
Showing 21 changed files with 306 additions and 75 deletions.
154 changes: 141 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<a href="https://www.nuget.org/packages/WebsocketRPC.AspCore/"> <img src="https://img.shields.io/badge/WebSokcetRPC.AspCore-v1.x-blue.svg?style=flat-square" alt="NuGet packages version"/> </a>
</p>

**WebSokcetRPC** - RPC over weboskcets for .NET
Leightweight .NET framework for making RPC over websockets. Supports full duplex connections; .NET or Javascript clients.
**WebSokcetRPC** - RPC over websocket for .NET
Lightweight .NET framework for making RPC over websockets. Supports full duplex connections; .NET or Javascript clients.

<!--
> **Tutorial:** <a href="https://www.codeproject.com/Articles/0000/Introducing-Leightweight-WebSocket-RPC-library" target="_blank">CodeProject article</a>
Expand All @@ -22,50 +22,178 @@ Leightweight .NET framework for making RPC over websockets. Supports full duplex
The only dependency is <a href="https://www.newtonsoft.com/json">JSON.NET</a> library used for serialization/deserialization.

+ **Simple**
There is only one relevant method: **Bind** for binding object/interface onto connection, and **CallAsync** for calling RPCs.
There are two relevant method: **Bind** for binding object/interface onto connection, and **CallAsync** for making RPCs.

+ **Use 3rdParty assemblies as API(s)**
Implemented API, if used only for RPC, does not use anything from the library.

+ **Automatic Javascript code generation** *(WebsocketRPC.JS package)*
Javascript websokcet client code is automatically generated **_(with JsDoc comments)_** from an existing .NET
+ **Automatic Javascript code generation** *(WebSocketRPC.JS package)*
Javascript websocket client code is automatically generated **_(with JsDoc comments)_** from an existing .NET
interface (API contract).


## Sample
## <a href="Samples/"> Samples</a>

To scratch the surface... *(RPC in both directions, multi-service, .NET clients)*
Check the samples by following the link above. The snippets below demonstrate the base functionality.

**Server**
#### 1) .NET <-> .NET (raw messaging)
The sample demonstrates communication between server and client using raw messaging. The server relays client messages.

**Server** (C#)
``` csharp
Server.ListenAsync("http://localhost:8000/", CancellationToken.None, (c, wc) =>
{
c.OnOpen += () => Task.Run(() => Console.WriteLine("Opened"));
c.OnClose += (status, description) => Task.Run(() => Console.WriteLine("Closed: " + description));
c.OnError += ex => Task.Run(() => Console.WriteLine("Error: " + ex.Message));

c.OnReceive += msg => await c.SendAsync("Received: " + msg); //relay message
})
.Wait(0);
```

**Client** (C#)
``` csharp
Client.ListenAsync("ws://localhost:8000/", CancellationToken.None, c =>
{
c.OnOpen += () => await c.SendAsync("Hello from a client");
},
reconnectOnError: true)
.Wait(0);
```
{empty} .


#### 2) .NET <-> .NET (RPC)
A data aggregator service is built. The server gets the multiple number sequences of each client and sums all the numbers.
The procedure is repeated for each new client connection.

**Server** (C#)
``` csharp
//client API contract
interface IClientAPI
{
int[] GetLocalNumbers();
}

....
async Task WriteTotalSum()
{
//get all the clients (notice there is no 'this' (the sample above))
var clients = RPC.For<IClientAPI>();

//get the numbers sequences
var numberGroups = await clients.CallAsync(x => x.GetLocalNumbers());
//flatten the collection and sum all the elements
var sum = numberGroups.SelectMany(x => x).Sum();

Console.WriteLine("Client count: {0}; sum: {1}.", clients.Count(), sum);
}

//run server
Server.ListenAsync("http://localhost:8000/", CancellationToken.None,
(c, wc) =>
{
c.Bind<IClientAPI>();
c.OnOpen += WriteTotalSum;
})
.Wait(0);

/*
Output:
Client count: 1; sum: 4.
Client count: 3; sum: 14.
...
*/
```

**Client** (C#)
``` csharp
//client API
class ClientAPI
{
int[] GetLocalNumbers()
{
var r = new Random();

var numbers = new int[10];
for(var i = 0; i < numbers.Length; i++)
numbers[i] = r.Next();

return numbers;
}
}

....
//run client
Client.ListenAsync("ws://localhost:8000/", CancellationToken.None,
c => c.Bind(new ClientAPI())).Wait(0);
```
{empty} .

#### 3) .NET <-> Javascript (RPC)
Simple math service is built and invoked remotely. The math service has a single long running method which adds two numbers (server side).
Client calls the method and receives progress update until the result does not become available.

**Server** (C#)
``` csharp
//client API contract
interface IReportAPI
{
void WriteProgress(int progress);
}

//server API
class MathAPI
{
public async Task<int> LongRunningTask(int a, int b)
{
await Task.Delay(250);
for (var p = 0; p <= 100; p += 5)
{
await Task.Delay(250);
//update only the client which called this method (hence 'this')
await RPC.For<IReportAPI>(this).CallAsync(x => x.WriteProgress(p));
}

return a + b;
}
}

....
//generate js code
//generate js code with JsDoc documentation taken from XML comments (if any)
File.WriteAllText("MathAPI.js", RPCJs.GenerateCallerWithDoc<MathAPI>());
//run server
Server.ListenAsync("http://localhost:8000/", CancellationToken.None,
(c, wc) => c.Bind<MathAPI>(new MathAPI())).Wait(0);
(c, wc) => c.Bind<MathAPI, IReportAPI>(new MathAPI())).Wait(0);
```

**Client**
**Client** (Javascript)
``` javascript
//init 'MathAPI'
var api = new MathAPI("ws://localhost:8000");
//implement the 'IReportAPI'
api.writeProgress = p => console.log("Progress: " + p + "%");

//connect to the server and call the remote function
api.connect(async () =>
{
var r = await api.longRunningTask(5, 3);
console.log("Result: " + r);
});

/*
Output:
Progress: 0 %
Progress: 5 %
Progress: 10 %
...
Result: 8
*/
```


{empty} .


## Getting started
+ Samples
<!--
Expand Down
2 changes: 1 addition & 1 deletion Samples/ClientJs/Site/Index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
var api = new LocalAPI("ws://localhost:8001");

//implement the interface
api.writeProgress = function (p)
api.writeProgress = p =>
{
document.getElementById("progress").innerHTML = "Completed: " + p * 100 + "%";
}
Expand Down
2 changes: 1 addition & 1 deletion Samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The recommendation is to run samples in the following order (from the more simpl

1. **RawMsgJs**
Javascript client and C# server.
Sending/receiving raw text mesages.
Sending/receiving raw text messages.
*Server is receiving messages form a client and sends back altered messages.*

2. **ServerClientSample/++**
Expand Down
8 changes: 4 additions & 4 deletions Samples/RawMsgJs/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ static void Main(string[] args)

//generate js code
File.WriteAllText($"./Site/{nameof(MessagingAPI)}.js", RPCJs.GenerateCaller<MessagingAPI>());

//start server
var cts = new CancellationTokenSource();
var t = Server.ListenAsync("http://localhost:8001/", cts.Token, (c, ws) =>
Expand All @@ -31,9 +31,9 @@ static void Main(string[] args)
c.BindTimeout(TimeSpan.FromSeconds(30));

c.OnOpen += async () => await c.SendAsync("Hello from server using WebSocketRPC");
c.OnClose += () => { Console.WriteLine("Connection closed."); return Task.FromResult(true); };
c.OnError += e => { Console.WriteLine("Error: " + e.Message); return Task.FromResult(true); };
c.OnClose += (s, d) => Task.Run(() => Console.WriteLine("Connection closed: " + d));
c.OnError += e => Task.Run(() => Console.WriteLine("Error: " + e.Message));

c.OnReceive += async msg =>
{
Console.WriteLine("Received: " + msg);
Expand Down
3 changes: 2 additions & 1 deletion Samples/ServerClientSample/Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ static void Main(string[] args)
var r = await RPC.For<ILocalAPI>().CallAsync(x => x.LongRunningTask(5, 3));
Console.WriteLine("\nResult: " + r.First());
};
});
},
reconnectOnError: true);

Console.Write("{0} ", nameof(TestClient));
AppExit.WaitFor(cts, t);
Expand Down
2 changes: 1 addition & 1 deletion Samples/ServerClientSample/Run.bat
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
set serverApp = "Server\bin\Server.exe"
set clientApp = "Server\bin\Client.exe"
set clientApp = "Client\bin\Client.exe"

if NOT EXIST %serverApp% (
echo Build 'Server' project first.
Expand Down
17 changes: 15 additions & 2 deletions Samples/ServerClientSample/Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using SampleBase;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using WebSocketRPC;
Expand Down Expand Up @@ -30,15 +31,27 @@ public class Program
//if access denied execute: "netsh http delete urlacl url=http://+:8001/" (delete for 'localhost', add for public address)
static void Main(string[] args)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Server\n");

//start server and bind to its local and remote API
var cts = new CancellationTokenSource();
var t = Server.ListenAsync("http://localhost:8001/", cts.Token, (c, wc) => c.Bind<LocalAPI, IRemoteAPI>(new LocalAPI()));
var t = Server.ListenAsync("http://localhost:8001/", cts.Token, (c, wc) =>
{
c.Bind<LocalAPI, IRemoteAPI>(new LocalAPI());

c.OnOpen += () => Task.Run((Action)writeClientCount);
c.OnClose += (s, d) => Task.Run((Action)writeClientCount);
});

Console.Write("{0} ", nameof(TestServer));
AppExit.WaitFor(cts, t);
}

static void writeClientCount()
{
var cc = RPC.For<IRemoteAPI>().Count();
Console.WriteLine("Client count: " + cc);
}
}
}
6 changes: 4 additions & 2 deletions Source/WebSocketRPC.Base/ClientServer/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ public static class Client
/// <param name="uri">The target uri of the format: "ws://(address)/[path]".</param>
/// <param name="token">Cancellation token.</param>
/// <param name="onConnect">Action executed when connection is established.</param>
/// <param name="reconnectOnError">True to reconnect on error, false otherwise.</param>
/// <param name="reconnectOnError">True to reconnect on error, false otherwise.
/// <para>If true, exceptions will not be thrown. Set to false when debugging.</para>
/// </param>
/// <param name="reconnectOnClose">True to reconnect on normal close request, false otherwise.</param>
/// <param name="secondsBetweenReconnect">The number of seconds between two reconnect attempts.</param>
/// <param name="setOptions">Websocket option set method.</param>
/// <returns>Client task.</returns>
/// <exception cref="Exception">Socket connection exception thrown in case when <paramref name="reconnectOnError"/> and <paramref name="reconnectOnClose"/> is set to false.</exception>
public static async Task ConnectAsync(string uri, CancellationToken token, Action<Connection> onConnect, bool reconnectOnError = true,
public static async Task ConnectAsync(string uri, CancellationToken token, Action<Connection> onConnect, bool reconnectOnError = false,
bool reconnectOnClose = false, int secondsBetweenReconnect = 0, Action<ClientWebSocketOptions> setOptions = null)
{
var isClosedSuccessfully = true;
Expand Down
23 changes: 15 additions & 8 deletions Source/WebSocketRPC.Base/ClientServer/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public static Encoding Encoding
}
}

static string messageToBig = "The message exceeds the maximum allowed message size: {0} bytes.";
static string messageToBig = "The message exceeds the maximum allowed message size: {0} of allowed {1} bytes.";

#endregion

Expand All @@ -97,6 +97,11 @@ internal protected Connection(WebSocket socket, IReadOnlyDictionary<string, stri
/// </summary>
public IReadOnlyDictionary<string, string> Cookies { get; private set; }

/// <summary>
/// Gets whether the connection is opened or not.
/// </summary>
public bool IsAlive => socket?.State == WebSocketState.Open;

#region Events

/// <summary>
Expand All @@ -110,7 +115,7 @@ internal protected Connection(WebSocket socket, IReadOnlyDictionary<string, stri
/// <summary>
/// Close event.
/// </summary>
public event Func<Task> OnClose;
public event Func<WebSocketCloseStatus, string, Task> OnClose;
/// <summary>
/// Error event Args: exception.
/// </summary>
Expand Down Expand Up @@ -170,16 +175,16 @@ private void invokeOnReceive(string msg)
{ }
}

private void invokeOnClose()
private void invokeOnClose(WebSocketCloseStatus closeStatus, string statusDescription)
{
if (OnClose == null)
return;

try
{
var members = OnClose.GetInvocationList().Cast<Func<Task>>();
var members = OnClose.GetInvocationList().Cast<Func< WebSocketCloseStatus, string, Task >>();

Task.WhenAll(members.Select(x => x()))
Task.WhenAll(members.Select(x => x(closeStatus, statusDescription)))
.ContinueWith(t => InvokeOnError(t.Exception), TaskContinuationOptions.OnlyOnFaulted)
.Wait(0);
}
Expand Down Expand Up @@ -207,7 +212,8 @@ public async Task<bool> SendAsync(string data)
var bData = Encoding.GetBytes(data);
if (bData.Length >= MaxMessageSize)
{
await CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, MaxMessageSize));
//InvokeOnError(new NotSupportedException(String.Format(messageToBig, maxMessageSize)));
await CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, bData.Length, maxMessageSize));
return false;
}

Expand Down Expand Up @@ -256,7 +262,7 @@ public async Task CloseAsync(WebSocketCloseStatus closeStatus = WebSocketCloseSt
}
finally
{
invokeOnClose();
invokeOnClose(closeStatus, statusDescription);
clearEvents();
}
}
Expand Down Expand Up @@ -312,7 +318,8 @@ async Task listenReceiveAsync(CancellationToken token)

if (count >= maxMessageSize)
{
await CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, maxMessageSize));
//InvokeOnError(new NotSupportedException(String.Format(messageToBig, maxMessageSize)));
await CloseAsync(WebSocketCloseStatus.MessageTooBig, String.Format(messageToBig, count, maxMessageSize));
return;
}
}
Expand Down
Loading

0 comments on commit b819694

Please sign in to comment.