© 1999-2003, Flemming Koch Jensen
Alle rettigheder forbeholdtReactor Pattern
Opgaver
Problem Når vi vil lave, og anvende, en flertrådet server har vi en række problemer.
Ikke alle platforme understøtter tråde
I Java er vi forvent med tråde som en del af sproget - det er en sjældenhed. Man må almindeligvis regne med at muligheden for at programmere med tråde er platformsafhængig, og at det sjældent er enkelt eller bekvemt at arbejde med. I Java er det modsat relativ enkelt at lave flertrådede programmer, mens det i f.eks. C++ er mere kompliceret. Det betyder samtidig at programmer der anvender tråde er mere udfordrende at porte til forskellige platforme; hvilket også virker demotiverende, når man står med valget: tråde eller ej?
Kritiske regioner
Kritske regioner er i det hele taget uhensigtsmæssige. De opstår fordi en server arbejder med en række resourcer, som de enkelte klienter ønsker at arbejde med. Med flere samtidige tråde, der repræsenterer klienterne, vil der være kritiske regioner i forbindelse med meget af det workerne skal gøre. I forbindelse med udviklingen af en flertrådet server skal man derfor bruger resourcer på at opbygge et system af hensigtmæssige kritiske regioner, og teste dem.
Sammenfatning
Der er en række ting, der er uhensigtsmæssige ved en flertrådet server. Specielt de mange tråde og administrationen af disse i relation til de kritiske regioner. Endelig er der spørgsmålet, om tråde overhovedet er tilgængelige på den platform som vi skal implementere serveren på.
Løsning
Enkelt-trådet Løsningen er at lav en enkelt-trådet server, der demultiplexer requests fra klienterne til en sekvens af requests. Dette fjerner behovet for flere tråde, samtidig med at kun én request bliver håndteret af gangen. Dette fjerner derfor også problemet med kritske regioner i relation til serveren. Demultiplexing vil sige at et parallelt input omformes til et sekventielt: Figur 1:
Demulti-plexing
Afbildning Vi har her valgt at ordne de indkomne events i vilkårlig rækkefølge. Demultiplexing er med andre ord en afbildning af et parallelt input over i et sekventielt output, som man kan arbejde videre med. Har vi derfor en række klienter der sender requests til en server, kan vi demultiplexe dem ved at håndtere dem en ad gangen i stedet for at håndtere dem parallelt, med hver sin tråd. Til at håndtere de forskellige events (indkommende requests) kan man anvende event-handlere, der kan sættes til at håndtere en bestemt slags events fra en bestemt klient. På den måde kan man variere, hvad der sker når en klient sender en request, ved at sætte skiftende event-handlere til at håndtere disse events. Til at dispatche (dk.: afsende) events til event-handlere, kan man anvende et centralt objekt, hvor man registrerer hvilke event-handlere der tager sig af hvilke events fra hvilke klienter. Et sådant objekt kan evt. anvende et andet objekt til at demultiplexe. Reactor Det objekt der demultiplexer kalder naturligt nok Demultiplexeren, mens det objekt der holder styr på hvilke event-handlere der tager sig af hvad, kaldes en Reactor - den reagerer (eng.: react) på de events der kommer. Lad os se klassediagrammet for Reactor Pattern.
Klassediagram
Figur 2:
Klasse-diagrammet
Her holder Reactor'en styr på registreringerne vha. et HandleSet, der indeholder de enkelte Handle's. Et handle er et objekt der repræsenterer den enkelte klient i serverens objektsystem. Eftersom et handle repræsenterer en klient, er den også event-kilden.
Interaktion
Det følgende sekvensdiagram viser et simpelt forløb: Figur 3:
Sekvens-
diagram
Først tilmelder Client en EventHandler hos Reactor. Dernæst kalder den handleEvents på Reactor'en (der ikke returnerer før serveren terminerer). handleEvents henter events fra Demultiplex'eren ved at kalde select, der blokkerer indtil der kommer requests fra en eller flere af klienterne. select returnerer de berørte Handle's så Reactor'en kan identificere events og dispatche dem til den rette EventHandler. I eksemplet ovenfor er dette gjort to på hinanden følgende gange.
Implementation
Inden vi skal se et større eksempel, der anvender Reactor Pattern, vil vi først berøre en række generelle spørgsmål, der vedrører implementationen af Reactor Pattern:
Demultiplexer'en
Et af de spørgsmål man kan stille i realtion til Demultiplex'eren er: om den overhovedet er nødvendig som selvstændig klasse. Den har kun én metode og den er på den måde et eksempel på funktionel dekomposition. Man vil ofte finde det bekvemt at lade Demultiplex'erens funktionalitet indgå direkte i Reactor-klassen, da det samtidig giver den nemmere adgang til Handle's.
Registreringer
Enkelt-event-tilmeldinger Skal det være muligt at tilmelde en event-handler flere forskellige events samtidig. Det er en mulighed, men det vil besværliggøre til/fra-melding i relation til de enkelte events, hvis man ikke (i det mindste) internt i Reactor'en deler sådanne registreringer op i enkelt-event-tilmeldinger. Hvis man f.eks. har tilmeldt en event-handler til at håndtere flere forskellige events fra et bestemt handle; hvordan skal man så håndtere en framelding på en af disse events. Man kan naturligvis justere den registrerede tilmelding, så den ikke længere indbefatter den berørte event, men det giver en del ekstra pille-arbejde (også hvis man vil udvide antallet af events som en event-handler skal håndtere fra handle), som man helt slipper for, hvis man kun arbejder med enkelt-event-tilmeldinger.
Dynamisk opgradering
Dynamikken i til/fra-melding af event-handlere kan også bruges til at opgradere en server dynamisk. Så længe opgraderingen kun går på hvilke event-handlere der er til rådighed, kan nye EventHandler-klasser loades dynamisk, og en server behøver på den måde ikke blive lukket ned for at blive opgraderet.
Varighed af event-håndtering
Tager det relativ lang tid at håndtere en event, er det i en flertrådet server primært noget der går ud over den tilhørende klient. Når vi demultiplexer, vil event-håndtering for de enkelte klienter, skulle vente på hinanden. Det er derfor uhensigtsmæssigt, hvis det tager lang tid at håndtere visse events. Hastigheden hvormed der kommunikeres på et netværk tåler ikke sammenligning med den interne hastighed i en computer, så små "tids-klumper" i event-håndteringen gør normalt ikke noget, de skal blot ikke blive for store. Hvis man ikke kan undgå, at håndteringen af visse events tager lang tid, bør man evt. overveje at bruge en flertrådet server i stedet.
Eksempel: Log-server
zip-file Det følgende eksempel består af relativ mange klasser/filer. Kildeteksten er derfor samlet i en zip-file, så man slipper for at copy/paste. I venstre margin er der en link til denne zip-file. I dette eksempel, vil vi implementere en Log-server vha. Reactor Pattern. Formålet med Log-serveren, er at klienter skal kunne logge linier af tekst (i det følgende kaldet linier), ved at sende dem til serveren. Serveren gemmer alle de linier den modtager. For hver linie den bliver bedt om at logge, returnerer den et nummer, der kan bruges til at genkalde den pågældende linie. Det betyder at såvel klienten selv, som andre klienter kan læse linier ved at sende de tilhørende numre til serveren. Log-serveren har følgende kommandoer: LOGIN <userid> <password>
LOG <linie>
GET <nummer>
SWITCH <userid> <password>
LOGOUT
LOGIN bruges til at logge ind på Log-serveren. Der findes to slags brugere: Dem der har ret til at kalde bruge LOG- og GET-kommandoerne, og dem der kun må bruge GET-kommandoen. SWITCH-kommandoen bruges til at skifte bruger uden at afbryde forbindelsen. Det svarer til at lave et nyt login. Man kan f.eks. bruge det til at skifte til en bruger, der kun må anvende GET, for at sikre at klienten (programmet) ikke ved en fejl begynder at logge nye linier. Endelig afslutter man med LOGOUT.
Klienten
Lad os først se hvordan en klient kan anvende en sådan server - mao. vores testanvendelse: Kildetekst 1:
Klienten
Client.java package client; import java.net.*; import java.io.*; import common.*; public class Client extends Thread { private BufferedWriter writer; private BufferedReader reader; private static final String[] lines = { "login bart safe", "log den første linie", "log dette er en anden linie", "switch homer secure", "log kan jeg det?", "get 1", "logout" }; public void run() { try { Socket connection = new Socket( "127.0.0.1", 6010 ); System.out.println( "[Client] Connection opened" ); writer = new BufferedWriter( new OutputStreamWriter( connection.getOutputStream() ) ); reader = new BufferedReader( new InputStreamReader( connection.getInputStream() ) ); Sleeper.nap(); // være sikker på server er online for ( int i=0; i<lines.length; i++ ) { sendLine( lines[i] ); if ( isError() ) System.out.println( "[Client] der skete en fejl" ); Sleeper.sleep( 1 ); } writer.close(); System.out.println( "[Client] Connection closed" ); } catch ( IOException e ) { System.out.println( "[Client] I/O error" ); } } private boolean isError() throws IOException { return readLine().startsWith( "ERROR:" ); } private String readLine() throws IOException { String line = reader.readLine(); System.out.println( "[Client] received: " + line ); if ( line == null ) throw new IOException( "End of stream" ); return line; } private void sendLine( String line ) throws IOException { System.out.println( "[Client] sending line: " + line ); writer.write( line ); writer.newLine(); writer.flush(); Sleeper.nap(); } }Da vores klient skal fungere som testanvendelse, lader vi det være givet på forhånd; hvilke kommandoer den vil sende til serveren. Vi har for nemheds skyld placeret disse i et array, og lader klienten sende dem fra en for-løkke, der gennemløber array'et. Det eneste interessante ved klienten, er service-metoden: isError. Den afgør på grundlag af det svar der kommer tilbage fra serveren, på hver kommando, om kommandoen blev udført med succes eller med fejl. Alle linier serveren sender tilbage til klienten starter enten med: "OK: " eller "ERROR: ", alt efter om det gik godt eller skidt!
Serveren
Serveren implementeres med en lang række klasser, så et klasse-diagram er på sin plads: Figur 4:
Klasse-diagram for Log-server
Packages Vi placerer disse klasser i en række packages. Client-klassen placeres i en package for sig selv: client, og Sleeper-klassen placeres ligeledes i en package for sig selv: common. Alle de resterende klasser indgår i serveren og placeres derfor i en package: server. I server-package placeres EventHandler og alle dens subklasser i en subpackage: server.handler. I diagrammet er der vist en relation mellem EventHandler-interfacet og de to Singletons: Users og Log - denne relation gør sig kun gældende for visse af EventHandler's realiseringer, og er vist for EventHandler, da vi for overskuelighedens skyld har udeladt dens mange subklasser (de optræder i et senere diagram). Som man ser mangler Demultiplexer-klassen. Funktionaliteten i denne klasse er placeret i Reactor-klassen, da dette giver bekvem adgang til oplysningerne om hvilke handles, der har tilmeldte event-handlere. Disse oplysninger opbevares i en container, der indeholder instanser af Registration-klassen. Til at identificere de forskellige events, er der defineret en række konstanter i en abstrakt klasse: Event, samt et par nyttige metoder til konverteringer: Kildetekst 2:
Events
Event.java package server; public abstract class Event { public static final int LOGIN_EVENT = 0; public static final int LOGOUT_EVENT = 1; public static final int LOG_EVENT = 2; public static final int GET_EVENT = 3; public static final int SWITCH_EVENT = 4; public static final int ANY_EVENT = 5; public static final int FIRST_EVENT = LOGIN_EVENT; public static final int LAST_EVENT = SWITCH_EVENT; public static final int UNKNOWN_EVENT = -1; public static String eventToName( int event ) { switch ( event ) { case 0: return "LOGIN_EVENT"; case 1: return "LOGOUT_EVENT"; case 2: return "LOG_EVENT"; case 3: return "GET_EVENT"; case 4: return "SWITCH_EVENT"; case 5: return "ANY_EVENT"; default: return "<unknown event: " + event + ">"; } } public static int nameToEvent( String name ) { String upperCase = name.toUpperCase(); if ( !upperCase.endsWith( "_EVENT" ) ) upperCase += "_EVENT"; if ( upperCase.startsWith( "LOGIN_EVENT" ) ) return LOGIN_EVENT; else if ( upperCase.startsWith( "LOGOUT_EVENT" ) ) return LOGOUT_EVENT; else if ( upperCase.startsWith( "LOG_EVENT" ) ) return LOG_EVENT; else if ( upperCase.startsWith( "GET_EVENT" ) ) return GET_EVENT; else if ( upperCase.startsWith( "SWITCH_EVENT" ) ) return SWITCH_EVENT; else if ( upperCase.startsWith( "ANY_EVENT" ) ) return ANY_EVENT; else return UNKNOWN_EVENT; } }Den første anvendelse af disse events finder man i selve Server-klassen, der også udemærker sig ved at have ServerSocket: Kildetekst 3:
Serveren
Server.java package server; import java.net.*; import java.io.*; import server.handler.*; public class Server extends Thread { private boolean stop; public Server() { stop = false; } public void run() { try { ServerSocket server = new ServerSocket( 6010 ); server.setSoTimeout( 100 ); // 1/10 sekund System.out.println( "[Server] Online" ); while ( !stop ) { try { Socket socket = server.accept(); Reactor.i().addHandler( new Handle( socket ), new LoginHandler(), Event.LOGIN_EVENT ); } catch ( SocketTimeoutException e ) { // ignore } } System.out.println( "[Server] Offline" ); } catch ( IOException e ) { System.out.println( "[Server] I/O error" ); } } public void shutdown() { stop = true; } }I forbindelse med modtagelse af en indkommende forbindelse foretager serveren sig flere ting (markeret med blåt). Den giver socket videre til en instans af Handle-klassen, der tager sig af al kommunikation med klienten.
Den laver en LoginHandler til at håndtere login fra klienten, som er den eneste event vi vil acceptere lige efter forbindelsen er oprettet.
Den tilmelder denne event-handler til Reactoren, så login-events fra det pågældende handle vil blive håndteret af event-handleren.
Lad os først se nærmere på Handle-klassen: Kildetekst 4:
Handle
Handle.java package server; import java.net.*; import java.io.*; public class Handle { private Socket socket; private BufferedReader reader; private BufferedWriter writer; public Handle( Socket socket ) throws IOException { this.socket = socket; reader = new BufferedReader( new InputStreamReader( socket.getInputStream() ) ); writer = new BufferedWriter( new OutputStreamWriter( socket.getOutputStream() ) ); } public String readLine() { try { return reader.readLine(); } catch ( IOException e ) { // simplificering af eksemplet e.printStackTrace(); return null; } } public void sendError( String message ) { writeLine( "ERROR: " + message ); } public void sendOk( String message ) { writeLine( "OK: " + message ); } private void writeLine( String line ) { try { writer.write( line ); writer.newLine(); writer.flush(); System.out.println( "[Handle] send: " + line ); } catch ( IOException e ) { // simplificering af eksemplet e.printStackTrace(); } } public boolean ready() { try { return reader.ready(); } catch ( IOException e ) { // simplificering af eksemplet e.printStackTrace(); return false; } } }Sammenligner man med Client-klassen vil man bemærke flere ligheder. Først og fremmest kunne klasserne have glæde af en fælles super-klasse, til at elliminere kode-redundans i forbindelse med writeLine-metoden og reader/writer. Vi har dog valgt at undlade dette, for at gøre eksemplet lidt lettere at læse i mindre stykker. Dernæst bemærker man de to service-metoder: sendError og sendOk. Som nævte i forbindelse med Client-klassen, vil alle linier sendt fra serveren til klienten starter med enten: "OK: " eller "ERROR: ". På server-siden realiseres dette ved at disse metoder er den eneste mulighed serveren har for at sende beskeder til klienten, idet selve writeLine-metoden er private! ready-metoden bruges i forbindelse med demultiplex'ingen. Hvis reader'en er ready er der en ny event på vej. Vi kommer naturligvis ikke udenom Reactor-klassen. Lad os se på den før vi ser nærmere på EventHandler-klasserne: Kildetekst 5:
Reactor
Reactor.java package server; import java.util.LinkedList; import java.util.HashSet; import java.util.Iterator; import java.io.*; import server.handler.*; import common.*; public class Reactor { private LinkedList registrations; private Reactor() { registrations = new LinkedList(); } public synchronized void addHandler( Handle handle, EventHandler handler, int event ) { Registration reg = new Registration( handle, handler, event ); System.out.println( "[Reactor] adding: " + reg ); registrations.add( reg ); } public synchronized EventHandler removeHandler( Handle handle, int event ) { Iterator it = registrations.iterator(); while ( it.hasNext() ) { Registration reg = (Registration) it.next(); if ( reg.isHandle( handle ) && reg.handlesEvent( event ) ) { System.out.println( "[Reactor] removing: " + reg ); it.remove(); if ( event != Event.ANY_EVENT ) return reg.getHandler(); } } return null; } /* * Event loop */ public void handleEvents() { while ( true ) { HashSet readyHandles = new HashSet(); LinkedList potentialRegistrations = new LinkedList(); synchronized ( this ) { Iterator it = registrations.iterator(); while ( it.hasNext() ) { Registration reg = (Registration) it.next(); if ( reg.getHandle().ready() ) { readyHandles.add( reg.getHandle() ); potentialRegistrations.add( reg ); } } } if ( readyHandles.size() == 0 ) // don't hammer the CPU, // unless there's incomming data Sleeper.nap(); else { System.out.println( "[Reactor] incomming detected" ); Iterator handleIterator = readyHandles.iterator(); while ( handleIterator.hasNext() ) { Handle handle = (Handle) handleIterator.next(); String firstLine = handle.readLine().trim(); System.out.println( "[Server] received: " + firstLine ); String[] tokens = firstLine.split( " " ); int event = Event.nameToEvent( tokens[0] ); if ( event == Event.UNKNOWN_EVENT ) { handle.sendError( "unknown command: " + firstLine ); continue; } Iterator registrationIterator = potentialRegistrations.iterator(); while ( registrationIterator.hasNext() ) { Registration reg = (Registration) registrationIterator.next(); if ( reg.isHandle( handle ) && reg.handlesEvent( event ) ) { reg.getHandler().handleEvent( handle, event, tokens ); break; } } } System.out.println( "[Reactor] --- finished handling event ---------" ); } } } private class Registration { private Handle handle; private EventHandler handler; private int event; public Registration( Handle handle, EventHandler handler, int event ) { this.handle = handle; this.handler = handler; this.event = event; } public boolean isHandle( Handle handle ) { return this.handle == handle; } public boolean handlesEvent( int event ) { return this.event == event || event == Event.ANY_EVENT; } public boolean usesHandler( EventHandler handler ) { return this.handler == handler; } public EventHandler getHandler() { return handler; } public Handle getHandle() { return handle; } public String toString() { StringBuffer sb = new StringBuffer(); String handlerName = handler.getClass().getName(); if ( handlerName.indexOf( '.' ) >= 0 ) handlerName = handlerName.substring( handlerName.lastIndexOf( '.' ) + 1 ); sb.append( "[Registration: handler:" + handlerName ); sb.append( ", event: " ); switch ( event ) { case Event.GET_EVENT: sb.append( "GET_EVENT" ); break; case Event.LOG_EVENT: sb.append( "LOG_EVENT" ); break; case Event.LOGIN_EVENT: sb.append( "LOGIN_EVENT" ); break; case Event.LOGOUT_EVENT: sb.append( "LOGOUT_EVENT" ); break; case Event.SWITCH_EVENT: sb.append( "SWITCH_EVENT" ); break; } sb.append( "]" ); return sb.toString(); } } /* * Singleton */ private static Reactor instance; static { instance = null; } public static Reactor i() { if ( instance == null ) instance = new Reactor(); return instance; } }Klassen består i alt væsentlighed af fire dele (markeret med skiftende blå og rød farve): 1. Metoder til at til/fra-melde event-handlere.
2. handleEvents-metoden.
3. Den indre klasse: Registration, der håndterer den enkelte tilmelding af en event-handler.
1. og 4. del vil vi gå let hen over. Til/fra-melding af event-handlere ser vi på, hvor det anvendes, og implementationen af Reactor som en Singleton er ukompliceret. Derimod vil vi se nærmere på 2. og 3. del. Vi vil først se på Registration-klassen:
Registration-klassen
3-tuple En registrering er en tilmelding af en event-handler til at håndtere en bestemt event i forbindelse med et bestemt handle. Mao. en instans af Registration-klassen er et 3-tuple: ( handle, event-handler, event ). Reactor-klassen bruger en LinkedList: registrations, som container til disse registreringer. Klassen er rimelig simple, pånær toString-metoden, hvor der gøres en del ud af at præsentere datakernen læseligt.
handleEvents-metoden
Denne metode kaldes af klienten, og den er den centrale proces i Reactor'en. Lad os se en skitseret udgave af denne metode: Skitseret udgave af handle-
Events-
metoden
public void handleEvents() { while ( true ) { HashSet readyHandles = new HashSet(); LinkedList potentialRegistrations = new LinkedList(); synchronized ( this ) { // synkroniseret beskyttelse af 'registrations' // gennemløber 'registrations' while ( it.hasNext() ) { // hvis indkommende event på tilhørende handle, så føj // til 'readyHandles' og 'potentialRegistrations' } } // gennemløber 'readyHandles' while ( handleIterator.hasNext() ) { // if-sætninger der identificerer event if ( ... ) ... // gennemløber 'potentialRegistrations' while ( registrationIterator.hasNext() ) { if ( registrerings handler håndterer event ) { ...handleEvent( handle, event, tokens ); break; } } } } }Automatisk fjerne doubletter Vi anvender her et HashSet. Vi vælger at bruge et HashSet, da vi på den måde frit kan add'e til containeren: readyHandles, uden at skulle bekymre os om doubletter, de bliver automatisk fjernet. while-løkken, der gennemløber registreringerne, vil derfor frit kunne tilføje det samme handle flere gange, uden at readyHandles vil komme til at indeholde mere end en reference til hvert handle. Finde event-handler Den næste while-løkke gennemløber de handles der er klar med en event (dvs. har tekst fra klienten, der venter på at vi læser det). For hver af disse handles leder vi i potentialRegistrations efter en event-handler til den pågældende event i forbindelse med det konkrete handle. potentialRegistrations er den delmængde af registreringerne, der har handles der er klar med en event; hvilket til en vis grad begrænser søgning. Tokens De tokens der sendes med som tredie parameter i kaldet af handleEvent, er ordene fra den første linie - den der identiciferede kommandoen, og allerede er læst fra BufferedReader. Disse ord sendes med som et array af String's.
Event-handlerne
Efter at have set på Reactor-klassen, vil vi nu gennemgå event-handlerne. I forbindelse med denne gennemgang vil vi også berøre de to resterende klasser: Log og Users. Lad os først se den del af klasse-hierarkiet, der beskriver event-handlerne: Figur 5:
Event-handlerne
Lad os dernæst se selve EventHandler-interfacet: Kildetekst 6:
Event-Handler-interfacet
EventHandler.java package server.handler; import server.*; public interface EventHandler { public void handleEvent( Handle handle, int event, String[] tokens ); }Man bemærker at tokens er et array af String's; hvilket gør det nemt for eventhandlerne at iterere dem.
AccessHandler'ne
De to event-handlere, der styrer klientens rettigheder: LoginHandler og SwitchHandler, har en fælles super-klasse: AccessHandler: Kildetekst 7:
Den fælles super-klasse
AccessHandler.java package server.handler; import server.*; public abstract class AccessHandler implements EventHandler { public boolean authentication( Handle handle, int event, String[] tokens ) { try { String userid = tokens[1]; String password = tokens[2]; EventHandler handler=null; if ( !Users.loginOkay( userid, password ) ) { handle.sendError( "command failed" ); return false; } else { if ( Users.writeRight( userid ) ) { // read/write handle.sendOk( "You have read/write access" ); handler = new ReadWriteHandler(); } else { // read only handle.sendOk( "You have read only access" ); handler = new ReadOnlyHandler(); } Reactor.i().addHandler( handle, handler, Event.LOG_EVENT ); Reactor.i().addHandler( handle, handler, Event.GET_EVENT ); } // user logged in return true; } catch ( IndexOutOfBoundsException e ) { handle.sendError( "Userid and/or password missing" ); } return false; } }Klassen er abstrakt, og formålet med den er kun at de to subklasser kan deles om metoden: authentication. I authentication-metoden instantieres der enten en ReadWriteHandler eller en ReadOnlyHandler, alt efter hvor meget klienten har ret til. Dernæst tilmeldes denne instans i Reactor'en, så den kan håndtere LOG- og GET-events. Vi vil se nærmere på disse to EventHandler-klasser senere. Test-stub I forbindelse med kontrol af brugernes rettigheder, anvendes en klasse: Users. Users-klassen er en banal klasse, med to statiske metoder. I forbindelse med vores eksempel er der ingen grund til at lægge det store arbejde i at implementere f.eks. database-adgang vedrørende brugernes rettigheder, og Users-klassen skal mest af alt betragtes som en test-stub: Kildetekst 8:
Users-klassen
Users.java package server; public class Users { public static boolean loginOkay( String userid, String password ) { if ( userid.equals( "homer" ) && password.equals( "secure" ) ) return true; else if ( userid.equals( "bart" ) && password.equals( "safe" ) ) return true; else return false; } public static boolean writeRight( String userid ) { return userid.equals( "bart" ); } }Brugeren: "bart" har lov til både at tilføje til loggen og læse tidligere logs, mens brugeren "homer" kun kan aflæse tidligere logs (navnene: Homer og Bart, er inspireret af tegneserien: "The Simpsons"). Som det ses af AccessHandler-klassen kaldes først loginOkay-metoden, og såfremt denne bekræfter korrekt brugernavn og password, kaldes dernæst writeRight-metoden, for at afklare om brugeren har ret til at skrive i loggen. Lad os se den første af AccessHandler's subklasser: LoginHandler-klassen: Kildetekst 9:
Login-
klassen
LoginHandler.java package server.handler; import server.*; public class LoginHandler extends AccessHandler { public void handleEvent( Handle handle, int event, String[] tokens ) { if ( event == Event.LOGIN_EVENT ) if ( authentication( handle, event, tokens ) ) { Reactor.i() .addHandler( handle, new SwitchHandler(), Event.SWITCH_EVENT ); Reactor.i() .addHandler( handle, new LogoutHandler(), Event.LOGOUT_EVENT ); Reactor.i().removeHandler( handle, Event.LOGIN_EVENT ); } } }Som alle andre konkrete event-handler-klasser kontrollerer handleEvent-metoden først om den event der har udløst kaldet, er den som event-handleren kan håndtere. Dette er særlig relevant når en event-handler kan håndtere flere forskellige events. Dernæst foretages et kald til super-klassens authentication-metode, der returnerer boolsk om login lykkedes. Såfremt dette er tilfældet tilmeldes event-handlere til at håndtere henholdsvis SWITCH- og LOGOUT-events. Bemærk at super-klassens authentication-metode allerede har tilmeldt den relevante event-handler til at håndtere LOG-events (mere om disse event-handlere senere). Dernæst har vi den anden AccessEventHandler-klasse: Kildetekst 10:
Switch-klassen
SwitchHandler.java package server.handler; import server.*; public class SwitchHandler extends AccessHandler { public void handleEvent( Handle handle, int event, String[] tokens ) { EventHandler logHandler = Reactor.i().removeHandler( handle, Event.LOG_EVENT ); EventHandler getHandler = Reactor.i().removeHandler( handle, Event.GET_EVENT ); if ( event == Event.SWITCH_EVENT ) { if ( !authentication( handle, event, tokens ) ) { // switch failed Reactor.i().addHandler( handle, logHandler, Event.LOG_EVENT ); Reactor.i().addHandler( handle, getHandler, Event.GET_EVENT ); } } } }Bruger-skifte Her framelder handleEvent-metoden de event-handlere, der tager sig af LOG- og GET-events. Hvis Users-klassen ikke siger god for bruger-skiftet bliver disse tilmeldt igen, så der ikke sker nogen ændring i håndteringen af disse events. Hvis derimod bruger-skiftet blev accepteret af Users-klassen, vil super-klassens authentication-metode have tilmeldt nye event-handlere til netop disse events.
LogHandler'ne
Vi er allerede ovenfor stødt på LogHandler-klasserne. Deres fælles abstrakte super-klasse har følgende implementation: Kildetekst 11:
Fælles super-klasse
LogHandler.java package server.handler; import server.*; public abstract class LogHandler implements EventHandler { public void handleEvent( Handle handle, int event, String[] tokens ) { if ( event == Event.LOG_EVENT ) handleLogEvent( handle, tokens ); else if ( event == Event.GET_EVENT ) handleGetEvent( handle, tokens ); } public abstract void handleLogEvent( Handle handle, String[] tokens ); public void handleGetEvent( Handle handle, String[] tokens ) { try { handle.sendOk( Log.i().get( Integer.parseInt( tokens[1] ) ) ); } catch ( NumberFormatException e ) { handle.sendError( "syntax error in number" ); } catch ( IndexOutOfBoundsException e ) { handle.sendOk( "missing number" ); } } }handleEvent-metoden fungerer som template-metode i et Template Method Pattern; hvor handleLogEvent- og handleGetEvent-metoderne optræder som hook-metoder. I super-klassen er implemeteret en handleGetEvent-metode. Det skyldes at begge event-handlere håndterer GET-events ens, idet de begge tillader at klienten læser fra loggen (det er derfor ikke udnyttet i subklasserne, at denne metode er en hook-metode). Derimod er det forskelligt hvordan de håndterer LOG-events; hvorfor dette er henvist til subklasserne. Den første af disse er ReadWriteHandler-klassen: Kildetekst 12:
Må skrive i loggen
ReadWriteHandler.java package server.handler; import server.*; public class ReadWriteHandler extends LogHandler { public void handleLogEvent( Handle handle, String[] tokens ) { StringBuffer sb = new StringBuffer(); for ( int i=1; i<tokens.length; i++ ) sb.append( tokens[i] + " " ); int no = Log.i().register( sb.toString().trim() ); handle.sendOk( "" + no ); } }Her tillades skrivning til loggen, og der foretages et kald til Log-klassen (vi ser nærmere på denne klasse senere). I den anden subklasse: ReadOnlyHandler, sker der noget andet: Kildetekst 13:
Må ikke skrive i loggen
ReadOnlyHandler.java package server.handler; import server.*; public class ReadOnlyHandler extends LogHandler { public void handleLogEvent( Handle handle, String[] tokens ) { handle.sendError( "You don't have write permission" ); } }Denne EventHandler tillader nemlig ikke, at der skrives til loggen, og den sender blot en fejlmeddelelse tilbage klienten. Den Log-klasse der anvendes er følgende Singleton: Kildetekst 14:
Log-klassen
Log.java package server; import java.util.LinkedList; public class Log { private LinkedList lines; private Log() { lines = new LinkedList(); } public int register( String line ) { lines.add( line ); return lines.size()-1; } public String get( int no ) { return (String) lines.get( no ); } /* * Singleton */ private static Log instance; static { instance = null; } public static Log i() { if ( instance == null ) instance = new Log(); return instance; } }Test-stub Den opbevarer log-linierne i en LinkedList, og er ukompliceret. Ligesom for Users-klassens vedkommende, er der også her, primært tale om en test-stub. Den sidste af event-handlerne er LogoutHandler, der afslutter en session med klienten: Kildetekst 15:
Logout-klassen
LogoutHandler.java package server.handler; import server.*; public class LogoutHandler implements EventHandler { public void handleEvent( Handle handle, int event, String[] tokens ) { if ( event == Event.LOGOUT_EVENT ) { Reactor.i().removeHandler( handle, Event.ANY_EVENT ); handle.sendOk( "bye" ); } } }HandleEvent-metoden sender et bekræftende svar til klienten, og framelder alle event-handlere der vedrører dens handle.
Testanvendelse
Endelig har vi en testanvendelse. Da testanvendelsen primært er indeholdt i Client-klassen, henvises der samtidig til denne klasse. Kildetekst 16:
Test-anvendelse
Main.java import client.*; import server.*; public class Main { public static void main( String[] argv ) { new Server().start(); new Client().start(); Reactor.i().handleEvents(); } }
[Server] Online [Client] Connection opened [Reactor] adding: [Registration: handler:LoginHandler, event: LOGIN_EVENT] [Client] sending line: login bart safe [Reactor] incomming detected [Server] received: login bart safe [Handle] send: OK: You have read/write access [Reactor] adding: [Registration: handler:ReadWriteHandler, event: LOG_EVENT] [Reactor] adding: [Registration: handler:ReadWriteHandler, event: GET_EVENT] [Reactor] adding: [Registration: handler:SwitchHandler, event: SWITCH_EVENT] [Reactor] adding: [Registration: handler:LogoutHandler, event: LOGOUT_EVENT] [Reactor] removing: [Registration: handler:LoginHandler, event: LOGIN_EVENT] [Reactor] --- finished handling event --------- [Client] received: OK: You have read/write access [Client] sending line: log den første linie [Reactor] incomming detected [Server] received: log den første linie [Handle] send: OK: 0 [Reactor] --- finished handling event --------- [Client] received: OK: 0 [Client] sending line: log dette er en anden linie [Reactor] incomming detected [Server] received: log dette er en anden linie [Handle] send: OK: 1 [Reactor] --- finished handling event --------- [Client] received: OK: 1 [Client] sending line: switch homer secure [Reactor] incomming detected [Server] received: switch homer secure [Reactor] removing: [Registration: handler:ReadWriteHandler, event: LOG_EVENT] [Reactor] removing: [Registration: handler:ReadWriteHandler, event: GET_EVENT] [Handle] send: OK: You have read only access [Reactor] adding: [Registration: handler:ReadOnlyHandler, event: LOG_EVENT] [Reactor] adding: [Registration: handler:ReadOnlyHandler, event: GET_EVENT] [Reactor] --- finished handling event --------- [Client] received: OK: You have read only access [Client] sending line: log kan jeg det? [Reactor] incomming detected [Server] received: log kan jeg det? [Handle] send: ERROR: You don't have write permission [Reactor] --- finished handling event --------- [Client] received: ERROR: You don't have write permission [Client] der skete en fejl [Client] sending line: get 1 [Reactor] incomming detected [Server] received: get 1 [Handle] send: OK: dette er en anden linie [Reactor] --- finished handling event --------- [Client] received: OK: dette er en anden linie [Client] sending line: logout [Reactor] incomming detected [Server] received: logout [Reactor] removing: [Registration: handler:SwitchHandler, event: SWITCH_EVENT] [Reactor] removing: [Registration: handler:LogoutHandler, event: LOGOUT_EVENT] [Reactor] removing: [Registration: handler:ReadOnlyHandler, event: LOG_EVENT] [Reactor] removing: [Registration: handler:ReadOnlyHandler, event: GET_EVENT] [Handle] send: OK: bye [Reactor] --- finished handling event --------- [Client] received: OK: bye [Client] Connection closedBemærk, at programmet ikke terminerer, da server-tråden bliver ved med at køre.
Litteratur
"Pattern Languages of Program Design, (no. 1)", redigeret af James O. Coplien og Douglas C. Schmidt, Addison-Wesley, 1995. Kapitel 29: "Reactor: An Object Behavioral Pattern for Concurrent Event Demultiplexing and Event Handler Dispatching", s. 529-545, er den første beskrivelse af Reactor Pattern.
"Pattern-Oriented Software Architecture, Vol 2.", Douglas Schmidt oa., Wiley, 2000. Kapitlet: "Reactor", s.179-214, beskriver Reactor Pattern 5 år efter Douglas Schmidt publiserede det i PLoPD-1 (se ovenfor). Hans nye beskrivelse indeholder flere elementer - bla. bliver Demultiplexer'en er klasse for sig selv, i stedet for at være en del af Reactor-klassen. Den nye beskrivelse indeholder også en anvendelse af Handle Pattern (Bridge Pattern) i forbindelse med Reactor'en; hvilket sammen med andre detaljer gør beskrivelsen lidt vanskeligere overskue, hvis man ikke kender Reactor Pattern i forvejen..