gubus 2.0

2010. 12. 25.

WCF használata vastag kliens alkalmazásoknál I.

Kategória: .NET — gubus @ 10:51

Néha kliens alkalmazásokban látni a következő WCF hívási mintát:

using (var client = new FooServiceReference.FooServiceClient("myBinding"))
{
	var f = client.GetFoo();
}

A probléma a fenti kóddal az, hogy a kliens minden alkalommal újraépíti a WCF kommunikációt a szerver felé, ami több erőforrásba kerül, mintha egy már megnyitott proxy-n hívná meg a szervert.

Fontos az is, hogy a proxy objektum Fault állapotba kerülhet akkor, ha olyan katasztrofális hiba keletkezett a kliens-szerver kommunikáció közben, melynél nem folytatható tovább a kommunikáció.
Példa a Fault állapot bekövetkeztére:
- reliable session használatakor a hívás timeout-ra fut,
- a szerver SOAP Fault üzenetet küld vissza (kivételt dobott a szerver),
- a kommunikáció iniciliazálásakor az ICommunicationObject.Open() hívás nem sikerül,
- session alapú kapcsolatnál olyan protokoll vagy szerver hiba keletkezik, amely miatt a session érvénytelen lesz.

Ha a proxy példány Fault állapotba kerül, akkor nem lehet tovább használni, nem szabad Close()-t hívni rajta (kivételt dob), és az Abort() meghívásával kell felszabadítani az általa lefoglalt erőforrásokat.

A fenti minta így hibásan viselkedik a Fault állapot bekövetkeztekor, mert a proxy-n hívott Dispose() a Close()-t hívja meg, ami pedig Fault állapotban a Dispose() elszállását fogja okozni, tehát egy kommunikációs kivétel feldolgozása közben keletkezni fog egy másik, az eredeti hibát elfedő kivétel. Erre a hibára remek körbebástyázós megoldást találtak ki Redmondban:

Avoiding Problems with the Using Statement

AnswerJuly CTP – Guidance on factory close and message faults

Csak a try-catch beágyazást ipari méretben alkalmazva spagetti kódot fog eredményezni.

A hatékonyság miatt az is kívánatos lenne, ha az alkalmazás az induláskor hozná létre és inicializálná a proxy objektumokot, ezt a meglévő proxy-t használná az egész élete alatt, és ha az Faulted állapotva kerülne, akkor automatikusa zárná a proxy-t, és nyitná az újat.

A problémát elég jól részletezi a következő két cikk:

Performance Improvement for WCF Client Proxy Creation in .NET 3.5 and Best Practices

WCF Guidance for WPF Developers

Ezek után mindenki szépen neki is foghat a saját barkács proxy kezelőjének megírásához:

1) WCF Client Proxy IDisposable – Generic WCF Service Proxy
A megoldás a Fault állapotba kerülést jól kezeli, de minden hívásnál új Channel-t és ChannelFactory-t hoz létre, ami viszont a sebességnek nem tesz jót.

2. Exception Handling WCF Proxy Generator
Egy Visual Studio extension, melyet Michele Leroux Bustamante fejlesztett ki.
Jellemzők:
- Visual Studio 2008 alá készült az extension
- az extension a ExceptionHandlingProxyBase ősből leszármaztatott proxy osztályokat gyárt. A ExceptionHandlingProxyBase a hölgy saját fejlesztése, egy általános kliens, gyakorlatilag egy alapoktól újraírt ClientBase-nek felel meg.

A megoldás működik, precíz, és látszik a befektetett munka, de mostanában a csapból is a kódgenerátorok folynak, és a plusz bonyolultságtól meg a plusz macerától idegbajt kapok.

3. Going Proxy-less – The WCF Proxy Factory

Nem tűnik elterjedt, vagy kipróbált dolognak.

4. Christian Weyer elődása és példaprogramjai

TechDays Sweden 2009: Presentation slides and demos for ‘WCF Tips & Tricks” session

Az előadás fóliái jók, viszont a példaprogramok a szerző Thinktecture.ServiceModel.dll osztálykönyvtárát használják, melynek nincs forráskódja, és maga az assembly is csak demonstrációs célból van publikálva.

5. WCF Contrib

