WCF és ami (elképzelhetően) mögötte van

Miután átvettük a Ganz Ifjúsági Műhely informatikai feladatait valamelyest féltünk a „szekrényből kieső csontvázaktól”, a szokásos minőségű átvett kódoktól. Átnézve a használt kódokat és a mellékes szálakon létrejött projekteket azonban akadtak igencsak kellemes, sőt meghökkentő minőségű és komplexitású kódok is. Az egyik ilyen annyira különleges, hogy az egyesülettel egyeztetve szeretnénk szakmai fórumunkon is bemutatni.

A WCF, azaz Windows Communication Foundation sokáig a Microsoft és a .NET go-to távoli függvényhívás megoldása volt (egyes hírek szerint közeleg az End of Life dátum), de még mindig széles körben elterjedt. Lényege, hogy a szerver-kliens kommunikációt teljesen transzparensen oldja meg, csatlakozás után kapunk egy objektumot és mintha csak egy metódust hívnánk meg, a háttérben egy komplett szerver-kommunikáció lezajlik, akár full-duplex módon.

Függetlenül attól, hogy szeretünk-e transzparens rendszereket használni, az könnyen belátható, hogy ilyen rendszerek tervezése és implementálása egyáltalán nem triviális feladat. Hogy egy ifjúsági egyesület mégis miért állt neki (és fejlesztette le működőképesen az alapjait!), rejtély… És mégis, Githubon elérhető a GEV Remoting könyvtár, amely nagyon kreatívan vesz inspirációt a WCF-től.

Nézzük a rendszert!

Távolról közelítve egy szerver-kliens felépítése a következőképpen néz ki a rendszerben:

TestService server = new TestService();
RemoteService service = new RemoteService(server, 5500);

TestInterface client = RemoteService.SubscribeToRemoteService<TestInterface>("127.0.0.1", 5500);

Console.WriteLine("Server from local call is calculating 1+2= {0}", server.Add(1, 2));
Console.WriteLine("Server from remote call calculating 1+2= {0}", client.Add(1, 2));

Van egy szolgáltatásunk, amelyből létrehozunk egy távolról elérhető szolgáltatást. Ehhez a távolról elérhető szolgáltatáshoz egy statikus függvényen keresztül tudunk kapcsolódni ami egy interface-t ad vissza. Innentől kezdve mind lokálisan, mind távolról hívhatóak a szolgáltatás metódusai. Ugyan a jelenlegi példában minden helyi gépen fut, de a server és client objektumok két külön gép két külön programjában is lehetnének – hiszen a távoli függvényhívásnak pont ez a lényege.

Ha megnézzük a felhasznált interfészeket akkor voltaképpen semmi érdekeset nem láttunk, teljesen triviális definíciók és implementálásokat találunk csak:

/// <summary>
/// Generic interface that a service interface can derive from.
/// </summary>
public interface IRemoteService
{
}

public interface TestInterface : IRemoteService
{
    int Add(int a, int b);
}

public class TestService : TestInterface
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

A mágia abban a bizonyos SubscribeToRemoteService statikus metódusban van. Ebben meghívásra kerül a RemoteInterfaceGenerator.GenerateService metódus, amiben nem mással mint Roslynnal, a .NET szkript-fordítójával legenerálásra kerül egy dinamikus C#-osztály és -objektum a szolgáltatás interfésze alapján:

MethodInfo[] methodInfos = typeof(T).GetMethods(BindingFlags.Public | BindingFlags.Instance);

StringBuilder sourceCode = new StringBuilder();

foreach (MethodInfo method in methodInfos)
{
    if(!method.IsSpecialName)
    {
        sourceCode.Append(GenerateSourceForMethod(method));
    }
}

string typeName = String.Format("GeneratedRemoteServiceClass_{0}", new Random().Next());

string envelope = "using System;\n" +
                  "using GEV.Remoting;\n" +
                  "using System.Collections.Generic;\n\n" +
                  "public class {0} : {1}\n" +
                  "{{\n" +
                  "    private ServiceSubscriber m_Proxy = new ServiceSubscriber(\"{3}\", {4});\n\n" +
                  "{2}\n" +
                  "}}\n" +
                  "\n" +
                  "return new {0}();";

string completeSource =  String.Format(envelope, typeName, typeof(T).FullName, sourceCode.ToString(), host, port);

Könyveket lehetne írni a megoldás erősségeiről, gyengeségeiről, a fából-vaskarika eseményhorizontról amit létrehoz, sajnos kénytelenek vagyunk megelégedni egy pár mondatos összefoglalóval.

A szkript első körben reflectiönnel (nem tudunk elszakadni a témától :)) lekéri az átadott, megvalósított szolgáltatás összes publikus és példányszintű metódusát. Ezekből aztán generál egy forráskód-részletet (ld. később), majd az így létrejött részlet-gyűjteményt becsomagolja egy dinamikus elnevezésű osztályba. Ehhez az osztályhoz hozzáad még egy ServiceSubscriber objektumot is, amely a valós szerver-kommunikációt hajtja végre. Végül a létrehozott osztályból létrehozott új példánnyal visszatér a szkript. Aki dolgozott már Roslynnal az tudja, hogy ott ilyen „inline” szkriptek megengedettek, és valóban a szkript futtatása után (akárcsak ahogy akarjuk) egy megvalósított, transzparens távoli-szolgáltatás kommunikátor példányunk a végeredmény.

