WCF ohne Exceptions und Verbindungsabbrüche

WCF ohne Exceptions und Verbindungsabbrüche

WCF ist eine feine Sache. Wenn sie (also die Kommunikation) funktioniert.

Für Interprozesskommunikation nehme ich normalerweise einen REST-WebService, insbesondere, wenn diese Prozesse verschiedene Technologien benutzen, wie z. B. C#, Python oder JavaScript.

Für C#<->C# (bzw. .net>->.net) Prozesskommunikation gibt es jedoch eine einfache Alternative zum selbstgeschriebenen REST Service: WCF. (Hier hatte ich früher auch einmal mit Streams auf SharedMemory und System Mutexen gearbeitet, was durchaus auch geht, aber recht Low Level ansetzt.)

Gründe für und gegen REST

REST ist technologieunabhängig. Jede Sprache stellt Funktionen und Bibliotheken bereit, um REST Services zu erzeugen und mit diesen zu kommunizieren. Die Formate sind standardisiert (insbesondere JSON) und die Latenz ist oft vernachlässigbar. Die Kommunikation ist ferner relativ fehlertolerant.

Auch lassen sich so Client und Server sowohl auf derselben Maschine wie auch auf unterschiedlichen Maschinen betreiben, mit minimalen Änderungen (bzw. nur einer geänderten URL).

Was zwischen untypisierten REST-Services und typisierten Sprachen wie C# jedoch auffällt ist die relativ aufwendige Handhabung der Konvertierung der Ergebnisse bzw. die Fehlerhandhabung.

So war ich hier bei folgender recht unhandlicher Struktur für Rückgabewerte gelandet:

public class ReturnParameters
{
    public string Result;
    public List<string> ErrorMessages;
    public Dictionary<string, string> Data;
}

jeweils mit manuellen Konvertierungen der letzten Endes durchaus typisierten Parameter, sowie möglicher Fehler und Fehlermeldungen.

Eintritt WCF

WCF bietet hier vieles bereits von Hause aus.

So bekommt man typisierte Rückgabewerte und auch ein Aufruf wird 1:1 über die Grenzen des eigenen Prozesses hinweggeroutet, Exception-Handling inklusive für nicht erwartete Ergebnisse.

Ist das ganze Drum Rum erst einmal erzeugt, so hat man eine sehr elegante Möglichkeit, über ein zwischen Client und Server geteiltes Interface im Client Funktionalität aufzurufen, die im Server bereitgestellt wird.

Ferner verspricht WCF, den Kommunikationskanal einfach wählen zu können. Früher sehr beliebt (und bei WCF immer noch Standard) ist SOAP, es geht aber auch REST, Named Pipes und andere. Bei einem REST Service wird ferner auch direkt die passende Dokumentation generiert und ist ebenfalls über HTTP verfügbar.

WCF: Nicht alles ist Gold, was glänzt

Hat man sich durch die zugegebenermaßen nicht triviale Konfiguration des WCF Services und Clients durchgearbeitet (und diese implementiert) zeigen sich die ersten Probleme.

So kann ein REST Service z. B. nicht ausschließlich auf localhost horchen, er horcht immer auf * – mögliches Sicherheitsproblem (wir müssen uns auf die Firewall verlassen) und Berechtigungs-Probleme (wir benötigen Admin-Rechte, um auf * zu lauschen) inklusive.

Bei mir ein klarer Fall: ich habe beide Prozesse aus localhost, insofern wähle ich die hier passende Lösung, nämlich NamedPipes anstelle von REST.

Ganz trivial ist der Umstieg jedoch nicht, so haben Pipes z. B. keinen Port (so fällt z. B. auch die Möglichkeit, auf localhost zu testen, ob ein Port bereits benutzt wird um Kollisionen bei mehreren Services zu vermeiden, weg) und werden auch anders konfiguriert.

Nach dieser Änderung neu bekam ich jedoch auch größere Zuverlässigkeitsprobleme mit dem Service (vielleicht hatte ich diese bereits zuvor, ich hatte den Service nicht exzessiv genutzt, HTTP sollte jedoch vom Protokoll her deutlich fehlertoleranter sein). Service oder Client verloren immer mal wieder die Verbindung, ein Neustart beider Prozesse war jeweils nötig.

Mögliche Fehlermeldungen:

The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state.
There was an error writing to the pipe: Unrecognized error 232 (0xe8)

