Data Transfer Protocol - Methoden/Funktionen über Streams aufrufen

    • C#
    • .NET (FX) 4.5–4.8

    Es gibt 13 Antworten in diesem Thema. Der letzte Beitrag () ist von BiedermannS.

      Data Transfer Protocol - Methoden/Funktionen über Streams aufrufen

      Hi,
      WCF war mir immer zu umständlich und ich hatte zu wenig Kontrolle, außerdem sind Events extrem kompliziert zu implementieren und ein gescheites Authentifizierungssystem wollte auch nicht klappen, also habe ich diese kleine Bibliothek geschrieben, welche über egal welche Verbindung Methoden und Funktionen mit oder ohne Parameter aufrufen kann. Das ganze ist sehr einfach zu benutzen:
      Auf der Serverseite habt ihr folgenden Code:

      C#-Quellcode

      1. var processor = new DtpProcessor();
      2. processor.RegisterMethod("WriteToConsole", parameters => Console.WriteLine(parameters.GetString(0)));
      3. processor.RegisterFunction("GetMachineName", parameters => Environment.MachineName);


      und auf der Client-Seite dann folgenden Code:

      C#-Quellcode

      1. var factory = new DtpFactory(x => myStream.Write(x));
      2. factory.ExecuteMethod("WriteToConsole", "Hello World!");
      3. var machineName = factory.ExecuteFunction<string>("GetMachineName");


      Das ist beinah auch schon alles. Jetzt müssen nur noch die einzelnen Punkte miteinander vernetzt werden:


      und schon ist es fertig. Es können natürlich nicht nur Strings und andere primitive Datentypen übertragen werden, sondern auch alle Klassen und sogar abstrakte Klassen sind ohne Probleme möglich (die Typen der abstrakten Klassen müssen dann beim Senden und Empfangen angegeben werden, einfach die Überladung von ExecuteMethod/ExecuteFunction, parameters.GetValue und RegisterMethod/RegisterFunction anschauen). Serialisiert wird mit dem Netserializer, welcher eine hohe Performance verspricht. Außerdem werden die Daten mit dem LZF Algorithmus komprimiert.

      Hier sind die wichtigsten Ausschnitte aus dem TCP Beispiel, welches auch auf GitHub verfügbar ist:
      Client

      C#-Quellcode

      1. public class TcpClientTest : IDisposable
      2. {
      3. private readonly BinaryReader _binaryReader;
      4. private readonly BinaryWriter _binaryWriter;
      5. private readonly NetworkStream _networkStream;
      6. private readonly Func<byte> _readByteDelegate;
      7. private readonly TcpClient _tcpClient;
      8. private readonly object _writeLock = new object();
      9. private bool _isDisposed;
      10. private TcpClientTest(TcpClient tcpClient)
      11. {
      12. _tcpClient = tcpClient;
      13. _networkStream = tcpClient.GetStream();
      14. _binaryWriter = new BinaryWriter(_networkStream);
      15. _binaryReader = new BinaryReader(_networkStream);
      16. DataTransferProtocolFactory = new DtpFactory(SendData);
      17. _readByteDelegate += _binaryReader.ReadByte;
      18. _readByteDelegate.BeginInvoke(EndRead, null);
      19. }
      20. public DtpFactory DataTransferProtocolFactory { get; }
      21. public void Dispose()
      22. {
      23. if (_isDisposed)
      24. return;
      25. _isDisposed = true;
      26. using (_binaryReader)
      27. using (_binaryWriter)
      28. using (_networkStream)
      29. _tcpClient.Close();
      30. Disconnected?.Invoke(this, EventArgs.Empty);
      31. }
      32. public event EventHandler Disconnected;
      33. private void SendData(byte[] data)
      34. {
      35. if (_isDisposed)
      36. return;
      37. lock (_writeLock)
      38. {
      39. _binaryWriter.Write((byte) ClientPackageToken.DataTransferProtocol);
      40. _binaryWriter.Write(data.Length);
      41. _binaryWriter.Write(data);
      42. }
      43. }
      44. private void EndRead(IAsyncResult asyncResult)
      45. {
      46. try
      47. {
      48. var parameter = _readByteDelegate.EndInvoke(asyncResult);
      49. var size = _binaryReader.ReadInt32();
      50. var bytes = _binaryReader.ReadBytes(size);
      51. switch ((ServerPackageToken) parameter)
      52. {
      53. case ServerPackageToken.DataTransferProtocolResponse:
      54. DataTransferProtocolFactory.Receive(bytes);
      55. break;
      56. }
      57. _readByteDelegate.BeginInvoke(EndRead, null);
      58. }
      59. catch (Exception)
      60. {
      61. Dispose();
      62. }
      63. }
      64. }


      C#-Quellcode

      1. private void showMessageBoxButton_Click(object sender, EventArgs e)
      2. {
      3. _tcpClientTest.DataTransferProtocolFactory.ExecuteMethod("ShowMessageBox", showMessageBoxTextBox.Text ?? "");
      4. }
      5. private void getServerInformationButton_Click(object sender, EventArgs e)
      6. {
      7. var information =
      8. _tcpClientTest.DataTransferProtocolFactory.ExecuteFunction<ServerInformation>("GetServerInformation");
      9. MessageBox.Show(
      10. $"MachineName: {information.MachineName}\r\nUserName: {information.UserName}\r\nOperatingSystem: {information.OperatingSystem}\r\nPageSize: {information.PageSize}");
      11. }



      Server

      C#-Quellcode

      1. public class TestClient : IDisposable
      2. {
      3. private readonly BinaryReader _binaryReader;
      4. private readonly BinaryWriter _binaryWriter;
      5. private readonly NetworkStream _networkStream;
      6. private readonly Func<byte> _readByteDelegate;
      7. private readonly TcpClient _tcpClient;
      8. private bool _isDisposed;
      9. private readonly DtpProcessor _dtpProcessor;
      10. private readonly object _sendLock = new object();
      11. public TestClient(TcpClient tcpClient)
      12. {
      13. _tcpClient = tcpClient;
      14. _networkStream = tcpClient.GetStream();
      15. _binaryWriter = new BinaryWriter(_networkStream);
      16. _binaryReader = new BinaryReader(_networkStream);
      17. _dtpProcessor = new DtpProcessor();
      18. InitializeDataTransferProtocol();
      19. _readByteDelegate += _binaryReader.ReadByte;
      20. _readByteDelegate.BeginInvoke(EndRead, null);
      21. }
      22. public void Dispose()
      23. {
      24. if (_isDisposed)
      25. return;
      26. _isDisposed = true;
      27. using (_binaryReader)
      28. using (_binaryWriter)
      29. using (_networkStream)
      30. _tcpClient.Close();
      31. Disconnected?.Invoke(this, EventArgs.Empty);
      32. }
      33. public event EventHandler Disconnected;
      34. private void InitializeDataTransferProtocol()
      35. {
      36. _dtpProcessor.RegisterMethod("ShowMessageBox", parameters => MessageBox.Show(parameters.GetString(0)));
      37. _dtpProcessor.RegisterFunction("GetServerInformation",
      38. parameters =>
      39. new ServerInformation
      40. {
      41. MachineName = Environment.MachineName,
      42. OperatingSystem = Environment.OSVersion.ToString(),
      43. PageSize = Environment.SystemPageSize,
      44. UserName = Environment.UserName
      45. });
      46. }
      47. private void EndRead(IAsyncResult asyncResult)
      48. {
      49. try
      50. {
      51. var parameter = _readByteDelegate.EndInvoke(asyncResult);
      52. var size = _binaryReader.ReadInt32();
      53. var bytes = _binaryReader.ReadBytes(size);
      54. switch ((ClientPackageToken) parameter)
      55. {
      56. case ClientPackageToken.DataTransferProtocol:
      57. var result = _dtpProcessor.Receive(bytes);
      58. lock (_sendLock)
      59. {
      60. _binaryWriter.Write((byte) ServerPackageToken.DataTransferProtocolResponse);
      61. _binaryWriter.Write(result.Length);
      62. _binaryWriter.Write(result);
      63. }
      64. break;
      65. }
      66. _readByteDelegate.BeginInvoke(EndRead, null);
      67. }
      68. catch (Exception)
      69. {
      70. Dispose();
      71. }
      72. }
      73. }



      Ein vollständiges Beispiel über TCP könnt ihr hier finden. Das Projekt ist auf GitHub verfügbar: github.com/Alkalinee/DataTransferProtocol
      Vielleicht kann's ja jemand gebrauchen :)
      Mfg
      Vincent

      Ich versuch das ganze gerade in VB zu verwenden, hab aber leider relativ wenig Erfahrung was c# angeht. Was mich stört ist das =>, auf msdn heist es nur dies eine Lambdadeklaration ist, was mir ungefähr gar nichts sagt, Code converter haben auch nicht den gwünschten erfolg gebracht. Wie übersetzte ich das

      processor.RegisterMethod("WriteToConsole", parameters => Console.WriteLine(parameters.GetString(0)));

      korrekt in vb?
      Intel i7-4710HQ |Nvidia GTX 860M | 1TB SSHD| 8GB RAM 1600Mhz :saint:
      Intel Core Duo2 | 320GB | 4 GB RAM | Linux Debian :D
      AMD E-350 | 320GB| 6GB RAM :thumbdown:

      VB.NET-Quellcode

      1. processor.RegisterMethod("WriteToConsole", Function(parameters) Console.WriteLine(parameters.GetString(0)))


      Grüße
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Programmier-Fragen per PN, denn dafür ist das Forum da :!:
      @Trade, @Quakxi

      VB.NET-Quellcode

      1. processor.RegisterMethod("WriteToConsole", Sub(parameters) Console.WriteLine(parameters.GetString(0)))


      Jo, dieses => ist relativ kompliziert zu verstehen (außer wenn man's dauernd benutzt), denn es steht sowohl für Sub() als auch für Function(), je nach Situation. Konverter werden das auch falsch übersetzen, da Sie ja nicht die Signatur der Methode kennen :D
      Mfg
      Vincent

      factory.ExecuteMethod("WriteToConsole", "Hello World!"); find ich jetzt ehrlich gesagt nicht so pralle. Kannst du das nicht über ein Interface lösen und du mappst die Methoden im Hintergrund? WCF macht das genau so und das finde ich bedeutend besser als 2 potentielle Fehlerquellen zu haben: Methodenname falsch; falsche Argumente.
      @ThuCommix
      Ich habe das bereits versucht, also es wie WCF zu machen. Dazu habe ich mir den Code von WCF durchgelesen (aus Microsofts Reference Source), das Problem ist nur leider, dass da ohne Ende native Funktionen aufgerufen werden, welche auf WCF zugeschnitten sind. Die zwei Fehlerquellen hast du bei WCF genauso, außer wenn du das Interface in eine 3e Library ablegst auf die beide zugreifen. Ich sehe da jetzt nicht so ein hohen Fehlerrisiko, wenn man will, kann man ja die Methodennamen in consts strings in einer 3en Library ablegen (oder gar ein Enum, welches dann per ToString() als Name dient?). Außerdem hatte ich bei WCF Probleme, von der erstellten Klasse dann auf Funktionen des Programms zuzugreifen, die hätten ja dann alle statisch/per Singleton verfügbar sein müssen. Ich finde das deutlich besser als WCF, kommt aber natürlich auch auf den Anwendungszweck an.
      Mfg
      Vincent

      Remote Calls sollten immer ein gut definiertes Interface haben. Das Problem hier ist, wenn du den RPC call öfter brauchst, musst du im Client einen eigenen Wrapper schreiben, damit du in Zukunft keine Tippfehler einbaust. In diesem Fall kann man es gleich über eine externe DLL lösen und hat somit das Problem nicht.

      Bei Remoting kann man dies über ein MarshalByRefObject lösen, wodurch sowohl Client als auch Server eine Instanz des Objects haben und auf die public Methoden des Objects zugreifen können. Das ist zwar nicht ganz so einfach zu benutzen (Initialisierung), allerdings wesentlich sicherer (Typesafe).

      Oder man verwendet einfach eine Message-basierte Lösung ala Akka.net.
      SWYgeW91IGNhbiByZWFkIHRoaXMsIHlvdSdyZSBhIGdlZWsgOkQ=

      Weil einfach, einfach zu einfach ist! :D
      Naja, man sollte schon erwarten, dass der, der diese Bibliothek benötigt, einigermaßen gut Tippen kann, abgesehen davon, dass auch Fehler fliegen, wenn da irgendetwas falsch ist. Mir war wichtig, dass es so minimal wie möglich ist, ich habe mir schon gedacht, dass es keine große Anwendung findet :D
      Mfg
      Vincent

      Das kommt immer auf die Größe des Projekts an. Je größer und langlebiger das Ganze ist, umso sinnvoller ist es, ein wohldefinierte Schnittstelle zu haben. Eine Schnittstelle ist dann gut, wenn man sie leicht richtig verwenden und schwer bis gar nicht falsch verwenden kann.

      Aber da ich auch gerne herum bastle, hab ich den Code etwas erweitert um folgendes zu ermöglichen:

      Beim Server statt:

      VincentTB schrieb:

      C#-Quellcode

      1. var processor = new DtpProcessor();
      2. processor.RegisterMethod("WriteToConsole", parameters => Console.WriteLine(parameters.GetString(0)));
      3. processor.RegisterFunction("GetMachineName", parameters => Environment.MachineName);


      das:

      C#-Quellcode

      1. dynamic processor = new DtpProcessor();
      2. processor.WriteToConsole = new DtpMethod(parameters => Console.WriteLine(parameters.GetString(0)));
      3. processor.GetMachineName = new DtpFunction(parameters => Environment.MachineName);



      Und beim Client, statt:

      VincentTB schrieb:

      C#-Quellcode

      1. var factory = new DtpFactory(x => myStream.Write(x));
      2. factory.ExecuteMethod("WriteToConsole", "Hello World!");
      3. var machineName = factory.ExecuteFunction<string>("GetMachineName");


      das:

      C#-Quellcode

      1. dynamic factory = new DtpFactory(x => myStream.Write(x));
      2. factory.WriteToConsole("Hello World!");
      3. var machineName = factory.GetMachineName<string>();


      Das macht das Ganze zwar nicht sicherer im Aufruf oder leichter zu verstehen, ist aber schöner anzusehen als eine Funktion über einen String auszuführen :P

      Wenn du willst schick ich dir ein Pull-Request, dann kannst du dir das mal ansehen :D
      SWYgeW91IGNhbiByZWFkIHRoaXMsIHlvdSdyZSBhIGdlZWsgOkQ=

      Weil einfach, einfach zu einfach ist! :D
      Ich finde da Strings wesentlich rationaler. Ich verstehe auch nicht, wie man da etwas falsch machen kann. Funktion auf Server erstellen, Namen kopieren, Funktion im Client aufrufen. Wenn man eine wirkliche Abhängigkeit will, kann man die Namen der Funktionen und Methoden in einer 3en Library deponieren, wenn man eine Funktion dann löscht, weil man denkt, dass man sie nirgendwo verwendet, dann schmeißt der Compiler einen Fehler, wenn man Sie doch irgendwo verwendet haben sollte. So funktionieren dann auch die Features von Resharper (Find Usages, etc.).
      Mfg
      Vincent

      Darum hab ich auch geschrieben, dass es dadurch nicht leichter verständlich ist. :)
      War auch mehr ein Proof-of-concept, um zu sehen ob es möglich ist. Hab mich schon lange nicht mehr mit dynamic auseinandergesetzt und hab das als "Übung" genommen. :D

      VincentTB schrieb:

      Ich verstehe auch nicht, wie man da etwas falsch machen kann.

      Hast du schon mal an größeren Projekten gearbeitet? Oder an Libraries? Dich mit Themen wie API-Design beschäftigt? Wenn ja, dann solltest du wissen wie leicht man etwas falsch machen kann. Solange es sich nur um kleine Projekte handelt, mag das ja okay sein, aber bei größeren wird sowas schnell zum Problem. Vor allem wenn man in größeren Teams arbeitet.

      Dass man das Ganze in eine externe Library auslagern kann ist mir bewusst. So ein Wrapper ist schnell geschrieben. Allerdings verlierst du jegliche TypeSafety innerhalb des Wrappers. Was zwar kein Weltuntergang ist, sondern eher nervig.

      Alles in allem finde ich die Lib ja nicht schlecht. Ist, nach dem kurzen Blick den ich rein gemacht habe, gut gemacht. Allerdings lässt sich das selbe über Remoting lösen, wo ich direkt nach der Implementierung bereits eine externe Library habe, welche alle implementierten Methoden bereits auf dem Remote Server ausführt. Ist zwar nicht so einfach, dafür allerdings TypeSafe.
      SWYgeW91IGNhbiByZWFkIHRoaXMsIHlvdSdyZSBhIGdlZWsgOkQ=

      Weil einfach, einfach zu einfach ist! :D
      @Thias

      Klar, sind zwar nur ein paar kleine Änderungen und es ist nicht alles abgedeckt, aber du kannst es dir gerne ansehen. :)

      Ich hab den Fork davon eh in meinem Githup Repo:
      https://github.com/hardliner66/DataTransferProtocol

      Wo die Änderungen durchgeführt worden sind, kannst du in der Commit History nachsehen und wenn du Fragen hast, sag Bescheid.
      SWYgeW91IGNhbiByZWFkIHRoaXMsIHlvdSdyZSBhIGdlZWsgOkQ=

      Weil einfach, einfach zu einfach ist! :D