WCF - Tutorial 2

In meinem zweiten WCF-Tutorial schreibe ich darüber, wie man mehrere Clients mit einem Host kommunizieren lässt. Es geht im Prinzip darum, dass sich mehrere Client miteinander unterhalten können und der Host das ganze managend. Also eine art Chatprogramm. Die Clients können sich dabei nach Wunsch ab- und anmelden. Man könnte das ganze natürlich wie in meinem ersten Tutorial machen und für beide Seiten einen Dienst definieren. Das ganze würde auch funktionieren, sogar mit einer dynamischen Anzahl an Clients. Einfacher ist es jedoch mit den in der WCF mitgelieferten Callbackfunktionen. Der Client muss sich letztendlich nur anmelden und wird in eine Liste bei dem Host aufgenommen. Alles andere regelt WCF für uns. Wer den ersten Teil WCF - Tutorial 1 | beBug noch nicht gelesen hat sollte das nachholen, sofern er von WCF bisher noch nichts gehört hat.



Zu Beginn erstellen wir 3 Projekte. Eine Windows Forms-Anwendung für den Client, eine Konsolenanwendung für den Host und eine Dienstbibliothek für die gemeinsam genutzten Objekte. Ich hab die Projekte Client, Host und Datalib genannt. Für alle Projekte muss dabei ein Verweis auf System.ServiceModel hinzugefügt werden. Dem Host und dem Client wird zusätzlich noch ein Verweis auf das Datalib-Projekt hinzugefügt.
Beginnen wir mit dem Datalib-Projekt. Hier werden für Host und Client alle Objekte bereitgestellt, die beide kennen müssen um miteinander zu kommunizieren.
Jeder Client soll eine Nachricht an den Host schicken, in der sein Name und seine eigentliche Nachricht, die er geschrieben hat steht. Das ganze lagern wir in eine eigene Klasse aus, die ich ChatMessage genannt habe.
//ChatMessage.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace Datalib
{
[DataContract]
public class ChatMessage
{
string m_Name;
string m_Message;

[DataMember]
public string Name
{
set { m_Name = value; }
get { return m_Name; }
}

[DataMember]
public string Message
{
set { m_Message = value; }
get { return m_Message; }
}
}
}

Jede Klasse, die über das Netzwerk verschickt werden soll muss zuerst serialisiert und auf der anderen Seite wieder deserialisiert werden. Um die Klasse Serialisierbar zu machen schreiben wir deshalb als Attribut [DataContract] darüber. Jeden Member der Klasse müssen wir nun explizit als serialisierbar deklarieren, wenn wir ihn auch serialisieren wollen. Es gibt beispielsweise auch Member, die nur für den Client oder Host wichtig sind. Diese müssen dann natürlich nicht serialisiert werden. Wir haben allerdings keine solchen Member und müssen alle unsere Member serialisieren. Das geschieht durch das Attribut [DataMember]. Das sollte am besten nicht direkt über die Variable, sondern über deren Getter- und Setter-Methoden geschrieben werden.
Als nächstes legen wir die Funktion fest, die der Host ausführt und mit der er an einen Client die entsprechende Nachricht schickt.
//IClientFunctions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Datalib
{
public interface IClientFunctions
{
[OperationContract(IsOneWay = true)]
void ShowMessage(string message);
}
}

Das ganze haben wir als Interface festgelegt, da dem Host später egal sein kann was der Client damit macht. Er muss nur wissen wie er ihn anzusprechen hat. Der Client kann die Methode ganz nach eigenen Wünschen benutzen und wird auch nichts festgelegt. Wie in meinem ersten Tutorial geben wir der Funktion auch hier das Attribut [OperationContract]. IsOneWay legt fest, dass diese Funktion nur in eine Richtung gültig ist. Das ist sinnvoll, da der Host diese Nachricht an alle Clients schickt, die Clients jedoch nicht zurück an den Host. Um mit dem Host zu kommunizieren bekommen die Clients nun eine eigene Klasse.
//IHostFunctions.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Datalib
{
[ServiceContract(CallbackContract = typeof(IClientFunctions))]
public interface IHostFunctions
{
[OperationContract]
void Connect();

[OperationContract]
void Disconnect();

[OperationContract]
void Send(ChatMessage cm);
}
}

