[edit]
1 Introduction #
This tutorial is about server programming in concurrent Haskell.
Although, Simon Marlows' web server written in Haskell
[1]
is a good reference, tutorial for newbies driven by simple example is needed.
It is a great pleasure to program servers in applicative languages supporting concurrency.
Erlang is an excellent example which had been highly successful in the industry.
Erlang's message passing sylte concurrency support is really awesome.
You should try it if you ever had interest in server programming.
Some haskell implementaions including ghc and hugs support concurrency via lightweighed thread in the runtime system.
In GHC's Control.Concurrent module there is a shared mutexed variable type MVar.
Though MVar is not so high level as Erlang's message,
you can still enjoy the power of modern pure functional language
handling concurrencies in server programming elegantly.
The aim of this introductory tutorial is to introduce how to program servers in Haskell with concurrency support.
Therefore extensions or libraries beyond the scope of Haskell 98 is limited except for concurency control and high level network libraries.
The readers of this document should clearly notice that the example server program codes are far from production quality.
Since the example codes provided in this tutorial only uses the high level network libraries rather than using lower level IO and socket libraries,
the input and output can be inefficient and can suffer from client abuse.
[2]
However the example is composed with care to escape from common pitfalls of concurrent programming
such as busy wating or unnecessary blocking from misused mutex.
[edit]
4 Echo Server #
Only 10 lines except import declarations!
Echo server is the most simple server, which appears as the first example of most server programming tutorials.
Echo server does not share any information between clients.
Therefore we do not need any information sharing among threads.
import IO
import Control.Exception hiding (catch)
import Control.Concurrent
import Network
import System.Posix
main = withSocketsDo (installHandler sigPIPE Ignore Nothing >> main')
main' = listenOn (PortNumber 9900) >>= acceptConnections
acceptConnections sock = do
putStrLn "trying to accept" -- debug msg
conn@(h,host,port) <- accept sock
print conn -- debug msg
forkIO $ catch (talk conn `finally` hClose h) (\e -> print e)
acceptConnections sock
talk conn@(h,_,_) = hGetLine h >>= hPutStrLn h >> hFlush h >> talk conn
Running example
sever console
client console
This also works fine with multiple clients.
kyagrd@kyagrd:hsserv$ ./echo.bin
trying to accept
({handle: <socket: 4>},"220.85.155.11",49223)
trying to accept
<socket: 4>: hGetLine: end of file
kyagrd@kyagrd:kyagrd$ telnet kyagrd.dyndns.org 9900 Trying 220.85.155.11... Connected to kyagrd.dyndns.org. Escape character is '^]'. abcd abcd asdf1234 asdf1234 ^] telnet> quit Connection closed. kyagrd@kyagrd:kyagrd$
[edit]
5 Chat Server #
Chat server is much more complicated than echo server.
Chat server should get messages from the client and broadcast the messages to all of the clients.
As a result there should be sharing or passing of some information among the client threads.
These informations are list of connected client and the collected messages to broadcast.
These two informations are implemented as mutex variables.
In addition there is a broadcaster thread.
Another mutex variable works as a semaphore
[3]
to avoid busy wating of the broadcaster.
import IO
import Control.Exception hiding (catch)
import Control.Concurrent
import Network
import System.Posix
import List
import Monad
main = withSocketsDo (installHandler sigPIPE Ignore Nothing >> main')
main' = do
sock <- listenOn (PortNumber 9901)
mvH <- newMVar [] -- list of client handles
mvM <- newMVar [] -- messags stack ... not queue :)
mvS <- newEmptyMVar -- semaphore
let mvt = (mvH,mvM,mvS)
forkIO $ broadcast mvt `finally` putMVar mvS ()
acceptConnections sock mvt
acceptConnections sock mvt@(mvH,mvM,_ ) = do
print "trying to accept" -- for debugging message
conn@(h,host,port) <- accept sock
print conn -- for debugging message
modifyMVar_ mvH (return . (h:))
forkIO $ catch (talk conn mvt `finally` closeConn h mvH) print
acceptConnections sock mvt
talk conn@(h,host,port) mvt@(_ ,mvM,mvS) =
hGetLine h >>= stackMsg >> talk conn mvt
where stackMsg m = do
modifyMVar_ mvM (return . ((show (host,port)++':':m):))
tryPutMVar mvS () -- V
closeConn h mvH = modifyMVar_ mvH (return . delete h) >> hClose h
broadcast mvt@(mvH,mvM,mvS) = do
takeMVar mvS -- P
ms <- swapMVar mvM []
mapM_ (\m ->mapM_ (failSafe(\h->hPutStrLn h m>>hFlush h))=<<readMVar mvH) ms
broadcast mvt
where failSafe ioWithHarg h = catch (ioWithHarg h) print
Running example
server console
clent console
This also works fine with multiple clients.
kyagrd@kyagrd:hsserv$ ./chat.bin
"trying to accept"
({handle: <socket: 4>},"220.85.155.11",49500)
"trying to accept"
<socket: 4>: hGetLine: end of file
kyagrd@kyagrd:kyagrd$ telnet kyagrd.dyndns.org 9901
Trying 220.85.155.11...
Connected to kyagrd.dyndns.org.
Escape character is '^]'.
asdf
("220.85.155.11",49500):asdf
asdf1234
("220.85.155.11",49500):asdf1234
^]
telnet> quit
Connection closed.
kyagrd@kyagrd:kyagrd$
[edit]
6 Suggestions for further studies #
- Impove performance and reliability by replacing basic IO with lower level sockets with timeout and non blocking features.
- Mimick Erlang's message passing style by throwing dynamic typed exceptions to other threads.
[edit]
7 Download source codes and makefile #
This was tested under my debian sarge liux box using ghc6 package.
kyagrd@kyagrd:hsserv$ ls
chat.hs echo.hs makefile
kyagrd@kyagrd:hsserv$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 6.2.1
kyagrd@kyagrd:hsserv$ make
ghc --make echo.hs -o echo.bin -package network -package posix
Chasing modules from: echo.hs
Compiling Main ( echo.hs, echo.o )
Linking ...
ghc --make chat.hs -o chat.bin -package network -package posix
Chasing modules from: chat.hs
Compiling Main ( chat.hs, chat.o )
Linking ...
kyagrd@kyagrd:hsserv$ ls
chat.bin* chat.hs echo.bin* echo.hs makefile
chat.hi chat.o echo.hi echo.o
kyagrd@kyagrd:hsserv$ ./echo.bin
trying to accept
echo.bin: internal error: main thread has been GC'd
Please report this as a bug to glasgow-haskell-bugs@haskell.org,
or http://www.sourceforge.net/projects/ghc/
kyagrd@kyagrd:hsserv$ ./chat.bin
"trying to accept"
chat.bin: internal error: main thread has been GC'd
Please report this as a bug to glasgow-haskell-bugs@haskell.org,
or http://www.sourceforge.net/projects/ghc/
kyagrd@kyagrd:hsserv$ make clean
rm -f echo.bin chat.bin *.hi *.o
kyagrd@kyagrd:hsserv$ ls
chat.hs echo.hs makefile
The error internal error message comes out when pressing Ctrl-C.
[edit]
8 Document History #
- First made this page. -- kyagrd 2005-01-05
----
[1] http://www.haskell.org/haskellwiki/Applications There is a link to Simon Marlow's web server related information.[2] e.g. Client can block some server thread by not handling the server's response. The socket handle does not have timeout.
[3] The simplest bistate semaphore. GHC concurrnet library also provides more general semaphore implementations.
You may add comments here.
acceptConnections can raise exception because of accept or any other system signal.
(Thanks to Simon Jones's posting in haskell-cafe list) -- kyagrd 2005-01-05








