Concurrent implementation of the Daytime Protocol in Haskell

August 26, 2014

One example from Parallel and Concurrent Programming in Haskell is a concurrent network server. The server given in the book implements an informally specified doubling protocol, where each submitted line gets parsed as an Integer and returns the double of the input.

Back in May, Andrew Clarkson gave a talk at the PyMNtos meeting about Asynchronous IO in Python. As an example, he included an asynchronous version of a Daytime server.

Let’s take the Haskell doubling server, and make it a (TCP) Daytime server.

… but first, some imports.

> import Control.Monad (forever)

forever is useful for making loops out of void functions (IO ()).

> import Text.Printf (printf)

printf behaves similarly to how it does in other languages: it takes a format string and items to interpolate into the format string.

> import Data.Time (getCurrentTime, formatTime)
> import System.Locale (defaultTimeLocale)

These are used to get the current time, and format it with the default locale.

> import System.IO (
>     Handle

A handle is a place for IO to stream data.

>   , stdout

stdout is the default handle used for things like putStrLn.

>   , hPutStrLn

This is the handle equivalents of putStrLn. Rather than assuming the standard handle, the handle is passed in explicitly. One can define putStrLn (in point-free style) as putStrLn = hPutStrLn stdout.

>   , hClose

Handles need to be closed when you are done with them.

>   )
> import Network (
>     withSocketsDo

This is needed on Windows to initialize the networking subsystem. It is here for portability reasons.

>   , listenOn

This opens a socket on a specified port.

>   , PortID(PortNumber)

To specify the port, we’ll pass in a PortNumber.

>   , accept

accept takes a socket and returns a tuple of a Handle, a host, and the port.

>   )
> import Control.Concurrent (
>     forkFinally

forkFinally creates a new (light-weight) thread to run a specified process and when it completes, it “finally” runs another command. You’ll see when we get there.

>   , threadDelay

It turns out our single-threaded implementation is quite efficient, so we’ll add a slight delay to make clear how concurrency affects the server.

>   )

This program has four different main functions. The fourth one is the concurrent Daytime server, so we’ll use that implementation as our main main:

> main = main4

First, lets write a non-server version of what a Daytime server does. According to the specification:

“Once a connection is established the current date and time is sent out the connection as a ascii character string (and any data received is thrown away). The service closes the connection after sending the quote.”

A non-server of this would just be a simple version of date

> main1 :: IO ()
> main1 = do
>   ct <- getCurrentTime
>   let time = formatTime defaultTimeLocale "%F %T" ct
>   putStrLn time

This outputs the time, and halts.

In order to work with sockets, however, we’ll need to use the Handle equivalent program. We’ll also adding a slight delay to make the benefits of concurrency clear later.

To convert main1 to use handles explicitly, we’ll pass an output handle in, and use hPutStrLn instead of putStrLn:

> mainWith :: Handle -> IO ()
> mainWith outH = do
>   threadDelay (10^6) -- to simulate "real" work.
>   ct <- getCurrentTime
>   let time = formatTime defaultTimeLocale "%F %T" ct
>   hPutStrLn outH time

Ignoring the threadDelay, the equivalent program to main1 would be:

> main2 :: IO ()
> main2 = mainWith stdout

Now we can get on with implementing the server. The specification establishes port 13 for the Daytime protocol.1

> port :: Int
> port = 13

Let’s start out with the server implementation from the book:

> main3 :: IO ()
> main3 = withSocketsDo $ do
>   sock <- listenOn (PortNumber (fromIntegral port))
>   printf "Listening on port %d\n" port
>   forever $ do
>     (handle, host, port) <- accept sock
>     printf "Accepted connection from %s: %s\n" host (show port)

Here’s where things diverge. We use our mainWith in place of talk. Since we can pass handles into mainWith, we can pass handles returned by accept into mainWith:

>     mainWith handle

Don’t forget to close the handle after the connection has been handled (as specified)!

>     hClose handle

This handles each connection in sequence. Since handling the connection takes over a second of work (due to the thread delay), it can only respond to one connection per second.

Let’s add some concurrency! This first part is the same:

> main4 :: IO ()
> main4 = withSocketsDo $ do
>   sock <- listenOn (PortNumber (fromIntegral port))
>   printf "Listening on port %d\n" port
>   forever $ do
>     (handle, host, port) <- accept sock
>     printf "Accepted connection from %s: %s\n" host (show port)

Here’s where forkFinally comes in. Instead of calling mainWith handle, we fork a new thread to call it. When the thread completes, we (finally) close the handle.

>     forkFinally (mainWith handle)
>                 (\_ -> hClose handle)

Since a new thread is created for each connection, we are no longer limited to one connection per second.

To run this file2:

runhaskell daytime.lhs

To test the concurrency3:

yes "nc localhost 13" | parallel -j 32

This streams commands to connect to localhost on port 13, and uses parallel to have 32 worker threads running those commands. With main3, you should see one response per second, whereas with main4, you should see 32 responses per second.

  1. You may need root access in order to run this program. If you do not have root access, change the port to 1313 or some other number above 1024. Socket numbers below 1024 are generally protected on modern computers.

  2. Again, you may need root access.

  3. You’ll need GNU parallel installed, if you don’t have it.