Auch hier benutzen wir wieder ein Interface damit weder Host noch Client mit dem implementierten Ballast des anderen auskommen muss. Die Funktionen Connect und Disconnect können vom Client genutzt werden sich beim Host anzumelden. Mit der Funktion Send sendet der Client seine Nachricht an den Host. Er sendet dabei ein Objekt des Typs ChatMessage, das wir ja bereits implementiert haben.
Als nächstes wagen wir uns an den Host und erstellen eine Klasse HostMessageService die das Interface IHostFunctions verwendet.
//HostMessageService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using Datalib;

namespace Host
{
// Hier kann der InstanceContextMode eingestellt werden (Single, PerSession, PerCall)
// [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class HostMessageService : IHostFunctions
{
private static readonly List subscribers = new List();

public void Connect()
{
IClientFunctions callback = OperationContext.Current.GetCallbackChannel();
if (!subscribers.Contains(callback))
{
subscribers.Add(callback);
Console.WriteLine("New client connected" );
}
}

public void Disconnect()
{
IClientFunctions callback = OperationContext.Current.GetCallbackChannel();
if (subscribers.Contains(callback))
{
subscribers.Remove(callback);
Console.WriteLine("client disconnected" );
}
}

public void Send(ChatMessage cm)
{
List subscribersToDelete = new List();
IClientFunctions callback = OperationContext.Current.GetCallbackChannel();
foreach (IClientFunctions client in subscribers)
{
if (client != callback)
{
try
{
client.ShowMessage(cm.Name + ":t" + cm.Message);
}
catch(Exception e)
{
subscribersToDelete.Add(client);
Console.WriteLine("Client lost connection" + e.Message);
}
}
}
foreach (IClientFunctions client in subscribersToDelete)
{
if (subscribers.Contains(client))
{
subscribers.Remove(client);
}
}
}
}
}

In dieser Klasse wird schon alles geregelt, was wir für unsere Kommunikation brauchen. In der Liste subscribers werden alle angemeldeten Clients gespeichert. Diese ist vom Typ IClientFunktions. Wie gesagt muss der Host diese Klasse nicht implementieren, er muss nur wissen, wie er die Clients anzusprechen hat.
In der Funktion Connect wird zuerst ausgelesen, welcher Client sich anmelden möchte. Das geschieht durch :
IClientFunctions callback = OperationContext.Current.GetCallbackChannel();
Anschließend wird der Client in die Liste aufgenommen, falls er nicht schon dort vorhanden ist.
In der Funktion Disconnect wird genau das gleiche gemacht. Jedoch wird der Client hier nicht der Liste hinzugefügt, sondern entfernt.
Die Funktion Send ist auch nicht schwer zu verstehen. Zuerst schauen wir einmal welcher Client eine Nachricht senden möchte und merken ihn uns. In der foreach-Schleife wird die Nachricht nun allen angemeldeten Clients gesendet. Hier benötigen wir auch den Client den wir uns gemerkt haben. Da er uns die Nachricht gesendet hat brauchen wir ihm diese nicht zustellen. Das ganze kontrollieren wir mit if(client != callback). Der Try-Catch-Block dient zur Fehlererkennung. Falls ein Client seine Verbindung verloren kann die Nachricht nicht an ihn gesendet werden und eine Exception wir ausgelöst. Wir fangen diese ab und tragen den Client, der seine Verbindung verloren hat in eine zweite Liste „subscribersToDelete“ ein. Nachdem allen Clients eine Nachricht zugestellt wurde gehen wir diese Liste durch und löschen alle Clients, die ihre Verbindung verloren haben aus unserer Liste mit angemeldeten Clients.
Nun muss wieder eine App.config dem Host-Projekt hinzugefügt werden. Mit Hilfe des Dienstkonfigurationsedotors erstellt ihr diesmal einen Service an der Adresse net.tcp://localhost:8500/MyHostService und einem MetaBehavior an http://localhost:8501/MyHostServiceMeta. Anschließend könnt ihr mit dem svcutil wie gewohnt eine Proxy.cs erstellen.


