A simple TCP client
A simple example network client showing how to multiplex the reading of lines from the remote peer and the local user, using Software Transactional Memory to do message-passing and light-weight threads.
This text is literate Haskell, and has been tested with ghc 6.6 on Linux/x86. Type annotations are included for didactic purposes.
> module Main where > import Prelude hiding (catch)
Network is the simple networking
library, presenting a
> import Network (connectTo, withSocketsDo, PortID(..)) > import System.IO > import System.IO.Error (isEOFError) > import System.Environment (getArgs) > import Control.Exception (finally, catch, Exception(..)) > import Control.Concurrent > import Control.Concurrent.STM
main parses host and port from the command line
and connects to it.
Then it calls the
start function with the
An error handler is defined which
prints out exceptions, except for EOF.
Finally, the socket is ensured to be closed.
withSocketsDo is required on Windows
platforms, but does no harm otherwise.
> main = withSocketsDo $ do > [host, portStr] <- getArgs
PortNumbers are an instance of
Read. So, we
read it as an
Int, and then generalize to class
> let port = fromIntegral (read portStr :: Int) > sock <- connectTo host $ PortNumber port > start sock `catch` handler `finally` hClose sock > where > handler (IOException e) > | isEOFError e = return () > handler e = putStrLn $ show e
start takes care of the creation of threads
and channels to communicate between them. Each
thread spawned is responsible for listening to a given
handle, and forwarding any communications received along
the channel. Notice, in particular, how this listening
task has been abstracted into a higher-order monadic function.
The main thread is then used as the central "coordinating"
loop, as discussed below.
> start :: Handle -> IO () > start sock = do > netChan <- atomically newTChan > userChan <- atomically newTChan > netTID <- spawn $ listenLoop (hGetLine sock) netChan > userTID <- spawn $ listenLoop getLine userChan > mainLoop sock netChan userChan
spawn is a small wrapper
forkIO which adds a small
thread-specific exception handler that simply
passes any exceptions along to the main thread.
myThreadId is GHC-specific)
> spawn :: IO () -> IO ThreadId > spawn act = do > mainTID <- myThreadId > forkIO $ act `catch` throwTo mainTID
listenLoop pipes the output of calling
an action repeatedly into a channel.
listenLoop repeats forever, in
sequence, the action of invoking
then atomically writing its value to the channel.
> listenLoop :: IO a -> TChan a -> IO () > listenLoop act chan = > sequence_ (repeat (act >>= atomically . writeTChan chan))
Here is an explicit-recursion version of the above:
listenLoop = do v <- action atomically $ writeTChan chan v listenLoop action chan
Understanding why both versions of
are equivalent will help you understand that monadic
actions in Haskell are first-class values.
mainLoop demonstrates the usage of a simple
select function which awaits input from one
of two channels.
> mainLoop :: Handle -> TChan String -> TChan String -> IO () > mainLoop sock netChan userChan = do > input <- atomically $ select netChan userChan > case input of > -- from netChan, to user > Left str -> putStrLn str >> hFlush stdout > -- from userChan, to net > Right str -> hPutStrLn sock str >> hFlush sock > mainLoop sock netChan userChan
select multiplexes two
using the STM combinator
Read plainly, it states that
be consulted first, or else,
be read. Any values from
ch1 are tagged
Left and any values from
are tagged with
The reason why this works seamlessly is because STM will keep track of which channels it has attempted to read. If both have nothing available right now, then STM knows it can block until one of the two channels has data ready.
> select :: TChan a -> TChan b -> STM (Either a b) > select ch1 ch2 = do > a <- readTChan ch1; return (Left a) > `orElse` do > b <- readTChan ch2; return (Right b)
$ ghc --make Client1.lhs [1 of 1] Compiling Main ( Client1.lhs, Client1.o ) Linking Client1 ... $ ./Client1 localhost 25 220 localhost ESMTP Exim 3.36 helo localhost 250 localhost Hello mrd at localhost [127.0.0.1] quit 221 localhost closing connection $
This code has demonstrated a simple TCP client which can receive and transmit lines in an interactive session. The implementation used light-weight threads to multiplex handles and Software Transactional Memory for inter-thread communication. Several re-usable functions were defined which show the expressive power and simplicity of STM and higher-order monadic functions.