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:
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:
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.
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
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
- Imports System.Net
- Imports System.Net.Sockets
- Imports System.Text
- Public Class Client : Inherits TCPBase
- Private _TcpClient As TcpClient
- Private _Stream As NetworkStream
- Dim Buf(&H400 - 1) As Byte
- Public Sub New(ByVal TC As TcpClient)
- _TcpClient = TC
- _Stream = _TcpClient.GetStream
- _Stream.BeginRead(Buf, 0, Buf.Length, AddressOf EndRead, Nothing)
- End Sub
- Private Sub EndRead(ByVal ar As IAsyncResult)
- If MyBase.IsDisposed Then Return
- Dim read As Integer = _Stream.EndRead(ar)
- If read = 0 Then 'leere Datenübermittlung signalisiert Verbindungsabbruch
- CrossThread.RunGui(AddressOf OnStatusMessage, New MessageEventargs("CounterClient shut down"))
- CrossThread.RunGui(AddressOf MyBase.Dispose)
- Return
- End If
- Dim SB As New StringBuilder(Encoding.UTF8.GetString(Buf, 0, read))
- Do While _Stream.DataAvailable
- read = _Stream.Read(Buf, 0, Buf.Length)
- SB.Append(Encoding.UTF8.GetString(Buf, 0, read))
- Loop
- CrossThread.RunGui(AddressOf OnChatMessage, New MessageEventargs(SB.ToString))
- _Stream.BeginRead(Buf, 0, Buf.Length, AddressOf EndRead, Nothing)
- End Sub
- Public Overrides Sub Send(ByVal Msg As String)
- Dim Buf() As Byte = Encoding.UTF8.GetBytes(Msg)
- _Stream.Write(Buf, 0, Buf.Length)
- End Sub
- Protected Overrides Sub Dispose(ByVal disposing As Boolean)
- DisposeAll(_Stream, _TcpClient)
- End Sub
- End Class
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
- Imports System.Net.Sockets
- Imports System.Threading
- ''' <summary>
- ''' stellt den Erben "Server" und "Client" 2 verschiedene
- ''' Message-Events zur Verfügung, und ein Event-Raisendes Dispose
- ''' </summary>
- Public MustInherit Class TCPBase : Implements IDisposable
- Private _IsDisposed As Boolean = False
- Public Event Disposed As EventHandlerEx(Of TCPBase)
- Protected MustOverride Sub Dispose(ByVal disposing As Boolean)
- Public MustOverride Sub Send(ByVal Msg As String)
- ''' <summary>
- ''' Zur Ausgabe chat-verwaltungstechnischer Status-Informationen
- ''' </summary>
- Public Event StatusMessage As EventHandler(Of MessageEventargs)
- Protected Sub OnStatusMessage(ByVal e As MessageEventargs)
- RaiseEvent StatusMessage(Me, e)
- End Sub
- ''' <summary>Zur Ausgabe von Chat-Messages</summary>
- Public Event ChatMessage As EventHandler(Of MessageEventargs)
- Protected Sub OnChatMessage(ByVal e As MessageEventargs)
- RaiseEvent ChatMessage(Me, e)
- End Sub
- Public Sub RemoveFrom(Of T As TCPBase)(ByVal Coll As ICollection(Of T))
- Coll.Remove(DirectCast(Me, T))
- End Sub
- Public ReadOnly Property IsDisposed() As Boolean
- Get
- Return _IsDisposed
- End Get
- End Property
- Public Sub AddTo(Of T As TCPBase)(ByVal Coll As ICollection(Of T))
- Coll.Add(DirectCast(Me, T))
- End Sub
- Public Sub Dispose() Implements IDisposable.Dispose
- If _IsDisposed Then Return
- _IsDisposed = True
- Dispose(True) ' rufe die erzwungenen Überschreibungen von Sub Dispose(Boolean)
- OnStatusMessage(New MessageEventargs(Me.GetType.Name, " disposed"))
- RaiseEvent Disposed(Me)
- GC.SuppressFinalize(Me)
- End Sub
- 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
- Imports System.Net
- Imports System.Net.Sockets
- Public Class Server : Inherits TCPBase
- Private _Listener As TcpListener
- 'Pro Verbindung(sanfrage) wird ein Client-Objekt generiert, das den Datenaustausch dieser Verbindung abwickelt
- Private _Clients As New List(Of Client)
- Public Sub New(ByVal EP As IPEndPoint)
- _Listener = New TcpListener(EP)
- _Listener.ExclusiveAddressUse = False
- _Listener.Start()
- _Listener.BeginAcceptTcpClient(AddressOf EndAccept, Nothing)
- End Sub
- Sub EndAccept(ByVal ar As IAsyncResult)
- If MyBase.IsDisposed Then Return
- With New Client(_Listener.EndAcceptTcpClient(ar))
- AddHandler .ChatMessage, AddressOf Client_ChatMessage
- AddHandler .StatusMessage, AddressOf Client_StatusMessage
- AddHandler .Disposed, AddressOf Client_Disposed
- .AddTo(_Clients)
- End With
- CrossThread.RunGui(AddressOf OnStatusMessage, New MessageEventargs("TCPClient accepted"))
- _Listener.BeginAcceptTcpClient(AddressOf EndAccept, Nothing)
- End Sub
- #Region "_Clients-Ereignisverarbeitung"
- Private Sub Client_Disposed(ByVal Sender As TCPBase)
- 'den Client für die beendete Verbindung entfernen
- Sender.RemoveFrom(_Clients)
- End Sub
- Private Sub Client_ChatMessage(ByVal sender As Object, ByVal e As MessageEventargs)
- 'einkommende ChatMessages anzeigen, und an alle versenden
- Send(e.Message)
- End Sub
- Private Sub Client_StatusMessage(ByVal sender As Object, ByVal e As MessageEventargs)
- 'einkommende StatusMessages durchreichen (zur Anzeige)
- OnStatusMessage(e)
- End Sub
- #End Region '_Clients-Ereignisverarbeitung
- Public Overrides Sub Send(ByVal Msg As String)
- OnChatMessage(New MessageEventargs(Msg)) ' anzeigen
- For Each C As Client In _Clients ' an alle versenden
- C.Send(Msg)
- Next
- End Sub
- Protected Overrides Sub Dispose(ByVal disposing As Boolean)
- _Listener.Stop()
- For i As Integer = _Clients.Count - 1 To 0 Step -1
- _Clients(i).Dispose()
- Next
- End Sub
- 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
Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „ErfinderDesRades“ ()