For my Browserling startup I decided to use Haskell to write Windows software that manages the browsers. As I haven't written much about Haskell here on catonmat before, I decided to do a small write-up on how to create a TCP server in Haskell. To keep the article simple, this TCP server will just listen on the given port on command line and execute some simple commands.

Sidenote - People often ask me how do I learn new languages. Talking about Haskell, I learned the basics from Graham Hutton's awesome book Programming in Haskell and after that asked questions in #haskell Freenode channel, and Googled a ton.

Okay, let's get started. So first we need to include the necessary functions and data constructors from Haskell libraries. This particular TCP server will use the Network library to listen on a port and accept connections, System library to get command line arguments, System.IO library to change socket handle's buffering mode, to read and write it, and Control.Concurrent library to spawn new Haskell threads.

So let's import the necessary stuff,

import Network (listenOn, withSocketsDo, accept, PortID(..), Socket)
import System (getArgs)
import System.IO (hSetBuffering, hGetLine, hPutStrLn, BufferMode(..), Handle)
import Control.Concurrent (forkIO)

Every Haskell program begins with the main function, so let's write one. Our main function will parse the port command line argument, start the server on the port, and then call sockHandler function that will accept connections and put them in a new thread,

main :: IO ()
main = withSocketsDo $ do
    args <- getArgs
    let port = fromIntegral (read $ head args :: Int)
    sock <- listenOn $ PortNumber port
    putStrLn $ "Listening on " ++ (head args)
    sockHandler sock

The withSocketsDo function is only necessary for Windows and it initializes the Windows winsock stuff.

The getArgs function returns a list of arguments that were passed on command line. For example, if we run the program as server.exe 5555, then args gets assigned a list ["5555"].

The next line converts the first element in the argument list to an Integer.

Next we start listening on the port by using listenOn. The code listenOn $ PortNumber port is just short for listenOn (PortNumber port). The dollar sign function $ allows to avoid using parenthesis.

Then we simply print a string to console which port we're listening by using putStrLn from Prelude Haskell library, which is almost always implicitly imported.

Next we call sockHandler function and pass it the sock that was returned from the listenOn function.

Now let's take a look at sockHandler function. This function takes a Socket and returns "nothing", so its type signature is sockHandler :: Socket -> IO (),

sockHandler :: Socket -> IO ()
sockHandler sock = do
    (handle, _, _) <- accept sock
    hSetBuffering handle NoBuffering
    forkIO $ commandProcessor handle
    sockHandler sock

If we look at accept function's documentation, we find that it returns a tuple of 3 elements (Handle, HostName, PortNumber). In this tutorial we are not interested in the connecting party's hostname or port, so we use _ to discard last two args. accept is a blocking call, so it will return the handle only when a new connection comes in.

Next we use hSetBuffering to change buffering mode for the client's socket handle to NoBuffering, so we didn't have buffering surprises.

Then we use forkIO to call commandProcessor function in a new Haskell thread. We do this so that we could handle more than one client connection. We'll write commandProcessor soon.

Finally we recurse and call the same sockHandler function again to handle more incoming connections. Haskell optimizes for tail recursion, so we never get stack overflows in long run.

Now let's write commandProcessor. It takes a Handle and returns IO () so its type is commandProcessor :: Handle -> IO ()

commandProcessor :: Handle -> IO ()
commandProcessor handle = do
    line <- hGetLine handle
    let cmd = words line
    case (head cmd) of
        ("echo") -> echoCommand handle cmd
        ("add") -> addCommand handle cmd
        _ -> do hPutStrLn handle "Unknown command"
    commandProcessor handle

Here we use hGetLine to get a line of text from handle, then we use the words from Prelude to split the line into words, and then we use the case statement on head of the command to find out which command was sent to the server, and call the right command function.

For example, if you send echo hello world to the server, line gets set to the string "echo hello world", then words function splits it into a list of words, ["echo", "hello", "world"], and head ["echo", "hello", "world"] is just "echo", so the echoCommand executes. Same for addCommand.

If it's an unknown command, we send the string "Unknown command" back to the client by using hPutStrLn function that writes the given string followed by a new line to the given handle.

Finally we recurse on commandProcessor so that we could execute several commands over the same connection.

Here is the echoCommand. It sends everything it receives back to the client. This function's type is Handle -> [String] -> IO (), because it takes the client socket handle, the list of command words, which are string, and returns nothing, so

echoCommand :: Handle -> [String] -> IO ()
echoCommand handle cmd = do
    hPutStrLn handle (unwords $ tail cmd)

Here hPutStrLn sends the given string to the given handle. The string in this case is unwords $ tail cmd, which is basically everything but the first word. So for example, if cmd was ["echo", "hello", "world"], then tail cmd is ["hello", "world"] and unwords ["hello", "world"] is the string "hello world".

The other command is addCommand, it takes the two numbers the client sends to it, adds them together and sends back,

addCommand :: Handle -> [String] -> IO ()
addCommand handle cmd = do
    hPutStrLn handle $ show $ (read $ cmd !! 1) + (read $ cmd !! 2)

Here we first take the second cmd !! 1 and third cmd !! 2 elements from cmd string list, then read it converts it to integers, which get added together, and then show converts the result back to a string, which gets sent back to client.

Here is the whole program, server.hs:

import Network (listenOn, withSocketsDo, accept, PortID(..), Socket)
import System (getArgs)
import System.IO (hSetBuffering, hGetLine, hPutStrLn, BufferMode(..), Handle)
import Control.Concurrent (forkIO)

