Sat, 14 Jan 2012
A Simple Telnet Client Using Data.Conduit.
The Conduit library is a second generation approach to the problem of guaranteeing bounded memory usage in the presence of lazy evaluation. The first generation of these ideas were libraries like Iteratee, Enumerator, and IterIO. All of these first generation libraries use the the term enumerator for data producers and iteratee for data consumers. The new Conduit library calls data producers "sources" and data consumers "sinks" to make them a little more approachable.
The other big difference between Conduit and the early libraries in this space is to do with guaranteeing early clean up of potentially scarce resources like sockets. Although I have not looked in any detail at the IterIO library, both Iteratee and Enumerator simply rely on Haskell's garbage collector to clean up resources when they are no longer required. The Conduit library on the other hand uses Resource transformers to guarantee release of these resources as soon as possible.
The client looks like this (latest available here):
import Control.Concurrent (forkIO, killThread) import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad.Trans.Resource import Data.Conduit import Data.Conduit.Binary import Network (connectTo, PortID (..)) import System.Environment (getArgs, getProgName) import System.IO main :: IO () main = do args <- getArgs case args of [host, port] -> telnet host (read port :: Int) _ -> usageExit where usageExit = do name <- getProgName putStrLn $ "Usage : " ++ name ++ " host port" telnet :: String -> Int -> IO () telnet host port = runResourceT $ do (releaseSock, hsock) <- with (connectTo host $ PortNumber $ fromIntegral port) hClose liftIO $ mapM_ (`hSetBuffering` LineBuffering) [ stdin, stdout, hsock ] (releaseThread, _) <- with ( forkIO $ runResourceT $ sourceHandle stdin $$ sinkHandle hsock ) killThread sourceHandle hsock $$ sinkHandle stdout release releaseThread release releaseSock
There are basically three blocks, a bunch of imports at the top, the program's entry point main and the telnet function.
The telnet function is pretty simple. Most of the function runs inside a runResourceT resource transformer. The purpose of these resources transformers is to keep track of resources such as sockets, file handles, thread ids etc and make sure they get released in a timely manner. For example, in the telnet function, the connectTo function call opens a connection to the specified host and port number and returns a socket. By wrapping the connectTo in the call to with then the socket is registered with the resource transformer. The with function has the following prototype:
with :: Resource m => Base m a -- Base monad for the current monad stack -> (a -> Base m ()) -- Resource de-allocation function -> ResourceT m (ReleaseKey, a)
When the resource is registered, the user must also supply a function that will destroy and release the resource. The with function returns a ReleaseKey for the resource and the resource itself. Formulating the with function this way makes it hard to misuse.
The other thing of interest is that because a telnet client needs to send data in both directions, the server-to-client communication path and the client-to-server communication run in separate GHC runtime threads. The thread is spawned using forkIO and even though the thread identifier is thrown away, the resource transformer still records it and will later call killThread to clean up the thread.
The main core of the program are the two lines containing calls to sourceHandle and sinkHandle. The first of these lines pulls data from stdin and pushes it to the socket hsock while the second pulls from the socket and pushes it to stdout.
It should be noted that the final two calls to release are not strictly necessary since the resource transformer will clean up these resources automatically.
The experience of writing this telnet client suggests that the Conduit library is certainly easier to use than the Enumerator or Iteratee libraries.