Remoting per TcpChannel

    • VB.NET

    Es gibt 1 Antwort in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Remoting per TcpChannel

      TcpClient und TcpListener können zwar eine schöne Tcp-Verbindung herstellen, aber die besteht dann nur aus einem Stream, wo man Bytes reinschreiben bzw. auslesen kann. Damit kann man eiglich noch nichts tun, denn es fehlt die Info, was mit den Bytes zu geschehen hat: Soll das nun eine Mitteilung sein (und an wen), oder will man einen ChatRoom betreten oder sonstwas.
      Also muß ein BefehlsProtokoll her, also eine Art Meta-Sprache, die festlegt, welche Bytes was bedeuten.
      Etwa kann man Messages immer so aufbauen, dass 2 Bytes den Befehl festlegen, 4 Bytes die folgende Länge des Datenpakets, und dann folgt halt das Datenpaket.
      Und im Server musses einen Select Case geben, wo dem Befehl entsprechend reagiert wird, und im Client auch ein Select Case, aber anderen Inhalts, denn der Client tut ja anderes als der Server.
      Also nicht unkompliziert das.

      Remoting ist nicht so
      Bei Remoting kann man direkt (Remote-)Methoden der Gegenseite aufrufen. Sogar Functions mit beliebigen Rückgabewerten! Welche Methoden das sind wird in einem Interface festgelegt, also alles fein hardcodet und compiler-geprüft - da kann kein Mismatch auftreten, wie beim Befehls-Protokoll.
      Um das Theater mit den Datenpaketen, und was die Sachen zu bedeuten haben kümmert sich irgendeine Hexerei im Namespace System.Runtime.Remoting.

      Aber leider ist Remoting auch nicht einfach
      Zum Beispiel das mit den Rückgabewerten: das kann man fast gleich wieder vergessen. Denn wenn man eine Function der Gegenseite aufruft, und die Verbindung lahmt, dann ist der Aufrufer direkt blockiert. Also die ausgehende Kommunikation immer asynchron ausführen, und damit ist das Thema Rückgabewert gegessen.
      Und die eingehende Kommunikation kommt natürlich immer im NebenThread des Empfängers an, muß also üblicherweise erst in den MainThread gewechselt werden, um Threading-Probleme bei der Anzeige, aber auch bei der Datenverarbeitung zu vermeiden.
      Da kann man mit Synclock optimieren, um ohne Threadwechsel in den MainThread auszukommen, aber das muß sehr sehr sachkundig geschehen, und u.U. an vielen verschiedenen Stellen im Programm.
      In meinem Sample gehe ich mit der Control.Invoke-Keule drüber, dann bin ich im Mainthread, und damit auf der sicheren Seite :P

      Schon die Anlage des Programmier-Systems ist bei Inter-Prozess-Kommunikation immer ziemlich umständlich: Eigentlich kann man nicht die eine Seite ohne die andere programmieren, aber beim Test müssen dann doch 2 gänzlich unabhängige Prozesse gestartet werden.
      Aber mit einer Solution kann man halt nur einen Prozess debuggen.
      Also ich hab das so gelöst, dass ich mit einem Mini-Launcher wählen kann, ob ich die Client- oder die Server-Anwendung debuggen will. Der jeweils andere Part wird dann direkt als Exe gestartet, mittels Process.Start().
      Und 3 Projekte braucht man immer: Server, Client und Common, wobei letzteres nur die Kommunikations-Schnittstelle definiert.
      Auf Common wird von den beiden anderen verwiesen, und damit ist die Kompatiblität von Server und Client gewährleistet.
      Ich hab ins Common-Projekt noch 3 weitere Klassen gepackt, einmal ServerInfo, von wo Information zur Remoting-Konfiguration abgerufen wird (müssen ja auch beide wissen). Und eine ServerBase und eine ProxyFactory.
      ServerBase ist im Server zu beerben, und dort sind dann die SchnittstellenMember dranzuprogrammieren.
      ProxyFactory wird nicht beerbt, sondern der Client instanziert son Ding, und kann dann über den Channel einen (oder theoretisch auch mehrere) Server-Objekt-Proxies abrufen.
      So funktioniert das halt: Im Server existiert ein ServerObject, was die Methoden der IServerObject-Schnittstelle implementiert. Der Client kann sich einen Proxy davon holen, und was er auf den Methoden des Proxies herumorgelt, kommt direkt - quasi Computer-Vodoo - beim Server wieder raus.
      Die Gegenrichtung habich noch unheimlicher implementiert:
      Beim Handshake muss der Client Delegaten aller Methoden übergeben, die er remoted haben möchte. Diese Delegaten bunkert der Server in seiner Datenverarbeitung, und so kann er ganz gezielt jeden einzelnen Client ansprechen, auf jeder der bereitgestellten Remote-Methoden.
      Der Server könnte auch Events versenden (ja, das geht auch über Remoting!), aber damit würde er immer unterschiedslos an alle Clients senden, was zB bei einem Chat mit ChatRooms ühaupt nicht wünschenswert ist.

      Ungelöste Probleme
      Das ServerObject muss von MarshalByRefObject erben, sonst geht es nicht durch den Channel. MarshalByRefObject's Hauptaufgabe scheint zu sein, einen LifeTimeService anzubieten, der im Hintergrund regelmäßig prüft, ob die Gegenseite ühaupt noch da ist. Weil ein Client könnte sich ja verabschieden, ohne bescheid zu geben. Da würden sich mit der Zeit lauter tote Client-Datensätze im Server sammeln, und v.a., wenn er die Toten ansprechen will, dürfte das jedesmal jeweils einen Thread blockieren für ich weiß nicht, wie lange - also das geht nicht, da soll sich gerne das MarshalByRefObject drum kümmern, dass die Toten auch begraben werden können.
      Aber ich hab noch nicht herausgefunden, wie man damit umgeht, und ob man damit ühaupt umgehen muß - wäre ja schon gut, wenn man den Datenbestand bei Gelegenheit aufräumen könnte.
      Besonders eigenartig finde ich, dass ein Client sich nicht disconnecten kann. Ich führe alles aus, um ihn vom Netz zu nehmen - allein, das Server-Object-Voodoo funktioniert einfach weiter.
      Ganz besonders ungelöst ist die Sicherheitsfrage: Prinzipiell ermöglicht die Channelkonfiguration auch eine TLS-gesicherte Kommunikation, also das wäre perfekt wasserdicht. Leider hab ich das noch nie gebacken gekriegt, weil TLS zu implementieren erfordert iwie ein Zertifikat zu installieren, glaub sowohl auffm Server als auch auffm Client - also mit der Zertifiziererei komme ich bislang nicht klar.

      Die Anwendung
      Wie gesagt: Interprozess-Kommunikation tuts kaum je unter 3 Projekten. Bei mir kommt da noch das Launcher-Projekt "RemotingTester" hinzu sowie ein Helpers-Projekt mit Sachen, ohne die ich garnet mehr coden mag.

      Man sieht, wie die Projekte aufeinander verweisen, Common ist am kleinsten, Server und Client verweisen je auf Common, und "RemotingTester" muß sowohl Server als auch Client kennen, damit der Launcher davon auswählen kann.
      Dateien

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Ich fang mal an mit Common.IServerObject:

      VB.NET-Quellcode

      1. ''' <summary> sowohl beim Server als auch beim Client erforderliche Infos zu RemotingChannels </summary>
      2. Public NotInheritable Class ServerInfo
      3. Private Sub New()
      4. End Sub
      5. Public Const Host As String = "localhost"
      6. Public Const ChannelName As String = "ChannelName"
      7. Public Const Port As Integer = 15000
      8. End Class
      9. ''' <summary>
      10. ''' Definition der Kommunikation: Subs definieren, wie der Client auf den Server zugreift (Handshake, Message), die Callbacks der Handshake-Signatur definieren, wie der Server auf den Client zugreift (HandshakeSuccess, Message, MoveForm)
      11. ''' </summary>
      12. Public Interface IServerObject
      13. Sub HandShake(nick As String, callbackHandshakeSuccess As Action(Of Integer?), _
      14. callbackMessage As Action(Of String), callbackMoveForm As Action(Of Integer))
      15. Sub Message(id As Integer, msg As String)
      16. End Interface
      In die Datei habich gleich die ServerInfo-Klasse mit reingepackt.
      Also hier kann man rumfummeln, um einerseits die Channel-Konfiguration einzustellen, andererseits um zu bestimmen, welche Methoden mit welchen Argumenten der Client im Server aufrufen kann.
      Es sind nur 2 Methoden, nämlich HandShake() und Message().
      Allerdings hats HandShake() wirklich in sich - wortwörtlich: Nämlich dort werden neben dem gewünschten Nick v.a. die Delegaten übergeben, mit denen der Server im Client rumfummeln darf:
      • callbackHandshakeSuccess As Action(Of Integer?): damit gibt der Server Antwort, ob er den Handshake ühaupt akzeptiert. Im Erfolgsfall übermittelt er eine Id als Session-Id, die der Client dann bei jeder Message mit anzugeben hat.
        Integer? ist ja die Kurzschreibe von Nullable(Of Integer), und das dolle anne Nullable-Struktur ist, man kann damit auch ausdrücken, dass kein Wert gegeben sein soll.

      • callbackMessage As Action(Of String): Mit der Action(Of String) kann der Server Text an den Client senden.

      • callbackMoveForm As Action(Of Integer)): eine Spielerei, um zu zeigen, dass Remoting kein Versenden von Text oder Bytes ist, sondern auf der Gegenseite werden direkt die definierten Methoden auch aufgerufen. An diesem Delegaten etwa hängt im Client eine Methode, die die Form.Top-Property ändert, also das Form hopft rauf und runter, wie der Server es wünscht.
      Die Sub Message(id As Integer, msg As String) nimmt sich dagegen primitiv aus: der Server bekommt die Session-Id des Clients und den Text übergeben.

      So, hier mal Bildchen, dass man sich ein Bild davon machen kann, was am Ende dabei rauskommt:

      Das sind jetzt 3 Clients am Server angemeldet. Nick3 hat "Test Test" an den Server geschrieben - der hats in seine Sende-Box übernommen, und über den DGV-Button an Nick2 weitergeschickt. Also man sieht: Der Server hat volle Kontrolle, von wem wann was an wen geschickt wird.

      Server-Code
      "Server" ist allerdings zweideutig: Es gibt das ServerObject, was ja IServerObject implementiert, und wovon der Client sich einen Proxy holen kann, und es gibt die eigliche Server-Anwendung.
      Aber erstmal gugge ServerObject:

      VB.NET-Quellcode

      1. Public Class ServerObject : Inherits ServerBase : Implements IServerObject
      2. Public Event MessageIn As Action(Of Integer, String)
      3. Public Event HandShakeIn(nick As String, callbackHandshakeSuccess As Action(Of Integer?), _
      4. callbackMessage As Action(Of String), callbackMoveForm As Action(Of Integer))
      5. Private Sub HandShake(nick As String, callBackHandshake As Action(Of Integer?), _
      6. callBackMessage As Action(Of String), callBackMoveForm As Action(Of Integer)) Implements IServerObject.HandShake
      7. CrossThreadX.RunGui(Sub() RaiseEvent HandShakeIn(nick, callBackHandshake, callBackMessage, callBackMoveForm))
      8. End Sub
      9. Private Sub Message(id As Integer, msg As String) Implements IServerObject.Message
      10. CrossThreadX.RunGui(Sub() RaiseEvent MessageIn(id, msg))
      11. End Sub
      12. End Class
      Die unteren beiden Subs sind die Implementationen des Interfaces (steht ja auch dran). Diese sind dem Client sichtbar. Die Events sind ihm nicht sichtbar, und wie man sieht macht das ServerObject nix als bei eingehendem Aufruf im Gui-Thread jeweils ein Event zu raisen und alle Argumente schlicht durchzureichen (Zeilen#10, #14).
      Also die kleinstmögliche Klasse überhaupt, und 100% schematisch gecodet (pro Interface-Sub ein Event), dass man dafür auch ein Tool schreiben könnte, was solch Code generiert.

      Empfangen werden die Events im Server-Form, und da findet dann auch bischen Datenverarbeitung statt:

      VB.NET-Quellcode

      1. Public Class frmServer
      2. Private WithEvents _Server As ServerObject
      3. Private Sub ckOnline_CheckedChanged(sender As Object, e As EventArgs) Handles ckOnline.CheckedChanged
      4. If ckOnline.Checked Then
      5. _Server = New ServerObject
      6. txtLog.AppendLine("gone online")
      7. Else
      8. _Server.Dispose()
      9. _Server = Nothing
      10. End If
      11. End Sub
      12. Private Sub _Server_HandShakeIn(nick As String, callbackHandshake As Action(Of Integer?), callbackMessage As Action(Of String), callbackMoveForm As Action(Of Integer)) Handles _Server.HandShakeIn
      13. Dim rwClient = ServerDts.Client.All.FirstOrDefault(Function(rw) rw.Nick = nick)
      14. If rwClient.NotNull Then
      15. callbackHandshake(Nothing)
      16. txtLog.AppendLine("nick '", nick, "' rejected")
      17. Else
      18. rwClient = ServerDts.Client.AddClientRow(nick)
      19. rwClient.MessageCallback = callbackMessage
      20. rwClient.MoveFormCallback = callbackMoveForm
      21. callbackHandshake(rwClient.ID)
      22. txtLog.AppendLine("nick '", nick, "' accepted")
      23. End If
      24. End Sub
      25. Private Sub _Server_MessageIn(id As Integer, msg As String) Handles _Server.MessageIn
      26. Dim rwClient = ServerDts.Client.FindByID(id)
      27. CrossThreadX.RunGui(Sub()
      28. Dim txt = rwClient.Nick.And(": ", msg)
      29. txtToSend.Text = txt
      30. txtLog.AppendLine(txt)
      31. End Sub)
      32. End Sub
      33. Private Sub ClientDataGridView_CellContentClick(sender As Object, e As DataGridViewCellEventArgs) Handles ClientDataGridView.CellContentClick
      34. Dim rwClient = ClientDataGridView.DataRow(Of ClientRow)(e.RowIndex)
      35. Dim clm = ClientDataGridView.Columns(e.ColumnIndex)
      36. Select Case True
      37. Case clm Is clmBtSend
      38. CrossThreadX.RunAsync(rwClient.MessageCallback, txtToSend.Text)
      39. Case clm Is clmBtDown
      40. CrossThreadX.RunAsync(rwClient.MoveFormCallback, 50)
      41. Case clm Is clmBtUp
      42. CrossThreadX.RunAsync(rwClient.MoveFormCallback, -50)
      43. End Select
      44. End Sub
      45. End Class
      • Ganz oben zunächst mal das ServerObject mit seine Events.

      • ckOnline_CheckedChanged() dient natürlich dazu, das ServerObject zu instanzieren bzw wegzuhauen, womit On-/Off-Line gegangen ist.

      • _Server_HandShakeIn() guckt, ob der Nick im Dataset bereits vorhanden ist, und lehnt dann ab.
        Falls der Nick frei ist, wird ein Datensatz angelegt, und darin neben dem Nick auch die Callbacks gebunkert für Message und MoveForm. Der Handshake-Callback wird nicht gebunkert, der wird nur einmal genutzt, um die Session-Id (ist bei Anlage des Datensatzes vom Dataset generiert worden) dem Client zurückzuschicken.

      • _Server_MessageIn() ist der Eventhandler für eingehende Messages: Anhand der Id wird der Sender-Client ermittelt, und sein Nick mit der Meldung wird nach txtToSend geschrieben - gewissermaßen AbschussRampe. (ja, und geloggt wird auch - wozu hab ich denn die Log-Box ;))

      • ClientDataGridView_CellContentClick() reagiert auf Betätigung der Buttons im DatagridView, wobei es immer nur den angewählten Client anspricht: Es kann entweder die auf der Abschussrampe hockende Message ans Ziel bringen, oder vom Client das Form um 50 Pixel rauf oder runter-hopsen lassen :D

      Client-Code
      Ja, und im Client-Form gehts auch nicht wesentlich komplizierter zu:

      VB.NET-Quellcode

      1. Public Class frmClient
      2. Private _ProxyMaker As ProxyFactory
      3. Private _ServerObject As IServerObject
      4. Private _ID As Integer = 0
      5. Private Sub ckOnline_CheckedChanged(sender As System.Object, e As System.EventArgs) Handles ckOnline.CheckedChanged
      6. If ckOnline.Checked Then
      7. _ProxyMaker = New ProxyFactory
      8. _ServerObject = _ProxyMaker.GetProxy
      9. _ServerObject.HandShake(txtNick.Text, AddressOf HandshakeSuccessIn, AddressOf MessageIn, AddressOf MoveFormIn)
      10. Else
      11. _ProxyMaker.Dispose()
      12. End If
      13. End Sub
      14. Private Sub btSend_Click(sender As System.Object, e As System.EventArgs) Handles btSend.Click
      15. CrossThreadX.RunAsync(AddressOf _ServerObject.Message, _ID, txtToSend.Text)
      16. End Sub
      17. #Region "Callback-Targets"
      18. Public Sub HandshakeSuccessIn(allocedId As Integer?)
      19. CrossThreadX.RunGui(Sub()
      20. txtLog.AppendLine("Handshake accepted:", allocedId.HasValue)
      21. If allocedId.HasValue Then
      22. _ID = allocedId.Value
      23. Else
      24. ckOnline.Checked = False
      25. End If
      26. End Sub)
      27. End Sub
      28. Public Sub MessageIn(msg As String)
      29. CrossThreadX.RunGui(AddressOf txtLog.AppendLine, msg)
      30. End Sub
      31. Public Sub MoveFormIn(value As Integer)
      32. CrossThreadX.RunGui(Sub() Me.Top += value)
      33. End Sub
      34. #End Region 'Callback-Targets
      35. End Class
      • Da gibts den ProxyMaker, den man instanziert und von dem man ein IServerObject abruft, wenn man Online geht.

      • btSend_Click() sendet halt einen Text - zu beachten allenfalls, dass dieses an einen NebenThread delegiert ist - falls das Senden laggen sollte, darf sich das nicht aufs Form übertragen.

      • Und dann die 3 Callback-Targets (alle 3 wechseln in den Gui-Thread):
        1. HandshakeSuccessIn() merkt sich die zugewiesene Session-Id oder setzt halt die Checkbox zurück.
        2. MessageIn(msg As String) gibt halt die eingehende Message aus
        3. MoveFormIn(value As Integer) lässt das Form rauf oder runter hopfen
        Wichtig ist, dass die Callback-Targets Public sind - andernfalls streikt der Remote-Channel


      So, falls jemand denkt, ich habe mir das alles nur ausgedacht, kann ich noch verweisen auf den CodeProject-Artikel, wo ich gelernt hab, dass Remoting auch Events bedient (und damit ebenso auch Callback-Delegaten), und von dem ich die Konfiguriererei abgeschrieben habe.

      Ach die Konfiguriererei!
      Die hab ich im Common-Projekt in Extra-Klassen verbannt, damit man das nicht angucken muß, wenn man die eigentliche Anwendung designed.
      Client-Konfiguration findet inne ProxyFactory statt:

      VB.NET-Quellcode

      1. Public Class ProxyFactory : Implements IDisposable
      2. Private tcpChan As TcpChannel
      3. Private _ServerUri As String 'eine Art ConnectionString zum Server
      4. Private ReadOnly _tpIServer As Type = GetType(IServerObject)
      5. ''' <summary> erstellt einen RemoteChannel und registriert das Remoting-Interface </summary>
      6. Public Sub New()
      7. _ServerUri = String.Concat("tcp://", ServerInfo.Host, ":", ServerInfo.Port, "/", ServerInfo.ChannelName)
      8. Dim clientChannelName = "Client" + ServerInfo.ChannelName
      9. If ChannelServices.GetChannel(clientChannelName) IsNot Nothing Then Return
      10. Dim clientProv = New BinaryClientFormatterSinkProvider()
      11. Dim serverProv = New BinaryServerFormatterSinkProvider()
      12. serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
      13. Dim props As New Hashtable() 'der TcpChannel-Konstruktor braucht sone bescheuerte Hashtable
      14. props("name") = clientChannelName
      15. props("port") = 0 'First available port - ist egal
      16. tcpChan = New TcpChannel(props, clientProv, serverProv)
      17. ChannelServices.RegisterChannel(tcpChan, False)
      18. If RemotingConfiguration.IsWellKnownClientType(_tpIServer) IsNot Nothing Then Return
      19. RemotingConfiguration.RegisterWellKnownClientType(_tpIServer, _ServerUri) 'sonst failt der Activator bei GetProxy()
      20. End Sub
      21. Public Function GetProxy() As IServerObject
      22. 'ruft durch den Channel einen Proxy-Verweis auf die konkrete IServerObject-Instanz im Server ab
      23. Return DirectCast(Activator.GetObject(_tpIServer, _ServerUri), IServerObject)
      24. End Function
      25. ''' <summary>
      26. ''' komischerweise bleibt trotzdem der Proxy intakt - mir ist keine Möglichkeit bekannt, den Channel wirklich zu schließen
      27. ''' </summary>
      28. Public Overridable Sub Dispose(isDisposing As Boolean)
      29. If IsDisposed Then Return
      30. ChannelServices.UnregisterChannel(tcpChan)
      31. tcpChan = Nothing
      32. End Sub
      33. Public ReadOnly Property IsDisposed() As Boolean
      34. Get
      35. Return tcpChan Is Nothing
      36. End Get
      37. End Property
      38. Public Sub Dispose() Implements IDisposable.Dispose
      39. Dispose(True)
      40. End Sub
      41. End Class


      Und hier noch last and least die ServerBase - Basisklasse des ServerObjects:

      VB.NET-Quellcode

      1. Public MustInherit Class ServerBase : Inherits MarshalByRefObject : Implements IDisposable
      2. Private _ServerChannel As TcpServerChannel
      3. Private _ChannelRef As ObjRef
      4. Public Sub New()
      5. 'komisches Zeugs halt
      6. Dim props As New Hashtable() 'der TcpChannel-Konstruktor braucht sone bescheuerte Hashtable
      7. props("port") = ServerInfo.Port
      8. props("name") = ServerInfo.ChannelName
      9. Dim serverProv As New BinaryServerFormatterSinkProvider()
      10. serverProv.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full
      11. _ServerChannel = New TcpServerChannel(props, serverProv)
      12. ChannelServices.RegisterChannel(_ServerChannel, False)
      13. _ChannelRef = RemotingServices.Marshal(Me, _ServerChannel.ChannelName) 'keine Ahnung, was das macht - ist aber erforderlich
      14. End Sub
      15. ''' <summary>
      16. ''' in dieser Form unsinnig, aber verdeutlicht das Remoting-LifeTime-Problem: Beim langlaufenden Server sammeln normalerweise sich "tote Clients" an.
      17. ''' MarshalByRefObject ist iwie eine Automation des IsAlive-Pings, und wie da gepingt wird, steuert das ILease-Objekt
      18. ''' </summary>
      19. Public Overrides Function InitializeLifetimeService() As Object
      20. Dim Lease = DirectCast(MyBase.InitializeLifetimeService(), Lifetime.ILease)
      21. Return Lease
      22. End Function
      23. Public Overridable Sub Dispose(isDisposing As Boolean)
      24. If IsDisposed Then Return
      25. RemotingServices.Unmarshal(_ChannelRef)
      26. ChannelServices.UnregisterChannel(_ServerChannel)
      27. _ServerChannel = Nothing
      28. _ChannelRef = Nothing
      29. End Sub
      30. Public ReadOnly Property IsDisposed() As Boolean
      31. Get
      32. Return _ServerChannel Is Nothing
      33. End Get
      34. End Property
      35. Public Sub Dispose() Implements IDisposable.Dispose
      36. Dispose(True)
      37. End Sub
      38. End Class
      Ich hoffe, man sieht den beiden Klassen an, dass die Channel-Konfiguriererei eher abgeschrieben ist, und nicht bis ins Detail verstanden. Also das Design ist schon durchdacht: Der Client hat keine Basisklasse, sondern eine ProxyFactory, mit der er einen Server-Proxy abrufen kann.
      Hingegen der Server beerbt die ServerBase-Basisklasse, und implementiert darin das IServerObject-Interface.
      Und natürlich beide IDisposable, damits auch wieder aufgeräumt werden kann, jedenfalls vom Prinzip her.

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()