main :: IO ()
main = withSocketsDo $ do
    args <- getArgs
    let port = fromIntegral (read $ head args :: Int)
    sock <- listenOn $ PortNumber port
    putStrLn $ "Listening on " ++ (head args)
    sockHandler sock

sockHandler :: Socket -> IO ()
sockHandler sock = do
    (handle, _, _) <- accept sock
    hSetBuffering handle NoBuffering
    forkIO $ commandProcessor handle
    sockHandler sock

commandProcessor :: Handle -> IO ()
commandProcessor handle = do
    line <- hGetLine handle
    let cmd = words line
    case (head cmd) of
        ("echo") -> echoCommand handle cmd
        ("add") -> addCommand handle cmd
        _ -> do hPutStrLn handle "Unknown command"
    commandProcessor handle

echoCommand :: Handle -> [String] -> IO ()
echoCommand handle cmd = do
    hPutStrLn handle (unwords $ tail cmd)

addCommand :: Handle -> [String] -> IO ()
addCommand handle cmd = do
    hPutStrLn handle $ show $ (read $ cmd !! 1) + (read $ cmd !! 2)

To use it, just run it via runhaskell server.hs 5555, and the server will start on port 5555. Or you can compile it to binary with ghc -threaded --make server.hs and then execute server binary.

And server.hs is also available for download,

Download server.hs

Download link: haskell tcp server, server.hs
Downloaded: 2975 times

This tutorial didn't cover error handling, but it's pretty easy to add through exception catching. I'll cover that in the 2nd part of the article! Until next time!

Comments

Jim Permalink
January 27, 2011, 22:04

Great article. Many Haskell tutorials are terribly abstract. Its great to find one that's so straight-forward and practical. Thanks!

January 28, 2011, 09:15

Simple and haskell rarely come together. Elegant and smart - yes.

January 28, 2011, 13:55

Excellent write up, I look forward to seeing more about Haskell from you. I'm working my way through Learn You A Haskell and while it has been very interesting to say the least it isn't as clear as this.

Y.S Permalink
January 28, 2011, 14:23

Hi.
Can you elaborate on the decision to use Haskell?
What were the other options?

January 28, 2011, 21:47

The other option was to write it in C++. But I decided to go with Haskell as I haven't written too many real Haskell programs.

January 28, 2011, 14:42

Very well written. Good work, Pete.

@Y.S.
I think Pete used Haskell because there are not many documents online on Haskell + Networking. Other options? You can do it in C, if you want to do it at low level or Python if you want to do it in fewer number of lines of code.

January 28, 2011, 15:34

@Y.S.
I am sorry, I think I misunderstood your question to why Pete used Haskell here. If your question is on why Pete is planning to use Haskell for Browserling, that's something only Pete can answer. And I am looking forward for that answer too.

January 28, 2011, 21:48

Thanks Sundar! Another reason Haskell is good for this is that you can compile Haskell to an executable. Can't do that easily with Python.

John Permalink
January 29, 2011, 01:11

if your interested in event-driven programing you should try sxe http://simonhf.wordpress.com/2010/10/09/what-is-sxe/ http://github.com/jimbelton/sxe

Omie Permalink
January 29, 2011, 19:12

This article inspired me to start learning Haskell !

Thanks :-)

Michael Litchard Permalink
February 01, 2011, 01:56

Thank you! I've had the good fortune to be in a position to bring haskell to my workplace. My project is an internal system that will require network programming and concurrency. I've been stressing about that, and then you publish this! I'm stressing no more.

Thanks again.

February 12, 2011, 14:49

Time to get my hands on Haskell now. :)

Andrew Permalink
February 03, 2012, 07:37

Great - I had been thinking about various ways to get Haskell code to talk to other code and stressing over how complicated all the lists of strings conversion was all going to get, which was going to keep hold of which data, whether I was going to have to develop a mini-framework at one end or the other, but I see now that they can just talk over TCP very, very easily. Fab post, thanks.

Pers Permalink
September 17, 2013, 08:15

Hi,

thanks for the nice example! In Ubuntu Raring you have to
apt-get install ghc libghc-network-dev
and change
import System (getArgs)
to
import System.Environment (getArgs)
(see http://stackoverflow.com/questions/4447130/make-could-not-find-module-system ) for the example to work out of the box.

I'd like to use your code in a little project. Would you mind publishing it under an open source license or putting it in the public domain?

Regards,
Pers

Taha Yavuz Bodur Permalink
January 17, 2014, 08:59

Thank you for this idiomatic, literate and real world Haskell program.

I was trying to learn from Kazu Yamamoto's HTTP Server: https://github.com/kazu-yamamoto/mighttpd.

The problem was i couldn't build the source on windows since it depends on unix library: http://hackage.haskell.org/package/unix-2.5.1.0. So i was looking for a windows implementation then I found your tutorial.

It is simple to reason enough, but i'm looking for a more complete example. So if you have some open source implementations which are easy to understand, could you please share?

It's always a joy to read your articles, so if you have time could you share a tutorial about building a haskell project which contaions multiple modules in it's source and have dependencies to some cabal packages?

Thanks, indeed.

February 07, 2014, 21:00

this is exactly what i looking for, thanks!

April 02, 2014, 02:07

Incredibly useful --- would love to see a TCP Client documented the same way.

Yoshikuni Jujo Permalink
April 27, 2014, 21:34

Thank you!
This is very useful.

I want TCP Client document too.

Leave a new comment

(why do I need your e-mail?)

(Your twitter name, if you have one. (I'm @pkrumins, btw.))

Type the word "server": (just to make sure you're a human)

Please preview the comment before submitting to make sure it's OK.

Advertisements