Következő bejegyzésemben egy számomra érdekesnek tartott témáról szeretnék némi felvilágosítást adni. Ez nem Airsoft lesz, hanem programozás, amit lehet sokan nem szeretnek, ettől függetlenül remélem érdekes és tanulságos lesz azoknak, akik ezzel foglalkoznak.
A mai téma az XML-RPC lesz, ami 2 rövidítés, méghozzá az Extensible Markup Language és a Remote Procedure Call. Hogy mik is ezek, hát az jó kérdés, megpróbálok külön külön választ adni és egyben, hogy mi is azaz XML-RPC szerver és hogyan is kellene csinálni egyet. Mindezt C# ban, mert az könnyű és nyugos, ráadásul kész van egy nap alatt.
Kezdjük az XML el
Az XML-t alapvetően arra találták ki, hogy egy olyan fájlt hozzanak létre, amivel bármikor bármit le tudnak írni. Képes bármilyen adathalmaz reprezentálására, úgy, hogy a programoknak nem szükséges ismerniük a pontos felépítést, mindössze az XML szabályoknak kell megfelelnie.
Rengeteg felhasználási helye van az XML nek és valószínűleg már mindenki találkozott vele valamilyen programnál, amit használt, ha máshol nem akkor a windowsban, ami maga is rengeteg XML fájlt használ adatok és beállítások tárolására.
Egy egyszerű XML fájl:
<?xml version="1.0" encoding="iso-8859-1"?>
<XmlTest>
<XmlValue name="nameofvalue" value="valueofvalue">
<![CDATA[Special text]]>
</XmlValue>
</XmlTest>
Az XML nagy előnye, hogy a saját szabályai alapján valóban bármilyen adat struktúrát le lehet benne írni, ezáltal egy teljesen általános adat tároló eszközt kapunk, amihez ma már rengeteg XML olvasó beszerezhető, illetve nem tart kifejezetten sokáig egy sajátot írni, főleg, ha a szabályok nagy részétől eltekintünk, ami legtöbbször könnyedén elengedhető egy saját project esetén.
Az RPC
Hát az RPC ről sokat nem is lehet mondani, hisz ez egy … talán fogalom. A lényege, hogy egy számítógépen valami módon kiadunk egy utasítást, ami végrehajtódik egy távoli számítógépen. Természetesen mielőtt bárki összeesküvés elméleteket gyártana, itt nem arról van szó, hogy fogom a Windows Notepadet és valami speciálsi billentyű kombinációval egyből lehet valaki más gépét irányítani.
Az RPC utasításokat a szerveren regisztrálják, a hozzá szükséges kliensen pedig csak ezeket lehet felhívni. Persze hiba mindig lehet a gépezetben, de feltételezzük, hogy nincs 🙂
XML-RPC?
Na és akkor hogy is jön ki a kettőből az XML-RPC? Egyszerűen. Itt a kettő a kommunikáció mikéntjét és a felhasználást jelöli, azaz XML ben utasítjuk a szervert a távoli eljárások felhívására. Az XML ről ugye már megbeszéltük, hogy bármilyen adat struktúrát le tud írni, így egy tetszőleges utasítást és annak paramétereit is le tudjuk írni, illetve az eljárás visszatérési értékét.
Ehhez persze az XML-RPC nek kell egy szabvány, ami megmondja, hogy az XML nek milyen formátumúnak kell lennie, hogy azt minden kliens és minden szerver valóban megértse. Ez a szabvány természetesen csak útmutatás, nem kötelező etz használni, csak akkor már nem beszélhetünk szabványos XML-RPC szerverről. Hogy ez miért fontos, mindenki döntse el magában.
Megjegyzem, hogy a WordPress is képes XML-RPC kommunikációra, ami lehetővé teszi saját WordPress szerkesztők írását (mint a Windows Live Writer), ami könnyedén tud kommunikálni a WordPress oldallal és feltölteni a legújabb bejegyzéseket, média fájlokat, stb.
Hogy mire is jó az XML-RPC valójában? A válasz egyszerű. EZ egy általános kommunikációs forma, 2 számítógép között, ahol nem számít a számítógépek felépítése és a rajtuk futó operációs rendszer típusa. A kliens egyszerűen a megadott formátumban közli a másik számítógéppel, hogy milyen függvényeket szeretne felhívni, az pedig képes válaszolni, hogy ezen függvényeknek mi volt a kimenete.
Természetesen van hátránya a rendszernek, nem is kevés. Először is az XML ugye nagyon nem tömör formátum, így a hálózati kommunikáció a sokszorosára dagad. Az XML további hátránya a feldolgozási sebessége. Jóval lassabb egy XML fájlt feldolgozni, mint egy saját bináris típust.
Egy másik hátrány maga a kommunikáció. Szabványos, HTTP kommunikáció, ami megkönnyíti az esetleges visszaéléseket, próbálkozásokat, illetve nehezíti a szerver fejlesztők munkáját ezek kivédése.
Specifikáció: http://xmlrpc.scripting.com/
Egy kérés példa:
POST /RPC2 HTTP/1.0
User-Agent: Frontier/5.1.2 (WinNT)
Host: betty.userland.com
Content-Type: text/xml
Content-length: 181
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
A szerver
Na de csapjunk most már bele a lecsóba kicsit. Írjunk XML-RPC szervert!
Ehhez én a C# ot választottam. Tudom, hogy nem a legoptimálisabb, nem a leggyorsabb (habár nem is olyan borzalmasan lassú, mint a Java), viszont rengeteg minden meg van már benne írva, amivel nem kell foglalkozni, csak használni, ezáltal egy több szálas, TCP / SSL kommunikációval működő szerver elkészítése nem több 1 könnyed napi munkánál. Azon esetekben ahol az XML-RPC jó megoldás és nem több tízezer felhasználót kell kiszolgálni másodpercenként, bőven elég lehet egy ilyen megoldás.
Amellett a C# most nagyon divatos 🙂
No, de kezdjük is el. Az első dolog, amire nem árt figyelni, hogy lehetőleg olyan kódot írjunk, amit a Mono is képes lefordítani, így C# ban is elmondhatjuk, hogy multi platform kódot írtunk, ráadásul jobb esetben még akár Unity3D ben is felhasználhatjuk, hisz az is Mono val fordít.
Hogy ki hol kezdi, az egyedi, én az XML felolvasóval kezdtem.
XML reader
A beépített System.XML túl rengeteg letölthető minimális XML olvasó található, vagy akár írhatunk mi is egyet saját kezűleg. Ez utóbbit inkább csak akkor javaslom, ha sok időnk van és szeretnénk ezzel szórakozni, máskülönben egyszerűbb leszedni egy működő minimál kódot. Részemről egyszerűen letöltöttem ezt: SmallXMLParser. Apró módosításokkal teljesen személyre szabható és optimalizálható.
Felmerülhet a kérdés, hogy mi értelme ennek. A válasz viszont annyira nem egyszerű. Ha kizárólag Windows ra fejlesztünk, akkor nem sok értelme van, hisz ott a System.XML, amit nyugodt szívvel használhatunk, hisz ott a Windowsban mindig. Ám mi van akkor, ha több rendszerre szeretnénk eljutni, vagy ha Unity3D vel akarjuk kombinálni. Ezen esetekben nem biztos, hogy van System.XML.dll ünk, vagy egyszerűen csak nem akarjuk letölteni az egész DLL-t. Ugyancsak a SmallXMLParser mellett szól, hogy jóval kevesebb dologgal foglalkozik mint a minden szabályt figyelembe vevő XML olvasók, ezáltal jóval gyorsabb lehet a kód!
Persze ha tudjuk, hogy egészen pontosan milyen platformra fordítunk, felmerülhet a natív kód alkalmazása, azaz C/C++ ban írhatunk saját XML olvasót, de nagyon figyeljünk, hogy a Natív – Managed kód váltás is nagyon erőforrás igényes tud lenni!
Szerver
Nem tudom nem-e túlzás szervernek nevezni mindazt, ami C# ban ahhoz kell, hogy egy porton figyeljünk és adatokat fogadjunk. Rettentően egyszerű dolgunk van, mert mint írtam a C# ban már minden készen van! Míg C/C++ ban komoly szenvedés tud lenni egy socket és az SSL layer összehozása, addig C# ban egy egyszerű beállítás (majdnem :))
public bool Start( IPAddress aIP, int aPort, IServiceProvider aServiceProvider, String aCertFile )
{
ServiceProvider = aServiceProvider;
Stopping = false;
mConnectionCallback = new AsyncCallback( OnConnectionHandler );
mAcceptCallback = new WaitCallback( OnAcceptHandler );
mListener = new TcpListener( aIP, aPort );
try
{
mListener.Start();
mListener.BeginAcceptSocket( mConnectionCallback, null );
}
catch /*( System.Exception ex )*/
{
return false;
}
if ( ( aCertFile != null )
&& ( File.Exists( aCertFile ) ) )
{
if( !SSLInitialized )
{
ServicePointManager.ServerCertificateValidationCallback += ( s, cs, ca, p ) => true;
SSLInitialized = true;
}
mCertificate = new X509Certificate2( aCertFile, "mycertpassword" );
SSLEnabled = true;
}
else
{
SSLEnabled = false;
}
return true;
}
Íme. Egy nagyon egyszerű kódocska, ami alig 1-2 érdekességet azért tartalmaz 🙂
Az egyik ilyen fontos dolog, hogy a szerverben kiüssük az SSL certificate validálását abban az esetben, ha csak titkosítani akarunk, de nincs hiteles SSL Certification ünk.
Az alábbi sorral ezt el is végezhetjük:
ServicePointManager.ServerCertificateValidationCallback += ( s, cs, ca, p ) => true;
Hogy ez mi is? A ServicePointManager.ServerCertificateValidationCallback jéhez hozzá adunk egy új függvényt, ami mindössze annyit csinál, hogy true val visszatér.
http://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.servercertificatevalidationcallback.aspx
http://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback.aspx
Egyeseknek idegen lehet a ( s, cs, ca, p ) => true így leírva. Ez a C# Lambda Expression. Szépen átláthatatlanná lehet tenni vele a kódot, de azért néha jól tud jönni, ha nem akarunk annyit gépelni 🙂
Nos, a szerverünk már fut is, habár ha csatlakozunk is hozzá sok minden nem történik.
Az, hogy mi van az OnconnectionHandler ben nem írnám le, aki a témával foglalkozik hamar rá fog érezni, de azért 1-2 jó tanácsot leírnék:
Mindenképpen hozzunk létre egy osztályt, ami az új kapcsolat adatait tartalmazza. Socket, ReadBuffer, esetleges hozzá tartozó biztonsági szintek, beállítások, események, így jóval könnyebb lesz kezelnünk egy egy kapcsolatot, vagy felhasználót.
A másik a ThreadPool.QueueUserWorkItem nevű függvény, ami miatt az mAcceptCallback létre lett hozva. Elfogadott, hogy az accept kicsit lassabb, ha már létrejött a fizikai kapcsolat, úgyhogy érdemes az ezzel járó inicializálást akkor elvégezni, amikor “van szabadidőnk”.
Kommunikáció
Van XML ünk és van szerverünk, de még nem dőlhetünk hátra, hisz a hálózati kommunikáció még C# ban sem fenékig tejfel, legalábbis én még nem találtam meg benne azt a beállítást, ami mindent megoldana 🙂
Alapvető probléma, hogy a hálózaton a csomagok még TCP-n sem úgy jönnek, ahogy azokat elküldjük. Nagy könnyebbség, hogy biztosan sorba vannak rendezve és biztosan mindegyik megérkezik, de abban nem lehetünk biztosak, hogy egy olvasással a teljes elküldött adat mennyiséget felolvastuk.
Hogy kicsit talán érthetőbb legyek, elküldjük az alábbi kérést:
POST /RPC2 HTTP/1.0
User-Agent: Frontier/5.1.2 (WinNT)
Host: betty.userland.com
Content-Type: text/xml
Content-length: 181
<?xml version="1.0"?>
<methodCall>
<methodName>examples.getStateName</methodName>
<params>
<param>
<value><i4>41</i4></value>
</param>
</params>
</methodCall>
Ekkor a szerver ugye észreveszi, hogy jött neki adat csomag az adott hálózati kapcsolaton.
Mi megpróbáljuk felolvasni a teljes adatot, ami ennyi lesz csak:
POST /RPC2 HTTP/1.0
User-Agent: Frontier/5.1.2 (WinNT)
Host: betty.userland.com
Content-Type: text/xml
Content-length: 181
<?xml version="1.0"?>
<methodCall>
<methodName
Nem az egész XML, amire nekünk szükségünk lenne, ezért várnunk kell és ha megjött a maradék, akkor feldolgozni a kérést.
Ha nem jött le az egész fejléc (aminek a vége a \r\n\r\n) akkor ezt a vezérlő karakter láncot meg kell várjuk. Ha ez megérkezett akkor a Content-length ben megadott mennyiségű bájtot kell még felolvasnunk, ennyi adatnak kell megérkeznie. Ha ezek megvannak, akkor elkezdődhet a feldolgozás.
Procedure regisztráció
Ez nem egy túl bonyolult téma, egyszerűen csak a véleményen osztom itt meg mindenkivel. Én személy szerint szeretem a C# ban létező Attribute osztályokat, ezért egyszerűen írtam egy ilyen osztályt, amit egy class függvényeihez lehet adni.
[ AttributeUsage( AttributeTargets.Method, AllowMultiple = true ) ]
public class RpcCommandAttribute : Attribute
{
public string MethodName { get; private set;}
public RpcCommandAttribute( string aMethodName )
{
MethodName = aMethodName;
}
}
Regisztrációkor végig megyek az átadott osztály függvényein és amelyiken van RpcCommandAttribute azt feldolgozom és regisztrálom.
public void Register( IMethod aMethod )
{
Type MethodClassType = aMethod.GetType();
System.Reflection.MethodInfo[] Methods = MethodClassType.GetMethods();
foreach( System.Reflection.MethodInfo Method in Methods )
{
RpcCommandAttribute[] Attributes = Method.GetCustomAttributes( typeof(RpcCommandAttribute ), true ) as RpcCommandAttribute[];
foreach( RpcCommandAttribute CommandAttribute in Attributes )
{
MethodCallback Callback = (MethodCallback)Delegate.CreateDelegate( typeof( MethodCallback ), aMethod, Method );
mRegisteredMethods.Add( CommandAttribute.MethodName, Callback );
}
}
}
Itt jegyzem meg, hogy C# ban az Attribute osztályok végéről használatukkor elhagyható az Attribute rész, azaz az RpcCommandAttribute ot használhatjuk RpcCommand ként is!
A procedure call
Ez az a rész, ahol multi thread nek kell lennünk. Ugye egy egy függvény futása sokáig is eltarthat (már akár 1 másodpercig is), ezért nem tehetjük meg, hogy egyszerre csak 1 kérést szolgálunk ki. Könnyű a dolgunk, ha nincs olyan rész, ahol a kérések össze akadhatnak, mert akkor nem kell minden féle lockokkal és rw mutexekkel foglalkoznunk.
Azaz ez így nem teljesen igaz, ugyanis. A szerver a feldolgozott kéréseket egy Queue ba teszi, a szálak pedig innen szedik ki és hajtják Őket végre. Ennek a Queue nak thread safe nek kell lennie, máskülönben csúf fagyások elé nézünk 🙂
Egy kényelmes megoldás: ConcurrentStack. Sajnos jön a fekete leves, kizárólag 4.5 ös .Net ben elérhető, ami azt jelenti, hogy a Mono nem ismeri ezt a tároló osztályt. Azért pánikra semmi ok, szerencsére egy egyszerűbb thread safe stack et összedobni C# ban nem több fél óránál, így azoknak, akik Linux / Unity alá is szeretnék lefordítani, ennyit még rá kell áldozni a projektre 🙂
Tehát, a szerver, a feldolgozott RPC hívás információkat egy stack be teszi, a futó worker threadek pedig ezt a stacket figyelik. Ha van benne valami, akkor az első aki hozzáfér kiszedi belőle a kérést és elkezdi végrehajtani, ezáltal lehetőséget adva a többi szálnak is, hogy egy másik kérésen dolgozzanak.
A szálak száma nem végtelen. Nem túl szerencsés a CPU szám + 1 nél többet futtatni, de természetesen ez nagyban függ az eljárások fajtájától. Ha mindegyik rengeteg IO művelet, IO megszakítással, akkor lehet több szál is, hisz IO waitekben sokat állnak, ezáltal adnak lehetőséget a többi szálnak a CPU használatára. Ha minden függvény erősen CPU függő, akkor nem tudunk többet felvenni, mert egymást akadályozzák!
Végszó
Íme, nagyon nagy vonalakban ennyi egy XML-RPC szerver (erős túlzással :)), de a lényeget remélem sikerült összefoglalni, illetve pár dologra felhívni a figyelmet, esetleg ötletet adni. Természetesen aki ennél is többet szeretne tudni eme remek dologról, nyugodtan hozzászólhat és leírom, amit csak tudok 🙂
A lényeg, hogy fejlesztés közben mindig mérjük fel, hogy mennyi időnk és erőforrásunk van és ez által tervezzük meg a programunkat a felhasznált programnyelvtől, a felhasznált 3rd Party eszközökön át egészen a megvalósítás apró optimalizációjáig, hogy a lehető legjobb eszközt állítsuk elő, és mindig kódoljunk szépen, átláthatóan, máskülönben a saját kódunkat sem fogjuk felismerni 🙂