A metódusok „becsomagolt” forráskódja hasonló módon nyakatekert:

private static string GenerateSourceForMethod(MethodInfo info)
{
    string result = "";
    if (info.ReturnType != typeof(void))
    {
        result = "public {0} {1}({2})\n" +
                 "{{\n" +
                 "   object o =  this.m_Proxy.CallMethod(\"{1}\", new Dictionary<string, object>()\n" +
                 "   {{\n" +
                 "{3}\n" +
                 "   }});\n" +
                 "   if(o is {0})\n" +
                 "   {{\n" +
                 "       return ({0})o;\n" +
                 "   }}\n" +
                 "   else\n" +
                 "   {{\n" +
                 "       throw new Exception(\"TODO!\");\n" +
                 "   }}\n" +
                 "}}\n" +
                 "\n";
    }
    else
    {
        result = "public {0} {1}({2})\n" +
                 "{{\n" +
                 "   this.m_Proxy.CallMethod(\"{1}\", new Dictionary<string, object>()\n" +
                 "   {{\n" +
                 "{3}\n" +
                 "   }});\n" +
                 "}}\n" +
                 "\n";
    }
    List<string> parameters = new List<string>();
    List<string> callingParameters = new List<string>();

    foreach (var param in info.GetParameters())
    {
        parameters.Add(String.Format("{0} {1}", param.ParameterType.FullName, param.Name));
        callingParameters.Add(String.Format("       {{ \"{0}\", {0} }},", param.Name));
    }

    string returnType = GetTypeString(info.ReturnParameter.ParameterType);
    if (returnType.ToLower() == "system.void")
    {
        returnType = "void";
    }

    result = String.Format(result, returnType, info.Name, String.Join(", ", parameters), String.Join("\n", callingParameters));

    return result;
}

Ha azt mondanánk hogy semmi sem triviális a kódban még enyhén fogalmaztunk volna :). Első körben a visszatérési értékű és visszatérési érték nélküli kódok szétválasztódnak, de mindkét esetben a fő mozgatóelem a dinamikus osztályban létrehozott proxy-objektum CallMethod metódusa. Ezt leszámítva a kód nagyjából ugyanazon az elven működik mint az osztály-generálás csak itt a metódus paramétereihez kell egyenként definíciókat generálni. A CallMethod felparaméterezése ugyanakkor meglepően egyszerűen lett megoldva, egy Dictionary<string, object> kerül átadásra, ahol a kulcs a paraméter neve, értéke a paraméter értéke.

Még nézzük meg azt a bizonyos metódushívó-metódust:

public object CallMethod(string name, Dictionary<string, object> parameters)
{
    //Setting up threading
    ManualResetEvent eventer = new ManualResetEvent(false);

    //Calling the remote method
    MethodCall call = new MethodCall()
    {
        MethodName = name,
        Parameters = parameters,
    };

    this.m_Communicator.OutMessages.Enqueue(new BoxedObject(call));
    this.returnValues.Add(call.MessageId, new ReturnHandler()
    {
        ResetEvent = eventer,
        ReturnValue = null,
    });

    //Waiting for a return
    eventer.WaitOne();

    //Remote method call returned, setting return value
    object result = null;
    lock(this.returnLock)
    {
        result = this.returnValues[call.MessageId].ReturnValue;
        this.returnValues.Remove(call.MessageId);
    }

    //Returning raw object - strong-typed returning is handled by the proxy itself
    return result;
}

Mire idáig elérünk már talán „hagyományosabb” megoldásokat találunk. A metódushívást becsomagolásra kerül egy MethodCall objektumba – metódusnév és paraméter Dictionary – majd ezt az objektumot küldjük el egy kommunikátor objektummal – vélhetőleg emögött már csak TCP-kommunikáció van. Ezután a egy ManualResetEvent setelésére várunk – amely akkor fog megtörténni ha a szerver végrehajtotta a metódust és „visszatért” – amint ez megtörténik a metódushívás „azonosítója” szerinti ReturnHandler értéke feltöltődik a visszatérési értékkel. Ezt már vissza tudjuk adni mint egy object – és ahogy a megjegyzésből ill. a fenti kódrészletekből kiolvasható, a castolást már a dinamikusan generált kód fogja elvégezni.

Bármennyire is kaotikus, aki ismeri a WCF-et tudja, hogy kívülről az is hasonlóan működik és szerintünk ha nem is a valós belső lelkivilágot sikerült újra létrehozni, egy rendkívül frappáns (és kaotikus, és lenyűgöző) megoldást sikerült az egyesületnek lefejlesztenie.