A simple TCP client

Submitted by mrd on Thu, 01/18/2007 - 8:02pm.

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.

Submitted by jaybee on Mon, 01/22/2007 - 7:38am.

Thanks very much, this is very helpful...

You have a typo in main:

> main = withSocketsDo $ do
> [host, portStr] <- getArgs

Submitted by mrd on Mon, 01/22/2007 - 11:18am.

Indeed. The HTML filter on this thing sometimes nabs bits of code. Thanks!

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.