[Haskell-cafe] Long-running request/response protocol server using enumerator/iterator/iterIO/pipes/conduits/...

Paolo Capriotti p.capriotti at gmail.com
Tue Jun 26 22:25:50 CEST 2012

On Tue, Jun 26, 2012 at 8:22 PM, Nicolas Trangez <nicolas at incubaid.com> wrote:
> Hello Cafe,
> Some time ago I tried to implement a network service using iteratee (or
> enumerator, can't remember), but gave up in the end. More recently I
> wanted to create something similar (a similar protocol), but failed
> again.
> So I'm looking for some example code or something similar (Google only
> helped slightly).
> First of all, I don't care which API/library to use, I guess for my
> purpose all of enumerator, iteratee, iterIO, pipes, conduits,... are OK,
> so all feedback is welcome.
> Here's the catch. Most examples out there implement some server which
> accepts a single client request, interprets it, creates a response,
> returns this, and closes the connection (or something alike, think
> HTTP).
> The protocol I'd like to implement is different: it's long-running using
> repeated requests & responses on a single client connection. Basically,
> a client connects and sends some data to the server (where the length of
> this data is encoded in the header). Now the server reads & parses this
> (binary) data, sets up some initial state for this client connection
> (e.g. opening a file handle), and returns a reply. Now the client can
> send another request, server parses/interprets it using the connection
> state, sends a reply, and so on.
> Might sound easy (and actually it's pretty easy in most other languages
> I know, including an OCaml implementation), yet I fail to figure out how
> to get this done using some enumerator-style library.

With the current development version of pipes-core
(https://github.com/pcapriotti/pipes-core/tree/devel) I would write something
like the following (completely untested) code:

    import qualified Control.Pipe.Binary as B

    request :: PipeL IO ByteString ByteString u ()
    request = do
        h <- header
        let n = hdrSize h
        B.take n

    -- I assume a fixed-size header for simplicity
    header :: PipeL IO ByteString b u Header
    header = do
        h <- B.take headerSize >+> fold (<>) ByteString.empty
        return $ parseHeader h -- the function doing the actual parsing

    handler :: Pipe IO ByteString ByteString u ()
    handler = do
        -- server logic here
        -- just echo the input data as an example
        void idP

    server hInput hOutput
           -- read from the socket
         = B.handleReader hInput
           -- process all requests
       >+> forever (withUnawait $ request >+> handler)
           -- write to socket
       >+> B.handleWriter hOutput

Requests are handled sequentially by the `forever` loop. This whole pipeline
works with chunks of data represented as `ByteString`s, and `PipeL` (a new
feature of pipes-core 0.2.0) is used to pass leftover data along.

In a real implementation, you would also probably need to wrap `handler` in
something like:

    catch handler $ \e ->
        liftIO $ logException e

so that failures (or early termination) in `handler` don't bring the whole
pipeline down.

Sorry for the not very practical reply, involving experimental
unreleased code. :)


More information about the Haskell-Cafe mailing list