VersuchsChat mit leistungsfähigem Server

    • VB.NET

    Es gibt 80 Antworten in diesem Thema. Der letzte Beitrag () ist von n1nja.

      VersuchsChat mit leistungsfähigem Server

      Vorbemerkung: Inzwischen habe ich diesen Ansatz wesentlich weiterentwickelt, inklusive einer Art Standard für ein BefehlsProtokoll, sowie auch Datei-Übertragungen: siehe TcpKommunikation + Networkstream

      Hier täte ich mal meinen Versuchschat vorstellen. Das Verständnis des Codes setzt gute Kenntnisse in OOP vorraus, nämlich die Vererbungslehre. Weiters kommt Threading nach dem asynchronen Entwurfsmusters in Anschlag.

      Die Verwendung der asynchronen Read-Methode insbesondere des TcpClients ist - soweit ich weiß - die einzige Möglichkeit, mit der ein Server hunderte von TCP-Verbindungen halten kann, ohne dementsprechend auch hunderte von Threads alloziieren zu müssen.

      Die Struktur der Anwendung ist bisserl listig, denn ich habe sowohl Server als auch Client-Anwendung im selben Projekt implementiert - und sie benutzen sogar dasselbe Form. Man startet die App, und klickst "be Server", dann hat man einen Server gestartet, und wenn man "be Client" klickst, dann isse halt ein Client.

      Diese hohe Wiederverwendbarkeit beruht letztendlich darauf, dass das TCP-Protokoll symmetrisch ist - zwischen den Endpunkten einer Verbindung Server - Client gibt es keinerlei Unterschied - nur beim Server werden viele Endpunkte verwaltet - beim Client nur einer.
      Sowohl beim Server als auch beim Client werkeln ein TcpClient-Objekte, und da habe ich gleich einen Wrapper drum geschrieben, der einen String senden kann, und auch empfangen - egal, ob er sich in der Server-Anwendung befindet, oder in der Client-Anwendung.
      Beim Empfang löst dieser Client-Wrapper ein Event aus, und es obliegt der Anwendung, wie sie damit umgeht: Eine Client-Anwendung zeigt den empfangenen String halt an, eine Server-Anwendung zeigt ihn an, und ausserdem verschickt sie den String weiter an alle connecteten Clients.

      Hiermal der Client:

      VB.NET-Quellcode

      1. Imports System.Net
      2. Imports System.Net.Sockets
      3. Imports System.Text
      4. Public Class Client : Inherits TCPBase
      5. Private _TcpClient As TcpClient
      6. Private _Stream As NetworkStream
      7. Dim Buf(&H400 - 1) As Byte
      8. Public Sub New(ByVal TC As TcpClient)
      9. _TcpClient = TC
      10. _Stream = _TcpClient.GetStream
      11. _Stream.BeginRead(Buf, 0, Buf.Length, AddressOf EndRead, Nothing)
      12. End Sub
      13. Private Sub EndRead(ByVal ar As IAsyncResult)
      14. If MyBase.IsDisposed Then Return
      15. Dim read As Integer = _Stream.EndRead(ar)
      16. If read = 0 Then 'leere Datenübermittlung signalisiert Verbindungsabbruch
      17. CrossThread.RunGui(AddressOf OnStatusMessage, New MessageEventargs("CounterClient shut down"))
      18. CrossThread.RunGui(AddressOf MyBase.Dispose)
      19. Return
      20. End If
      21. Dim SB As New StringBuilder(Encoding.UTF8.GetString(Buf, 0, read))
      22. Do While _Stream.DataAvailable
      23. read = _Stream.Read(Buf, 0, Buf.Length)
      24. SB.Append(Encoding.UTF8.GetString(Buf, 0, read))
      25. Loop
      26. CrossThread.RunGui(AddressOf OnChatMessage, New MessageEventargs(SB.ToString))
      27. _Stream.BeginRead(Buf, 0, Buf.Length, AddressOf EndRead, Nothing)
      28. End Sub
      29. Public Overrides Sub Send(ByVal Msg As String)
      30. Dim Buf() As Byte = Encoding.UTF8.GetBytes(Msg)
      31. _Stream.Write(Buf, 0, Buf.Length)
      32. End Sub
      33. Protected Overrides Sub Dispose(ByVal disposing As Boolean)
      34. DisposeAll(_Stream, _TcpClient)
      35. End Sub
      36. End Class
      Die Events sind nicht direkt sichtbar, denn sie sind in die Basis-Klasse ausgelagert, und werden in obigem Code über die OnChatMessage/OnStatusMessage-Methoden ausgelöst. Und zwar gleich im Gui-Thread, um den berühmten unzulässigen threadübergreifenden Zugriffen vorzubeugen.

      Das mit der Basisklasse ist effizient, denn das Server-Objekt muß dieselben Events auslösen wie der ClientWrapper, und indem beide von TCPBase erben sind es dieselben Events.
      Weiters muss der Server auch die nach aussen hin identische Methode Send(Msg As String) bereitstellen, das habe ich als MustOverride implementiert: Sowohl das ClientWrapper-Objekt als auch das Server-Objekt haben die Methode Send(), nur intern unterschiedlich ausprogrammiert (der Server sendet an alle).

      Hier die TcpBase:

      VB.NET-Quellcode

      1. Imports System.Net.Sockets
      2. Imports System.Threading
      3. ''' <summary>
      4. ''' stellt den Erben "Server" und "Client" 2 verschiedene
      5. ''' Message-Events zur Verfügung, und ein Event-Raisendes Dispose
      6. ''' </summary>
      7. Public MustInherit Class TCPBase : Implements IDisposable
      8. Private _IsDisposed As Boolean = False
      9. Public Event Disposed As EventHandlerEx(Of TCPBase)
      10. Protected MustOverride Sub Dispose(ByVal disposing As Boolean)
      11. Public MustOverride Sub Send(ByVal Msg As String)
      12. ''' <summary>
      13. ''' Zur Ausgabe chat-verwaltungstechnischer Status-Informationen
      14. ''' </summary>
      15. Public Event StatusMessage As EventHandler(Of MessageEventargs)
      16. Protected Sub OnStatusMessage(ByVal e As MessageEventargs)
      17. RaiseEvent StatusMessage(Me, e)
      18. End Sub
      19. ''' <summary>Zur Ausgabe von Chat-Messages</summary>
      20. Public Event ChatMessage As EventHandler(Of MessageEventargs)
      21. Protected Sub OnChatMessage(ByVal e As MessageEventargs)
      22. RaiseEvent ChatMessage(Me, e)
      23. End Sub
      24. Public Sub RemoveFrom(Of T As TCPBase)(ByVal Coll As ICollection(Of T))
      25. Coll.Remove(DirectCast(Me, T))
      26. End Sub
      27. Public ReadOnly Property IsDisposed() As Boolean
      28. Get
      29. Return _IsDisposed
      30. End Get
      31. End Property
      32. Public Sub AddTo(Of T As TCPBase)(ByVal Coll As ICollection(Of T))
      33. Coll.Add(DirectCast(Me, T))
      34. End Sub
      35. Public Sub Dispose() Implements IDisposable.Dispose
      36. If _IsDisposed Then Return
      37. _IsDisposed = True
      38. Dispose(True) ' rufe die erzwungenen Überschreibungen von Sub Dispose(Boolean)
      39. OnStatusMessage(New MessageEventargs(Me.GetType.Name, " disposed"))
      40. RaiseEvent Disposed(Me)
      41. GC.SuppressFinalize(Me)
      42. End Sub
      43. End Class


      Das Server-Objekt enthält und verwaltet neben seinem TcpListener auch die Liste der TcpClients, die mit ihm verbunden sind. Von denen empfängt er auch die Events und reagiert entsprechend - zum einen selbst ein Event auslösen, zum anderen die Messages weiterversenden.

      VB.NET-Quellcode

      1. Imports System.Net
      2. Imports System.Net.Sockets
      3. Public Class Server : Inherits TCPBase
      4. Private _Listener As TcpListener
      5. 'Pro Verbindung(sanfrage) wird ein Client-Objekt generiert, das den Datenaustausch dieser Verbindung abwickelt
      6. Private _Clients As New List(Of Client)
      7. Public Sub New(ByVal EP As IPEndPoint)
      8. _Listener = New TcpListener(EP)
      9. _Listener.ExclusiveAddressUse = False
      10. _Listener.Start()
      11. _Listener.BeginAcceptTcpClient(AddressOf EndAccept, Nothing)
      12. End Sub
      13. Sub EndAccept(ByVal ar As IAsyncResult)
      14. If MyBase.IsDisposed Then Return
      15. With New Client(_Listener.EndAcceptTcpClient(ar))
      16. AddHandler .ChatMessage, AddressOf Client_ChatMessage
      17. AddHandler .StatusMessage, AddressOf Client_StatusMessage
      18. AddHandler .Disposed, AddressOf Client_Disposed
      19. .AddTo(_Clients)
      20. End With
      21. CrossThread.RunGui(AddressOf OnStatusMessage, New MessageEventargs("TCPClient accepted"))
      22. _Listener.BeginAcceptTcpClient(AddressOf EndAccept, Nothing)
      23. End Sub
      24. #Region "_Clients-Ereignisverarbeitung"
      25. Private Sub Client_Disposed(ByVal Sender As TCPBase)
      26. 'den Client für die beendete Verbindung entfernen
      27. Sender.RemoveFrom(_Clients)
      28. End Sub
      29. Private Sub Client_ChatMessage(ByVal sender As Object, ByVal e As MessageEventargs)
      30. 'einkommende ChatMessages anzeigen, und an alle versenden
      31. Send(e.Message)
      32. End Sub
      33. Private Sub Client_StatusMessage(ByVal sender As Object, ByVal e As MessageEventargs)
      34. 'einkommende StatusMessages durchreichen (zur Anzeige)
      35. OnStatusMessage(e)
      36. End Sub
      37. #End Region '_Clients-Ereignisverarbeitung
      38. Public Overrides Sub Send(ByVal Msg As String)
      39. OnChatMessage(New MessageEventargs(Msg)) ' anzeigen
      40. For Each C As Client In _Clients ' an alle versenden
      41. C.Send(Msg)
      42. Next
      43. End Sub
      44. Protected Overrides Sub Dispose(ByVal disposing As Boolean)
      45. _Listener.Stop()
      46. For i As Integer = _Clients.Count - 1 To 0 Step -1
      47. _Clients(i).Dispose()
      48. Next
      49. End Sub
      50. End Class


      Dazu gibts noch das Form sowie 2 Dateien mit Helferlein-Code, auf die ich hier nicht eingehe.

      Edit: Auf besonderen Wunsch uppe ich auch eine Version, bei der Server und Client nicht von derselben Basisklasse erben.
      Edit2: Beachtet auch den Chat auf WCF-Basis, den man von MS direkt erhält: Mini-Tipp: WCF - Chat (Windows Communication Foundation)
      WCF ist viel eleganter, insbesondere was die Implementierung eines Kommunikations-Protokolls angeht
      Dateien

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

      Router einrichten

      Soweit schomaschön, damit hat man also eine Anwendung, die sich als Server oder Client betätigen kann, sogar beides gleichzeitig (wenn man mehrere Forms öffnet), und dann können diese Forms untereinander chatten - via TCP.

      Beim ins Internet chatten kann sich der Router einem in den Weg stellen, weil der verwaltet ja eine IP, die vom INet ihm zugeteilt wurde.
      Also muß man beim Router über die KonfigurationsSite "http:\\192.168.1.1" ein PortForwarding konfigurieren.
      Dazu muß man auf genannter KonfigurationsSite unter "PortForwarding" die lokale IP des Zielrechners eingeben (bei mir: 192.168.1.2), denn auf die wird geforwardet. Und den Port, den man benutzen möchte (irgend eine zahl zw. 5001 und 9999)
      Ja, wenn man dann so ein feines Port-Forwarding eingerichtet hat, kann man den Versuchschat als Server starten, welcher auf die lokale IP listened (bekommt die Daten auf die lokale IP geforwardet).
      Und dem Client am anderen Ende der Welt muß man die globale IP mitteilen, die man etwa auf checkip.dyndns.org angezeigt bekommt. Und natürlich den geöffneten Port, den musser auch wissen, der Client.
      Dann kann sich der Client mit dem Server verbinden, und die 2 können chatten.

      Router-IP finden (falls die KonfigurationsSite "http:\\192.168.1.1" nicht stimmt)
      Also ein Kumpel wusstedas (das vorherige auch):
      Falls bei euch die IP-Adresse 192.168.1.1 des Routers nicht stimmt, kann diese wie folgt herausgefunden werden:
      1.) Als erstes die Komandozeilen-Konsole öffnen (Start->Ausführen->"cmd" eingeben und mit Enter bestätigen).
      2.) In die Konsole den Befehl "ipconfig" eingeben.
      Dann muss man in dem vielen Text, derdakommt, was suchen, was mit "StandardGateWay" bezeichnet wird - das ist die IP-Addresse des Routers.

      ErfinderDesRades schrieb:

      (irgend eine zahl zw. 5001 und 9999)
      Warum zwischen 5001 und 9999?
      In diesen drei Protokollen ist die Portnummer 16 Bit groß, d. h. sie kann Werte von 0 bis 65535 annehmen. Bestimmte Applikationen verwenden Portnummern, die ihnen von der IANA fest zugeordnet und allgemein bekannt sind. Sie liegen üblicherweise von 0 bis 1023, und werden als Well Known Ports bezeichnet. Von Port 1024 bis 49151 befinden sich die Registered Ports. Anwendungshersteller können bei Bedarf Ports für eigene Protokolle registrieren lassen, ähnlich wie Domainnamen. Die Registrierung hat den Vorteil, dass eine Anwendung anhand der Portnummer identifiziert werden kann, allerdings nur wenn die Anwendung auch den bei der IANA eingetragenen Port verwendet. Die restlichen Ports von Portnummer 49152 bis 65535 sind so genannte Dynamic und/oder Private Ports. Diese lassen sich variabel einsetzen, da sie nicht registriert und damit keiner Anwendung zugehörig sind.

      Kommunikation per NamedPipes

      Der Thread [Allgemein] Daten an andere .exe übergeben hat mich drauf gebracht, dass dieselbe Programm-Struktur ebenso verwendbar sein müsste, wenn man die Kommunikation per NamedPipes abwickelt statt per Tcp.
      NamedPipes sind im Netzwerk verfügbar, und ist natürlich wesentlich schneller und resourcenschonender als Tcp.

      Intern ticken die NamedPipes schon sehr anders als TcpListener und TcpClient, aber an der Architektur von Server und Client hat sich tatsächlich nix geändert, vergleiche:

      Die Basisklasse kann natürlich nicht mehr TcpBase heißen, und im Grunde müsste sie auch für den VersuchsChat auf "CommunicationBase" umbenannt werden, denn es ist dieselbe Klasse (zeigt sich hier als noch wiederverwendbarer, als ich selbst gedacht hatte).

      Jo, und der TcpClient im Client hat sich erübrigt, und im Server gibts jetzt nurnoch den PipeNamen, anstatt eines TcpListeners.
      Denn bei NamedPipes gibts keinen Listener, sondern jeder NamedPipeServerStream listens selber, und wird zum Verbindungs-Objekt, wohingegen bei Tcp der TcpListener beim Verbinden einen neuen TcpClient generiert als Verbindungs-Objekt.

      Vlt. noch interessant der verwendete Konstruktor des NamedPipeServerStreams:

      VB.NET-Quellcode

      1. Dim strm = New NamedPipeServerStream(_Name, PipeDirection.InOut, 20, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)
      2. strm.BeginWaitForConnection(AddressOf EndAccept, strm)
      Keine Fragen, oder? Direction.InOut, Byte-Übertragung, asynchron.
      Höchstens die "20" erklärt sich nicht selbst, die gibt an, dass maximal 20 PipeStreams auf diesem Namen registriert werden dürfen.

      Hier der Konstruktor des Gegenstücks:

      VB.NET-Quellcode

      1. Dim strm = New NamedPipeClientStream(".", _PipeName, PipeDirection.InOut, PipeOptions.Asynchronous)
      2. strm.Connect()
      Hier ist "." erwähnenswert - damit ist der lokale Computer als Server-Computer qualifiziert.
      Dateien

      ErfinderDesRades schrieb:

      Die Verwendung der asynchronen Read-Methode insbesondere des TcpClients ist - soweit ich weiß - die einzige Möglichkeit, mit der ein Server hunderte von TCP-Verbindungen halten kann, ohne dementsprechend auch hunderte von Threads alloziieren zu müssen.
      Macht .NET intern aber nicht genau das?
      Was?
      Hunderte von Threads alloziieren?

      Nein - machtes nicht.
      Intern wird da glaub iwas mittm ThreadPool gehext.
      Keine Ahnung wies funktioniert, aber es wird bei Benutzung der asynchronen Methoden nicht für jede Tcp-Verbindung ein eigener Thread alloziiert.
      Sondern nur bei Aktivität wird ein Thread aus dem Pool entnommen, und anschließend wieder zurückgegeben.

      Man kanns übrigens auch überprüfen, indem man zb 10 Clients an einen Server hängt, und dann die Threads debugt (Codestop, und dann Menü Debuggen-Fenster-Threads): Es sind weniger als 10.

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

      update: Verwendung von BinaryReader/Writer

      ich verwende in Clientklasse nun BinraryReader/-Writer zum Lesen/Schreiben in den NetworkStream.
      Nicht nur wird der Code dadurch noch bisserl übersichtlicher, v.a. eröffnet das die Möglichkeit, auch andere Daten als nur Strings zu übertragen.
      Statt der asynchronen TcpClient.BeginRead() - Methode wird nun von der BinaryReader.Read()-Methode ein Func(Of String)-Delegat gebildet, der per .BeginInvoke() ebensogut asynchron aufgerufen werden kann:

      VB.NET-Quellcode

      1. Imports System.Net.Sockets
      2. Imports System.IO
      3. Public Class Client : Inherits TCPBase
      4. Private _TcpClient As TcpClient, _Reader As BinaryReader, _Writer As BinaryWriter, _dlgReadString As Func(Of String)
      5. Public Sub New(TC As TcpClient)
      6. _TcpClient = TC
      7. Dim strm = _TcpClient.GetStream
      8. _Reader = New BinaryReader(strm)
      9. _Writer = New BinaryWriter(strm)
      10. _dlgReadString = AddressOf _Reader.ReadString
      11. _dlgReadString.BeginInvoke(AddressOf EndRead, Nothing)
      12. End Sub
      13. Private Sub EndRead(ar As IAsyncResult)
      14. If MyBase.IsDisposed Then Return
      15. Try
      16. Dim msg = _dlgReadString.EndInvoke(ar)
      17. CrossThread.RunGui(AddressOf OnChatMessage, msg)
      18. _dlgReadString.BeginInvoke(AddressOf EndRead, Nothing) 'asynchrone Rekursion
      19. Catch ex As EndOfStreamException
      20. CrossThread.RunGui(AddressOf OnStatusMessage, "CounterClient shut down")
      21. CrossThread.RunGui(AddressOf MyBase.Dispose)
      22. End Try
      23. End Sub
      24. Public Overrides Sub Send(Msg As String)
      25. _Writer.Write(Msg)
      26. End Sub
      27. Protected Overrides Sub Dispose(disposing As Boolean)
      28. DisposeAll(_Writer, _Reader, _TcpClient)
      29. End Sub
      30. End Class

      (update in post#1)
      Hallo,

      Ich kann Netzwerktechnisch in größeren umgebungen nur von NamePipes abraten, diese sind nicht Routing fähig, ich habe versucht nen Server in Netzwerk 10.140.0.0/22 zu starten und den Client in 10.141.0.0/22, mit den ergebniss dass sich das Programm aufhngt weil der Pipeclient auf denn PipeServer wartet.

      Desweiteren werden NamePipes ebenfalls über TCP übertragen, genauer gesagt über SMB, da eine NamePipe in Server Msssage Block als ganz normales NetBIOS / NetBIOS over TCP übertragen wird.

      LG, Herbrich

      PS: Kann man denn Clienten auch in Silverlight verwenden? Ich will ein Chat Programmieren

      zn-gong schrieb:

      genauer gesagt über SMB
      Warum sollte es dann nicht Routing-Fähig sein? Sicher, dass der Router nicht einfach SMB blockt bzw. nicht routet? Steht der Server vielleicht in 'ner DMZ, der das nicht gefällt?
      Von meinem iPhone gesendet
      Hallo,

      Ja, ich bin mir da sicher, weil der Server ja via Windows Explorrer \\jenniframe1 und \\jennifrane1.herbrich-activedirectory-domäne.tld ereichbar ist. Währe echt cool wen NamePipes laufen würden.

      LG, Herbrich
      Hallo,

      Hmm ja, WINS ist ne gute Idee, und WINS ist auch nicht zum Routen gedacht, es ist nur die name auflösung, DNS ist ja auch nicht Routbar, und trotzdem finden sichalle rechner in ganzen Netzwerk ohne Probleme. WINS reduziert lediglich die Brodcasts da alle host aus der WINS-DB abgefragt werden können und er so nicht suchen muss.

      Jedenfalls habe ich noch mal versucht die NamePipe Application zu starten, in lokalen Server Netz kein Problem, in Client Netz geht aber imho nichts -.-

      LG, Herbrich
      DNS wird nicht geroutet sondern Delegiert. d.H. ist giebt genau nur EINE EINZIGE Route zur domian, beim Router wird anhand der geringsten kosten der bestmögliche Weg von A nach B gesucht. Und was WINS angeht, es kann nur Repliziert werden und ist IMHO nicht Routbar. !

      Und SMB ist TCP als Routbar, aber die NamePipes eben nicht, und dass ist das was mich ärgert.

      LG, Herbrich
      DNS wird nicht geroutet sondern Delegiert. d.H. ist giebt genau nur EINE EINZIGE Route zur domian, beim Router wird anhand der geringsten kosten der bestmögliche Weg von A nach B gesucht. Und was WINS angeht, es kann nur Repliziert werden und ist IMHO nicht Routbar. !


      AUA!
      Das ist absolut falsch. Natürlich werden deine UDP (oder TCP) Pakete für deine DNS-Abfragen durchs Internet geroutet.
      Die Root-Nameserver delegieren lediglich die Subdomains der Rootzone (alias TLDs) zu den entsprechenden Registries, welche diese dann weiter zu den Nameservern des entsprechenden Registrars deligieren.

      WINS ist auch routebar, das habe ich bereits eingesetzt. Ob nun dein NamedPipes übers Netzwerk routebar ist, weiß ich nicht, das habe ich noch nicht getestet...
      Ja, aber schau dir mal das OSI Schicht model an,

      DNS, HTTP, SMB, NTNTP, usw..., dass ist alles nur der Payloud, geroutet wird nur das TCP. Eine Kiste auf einen Schief schwimmt ja auch nicht, sondern nur das Schief in den die kiste drinnen ist.

      LG, Herbrich
      Das ist mir durchaus bekannt ;)

      Und warum sollte dann deiner Meinung nach WINS, was ja NetBIOS auf IP ist nicht routebar sein?

      PS: Kann man denn Clienten auch in Silverlight verwenden? Ich will ein Chat Programmieren
      Herzlich Willkommen in 2014, es gibt zivilierte Technologien wie WebSockets für sowas.
      Silverlight ist noch toter als Flash und absolut unbenutzbarer Dreck.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Manawyrm“ ()

      Dancke, genau darauf wollte ich hiaus, der Paylod und nicht die technick der hören schichten selber :)

      Warum soll Silverlight tod sein? Wenn ich es in meinen Netzwerk benutzen will in Intranet? Und selbst Sharepoint 2010 hat ja wie wir alle Wissen(oder auch nicht^^) Silverlight drinnen :)

      LG, Herbrich
      Ich habe mir den Quelltext nicht angeschaut, aber ich glaube man kann das etwas besser separieren.

      Z. B. könnte man die Abstraktion so umgestalten, dass man den Protokoll-Kram (NamedPipes/TCP) in eine Message-Dispatcher-Klasse (oder ein Interface) auslagert und dann in den eigentlichen Client-/Server-Klassen nur eine Instanz dieser verwendet wird.

      So könnte man die Klassen auch mit ICMP, UDP, Ethernet, SSH oder Brieftauben verwenden. Man müsste halt nur einen eigenen Dispatcher dafür schreiben.
      Von meinem iPhone gesendet