-
Notifications
You must be signed in to change notification settings - Fork 14
Getting Started (rapid develop your first server client infrastructure with SignalGo)
In this tutorial we will learn how to use SignalGo to create easily a server-client basic infrastructure. It's for C# developers but users can easily translate code in others .NET supported languages as VB.NET for example. Both client and server have an UI (windows forms) user can interact with (usually server runs as Console app or in a windows service). There's absolutely no difficult to switch to WPF UI, Windows Service or Console App instead of Windows Forms: the purpose of this tutorial is to teach how to set up and run a SignalGo infrastructure so user can use best suitable technology he needs.
We will create a server-client infrastructure who can perform these simple basic tasks for example:
- User can manually start server on Server UI clicking a button "Start server"
- User can connect to server by clicking a button "Connect" on client UI
- Client calls some methods on Server receiving results.
- Server calls a method on a specific client receiving results.
- User kills, from Server, one or more clients by clicking "Kill client" button
In Visual Studio create a new Solution with 2 Windows Forms projects (one for client and one for server) and 1 Shared project (needed for a bunch of necessary interfaces)
Solution ServerClientSignalGo:
- ServerSignalGo (Windows Forms App)
- ClientSignalGo (Windows Forms App)
- SharedSignalGo (Shared Project)
Projects in solution need to be added the appropriate SignalGo NuGet packages. Right click on the project and select "manage NuGet packages" to open the UI package manager where you can search the package to install.
For ServerSignalGo project search and install:
- SignalGo.Net.Server
- SignalGo.Net.Shared
For ClientSignalGo project search and install:
- SignalGo.Net.Client
- SignalGo.Net.Shared
For SharedSignalGo project:
- No SignalGo NuGet package needed
Add 3 buttons (start server, kill clients) and a richtextbox (to log events and results) on the Form.
Add 2 buttons (connect to server, get list of persons), a richtextbox (to log events and results) on the Form and a datagridview (to show a list of complex object, in this case a list of Person.
Before reading next steps, we have to learn the following basic schema about SignalGo mechanism: we must always write 2 interfaces (3 if we want SignalGo handle automatically awaitable methods )
-
2 are for methods that client calls on server ( 1 for the async version of methods, if needed).
IServerMethodsServerSide this defines sync methods (mandatory) IServerMethodsClientSide this is optional. It inherits from IServerMethodsServerSide and here we defines **only** async version of methods written in IServerMethodsServerSide. SignalGo will execute these methods on another thread to prevent UI block for long running tasks. Usage will be: var result = await MyServerService.MyMethodAsync(params)). Without this interface the same result can be obtained in client code using something like this: var result = await Task.Run(() => MyServerService.MySyncMethod(params));
-
1 is for methods that server calls on client
IClientMethods this defines method prototypes that server can call on client to get a result from it.
We then will write, in ServerSignalGo project, the class ServerMethods that inherits from IServerMethodsServerSide in ClientSignalGo project, the class ClientMethods that inherits from IClientMethods
All method names written in this classes must correspond, obviously, to the ones written in interfaces. We must only write Sync version of methods since SignalGo will handle the correct async version for us (writing async versions will also throw an exception since json can't serialize task objects)
Summarizing, this is how our solution appears with classes and interfaces needed by SignalGo:
ServerClientSignalGo solution
- ServerSignalGo project (ServerMethods class)
- ClientSignalGo project (ClientMethods class)
- SharedSignalGo project (IServerMethodsServerSide, IServerMethodsClientSide, IClientMethods interfaces)
This is the magic of SignalGo, we will go deep into the mechanism in a separate document. Now, in this example, we want to perform these tasks:
- client must call a method on server that returns to client this message "Hello from Server to client =>[Your client's machine name]" (output example: "Hello from Server to client => PC-GERSIS"
- before returning the message to client, server must ask to client it's machine name to get proper [Your client's machine name], assemble the message and then return it to the client
- client then call a method on server that returns to client a list
We need it later for the method that returns List to client. You can addo it also to other project accessible to both server and client. It can be also a class coming from Entity Framework for example.
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
public string Address { get; set; }
public int Age { get; set; }
public System.Drawing.Bitmap Picture { get; set; }
}
7.2 Add the interface IServerMethodsServerSide to SharedSignalGo for server methods executed synchronously
This interface defines method prototypes that will be executed server side. In this example methods that will be called by clients are:
- one method HelloFromServer() that returns the string "Hello from Server to client => [Your client's machine name]" to client.
- one method SendList() that returns the List to client.
using SignalGo.Shared.DataTypes;
using System.Collections.Generic;
namespace SharedSignalGo
{
[ServiceContract("ServerService", InstanceType.SingleInstance)]
public interface IServerMethodsServerSide
{
//methods here will run server side always asyncronously in a separate thread
string HelloFromServer();
List<Person> SendList();
}
}
7.3 If you want let SignalGo handle automatically client UI responsiveness allowing you to use something like var result = await MyServerService.MyMethodAsync(params), add the interface IServerMethodsClientSide to SharedSignalGo for server methods executed asynchronously
This optional interface is needed to avoid UI freezing on client while executing method on server. It can inherits from IServerMethodsServerSide so we have only to write async prototypes versions of long running methods you want to be executed asynchronously. In this case, assuming that SendList() is a long running task, you must write the async version here putting the ASYNC suffix to the original name declared as sync. So, the function SendList() becomes Task<List> SendListAsync() in this new interface. Here's the code:
using SignalGo.Shared.DataTypes;
using System.Collections.Generic;
namespace SharedSignalGo
{
[ServiceContract("ServerService", InstanceType.SingleInstance)]
public interface IServerMethodsClientSide: IServerMethodsServerSide
{
//This is the async version of SendList method. SignalGo detects the "Async" word and makes the SendList method
//awaitable. Usage: var listPersons = await ServerService.SendListAsync();
Task<List<Person>> SendListAsync();
}
}
When using in main App, service must register always IServerMethodsClientSide interface! Since it inherits from IServerMethodsServerSide SignalGo will have both the sync and async versions of your methods! If you don't need async versions of your methods, we omit to write IServerMethodsClientSide and, obviously, we must register IServerMethodsServerSide interface.
This interface defines method prototypes that will be executed on client by a server call. In this example the only method that will be called by server is:
-
GetMachineName that returns string System.Environment.MachineName of the client back to the server
the method to kill clients is internally defined in SignalGo (CloseClient method), no need to write it.
using SignalGo.Shared.DataTypes;
namespace SharedSignalGo
{
[ServiceContract("ClientService")]
public interface IClientMethods
{
string GetMachineName();
}
}
Write the class ServerMethods, inheriting from IServerMethodsServerSide.
using SharedSignalGo;
using System.Collections.Generic;
using System.Drawing;
using System;
using SignalGo.Server.Models;
namespace ServerSignalGo
{
public class ServerMethods : IServerMethodsServerSide
{
// Server Side all methods must be not Async since SignalGo will automatically hanlde all in a separate thread
// Also task<T> objects cannot be serialized
public string HelloFromServer()
{
try
{
//SERVER HERE CALLS GetMachineName ON THE CLIENT WHO INVOKED HelloFromServer METHOD !!!!!
var callback = OperationContext.Current.GetClientCallback<IClientMethods>();
string result = callback.GetMachineName();
// END SERVER CALL
return "Hello from Server to client => " + result;
}
catch (Exception ex)
{
throw ex;
}
}
public List<Person> SendList()
{
try
{
List<Person> list_persons = new List<Person>();
for (int i = 0; i < 5; i++)
{
string img64 = "[a valid base64 encoded image here]";
byte[] bytes = Convert.FromBase64String(img64);
Image image;
using (System.IO.MemoryStream ms = new System.IO.MemoryStream(bytes))
{
image = Image.FromStream(ms);
}
Person pr = new Person
{
Name = "Name_" + i,
Surname = "Surname_" + i,
Address = "Adress_" + i,
Age = i,
Picture = new Bitmap(image)
};
list_persons .Add(pr);
}
return list_persons ;
}
catch (Exception ex)
{
throw ex;
}
}
}
}
Please note that **HelloFromServer** contains also a client callback!!!! In fact the server, before giving the result back to client, asks the client to give back it's machine name!!!! This is a good example of SignalGo powerful bi-directional data exchange!
Write the class ClientMethods, inheriting from IClientMethods.
using SharedSignalGo;
namespace ClientSignalGo
{
public class ClientMethods : IClientMethods
{
public string GetMachineName(int param)
{
return System.Environment.MachineName;
}
}
}
To start the server, handle the click event of button "start server"
using SharedSignalGo;
using SignalGo.Server.Models;
using SignalGo.Server.ServiceManager;
// form code here...
// we declare a new server provider
ServerProvider server = new ServerProvider();
private void btn_start_server_Click(object sender, EventArgs e)
{
try
{
//no need to start the server if it's already running
if (st_main.server.IsStarted == true)
{
return;
}
else
{
//initialize the service registering methods that can be executed on server by clients
server.InitializeService<ServerMethods>();
richtextbox_server.AppendText("Server starting..." + Environment.NewLine);
//we start the server
server.Start("http://192.168.1.12:8080/AppSignalGo");
//set up some settings
server.InternalSetting = new SignalGo.Server.Settings.InternalSetting() { IsEnabledDataExchanger = true };
richtextbox_server.AppendText("Server started " + Environment.NewLine);
//register client callback: the methods the server can execute on clients
server.RegisterClientCallbackInterfaceService<IClientMethods>();
richtextbox_server.AppendText("Waiting for incoming connections" + Environment.NewLine);
}
}
catch (Exception ex)
{
richtextbox_server.AppendText("Error: " + ex.Message + Environment.NewLine);
}
}
To kill one specific client (the first in this example) we must handle the "kill clients" button click event
private void btn_kill_Click(object sender, EventArgs e)
{
//get the context manually
SignalGo.Server.Models.OperationContext context = new SignalGo.Server.Models.OperationContext();
//server is your ServerProvider instance
context.ServerBase = st_main.server;
var AllClientsConnected = context.AllServerClients;
var OneClientRandom = (from p in AllClientsConnected select p).FirstOrDefault();
if (OneClientRandom != null)
{
st_main.server.CloseClient(OneClientRandom.ClientId);
richtextbox_server.AppendText("Client: " + OneClientRandom.ClientId + " killed succesfully" + Environment.NewLine);
}
}
To connect to the server, handle the click event of button "connect to server"
using SharedSignalGo;
// form code here...
ClientMethods callbacks;
IServerMethodsClientSide ServerService;
ClientProvider connector = new ClientProvider();
string serverAdress = "http://192.168.1.12:8080/AppSignalGo";
private async void btn_connect_ClickAsync(object sender, EventArgs e)
{
{
try
{
richTexBoxClient.AppendText("Connecting to Server..." + Environment.NewLine);
//this action prevent freezing the UI
await Task.Run((Action)(() =>
{
connector.Connect(serverAdress );
}));
richTexBoxClient.AppendText("Registering services" + Environment.NewLine);
//register the server callback
callbacks = connector.RegisterServerCallback<ClientMethods>();
//register the interface defining the async methods
ServerService = connector.RegisterClientServiceInterfaceWrapper<IServerMethodsClientSide>();
richTexBoxClient.AppendText("Services registered" + Environment.NewLine);
richTexBoxClient.AppendText("Connection esablished with " + serverAdress + " Your ID is " + connector.ClientId + Environment.NewLine);
//we call the method on server and print it into the richtextbox once received
var res = ServerService.HelloFromServer();
richTexBoxClient.AppendText(res + Environment.NewLine);
//in this action wwe handle OnDisconnected action of this client
connector.OnDisconnected = () =>
{
this.Invoke((Action)(() =>
{
{
//non MDI form: close will dispose too
this.Close();
}
}));
};
}
catch (Exception ex)
{
richTexBoxClient.AppendText(ex.Message + Environment.NewLine);
}
}
}
To retrieve the list of persons from the server, handle the click event of button "get list of persons"
private async void btn_persons_ClickAsync(object sender, EventArgs e)
{
richTexBoxClient.AppendText("Receiving Persons data. Wait..." + Environment.NewLine);
//we call the async version of SendList on server. This prevent UI freezing while running this task on server
var listPersons = await ServerService.SendListAsync();
richTexBoxClient.AppendText("Persons received" + Environment.NewLine);
//instantiate a bindingsource and bind list<Person> to it
BindingSource bsource = new BindingSource
{
DataSource = listPersons
};
// then bind the bindingsource to the datagridview to visualize resutls
dataGridViewPersons.DataSource = bsource;
}
Let's handle resource release on closing the main form:
private void F_client_FormClosing(object sender, FormClosingEventArgs e)
{
connector.Dispose();
}