Request
s von Clients, und lässt die Request
s
durch separate Threads bearbeiten. Request
in fünf Schritten: Lesen, Parsen, Response
erstellen, Response
an Client senden, wieder Lesen bei Keep Alive
Verbindung. global_mvar
).acceptConnections :: Config -> Socket -> IO ()
acceptConnections conf sock = do { (handle, remote) <- accept
sock ;
forkIO
(
catchAllIO ( talk conf handle remote
'finally'
hClose handle )
( \e -> logError
e )
)
acceptConnections
conf sock
acceptConnections
kriegt als Argumente eine Server Konfiguration conf
und ein auf Verbindung wartendes Socket
sock
.
Es wartet auf neue ankommende Request
s über
das Socket
. Wenn ein Request
ankommt,
wird ein neuer Thread mit forkIO
erzeugt, der diesen Request
dann bearbeitet, und die Schleife wartet wieder auf weitere Verbindungen.
Der neu erzeugte Thread kommuniziert anschliessend mit dem Client über
den Aufruf von talk
, das eine Funktion zur Kommunikation
in HTTP ist. finally
ermöglicht eine streng
sequenzierte Ausführung von Aktionen, unabhängig von Exceptions.
finally :: IO a -> IO b -> IO a
finally funktioniert wie in Java: Zuerst wird das erste Argument dann
das zweite Argument ausgeführt, auch wenn das erste Argument eine Exception
auslöst, und gibt den Wert vom ersten Argument zurück (oder
wirft die ausgelöste Exception wieder). Damit wird in der Schleife
gewährleistet, dass das Socket ordnungsgemäß geschlossen
wird auch wenn irgendwelche Fehler oder Bugs im Code auftreten.
catchAllIO :: IO a -> (Exception -> IO a) -> IO a
catchAllIO führt zuerst das erste Argument aus. Wenn dabei eine Exception ausgelöst wird, führt es das zweite Argument (den Exception Handler) aus, andernfalls liefert es des Ergebnis zurück. In der obigen Schleife wird catchAllIO zum Auffangen von allen Fehlern benutzt, die dann in die Error Log Datei geschrieben werden.
Request
s
besteht aus folgenden Schritten:Request
aus dem Socket
lesen. Request
parsen. Response
erstellen.Response
an Client senden.Socket
zurückkehren (zurück zum Schritt
1).Das Lesen aus dem Socket
erfolgt durch getRequest
:
getRequest :: Handle -> IO [String]
getRequest
gibt eine Liste von String
als return zurück.
Beispiel für einen Request
:
GET /index.html HTTP/1.1
Host: www.haskell.org
Date: Wed May 31 11:08:40 GMT 2000
Das Kommando des Request
s ist in diesem Fall GET
, index.html
ist der Name des angeforderten Objektes und 1.1
die Version des benutzten HTTP Protokolls. In den weiteren Zeilen (genannt
headers), werden weitere Informationen gegeben, die meistens optional
sind. Wenn der Server einen Header nicht versteht, soll er dieses ignorieren.
Danach kommt das Parsen des Request
s in eine Request
Struktur:
data Request =
Request
{ reqCmd
:: RequestCmd,
reqURI :: ReqURI,
reqHTTPVer :: HTTPVersion,
reqHeaders :: [RequestHeader] }
parseRequest :: Config -> [String] -> Either Response Request
Das Parsen kann hierbei zu einer Response
führen,
die einen Mißerfolg darstellt und wahrscheinlich eine "Bad Request" Response
,
die auch spezifischere Informationen enthalten kann, ist.
Wenn das Parsen erfolgreich war, wird eine Response
generiert:
data Response = Response { respCode :: Int,
respHeaders :: [String],
respCoding :: [TransferCoding],
respBody :: ResponseBody,
respSendBody :: Bool }
data ResponseBody = NoBody
| FileBody Integer{-size-} FilePath
| HereItIs String
genResponse :: Config -> Request -> IO Response
genResponse
überprüft den Request
auf Validität (Korrektheit) und erzeugt eine passende Response
.
Bei einem GET
Request
erhält
man eine Response
mit einem FileBody
.
Ein "invalid request" (ungültiger Request
) resultiert
in einer Fehlermeldung, die aus einer automatisch generierten HTML
besteht, die den Fehler in dem HereItIs
Body näher
beschreibt. Falls eine Response
aus einer ganzen
Datei besteht, wird die Datei nicht als String
in respBody
enthalten sein, sondern nur den Pfad der Datei in respBody
enthalten. Damit kann die Datei auf eine effizientere Art übertragen
werden, wie durch Konvertierung zum String
und zur
Datei zurück.
Anschließend, muss nur noch die Response
zum Client
gesendet werden:
sendResponse :: Config -> Handle -> Response -> IO ()
Zusammengefasst, sieht die resultierende talk
Funktion
in etwa folgendermaßen aus:
talk :: Config -> Handle -> HostAddress -> IO ()
talk conf handle haddr = do req <- getRequest handle
case parseRequest r of
Left resp -> do sendResponse
conf handle resp
return ()
Right req -> do resp <-
genResponse conf req
sendResponse conf handle resp
logAccess req resp haddr
if (isKeepAlive req)
then talk conf handle haddr
else return ()
Hierbei fehlt nur noch der Code zum Error-Logging und Timeout, was in
den folgenden Abschnitten behandelt wird. Und schließlich, resultiert logAccess
in einem Eintrag in die Log-Datei, was auch nachfolgend behandelt
wird.
Timeout-Mechanismus
Timeout ist bei einem Server notwendig,
wenn z.B. eine Verbindung zum Client abgebrochen ist oder der Client
eine außergewöhnlich lange Antwortzeit hat.
Mit dem Timeout können Verbindungen
zu solchen Clients abgebrochen werden und die entsprechenden Ressourcen
wieder freigegeben werden.
Hier eine generische Implementierung von einem timeout
:
timeout :: Int -- timeout in seconds
-> IO a --
action to run
-> IO b --
action to run on timeout
-> IO a
(timeout t a b)
lässt zuerst a
laufen,
bis es entweder sich beendet oder t
Sekunden vergangen
sind. Wenn sich a
innerhalb t
Sekunden
selbst beendet, wird das Ergebnis von a
zurück
gegeben, andernfalls wird a
terminiert und b
ausgeführt. Falls a
eine Exception wirft, wird diese
durch timeout
weitergeleitet. Aufgrund keiner anderen Seiteneffekten,
kann timeout
an beliebigen Stellen im Code verwendet werden.
Aufgrund asynchroner Excpetions, kann jedoch Aktion a
jederzeit terminiert werden, weshalb es erforderlich ist es exception
safe zu machen, d.h. alle variablen Datentypen dürfen in keinem
inkonsistenten Zustand gelassen werden oder irgendwelche Ressourcen ausgelassen
werden. In diesem Zusammenhang, ist es sogar notwendig den geasamten Code
exception safe zu machen, da stack overflow bzw. heap overflow als
asynchrone Exceptions ausgelöst werden. Um Code exception safe
zu machen, gibt es in Haskell folgende zwei Funktionen:
block :: IO a -> IO a
unblock :: IO a -> IO a
(block a)
führt a
aus, wobei asynchrone
Exceptions abgeblockt werden. Erst durch den Aufruf (unblock
a)
kann der Thread wieder von anderen Threads terminiert werden.
Auch diese beiden Funktionen können an beliebigen Stellen im Code verwendet
werde
Beispiel:
block ( do a <- takeMVar m
(unblock (...))
'catchAllIO'
(\e ->
do putMVar m a; throw e)
putMVar m a )
In diesem Beispiel soll gezeigt werden, dass ein Lock in Form von MVar
m
auch dann sicher freigesetzt werden kann, wenn eine Exception
geworfen wurde.
Eine andere Methode einen Code exception safe zu machen,
ist die Verwendung von der Kombination finally
und bracket
.
Damit kann man eine locking Sequenz einfacher schreiben:
bracket :: IO a -> (a -> IO b) -> (a -> IO
c) -> IO c
bracket (takeMVar m) (putMVar m) (...)
-- vereinfachte Schreibweise einer locking Sequenz
Request
s inklusive einiger Informationen
zu den versendeten Response
s aufgelistet werden.
Das Format eines Log-Eintrags, d.h. welche Datenfelder enthalten sind,
kann konfiguriert werden und kann auch andere Datenfelder zu Request und
Response enthalten.Aufgrund einiger standardisierten Log-Eintrag Formaten
von verbreiteten Servern und Programmen, die Log-Dateien auswerten und dazu
Reporte erstellen, wird in dem Haskell Web Server ein kompatibles Format
der Log-Datei erstellt:
logAccess :: Request
-> Response
-> HostAddress
-> TimeDiff
-> IO ()
Ein Thread das ein Request bearbeitet erzeugt einen Log-Eintrag, indem
es logAccess mit den Argumenten Request, Response, Client Adresse und der
Dauer der Bearbeitung (Zeit zwischen Erhalt des Requests und Beendigung
der Response) aufruft.
Das Schreiben des Eintrags in die Log-Datei wird jedoch von einem separatem
Thread durchgeführt. Die Log-Einträge werden dabei über
einen globalen Channel von einem Thread zum Log-Thread übergeben.
logAccess fügt den Log-Eintrag in den Channel ein und der Log-Thread
holt sich (und entfernt) den Log-Eintrag aus dem Channel und schreibt es
in die Log-Datei.
Ein separater Log-Thread hat den Vorteil, dass das System geringer belastet
wird, weil die Request-Threads sich gleich nach Abgabe des Eintrags in
den Channel beenden können und somit nicht weiter mit Schreiben in
die Log-Datei belastet werden. Zusätzlich, kann der Log-Thread mehrere
Einträge zusammenfassen und als einen einzelnen Block in die Log-Datei
schreiben.
Der Log-Thread kann darüberhinaus sich selbst wieder neu starten
(restart), wenn eine Exception ausgelöst wird. Bei einem Restart versucht
der Thread die Log-Datei wieder neu zu öffnen (re-open) um dann mit
dem Schreiben des nächsten Log-Eintrags fortzufahren.
Dieser Restart-Mechanismus wird ausserdem von dem main-Thread (Endlosschleife)
genutzt (mißbraucht), wenn ein Request zum auslesen der Log-Datei
empfangen wurde.
Das Error-Logging funktioniert nach dem gleichen Prinzip, d.h. es gibt
auch hier einen separaten Error-Log-Thread der in die Error-Log-Datei die
Einträge schreibt und sich selbst Restarten kann, falls er eine Exception
empfängt (wobei auch der Restart mit Exception in die Datei reingeschrieben
wird). Und auch hier werden die Error-Log-Einträge über einen
globalen Channel übergeben.
Die Error-Log-Einträge werden von den Exception Handlern erzeugt
und in den Channel eingefügt, wenn bei einem Request-Thread eine Exception
ausgelöst wurde.
MVar
sehen:
global_mvar :: MVar String
global_mvar = unsafePerformIO newEmptyMVar
unsafePerformIO
ist tatsächlich unsicher, weil
das Programm sich bei Ersetzung einer global_mvar
durch
ihren Wert (unsafePerformIO newEmptyMVar
) anders verhält.
Um das zu verhindern, muss jegliche Optimierung von Compiler umgangen werden.
Beim GHC muss man folgendes (irgendwo im Code) hinzufügen: {-#
NoInline global_mvar #-}
MVar
s statt IORef
s zu benutzen.
... [
Informatik und Master-Seminar SS2003 ] ... [ HWS
Gesamtübersicht ] ... [ Web Server
Implementierung ] ... [ Zusammenfassung
] ...