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:loop
Elindí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 NIL
A 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.