Ich habe zahlreiche Artikel, primär bei Stackoverflow (hier, hier, hier, hier, hier, hier, gelesen, wirklich hilfreich war dann jedoch ein sehr gute Einführung in WCF (enthält auch gute Video-Tutorials als Webcasts). Dort wurde ich dann darauf gestoßen, wie faulted Events korrekt gehandhabt werden, und was auch auf Client-Seiten getan werden muß, um mit Faults umzugehen.

Es bedarf verschiedener Fixe, besonders wichtig ist das Handling des Faulted Events sowie Client-seitig daß jede Aktion optional doppelt ausgeführt wird (die 2. Aktion sollte dann, wenn die erste in einen Fehler gelaufen ist, durchlaufen: 1. Aktion -> Fehler -> Fehlerbehandlung -> 2. Aktion -> Erfolg):

  • Timeout (optional)
    m_svcHost.AddServiceEndpoint(typeof(ITestSupportService), new NetNamedPipeBinding() { ReceiveTimeout = TimeSpan.MaxValue /* infinite */ }, "pipe");
  • große Empfangsgröße (falls benötigt)
    Binding binding = new NetNamedPipeBinding() { MaxReceivedMessageSize = 256 * 1024 };
  • Faulted event bei Server und Client
    // service
    m_svcHost.Faulted += m_svcHost_Faulted;
    // client
    var comObj = (ICommunicationObject)svcObj;
    comObj.Faulted += comObj_Faulted;
  • Wiederholung auf Client-Seite
    void ExecuteWCFSafe(Action a)
    {
        try
        {
            a();
        }
        catch (Exception ex)
        {
            if (ex is CommunicationException)
                try
                {
                    a();
                }
                catch (Exception ex2)
                {
                    LogError("WCF exception. Failed 2x, probably there is no server available. (Framework not running?) " + ex.ToString());
                }
            else
                LogError("WCF exception. Some non-communication exception occured. " + ex.ToString());
            //throw ex;
        }
    }

Auf Serverseite:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        ServiceHost m_svcHost = null;
        private void StartWCFService()
        {
            var strAddr = "net.pipe://localhost/MellowWCFService";
            Uri adrbase = new Uri(strAddr);
            m_svcHost = new ServiceHost(typeof(MellowService), adrbase);
            m_svcHost.AddServiceEndpoint(typeof(ITestSupportService), new NetNamedPipeBinding() { ReceiveTimeout = TimeSpan.MaxValue /* infinite */ }, "pipe");
            m_svcHost.Description.Endpoints[0].Behaviors.Add(new DispatcherSynchronizationBehavior() { AsynchronousSendEnabled = true });
            m_svcHost.Open();
            m_svcHost.Faulted += m_svcHost_Faulted;
        }
 
        void m_svcHost_Faulted(object sender, EventArgs e)
        {
            LogMessage("WCF Service channel is in state: " + m_svcHost.State);
            try
            {
                m_svcHost.Abort();
            }
            catch (Exception ex)
            {
                LogMessage("WCF Service aborting failed. Exception: " + ex.ToString());
            }
            m_svcHost = null;
            StartWCFService();
        }

Auf Clientseite:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
       IMellowService svcObj = null;
        private void CreateServiceChannel()
        {
            ChannelFactory<IMellowService> channelFactory = null;
            Binding binding = new NetNamedPipeBinding() { MaxReceivedMessageSize = 256 * 1024 };
            var strEPAdr = "net.pipe://localhost/MellowWCFService";
            var ep = new EndpointAddress(strEPAdr);
            svcObj = channelFactory.CreateChannel(ep);
 
            var comObj = (ICommunicationObject)svcObj;
            comObj.Faulted += comObj_Faulted;
        }
 
        void comObj_Faulted(object sender, EventArgs e)
        {
            var comObj = (ICommunicationObject)svcObj;
            comObj.Abort();
            CreateServiceChannel();
            LogWarning("WCF TestSupport channel had to be recreated");
        }
 
        T ExecuteWCFSafe<T>(Func<T> f)
        {
            try
            {
                return f();
            }
            catch (Exception ex)
            {
                if (ex is CommunicationException)
                    try
                    {
                        return f();
                    }
                    catch (Exception ex2)
                    {
                        LogError("WCF exception. Failed 2x, probably there is no server available. (Framework not running?) " + ex.ToString());
                        throw ex2;
                    }
                else
                {
                    LogError("WCF exception. Some non-communication exception occured. " + ex.ToString());
                    throw ex;
                }
            }
        }
 
        void ExecuteWCFSafe(Action a)
        {
            try
            {
                a();
            }
            catch (Exception ex)
            {
                if (ex is CommunicationException)
                    try
                    {
                        a();
                    }
                    catch (Exception ex2)
                    {
                        LogError("WCF exception. Failed 2x, probably there is no server available. (Framework not running?) " + ex.ToString());
                    }
                else
                    LogError("WCF exception. Some non-communication exception occured. " + ex.ToString());
                //throw ex;
            }
        }
 
        public void SampleFunction()
        {
            ExecuteWCFSafe(() => svcObj.SampleFunction());
        }
 
        public string ReturnString()
        {
            return ExecuteWCFSafe(() => { return svcObj.ReturnString(); });
        }

So läuft dann WCF auch mit Named Pipes zuverlässig. Insbesondere die korrekte Handhabung des Faulted-Events sowie die erneute Ausführung der Anfrage in einem try/catch Block haben hier die Verbesserung bewirkt.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.