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)
Module Network is the simple networking
library, presenting a Handle-based interface.
> 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
socket handle.
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 Num,
but not Read. So, we read it as an
Int, and then generalize to class Num
using fromIntegral.
> 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
around forkIO which adds a small
thread-specific exception handler that simply
passes any exceptions along to the main thread.
(Note: 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.
Read literally: listenLoop repeats forever, in
sequence, the action of invoking act and
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 listenLoop
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 TChans
using the STM combinator orElse.
Read plainly, it states that ch1 should
be consulted first, or else, ch2 should
be read. Any values from ch1 are tagged
with Left and any values from ch2
are tagged with Right.
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)
Transcript:
$ 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.
- mrd's blog
- Login to post comments
Thanks very much, this is very helpful...
You have a typo in main:
> main = withSocketsDo $ do
> [host, portStr] <- getArgs
Indeed. The HTML filter on this thing sometimes nabs bits of code. Thanks!