📜 ⬆️ ⬇️

We write our synchronous / asynchronous client-server

Hello.

In this article, we consider the principle of a multi-threaded TCP application server in which we implement synchronous and asynchronous calls, as well as access control for procedures and data compression.

How it all began
It all started with a difficult choice of where to start implementing the interaction between several different applications working via the Internet. It seemed that WCF would be able to solve such a problem, but, unfortunately, it is not without drawbacks and some problems, and its principle of operation greatly affects the speed of data transfer. It needed a simple solution and at the same time quite functional.

We begin our project with a class library in which we will create interfaces of procedures and functions for applications of various types.
public interface ICommon { string[] GetAvailableUsers(); void ChangePrivileges(string Login, string password); } public interface IDog { bool TryFindObject(out object obj); int Bark(int nTimes); } public interface ICat { void CutTheText(ref string Text); } 

Here we describe the signature of the procedures for three different access levels. As you might guess, Common will contain procedures available for all types of remote clients. Dog and Cat here, these are our two types of remote clients, the procedures for each of them will be available only to them.
')
Here we will create a class with the help of which we will exchange data between the server and clients.
 [Serializable] public class Message { public Message(string Command, object[] Parameters) { this.Command = Command; if (Parameters != null) this.prms = Parameters; } public bool IsSync; public bool IsEmpty = true; public readonly string Command; public object ReturnValue; public object[] prms; public Exception Exception; } 

Customer

The client will implement a proxy link between the interface methods and the server. To do this, create a class that implements the proxy:
 private class Proxy<T> : RealProxy where T : class { UniservClient client; public Proxy(UniservClient client): base(typeof(T)) { this.client = client; } public override IMessage Invoke(IMessage msg) { IMethodCallMessage call = (IMethodCallMessage)msg; object[] parameters = call.Args; int OutArgsCount = call.MethodBase.GetParameters().Where(x => x.IsOut).Count(); Message result = client.Execute(call.MethodName, parameters); parameters = parameters.Select((x, index) => result.prms[index] ?? x).ToArray(); return new ReturnMessage(result.ReturnValue, parameters, OutArgsCount, call.LogicalCallContext, call); } } 

And create properties for accessing the interface methods:
 public ICommon Common { get; private set; } public IDog Dog { get; private set; } public ICat Cat { get; private set; } 

Initialize proxies and properties:
 CommonProxy = new Proxy<ICommon>(this); DogProxy = new Proxy<IDog>(this); CatProxy = new Proxy<ICat>(this); Common = (ICommon)CommonProxy.GetTransparentProxy(); Dog = (IDog)DogProxy.GetTransparentProxy(); Cat = (ICat)CatProxy.GetTransparentProxy(); 