A WCF Contrib osztálykönyvtárat Amir Zuker készítette, gyakorlatilag ez is egy saját WCF kliens implementáció, de vannak szerver oldali kiegészítései is. Az előző mellett ez tűnik a legátfogóbb megoldásnak, példaprogamok vannak, viszont dokumentáció kevés. Támogatja az aszinkron hívásokat is.

6. Csak szinkron hívásokra egy egyszerűbb megoldás

Először is a ClientBase-t meg kell patkolni egy kicsit:

namespace WcfTools
{
    using System.ServiceModel;

    internal class ClientBaseEx<TInterface> : ClientBase<TInterface>
           where TInterface : class
    {
        public ClientBaseEx(string endpointConfigurationName)
            : base(endpointConfigurationName)
        {

        }

        public ClientBaseEx(string endpointConfigurationName, string remoteAddress)
            : base(endpointConfigurationName, remoteAddress)
        {

        }

        /// <summary>
        /// Protected field published with internal access
        /// </summary>
        internal TInterface Proxy
        {
            get { return Channel; }
        }

        protected override TInterface CreateChannel()
        {
            return base.CreateChannel();
        }
    }
}

Utána készül egy wrapper, mely a ClientBaseEx segítségével hívja meg a szervert:
namespace WcfTools
{
    using System;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.ServiceModel;
    using log4net;

    public class SafeClient<TInterface> where TInterface : class
    {
        protected static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        #region Static singleton instance
        private static SafeClient<TInterface> instance = new SafeClient<TInterface>();

        public static SafeClient<TInterface> Instance
        {
            get { return SafeClient<TInterface>.instance; }
        }
        #endregion
        #region Private variables
        private ClientBaseEx<TInterface> clientProxy = null;
        private object lockObj = new object();

        /// <summary>
        /// Csak akkor ad a Proxy mező értéket, ha Connect() meghívásra kerül. 
        /// </summary>
        private bool isConnected = false;
        #endregion
        #region Public properties

        public bool BasicAuthenticationEnabled { get; set; }
        public string BasicAuthenticationUserName { get; set; }
        public SecureString BasicAuthenticationPassword { get; set; }
        public string EndpointConfigurationName { get; set; }
        public string RemoteAddressDirectoryUrl { get; set; }
        public string RemoteAddressServiceFile { get; set; }

        /// <summary>
        /// Webszolgáltatás interfésze
        /// </summary>
        public TInterface Proxy
        {
            get
            {
                if (log.IsDebugEnabled) log.Debug("Start");

                TInterface result = null;

                lock (lockObj)
                {
                    if (isConnected)
                    {
                        CheckClientIsOpened();
                        result = clientProxy.Proxy;
                    }
                    else
                    {
                        if (log.IsDebugEnabled) log.Debug("Not connected!");
                    }
                }

                if (log.IsDebugEnabled) log.Debug("End");

                return result;
            }
        }

        #endregion
        #region Public functions
        /// <summary>
        /// Kapcsolódás
        /// </summary>
        public void Connect()
        {
            if (log.IsDebugEnabled) log.Debug("Start");

            lock (lockObj)
            {
                // a proxy ne a régi legyen, mert az autentikációs adatok lehet változtak
                DisconnectInternal();
                CheckClientIsOpened();
                isConnected = true;
            }

            if (log.IsDebugEnabled) log.Debug("End");
        }

        /// <summary>
        /// Kapcsolat bontása
        /// </summary>
        public void Disconnect()
        {
            if (log.IsDebugEnabled) log.Debug("Start");

            lock (lockObj)
            {
                DisconnectInternal();
            }

            if (log.IsDebugEnabled) log.Debug("End");
        }

        #endregion
        #region Private functions

