6
\$\begingroup\$

I wrote this echo server in Haskell as a way to introduced myself to programming with sockets in Haskell and generally practice Haskell. The echo server has a simple purpose: accept connections from a port (provided as a command line argument) and echo data read from the port back one line at a time.

import Control.Applicative
import Control.Concurrent
import Control.Monad
import Network.Socket
import System.Environment
import System.IO

main :: IO ()
main = withSocketsDo $ do
  port <- toEnum . read . head <$> getArgs 
  newSocket <- socket AF_INET Stream defaultProtocol 
  setSocketOption newSocket ReuseAddr 1
  bindSocket newSocket $ SockAddrInet port iNADDR_ANY
  listen newSocket 2
  runServer echo newSocket

runServer :: (String -> String) -> Socket -> IO()
runServer f s = forever $ do
  (usableSocket,_) <- accept s
  forkIO $ interactWithSocket f usableSocket 

interactWithSocket :: (String -> String) -> Socket -> IO()
interactWithSocket f s = do 
  handle <- socketToHandle s ReadWriteMode
  forever $ f <$> hGetLine handle >>= hPutStrLn handle 

echo :: String -> String  
echo = id

General review of my Haskell coding is appreciated as well as advice on better or alternate ways to interact with sockets in Haskell. Finally, I plan on implementing more complex servers in Haskell so, I'm looking for tips on how to make my code more extensible.

\$\endgroup\$
  • \$\begingroup\$ Nice code! Clean, readable and idiomatic. Way to go! \$\endgroup\$ – max taldykin Jul 31 '15 at 14:58
2
\$\begingroup\$

I quite like the code, there isn't much to say. There are small improvements to make - better command line arguments handling, and perhaps graceful handling of EOF (and possibly other exceptions) in the server code, but as it is it works also just fine.

Perhaps the only more important comment is that your server is vulnerable to a memory exhaustion attack, if an adversary passes extremely long line.

The most interesting part is how to make the server code extensible. You very nicely pass the server function String -> String as a parameter to the server implementation. But it transforms lines one by one, so it won't be possible to implement more complex interaction.

So the next step would be to apply a String -> String function to the whole input, in the spirit of interact (IIRC this is how early functional languages defined their input/output, before monads and the IO monad were invented). But this has drawbacks too:

  • Either you read the whole input and then produce the whole output, which is rather limiting.
  • Or you use lazy IO, which is very tricky and problematic. I certainly don't recommend it.
  • The server code can't do anything except computing output from input.

The last point can be solved by extending the type to String -> IO String. But still doesn't solve the lazy IO problem.

One step further would be to define a data type that'd consume a piece of output, produce a piece of input, and a continuation to proceed further:

data Application = String -> IO (String, Maybe Application)

This works nicely for applications that have well defined input/output tokes, such as interacting line-by-line. But for more complex operations, it's still not sufficient. In particular, sometimes you need to consume several input chunks until producing an output one, or vice versa. For example imagine you need to implement splitting input into lines by yourself - you need to read possibly multiple (or no) chunks until you encounter \n.

The solution to this are several libraries that more or less do the same thing: iteratees, conduit, pipes (for comparison see this question). Conduit is used in the Yesod web server and has a lot of different libraries. In conduit-extra you have libraries for connecting a conduit to a socket, for creating conduits from a parser, splitting input into lines etc. It also works on more efficient ByteStrings, rather than Strings.

\$\endgroup\$
  • \$\begingroup\$ Thank for the feedback. Since I first posted this code review I've modified my code to implement a HTTP/0.9 server. The biggest change I had to make in my code was moving away from line by line handling of input to reading the entire input at once; although, I used lazy IO for this witch you point out is not the best idea. Rewriting both these servers using something like Conduit seems like an interesting exercise. \$\endgroup\$ – ankh-morpork Jul 16 '16 at 12:59
  • \$\begingroup\$ @dohaqatar7 Off the top of my head, the main problems with lazy IO are: (1) finalizing resources - when should you close lazily opened file? (2) IO code leaking into pure code, this could be rather unexpected and very difficult to debug. The problem is that the semantics of pure code suddenly depends on the order of evaluation! (3) Risk of leaking memory. \$\endgroup\$ – Petr Pudlák Jul 16 '16 at 16:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.