Processing server commands:
 private void Listener() { while (true) { try { if (ListenerToken.IsCancellationRequested) return; if (!IsConnected) _Connect(); while (true) { if (ListenerToken.IsCancellationRequested) return; Message msg = ReceiveData<Message>(); if (msg.Command == "OnPing") { //   SendData(msg); if (Events.OnPing != null) Events.OnPing.BeginInvoke(null, null); continue; } if (msg.IsSync) { //     SyncResult(msg); } else { //    try { //   Action var pi = typeof(IEvents).GetProperty(msg.Command, BindingFlags.Instance | BindingFlags.Public); if (pi == null) throw new Exception(string.Concat(" \"", msg.Command, "\"  ")); var delegateRef = pi.GetValue(this, null) as Delegate; //   if (delegateRef != null) ThreadPool.QueueUserWorkItem(state => delegateRef.DynamicInvoke(msg.prms)); } catch (Exception ex) { throw new Exception(string.Concat("    \"", msg.Command, "\""), ex); } } } } catch (TaskCanceledException) { return; } catch (Exception ex) { if (Events.OnError != null) Events.OnError.BeginInvoke(ex, null, null); } finally { _Dicsonnect(); } Thread.Sleep(2000); } } 

The following methods are responsible for the execution of remote procedures:
 private Message Execute(string MethodName, object[] parameters) { lock (syncLock) { _syncResult = new Message(MethodName, parameters); _syncResult.IsSync = true; _OnResponce.Reset(); SendData(_syncResult); _OnResponce.Wait(); //    if (_syncResult.IsEmpty) {//  ,    throw new Exception(string.Concat("      \"", MethodName, "\"")); } if (_syncResult.Exception != null) throw _syncResult.Exception; //    return _syncResult; } } private void SyncResult(Message msg) { //     _syncResult = msg; _syncResult.IsEmpty = false; _OnResponce.Set(); //   } 

In general terms, the client works as follows:
When an application calls an interface method, execution proceeds to the proxy server. The proxy sends the server the name of the called method along with its parameters and blocks the thread waiting for the result of the execution. Analysis of server responses occurs in another thread, which allows the continuation of a blocked thread.

Server

Create a hierarchy of classes that will determine the availability of certain functions:
 public class Cat_Ring0 : Ring2, ICat { public Cat_Ring0(User u) : base(u) { up.UserType = UserType.Cat; } public void CutTheText(ref string Text) { Text = Text.Remove(Text.Length - 1); } } public class Dog_Ring0 : Dog_Ring1, IDog { public Dog_Ring0(User u) : base(u) { up.UserType = UserType.Dog; } public int Bark(int nTimes) { var ConnectedDogs = ConnectedUsers.ToArray().Where(x => x.UserType == UserType.Dog).Select(x => x.nStream); ConnectedDogs.AsParallel().ForAll(nStream => { SendMessage(nStream, new Message("OnBark", new object[] { nTimes})); }); return ConnectedDogs.Count(); } } public class Dog_Ring1 : Ring2 { public Dog_Ring1(User u): base(u) { up.UserType = UserType.Dog; } public bool TryFindObject(out object obj) { obj = "TheBall"; return true; } } public class Ring2 : Ring, ICommon { public Ring2(User u) : base(u) { } public string[] GetAvailableUsers() { return new string[] { "Dog0", "Dog1", "Tom" }; } public void ChangePrivileges(string Animal, string password) { switch (Animal) { case "Dog0": if (password != "groovy!") throw new Exception("  "); up.ClassInstance = new Dog_Ring0(up); break; case "Dog1": if (password != "_password") throw new Exception("  "); up.ClassInstance = new Dog_Ring1(up); break; case "Tom": if (password != "TheCat") throw new Exception("  "); up.ClassInstance = new Cat_Ring0(up); break; default: throw new Exception("   "); } } } public abstract class Ring { public readonly User up; public Ring(User up) { this.up = up; } } 

Now it is enough to place the procedure in a certain “ring” so that the corresponding client has access to it. Ring0 is the upper level of access; a user of this type has access not only to the procedures contained in it, but also to procedures in all inherited classes. Initially, the user enters Ring2, which implements only general methods accessible to all. Then, using ChangePrivileges (), a user can, having passed authorization, get into a certain type of “ring” with a certain level of access.

The main server operation comes down to the following method:
 private void ProcessMessage(Message msg, User u) { string MethodName = msg.Command; if (MethodName == "OnPing") return; //        MethodInfo method = u.RingType.GetMethod(MethodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); try { if (method == null) { throw new Exception(string.Concat(" \"", MethodName, "\" ")); } try { //    msg.ReturnValue = method.Invoke(u.ClassInstance, msg.prms); } catch (Exception ex) { throw ex.InnerException; } //  ref  out  msg.prms = method.GetParameters().Select(x => x.ParameterType.IsByRef ? msg.prms[x.Position] : null).ToArray(); } catch (Exception ex) { msg.Exception = ex; } finally { //     SendMessage(u.nStream, msg); } } 

The ClassInstance property contains an instance of a “ring” in which the procedure will be searched for by its name.

Example of the execution log:
image

The result is a simple and elegant solution similar to WCF.
The source can be taken here.

Source: https://habr.com/ru/post/190548/


All Articles