        /// <summary>
        /// Kapcsolat ellenőrzése: szükség esetén bontás, és újranyitás
        /// </summary>
        private void CheckClientIsOpened()
        {
            if (clientProxy != null)
            {
                if (clientProxy.State == CommunicationState.Faulted)
                {
                    if (log.IsDebugEnabled) log.Debug("State == CommunicationState.Faulted, aborting...");

                    try
                    {
                        clientProxy.Abort();
                    }
                    finally
                    {
                        clientProxy = null;
                    }
                }
                else if (clientProxy.State == CommunicationState.Closed)
                {
                    if (log.IsDebugEnabled) log.Debug("State == CommunicationState.Closed.");
                    clientProxy = null;
                }
            }

            if (clientProxy == null)
            {
                if (log.IsDebugEnabled) log.Debug("Creating WCF proxy....");

                //  a könyvtár jelet a végére tesszük, ha nincs ott, mert különben az sikertelen lesz az összefűzés
                this.RemoteAddressDirectoryUrl = CheckTailSlash(this.RemoteAddressDirectoryUrl);

                Uri uriMain = new Uri(this.RemoteAddressDirectoryUrl);
                Uri uriChild = new Uri(uriMain, this.RemoteAddressServiceFile);

                if (log.IsDebugEnabled) log.DebugFormat("Url: '{0}'", uriChild);

                clientProxy = new ClientBaseEx<TInterface>(this.EndpointConfigurationName, uriChild.ToString());

                if (BasicAuthenticationEnabled)
                {
                    clientProxy.ClientCredentials.UserName.UserName = this.BasicAuthenticationUserName;
                    clientProxy.ClientCredentials.UserName.Password = SecureStringToString(this.BasicAuthenticationPassword);
                }

                clientProxy.Open();

                if (log.IsDebugEnabled) log.Debug("Proxy OK.");
            }
        }

        private static string CheckTailSlash(string data)
        {
            string result = data.Trim();

            if (result.Length > 0)
            {
                char last = result[result.Length - 1];
                if (last != '/')
                {
                    result += "/";
                }
            }

            return result;
        }

        private void DisconnectInternal()
        {
            try
            {
                if (clientProxy != null)
                {
                    try
                    {
                        if (clientProxy.State == CommunicationState.Faulted)
                        {
                            clientProxy.Abort();
                        }
                        else
                        {
                            clientProxy.Close();
                        }
                    }
                    finally
                    {
                        clientProxy = null;
                    }
                }
            }
            finally
            {   // mindegy mi történt, a kapcsolat bontva van
                isConnected = false;
            }
        }


        private static string SecureStringToString(SecureString value)
        {
            IntPtr bstr = Marshal.SecureStringToBSTR(value);

            try
            {
                return Marshal.PtrToStringBSTR(bstr);
            }
            finally
            {
                Marshal.FreeBSTR(bstr);
            }
        }

        #endregion
    }
}

Példa a használatra:

// starting fat client 
var client = new SafeClient<FooServiceReference.IFooService>();
client.EndpointConfigurationName = "myBinding";
client.RemoteAddressDirectoryUrl = "http://localhost:52008/";
client.RemoteAddressServiceFile = "FooService.svc";      
client.Connect();

...

// fat client using same SafeClient<T> instance
client.Proxy.GetFoo();

try
{
	client.Proxy.GetFooTimeout();
}
catch (Exception ex)
{
	log.Error(ex);
}

try
{
	client.Proxy.GetFooFaulted();
}
catch (Exception ex)
{
	log.Error(ex);
}

client.Proxy.GetFoo();

...

//  fat client stopping
client.Disconnect();

A vastag kliensnek annyi dolga van, hogy induláskor építse fel a kapcsolatot, és utána a SafeClient példányt őrizgesse, és rajta keresztül hívja a szervert.
A SafeClient rendelkezik egy Instance nevű singleton példánnyal is, tehát akár saját példány nélkül is lehet használni.

Mikor érdemes külön példányokat használni:
- ha ugyanazt az interfészt más-más autentikációs beállításokkal kell hívni, mert ilyenkor nem lenne szerencsés, ha keverednének a szerveren az azonosítási információk.
- többszálú működésnél, ha két szál egyidőben hívja a szervert, és az egyik Fault állapotba billenti a proxy-t, akkor az a másik, még ugyanazzal a proxy példánnyal dolgozó szálra is hatással lesz. Ha ez zavaró, akkor szálanként kell példányokat használni, és valami pool-t kell képezni.

Vélemény? »

Vélemény?

RSS hírcsatorna a bejegyzéshez kapcsolódó hozzászólásokról. TrackBack URI

MINDEN VÉLEMÉNY SZÁMÍT!

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Módosítás )

Twitter kép

You are commenting using your Twitter account. Log Out / Módosítás )

Facebook kép

You are commenting using your Facebook account. Log Out / Módosítás )

Kapcsolódás: %s

Sablon: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.