I've been struggling with a similar situation: a client and server that communicate with binary-encoded messages, sending "heartbeats" (dummy messages) every 30 seconds, and timing out the connection if no response is received in 3 minutes.  The client sends data to the server, while also listening for configuration changes from the server.  The connection needs to interact with multiple threads on both sides.

See http://ofps.oreilly.com/titles/9781449335946/sec_conc-server.html (Parallel and Concurrent Programming in Haskell, Chapter 9) for a thorough example of using STM to write a simple networked application.  You may want to read some of the previous chapters if you have trouble understanding this one.

If your program becomes an overwhelming tangle of threads, tackle it like any other complexity: break your program into modules with simple interfaces.  In particular, build your connection module in layers.  For example, you could have a module that serializes messages:

    data Config = Config { host :: HostName, port :: PortNumber }

    data Connection = Connection
        { connBase :: Handle
        , connRecvState :: IORef ByteString
          -- ^ Leftover bytes from last 'recv'
        }

    connect :: Config -> IO Connection
    close :: Connection -> IO ()

    send :: Connection -> Request -> IO ()
    recv :: Connection -> IO (Maybe Response)

On top of that, timed messages:

    data Connection = Connection
        { connBase :: Base.Connection
        , connSendLock :: MVar SendState
        }

    data SendState = SendOpen | SendError SomeException

Here, both 'send' and a timer thread take the send lock.  If either fails, it places 'SendError' in the MVar to prevent subsequent accesses.

MVar locking is pretty cheap: about 100 nanoseconds per withMVar, versus several microseconds per network I/O operation.  Don't be afraid to stack MVar locks if it makes your code easier to maintain.

@Michael Snoyman: The Connection example above is one case where resumable conduits might be useful.  For Connection to use conduits, 'recv' and 'send' would have to feed conduits incrementally.  Otherwise, it'd need a different interface (e.g. return Source and Sink).

I wrote a conduit-resumable package [1], but did not release it because of a semantic issue regarding leftovers: if a conduit has leftovers, should they go back to the source, or stay with the conduit?  conduit-resumable does the former, but we'd want the latter here.

 [1]: https://github.com/joeyadams/hs-conduit-resumable