Dr. Vermes Mátyás1
2001. július
Szeretnénk felállítani egy olyan Kontó kiszolgálót, ami XMLRPC protokollon keresztül szolgáltat adatokat a Kontóból, illetve segítségével műveleteket végezhetünk a Kontóban, pl. lekérhetjük egy ügyfél forgalmát, kivonatát, felvihetünk egy ügyfelet, nyithatunk számlát, rögzíthetünk egy könyvelési tételt. Egy ilyen kiszolgáló rögzíti az üzleti logikát, és elrejti az adattárolás módját. A kliensekbe csak a megjelenítés kerül, ami a bankok igényei szerint többféle lehet anélkül, hogy a Kontó üzleti logikáját meg kellene változtatni, vagy többször implementálni. A megoldás arra is lehetőséget ad, hogy külső programok, mondjuk egy home banking rendszer dokumentált és ellenőrzött módon kapcsolódjon a Kontóhoz.
Az alábbi rétegek egymásra épülését tételezem fel:
1 | Jáva Terminál |
2 | SSL |
3 | CCC frontend programok |
4 | XMLPRC (HTTP) |
5 | Wrapper (elosztó) |
6 | Speciális CCC szerverek |
7 | Adatbázis |
Ez a táblázat interaktív kliens esetét mutatja. Nem interaktív kliensek-pl. a szerveren batch feldolgozást végző programok-a 3. szinten kapcsolódnak a rendszerbe. A 6. szinten működő szerverek egymás szolgáltatásait (a wrapperen keresztül) korlátozás nélkül igénybe vehetik.
Az XMLRPC protokoll részleteivel nem foglalkozunk, a ccc_socket és a ccc_xmlrpc könyvtár szolgáltatásai minden feladatra elegendőnek látszanak (mindig a legfrissebb változatra van szükség).
A wrapper nem xmlrpc szerver, hanem socket szinten dolgozik, és az üzeneteket mechanikusan közvetíti a felek között. A program egyszerűsíti a szervezést, ui. a résztvevőknek elég csak a wrapper hálózati címét ismerni.
A fentiekből adódóan a program az üzenetközvetítést elég hatékonyan csinálja. Bizonyos xmlrpc requestekre a wrapper közvetlenül válaszol:
A wrapper a system hívásokat sosem adja tovább, és a fenti két metódustól különböző hívások esetén "service not available" kivétel keletkezik. Ugyanezért a wrapperen keresztül nem érhetők el a szerverek által esetlegesen implementált szabványos "system" metódusok.
Az xmlrpc-s Kontó felhasználókat külön adatbázisban fogjuk nyilvántartani, így véletlenül sem keveredik össze a kétféle felhasználási mód.A felhasználók az rpcuser állományban vannak felsorolva, ennek szerkezete:
rekordszám : 4 fejléc hossz : 258 rekord hossz : 193 mezők száma : 7 UID C 16 0 2 TID C 16 0 18 GID C 64 0 34 NAME C 64 0 98 PASSWORD C 16 0 162 STARTDATE D 8 0 178 ENDDATE D 8 0 186
A mezők jelentése: UID felhasználói név, TID felhasználó típus, GID csoport azonosító, NAME teljes felhasználói név, PASSWORD jelszó, STARTDATE érvényesség kezdete, ENDDATE érvényesség vége.
A korábbi rendszerhez képest a tid és gid megkülönböztetése a leglényegesebb újdonság. A tid értékei ilyenek lehetnek:
A egyes felhasználói típusok (tid) számára engedélyezett metódusokat (plusz az engedély módját) tartalmazza rpcauth.
rekordszám : 8 fejléc hossz : 130 rekord hossz : 50 mezők száma : 3 TID C 16 0 2 METHOD C 32 0 18 PERMISSION C 1 0 50
Itt egy rekord jelentése a következő: A TID típusú felhasználónak a METHOD funkcióhoz PERMISSION engedélye van. A PERMISSION mező értékei lehetnek:
A csoport azonosító (gid) alapján döntik el a szerverek, hogy az adatbázis egy eleméhez van-e hozzáférése a kliensnek. A csoport azonosító tartalma szerverenként más és más lehet. Az LTP szerver példáján mutatjuk be a gid használatát.
A Kontó felhasználók beléptetésével, az engedélyek nyilvántartasával foglalkozik az rpcsession szerver.
function main(port) local server set printer to log-rpcsession additive set printer on set console off alertblock({|t,a|xmlrpc_alert(t,a)}) server:=xmlrpcserverNew(port) server:keepalive:=.t. //server:debug:=.t. //server:recover:=.f. server:addmethod("session.getversion",{|sid|getversion(sid)}) server:addmethod("session.login",{|u,p|login(u,p)}) server:addmethod("session.logout",{|sid|logout(sid)}) server:addmethod("session.validate",{|sid,prolong|validate(sid,prolong)}) server:addmethod("session.validatex",{|sid,prolong|validatex(sid,prolong)}) server:addmethod("session.who",{|sid|who(sid)}) server:addmethod("session.permission",{|sid,module|permission(sid,module)}) server:addmethod("session.groupid",{|sid|groupid(sid)}) server:addmethod("session.userid",{|sid|userid(sid)}) server:addmethod("session.username",{|sid|username(sid)}) server:addmethod("session.usertype",{|sid|usertype(sid)}) server:loopfreq:=5000 server:loopblock:={||fflush()} server:closeblock:={|s,r|xmlrpc_verifyconnection(s,r)} xmlrpc_register(server,"session",VERSION) server:loop return NIL
A session szerver jelenleg elég egyszerű, az alábbi néhány funkcióval rendelkezik:
A session szerver egyszerre legfeljebb XMLRPC_MAXSESSION darabszámú session-t engedélyez (default 128).
A session szerver egy felhasználótól maximum XMLRPC_MAXSAMEUID egyidejű bejelentkezést fogad el (default 4).
A szervereket az alábbihoz hasonló scripttel indíthatjuk:
#!/bin/bash export XMLRPC_WRAPPER=foton,45000 rpcwrapper.exe 45000 & rpcsession.exe 45001 & rpcteszt.exe 45002 &
Az XMLRPC_WRAPPER változóban minden szerverrel tudatjuk, hogy hol van a wrapper. Először a wrappert indítjuk el természetesen azon a gépen és porton, amit az előbbi változóban megadtunk.
Ezután elindítjuk a többi szervert. Ha a szervereknek nem adnánk meg explicit portszámot, akkor azok automatikusan választanának maguknak egy szabad portot, ekkor azonban ugyanaz a szerver egyidejűleg több porton is futhat, ami esetleg nem kívánatos. A port explicit megadása esetén, a szerver kilép, ha a megadott port nem szabad.
A szervereknek nem kell ugyanazon a gépen lenniük. A szerverek bármikor utólag is elindíthatók, ez alól csak a wrapper kivétel. Ha ez kilép, akkor a többi szerver is automatikusan kilép (így vannak megírva), és az egész rendszert újra kell indítani.
A klienseket az alábbi módon indítjuk:
#!/bin/bash export CCC_TERMINAL=dummy client1.exe & client1.exe & client1.exe & client1.exe & client1.exe & client1.exe & client1.exe & client1.exe & client1.exe & client1.exe &
A CCC klienseknek általában két paramétert fogunk megadni, a wrapper címét (host nevét) és portszámát. Ha ezek hiányoznak (mint a jelen esetben), akkor a {localhost,45000} default címen próbálkozik. A script a szerver nyúzása céljából egyszerre elindít 10 nem interaktív klienst. Az egész rendszer 50-60 folyamatosan kérdező klienssel még símán működik. Nyilván bír többet is, különösen, ha a klienseket nem kell lokálisan futtatni.
A ccc_xmlrpc könyvtárban van definiálva az alábbi xmlrpcServer osztály. Az ccc_xmlrpc könyvtárat a jelen alkalmazás igényei szerint állandóan frissítem, ezért fontos, hogy mindig a legutolsó változattal linkeljünk. Ugyanez áll a ccc_socket könyvtárra is.
function xmlrpcserverClass() static clid if( clid==NIL ) clid:=classRegister("xmlrpcserver",{objectClass()}) classMethod(clid,"initialize",{|this,p|xmlrpcserverIni(this,p)}) classMethod(clid,"response",{|this,s|xmlrpcserverResponse(this,s)}) classMethod(clid,"addmethod",{|this,m,b,h,s|xmlrpcserverAddmethod(this,m,b,h,s)}) classMethod(clid,"loop",{|this|xmlrpcserverLoop(this)}) classMethod(clid,"methodidx",{|this,m|xmlrpcserverMethodIdx(this,m)}) classMethod(clid,"methodblk",{|this,m|xmlrpcserverMethodBlock(this,m)}) classMethod(clid,"methodhlp",{|this,m|xmlrpcserverMethodHelp(this,m)}) classMethod(clid,"methodsig",{|this,m|xmlrpcserverMethodSignature(this,m)}) classMethod(clid,"methodlst",{|this|xmlrpcserverListMethods(this)}) classAttrib(clid,"port") //ezen a porton hallgatózik classAttrib(clid,"methods") //metódusok: {{m,b,h,s},...} classAttrib(clid,"keepalive") //tartja-e a kapcsolatot classAttrib(clid,"debug") //printeli-e a debug infót classAttrib(clid,"recover") //elkapja-e a hibákat classAttrib(clid,"server") //szerver név (HTTP header) classAttrib(clid,"evalarray") //kibontva adja-e át a <params> tagot classAttrib(clid,"loopfreq") //a select timeout-ja (ezred sec-ben) classAttrib(clid,"loopblock") //a select lejártakor végrehajtódik classAttrib(clid,"closeblock") //minden socket lezárásakor végrehajtódik classAttrib(clid,"socketlist") //az összes élő socket classAttrib(clid,"scklisten") //ezen a socketen hallgatózik end return clid
//XMLRPC teszt szerver static wrapper ***************************************************************************** function main(port) local server set printer to log-rpcteszt additive set printer on alertblock({|t,a|xmlrpc_alert(t,a)})Megnyitjuk a logfilét, kikapcsoljuk az alertet.
server:=xmlrpcserverNew(port) server:keepalive:=.t. //server:debug:=.t. //server:recover:=.f.Létrehozzuk a szerver objektumot, beállítjuk néhány jellemzőjét: a kliensekkel tartjuk a kapcsolatot, debug infót nem nyomtatunk, a hibákat elkapjuk, így hiba esetén automatikusan xmlrpc exception-t kap a kliens.
server:addmethod("teszt.hello",{|sid|hello(sid)}) server:addmethod("teszt.gettime",{|sid|gettime(sid)}) server:addmethod("teszt.echo",{|sid,p1,p2,p3,p4,p5,p6|echo(sid,p1,p2,p3,p4,p5,p6)})Feltöltjük a szervert a metódusokkal. Minden metódushoz tartozik egy kódblokk, ami végre fog hajtódni, ha a kliens meghívja. A szerver automatikusan csinál magának "system.listMethods", "system.methodHelp", "system.methodSignature" metódusokat (xmlrpc ajánlás), bár ezeket a wrapperen keresztül jelenleg nem lehet elérni.
xmlrpc_register(server,"teszt") wrapper:=xmlrpc_client() server:closeblock:={|s,r|verify_connection(s,r)}A szerver regisztrálja magát a wrappernél. Létrehoz egy xmlrpcclient objektumot, amit akkor használ, ha kliensként igénybe akarja venni a többi rpc szerver szolgáltatását. Beállítja a szerver closeblock-ját, ez automatikusan végre fog hajtódni, amikor egy socket lezáródik. Ez alkalmat ad a szervernek arra, hogy észrevegye, ha a wrapper kilépett, ilyenkor a program befejeződik.
server:loopElindítjuk a szerver főciklusát, amiben a requestek kiszolgálása történik. A program kilépéséig a loop-ban marad a vezérlés.
return NIL ***************************************************************************** static function verify_connection(server,r) local e if( server:socketlist[1]==r ) e:=errorNew() e:operation:="verify_connection" e:description:="wrapper died" eval(errorblock(),e) end return NILA wrapper kilépésének észlelése azon alapszik, hogy a socketlist első eleme mindig a wrapperhez kapcsolódik (xmlrpc_register teszi oda). Alább a metódusok implementációja következik.
***************************************************************************** static function hello(sid) local uid validate_session_id(sid) sid:=_chr2arr(base64_decode(sid)) uid:=sid[1][2] return "Hello '"+upper(uid)+"'!" ***************************************************************************** static function gettime(sid) validate_session_id(sid) return time() ***************************************************************************** static function echo(sid,p1,p2,p3,p4,p5,p6) validate_session_id(sid) return {p1,p2,p3,p4,p5,p6} ***************************************************************************** static function validate_session_id(sid) local e if( !wrapper:call("session.validate",sid) ) e:=errorNew() e:description:="invalid sid" eval(errorblock(),e) end return NIL *****************************************************************************
Figyeljük meg, hogy bármi gond van (pl. érvénytelen a sid), egyszerűen el kell szállítani a programot, ezt a szerver loop metódusa el fogja kapni (ha recover==.t.), és automatikusan xmlrpc kivétellé transzformálja, amit elküld a kliensnek. Az egyszerű programhibák miatti elszállásokkal is ez történik, ami megnehezíti a tesztelést, ezért tesztelés céljára a normál hibakezelés visszaállítható (recover==.f.).
A ccc_xmlrpc könyvtárban van definiálva az alábbi xmlrpcClient osztály. Az ccc_xmlrpc könyvtárat a jelen alkalmazás igényei szerint állandóan frissítem, ezért fontos, hogy mindig a legutolsó változattal linkeljünk. Ugyanez áll a ccc_socket könyvtárra is.
function xmlrpcclientClass() static clid if( clid==NIL ) clid:=classRegister("xmlrpcclient",{objectClass()}) classMethod(clid,"initialize",{|this,host,port|xmlrpcclientIni(this,host,port)}) classMethod(clid,"call",{|this,method,params|xmlrpcclientCall(this,method,params)}) classMethod(clid,"close",{|this|xmlrpcclientClose(this)}) classMethod(clid,"connect",{|this|xmlrpcclientConnect(this)}) classMethod(clid,"write",{|this,r|xmlrpcclientWrite(this,r)}) classMethod(clid,"read",{|this|xmlrpcclientRead(this)}) classAttrib(clid,"useragent") //kliens id (HTTP header) classAttrib(clid,"hostname") //szerver neve/ip címe classAttrib(clid,"host") //szerver ip címe classAttrib(clid,"port") //szerver portszám classAttrib(clid,"socket") //socket (file descriptor) classAttrib(clid,"keepalive") //tartja-e a kapcsolatot classAttrib(clid,"debug") //printeli-e a debug infót classAttrib(clid,"URI") //HTTP header (általában /RPC2) classAttrib(clid,"timeout") //ennyit vár a válaszra (ezred sec) end return clid
Az alábbi program célja a szerverek nyúzása, nem kell benne különösebb értelmet keresni.
function main(ipaddr,port) local client, sid, n, cnt:=0 set printer to ("log-client"+alltrim(str(getpid()))) set printer on if(ipaddr==NIL) ipaddr:="localhost" end if(port==NIL) port:=45000 end client:=xmlrpcclientNew(ipaddr,port) client:keepalive:=.t. //client:debug:=.t.Megvan az új kliens objektum, néhány tulajdonság beállítva.
while( .t. ) client:call("system.printstate") ?? sid:=client:call("session.login",{"vermes","hopp"}); fflush() for n:=1 to 1024 cnt++; client:call("session.validate",sid) cnt++; client:call("session.who",sid) cnt++; client:call("teszt.hello",sid) cnt++; client:call("teszt.gettime",sid) cnt++; client:call("teszt.echo",{ sid,1,"A",.t.,{}, date() }) ?? cnt; fflush() next client:call("session.logout",sid) sleep(1000) end return NIL
Az xmlrpc hívás egyszerűen név szerinti függvényhívásnak tekinthető, ahol
funcname(par1,par2,...)helyett ezt írjuk:
client:call("funcname",{par1,par2,...})Egyetlen paraméter esetén az array-be csomagolás nem kötelező, azt az interfész program automatikusan megteszi. Ha a szerver xmlrpc kivételt ad, akkor a call metódus nem tér vissza, hanem elszáll a program (kiértékelődik az errorblock), ami a normál eszközökkel kezelendő.
A CCC-CORBA objektumos megvalósítása elegánsabb, mint az xmlrpc. A szerver oldalon implementálni kell egy olyan osztályt, ami az IDL-ben megadott minden metódust tartalmaz. Ez a megszokott programozási módszerrel történik. A kliens oldalon használt metódushívás szintaktika egyszerűbb és szebb, mint a név szerinti függvényhívás, ráadásul a kliens oldali (proxy) objektumot előállító kód teljes egészében automatikusan generálódik az IDL-ből.
Az xmlrpc egyszerű komponensekből (HTTP üzenetek plusz szövegfeldolgozást jelentő XML) épül fel, ezért könnyen implementálható mindenféle nyelveken. A körítés (pl. a kapcsolatfelvétel) sincs úgy misztifikálva, mint a CORBA-nál, ezért a külső kliensprogram írók könnyebben elboldogulnak vele. Az is fontos szempont, hogy a saját rendszerünket nem terheljük olyan nehézsúlyú idegen komponenssel, mint egy CORBA könyvtár.
<value></value>
Ha közvetlenül az xmlrpc üzenet törzsét vizsgálnánk, akkor megállapítható volna, hogy a CCC program NIL-t, vagy üres stringet (<value><string></string></value>) küldött-e. Mivel azonban a szabvány szerint explicit típusmegjelölés hiányában az adat típusa string, azt mondhatjuk, hogy a NIL érték az xmlrpc üres stringjére képződik.
<value><string>ez egy string </string></value>
A szabvány szerint a stringek tetszőleges (akár bináris) adatokat is tartalmazhatnak, kivéve a < és & karaktereket, amiket < és & formában kell küldeni. Ezért a CCC is csak az előbbi transzformációt végzi a stringeken, de nem trimel, nem végez ékezetes karakter konverziót, stb. Érthetetlen és bosszantó, hogy a PHP xmlrpc interfész okosabb akar lenni a szabványnál, és további karaktereket is kódol, nevezetesen a > karaktert >-re és a " karaktert "-ra, amivel óhatatlanul zavarokat fog előidézni.
<value><double>100</double></value>
<value><boolean>1</boolean></value>
jelenti a logikai true értéket,
<value><boolean>0</boolean></value>
pedig a logikai false értéket. Megjegyzem, hogy a szabvány szerint az 1 és 0 érték nem helyettesíthető mással, pl. 1 helyett nem felel meg egy nemnulla szám, vagy a true string. A PHP xmlrpc interfész itt is bosszantóan eltér a szabványtól.
<value><dateTime.iso8601>20011005T00:00:00</dateTime.iso8601></value>
A CCC kódblokk típust az xmlrpc interfész a következő módon küldi: Először a block kiértékelődik, a kiértékelésnek egy szintaktikailag helyes <value> tagot tartalmazó stringet kell eredményeznie. Az interfész ezt a <value>-t fogja küldeni.
A base64 típusról: Három byte-ban összesen 24 bit van. Ha ezt a 24 bitet szétosztjuk négy byte-ra, akkor mindegyikre csak 6 bit jut. A base64 kódolás tehát minden 3 (tetszőleges adatot tartalmazó) byte-ból 4 db 6 bites byte-ot készít, ahol a kimenet csak betű és számkaraktereket tartalmaz, és ezért biztonságosan továbbítható a hálózaton.
A CCC xmlrpc interfész a fogadott base64 típust automatikusan stringre konvertálja (dekódolja). A base64 típus küldéséhez a blockok küldésekor történő automatikus kiértékelést használjuk.
function xmlrpcbase64(x) return {||"<base64>"+base64_encode(x)+"</base64>"}
A fenti segédfüggvénnyel tudunk CCC-ből <base64> típust küldeni. A base64_encode(x) és base64_decode(x) segédfüggvények végzik egy string base64 kódolását és visszaalakítását.
<value><array><data></data></array></value>
function xmlrpcstructClass() static clid if( clid==NIL ) clid:=classRegister("xmlrpcstruct",{objectClass()}) classMethod(clid,"initialize",{|this,av|xmlrpcstructIni(this,av)}) classAttrib(clid,"attrvals") //felüldefiniálás: method -> attr end return clid function xmlrpcstructNew(av) local clid:=xmlrpcstructClass() return objectNew(clid):initialize(av) function xmlrpcstructIni(this,av) objectIni(this) this:attrvals:=av return this
Amikor a CCC átvesz egy struktúrát, akkor azt array-re konvertálja {{name1,value1},{name2,value2}, ... } formában, ahol a külső array minden eleme az eredeti struktúra egy member-ének felel meg.
1ComFirm BT.
2 A timeout-ot nem célszerű egy-két percnél hosszabbra állítani. A hosszú timeout eredményeképpen a szerverekben felhalmozódnak a magukra hagyott sessionok arra várva, hogy egyszer talán majd újra jelentkezik a kliens, és folytatja a munkát. Csakhogy az illető kliensprogram esetleg hibás, minek folytán gyakran elszáll, és már sokadszorra indítják újra. Ezért a kliensprogram írók állandó sirámai ellenére rövid timeout-ot kell beállítani, a kliensprogramokat pedig úgy kell megírni, hogy a timeout lejártával képesek legyenek automatikusan újra bejelentkezni.
3 A továbbiakban a sid paraméter egységesen mindenhol a session id-t jelöli.