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.