//Proxy.cs
namespace Datalib
{
using System.Runtime.Serialization;


[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0" )]
[System.Runtime.Serialization.DataContractAttribute(Name="ChatMessage", Namespace="http://schemas.datacontract.org/2004/07/Datalib" )]
public partial class ChatMessage : object, System.Runtime.Serialization.IExtensibleDataObject
{

private System.Runtime.Serialization.ExtensionDataObject extensionDataField;

private string MessageField;

private string NameField;

public System.Runtime.Serialization.ExtensionDataObject ExtensionData
{
get
{
return this.extensionDataField;
}
set
{
this.extensionDataField = value;
}
}

[System.Runtime.Serialization.DataMemberAttribute()]
public string Message
{
get
{
return this.MessageField;
}
set
{
this.MessageField = value;
}
}

[System.Runtime.Serialization.DataMemberAttribute()]
public string Name
{
get
{
return this.NameField;
}
set
{
this.NameField = value;
}
}
}
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0" )]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="IHostFunctions", CallbackContract=typeof(IHostFunctionsCallback))]
public interface IHostFunctions
{

[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IHostFunctions/connect", ReplyAction="http://tempuri.org/IHostFunctions/connectResponse" )]
void connect();

[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IHostFunctions/disconnect", ReplyAction="http://tempuri.org/IHostFunctions/disconnectResponse" )]
void disconnect();

[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IHostFunctions/send", ReplyAction="http://tempuri.org/IHostFunctions/sendResponse" )]
void send(Datalib.ChatMessage cm);
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0" )]
public interface IHostFunctionsCallback
{

[System.ServiceModel.OperationContractAttribute(IsOneWay=true, Action="http://tempuri.org/IHostFunctions/ShowMessage" )]
void ShowMessage(string message);
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0" )]
public interface IHostFunctionsChannel : IHostFunctions, System.ServiceModel.IClientChannel
{
}

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0" )]
public partial class HostFunctionsClient : System.ServiceModel.DuplexClientBase, IHostFunctions
{

public HostFunctionsClient(System.ServiceModel.InstanceContext callbackInstance) :
base(callbackInstance)
{
}

public HostFunctionsClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) :
base(callbackInstance, endpointConfigurationName)
{
}

public HostFunctionsClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) :
base(callbackInstance, endpointConfigurationName, remoteAddress)
{
}

public HostFunctionsClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(callbackInstance, endpointConfigurationName, remoteAddress)
{
}

public HostFunctionsClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(callbackInstance, binding, remoteAddress)
{
}

public void connect()
{
base.Channel.connect();
}

public void disconnect()
{
base.Channel.disconnect();
}

public void send(Datalib.ChatMessage cm)
{
base.Channel.send(cm);
}
}

Nachdem die Proxy.cs dem Client-Projekt hinzugefügt wurde können wir mit der Oberfläche beginnen. Folgende Elemente fügen wir unserer Form1 hinzu:


  1. TextBox
    Name = txtBx_Chat
    Multiline = true
    Read only = true
    ScrollBars = Vertical
  2. TextBox
    Name = txtBx_Message
  3. TextBox
    Name = txtBx_Name
  4. Button
    Name = btn_Send
    Enabled = false
  5. Button
    Name = btn_Connect
  6. Button
    Name = btn_Disconnect
    Enabled = false
Der Code von Form1.cs sieht wie folgt aus:
//Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;

namespace Client
{
public partial class Form1 : Form, IHostFunctionsCallback
{
HostFunctionsClient client = null;

public Form1()
{
InitializeComponent();

InstanceContext context = new InstanceContext(this);
client = new HostFunctionsClient(context);
}

private void btn_Connect_Click(object sender, EventArgs e)
{
client.connect();
this.txtBx_Name.Enabled = false;
this.btn_Send.Enabled = true;
this.btn_Connect.Enabled = false;
this.btn_Disconnect.Enabled = true;
}

private void btn_Disconnect_Click(object sender, EventArgs e)
{
client.disconnect();
this.txtBx_Name.Enabled = true;
this.btn_Send.Enabled = false;
this.btn_Connect.Enabled = true;
this.btn_Disconnect.Enabled = false;
}

private void btn_Send_Click(object sender, EventArgs e)
{
Datalib.ChatMessage cm = new Datalib.ChatMessage();
cm.Name = this.txtBx_Name.Text;
cm.Message = this.txtBx_Message.Text;
if (client.State == CommunicationState.Opened)
{
client.send(cm);
}
DisplayMessage(this.txtBx_Name.Text + ":t" + this.txtBx_Message.Text);
this.txtBx_Message.Clear();
}

#region IHostFunctionsCallback Member

public void ShowMessage(string message)
{
DisplayMessage(message);
}

public void DisplayMessage(string message)
{
this.txtBx_chat.Text = this.txtBx_chat.Text + "rn" + message;
this.txtBx_chat.SelectionStart = this.txtBx_chat.Text.Length;
this.txtBx_chat.ScrollToCaret();
}

#endregion
}
}

Zuerst benutzen wir in unserer Klasse das Interface IHostFunctionsCallback. Dieses wurde in der Proxy.cs aus unserem Interface IHostFunctions generiert und bietet uns die Möglichkeit die Funktionen aus IHostFunctions für unser Callback zu benutzen. Unsere Klasse hat zudem ein Member client vom Typ HostFunctionsClient. Auch diese Klasse wurde in der Proxy.cs generiert und bietet uns später Zugriff auf unseren Host.
Im Konstruktor von Form1 erstellen wir eine Variable der Klasse InstanceContext. Diese Klasse erwartet beim erstellen eine Klasse, die ein Callback implementiert. Da unsere Form1 das bereits macht, können wir diese einfach mit this zuweisen. Das InstanceContext object geben wir nun dem Client mit, damit dieser auch weiß welches Callback wir benutzen wollen.
Mit dem Event btn_Connect_Click melden wir uns nach einem Klick auf unseren Connect-Button bei dem Host an. Wie gesagt können wir den Host über unseren Member client ansprechen.
Bei dem Event btn_Disconnect_Click verhält es sich genauso. Hier melden wir uns allerdings bei dem Host wieder ab.
Mit dem Event btn_Send_Click senden wir unsere Nachricht an den Client. Dazu erstellen wir erst einmal eine Instanz vom Typ ChatMessage, kontrollieren ob eine Verbindung mit dem Host besteht und Senden unsere Nachricht mit client.send an den Host.
Die Funktion ShowMessage ist nun unser Callback. Der Host kann genau diese Funktion für uns aufrufen, wenn er von einem anderen Client eine Nachricht empfangen hat. In der Funktion machen wir letztendlich nicht viel, sondern schreiben diese Nachricht nur in unser Chat-Fenster.

Bookmark and Share

21 Kommentare:

Anonym hat gesagt…

Vielen Dank für das gute Tutorial. Hat mir sehr geholfen. Wäre noch super wenn man das Projekt runterladen könnte.

bebug hat gesagt…

Download
Bitteschön.

Anonym hat gesagt…

danke für deinen tut.
hab das genauso gemacht, allerdings mit nem Dienstverweis....
er verbindet...
dann will ich senden: client.send(cm);
und er sagt: Die Funktion ist nicht implementiert

was soll ich da machen?
Client.open geht er durch, und im cm stehen die ganzen propertyinhalte...
was hab ich vergessen?

Anonym hat gesagt…

Frage zu der Methode "send", sie ist doch etwas verwirrend und zwar:
1.Wie stellst du fest, dass dieser Client was senden will?
2.Wie schickst du an die anderen Clients was?

Kannst du das irgendwie mit Sender und Empfänger benennen?
Danke

bebug hat gesagt…

Jeder Client wird vom Host gespeichert nachdem er sich angemeldet hat. Die Clients senden dem Host mit Send() ihre Nachricht. Der Host geht danach die gespeicherten Clients durch und sendet ihnen die Nachricht mit ShowMessage() zu.

Klassendiagramm
Sequenzdiagramm

Dalmaso hat gesagt…

Hey super Tut...

Klappt alles!!
Aber eine Frage stellt sich mir:
Woher kommt die App.config im Client?

Gruss

Flo hat gesagt…

Das funktioniert gleich wie in meinem ersten Tutorial beschrieben.

http://bebugsblog.blogspot.com/2010/01/wcf-tutorial-1.html

Anonym hat gesagt…

ich hätt da mal ne frage: müßte nicht auf das *gleichzeitige* eintreffen von nachrichten von verschiedenen clients eingegangen werden? entweder im code durch synchronisierung oder im text durch hinweise, ob und wie das (k)ein problem ist.

sonst schon ganz ordentlich

Flo hat gesagt…

Soweit es mir bekannt ist, eröffnet jeder Zugriff einen eigenen Programmpfad. Eine Synchronisierung ist daher auch nötig.

Anonym hat gesagt…

Hallo Flo, kann man dein Beispiel-App (echt gute Beschreibung) auch downloaden um damit spielen zu können. VG Andreas

Flo hat gesagt…

Der Download ist im 2ten Kommentar

Anonym hat gesagt…

Hallo zusammen,

klappt alles prima. Eine Frage hätte ich jedoch zur send-Methode Serverseitig: Warum MUSS man sich den rufenden Client merken und darf ihm nicht die Nachricht senden. Wäre für mich am logischsten. Jedoch erhängt sich der Client wenn ich das so machen. Ich nehme mal an, dass die Verbindung noch mit dem Aufruf Server => Client beschäftigt ist und daher nicht die Callback-Methode des gleichen clients rufen kann. Gruss & Danke

Florian Buchner hat gesagt…

Hallo,
in dem beschriebenen Scenario blockieren sich beide Anfragen gegenseitig. Um das Problem zu umgehen kann der Server zum Senden einen neuen Thread starten.

Anonym hat gesagt…

Hallo beBug,

die das übernehmen Deines Codes per Copy/Paste funktioniert nicht, da im Text die Typisierung
(spitze Klammer auf)IClientFunctions(spitze Klammer zu) nicht angezeigt wird, da es vom HTML verschluckt wird.

Ich habe mich schon gewundert wie das bei Dir laufen können soll. :-)

Gruß
Wolfgang

Anonym hat gesagt…

klappt alles prima aber warum bekommt mann beim Download eine EXE???

Florian Buchner hat gesagt…

Das liegt an dem sharehoster. Zum Download der zip-datei musste einfach das Häckchen da weg machen.

Anonym hat gesagt…

Hallo

Das Tutorial ist wirklich sehr hilfreich und vielen Dank, dass du den Code zur Verfügung gestellt hast.

Ich habe nur noch eine Schwierigkeit den Client zum laufen zubringen. Sobald ich den Host gestartet habe und auch läuft, öffne ich in einem zweiten VS 2012 den Client und führe den Code aus. Interessanterweise wird jedoch nochmals der Host gestartet und wirft mir eine Fehlermeldung da dieser ja schon läuft.
Wie starte ich den Client ohne dass der Host nochmals ausgeführt wird?

Besten Dank,
Hannes

Florian Buchner hat gesagt…

In Visual Studio kannst du während des Debuggens eines Programmes das andere mit einem Rechtsklick auf das Projekt und anschließendem Klick auf Debug starten.
Alternativ ist es möglich direkt die zweite Exe-Datei über den Explorer zu starten.

Anonym hat gesagt…

Super Tut, aber 2 Fragen:
1. Ich möchte die Host Adresse in Client frei eingeben können. Wie kann man das machen?
2. Wie bekomme ich den Host ohne Admin rechte zum laufen? Sonst ist das nur eine nette Fingerübung.

Anonym hat gesagt…

bei der Geschichte mit dem "WCF- Dienstkonfigurations editor" muss ich da den Interface Namen "IHostFunctions" eingeben?
bei der Generierung der App.Config hat er mir ein "" reingeschrieben und ist unterstrichen

Anonym hat gesagt…

*"service name="Host.IClientFunctions""

Kommentar veröffentlichen