Náš produkt WinStrom používá architekturu klient/server, jehož součástí je i WinStrom Server. Tato služba poskytuje služby WinStromu v síti. Kvůli průchodu přes firewally a NAT jsme museli eliminovat množství používaných portů a proto jsme použili protokolový multiplex. Současně tato služba používá vlastní protokol nazvaný MiniRMI – jedná se o optimalizovanou verzi RMI. V tomto článku si popíšeme způsob, jakým byly tyto funkce implementovány.
Tyto funkce jsou součástí produktu již od počátku února.
Protokolový multiplex
Jednou ze silných vlastností WinStromu je možnost vzdáleného přístupu. To znamená, že se odkudkoliv můžete připojit do své kanceláře a pracovat s ekonomickým systémem. K tomu ovšem potřebujete nastavit síť tak, aby toto umožňovala. Obvykle se jedná o nastavení firewallu či forwardování portů.
Nutností pak je, aby těchto portů bylo co nejméně – některé hardwarové routery či modemy umí forwardovat jen jeden port. Pro uživatele je také problém vyznat se v používaných portech při konfiguraci programu (vzpomeňte si jen na nastavování pošty).
Proto jsme se rozhodli do produktu implementovat protokolový multiplex. Jedná se o wrapper, který při navázání spojení od klienta přečte prvních několik bytů, určí o jaký protokol se jedná a předá jej ke zpracování.
Důvodem, proč podporujeme protokol HTTP je i podpora REST API a navazující funkce jako je podpora formátu iCalendar.
Základem tohoto multiplexu je webový server Jetty. Ten nastartujeme normálním způsobem, ale předáme mu vlastní implementaci třídy SocketConnector
. Ta po připojení klienta vytvoří wrapper nad třídou Socket
a přidá do něj podporu BufferedInputStream
. To umožní přečíst data a pak je pomocí metody reset
opět vrátit zpět. Můžeme tak přečíst data, identifikovat jejich typ a předat obsluze normální Socket
, ze kterého ještě nebylo nic přečteno (a tak ztraceno). Není tak nutné modifikovat navazující systémy.
Pokud tedy přijdou data, zjistíme, zda jim rozumíme: zkontrolujeme HTTP, JDBC spojení (kvůli zpětné kompatibilitě s naším mzdovým systémem), hlavička komprimovaného proudu GZIP. Pokud datům nerozumíme, prohlásíme, že se jedná o SSL spustíme „handshake“ a celý proces opakujeme.
Když už datům rozumíme a zjistíme, že se jedná o HTTP požadavek, vytvoříme třídu Connection
a zavoláme metodu handle
. Když se nejedná o HTTP požadavek, obsloužíme jej sami a z třídy SocketConnector
se normálně vrátíme.
Díky přímé podpoře pro GZIP, můžeme komprimovat celou komunikaci HTTP včetně hlaviček. Při volání krátkých metod, které vrací jen pár bytů, tak hlavičky mohou tvořit značnou část komunikace.
Pro komprimaci jsme chtěli původně použít GZIPInputStream. Nicméně tato třída nenabízí podporu průběžného zápisu dat (při volání flush). Paradoxem je, že tato implementace používá nativní knihovnu ZLIB, která tuto funkci umožňuje. Jde o to, že při volání metody flush()
se odešlou všechna zapsaná data, i když nebyla dokončena celá stránka. Poměr komprese je pak sice nižší, nicméně pak funguje síťová komunikace. Proto jsme museli použít Javovskou implementaci JZLib, která tento způsob práce podporuje. Tímto faktem jsem byl poměrně překvapen, protože věc, která mi v C/C++ vždy fungovala spolehlivě, v Javě zlobila.
Co nám tedy běží na jednom portu?
- HTTP
- HTTPS
- GZIP+HTTP (není podporováno prohlížeči)
- GZIP+HTTPS (není podporováno prohlížeči)
- JDBC (implicitně je vypnuté)
- MiniRMI
- časem i LDAP a LDAPS
MiniRMI
Vlastní RMI implementaci jsme zvolili kvůli bezpečnosti, rychlosti a také možnosti běhu přes NAT.
Součástí každé reference v normálním RMI je také IP adresa a port serveru. Nicméně náš produkt musí být schopný fungovat přes NAT a různé proxy či forwardování portů. Server tak nikdy neví, pod jakou adresou jej klient zná. Proto jsme tuto informaci museli odstranit.
RMI také kvůli distribuovanému uvolňování paměti (garbage collector) informuje druhou stranu o vytvoření či zrušení reference na vzdálený objekt. Pokud tedy přečtete objekt z naming service, dojde ke 4 voláním přes RMI: přečtení ze jmenné služby, zvýšení počtu referencí, zavolání metody a následně snížení počtu referencí. Další nevýhodou je, že každému objektu je přidělen identifikátor, který je po restartu neplatný (proto se používá Bindování). Nicméně my jsme potřebovali umožnit restart serveru, aniž by s tím klient měl problémy.
Proto jsme se rozhodli vytvořit vlastní implementaci RMI a nazvali jsme ji MiniRMI. Ostatní existující protokoly jsme zavrhli buď kvůli rychlosti, kvůli složitosti použití a nebo kvůli jejich velikosti: snažíme se zajistit, aby instalační balíček byl co nejmenší.
Základem naší implementace RMI je standardní ObjectOutputStream
, který při serializaci objektu, který implementuje rozhraní Remote
jej v metodě replaceObject
nahradí za jiný objekt s informacemi o napojení RemoteStubWrapper
. Na straně klienta je použit opět standardní ObjectInputStream
, který objekt s informacemi o napojení nahradí za implementaci dynamickou proxy (implementuje InvocationHandler
), který opět pomocí ObjectOutputStream
do streamu zapíše objekt, který obsahuje všechny informace o volání a naopak přijme výsledná data.
Kvůli bezpečnosti také nelze přes MiniRMI posílat implementace tříd, jako to umí klasické RMI.
Musím říct, že celá implementace je díky serializacím v Javě velmi jednoduchá při implementaci i při použití a současně také velmi rychlá. Sám jsem byl překvapen, jak snadné je něco takového vytvořit. Jen musím poznamenat, že inspirací mi byla implementace RMI od Petra „Endifa“ Tomana.
Při testech se MiniRMI díky výše uvedeným důvodům ukázalo jako velmi rychlé a strčilo do kapsy jak normální RMI, tak i různé další služby (SOAP, RPC, XML-RPC). Na druhou stranu absence těchto funkcí z něj činí jednoúčelový nástroj, který se hodí jen pro určité typy instalací.
Možná by vás také mohlo zaujmout, jak kvůli podpoře HTTPS generujeme self sign certifikát v Javě.
Velmi zajímavé. Zejména dnes, kdy je moderní prosazovat frameworky na všechno, se na takhle nízkoúrovňové programování narazí jen zřídka.
velmi zaujimavy clanok, mohli by byt kludne ucebnou osnovu zo sieti 😉 !
predpokladam ze medzi kandidatmi bol aj HttpInvoker (org.springframework.remoting.httpinvoker) …
chcel by som sa spytat kvoli comu ste ho zavrhli ?
je to kvoli tomu ze spring nepouzivate alebo bola pre Vas tato implementacia niecim nedostatocna ?
HttpInvoker by asi byl řešením. Nicméně zatím Spring ve WinStromu nepoužíváme a jeho přidáním bychom zvětšili instalátory o několik megabytů.
Dalším hlavním důvodem je, že jsem jej neobjevil. Měl jsme pocit, že Spring Remoting je jen zapouzdření a nějak jsem si nevšiml, že má i vlastní jednoduchý transport.
Nebylo by misto MiniRMI pouzit jiz existujici JBoss Remoting? http://www.jboss.org/jbossremoting http://jboss.cz/story/jboss-remoting-je-rychlejsi-nez-java-rmi-spring