Haskell Network Programming - TCP Client and Server

June 12, 2017
haskellnetworking

Using the network package we can build a low level TCP server and client. We will make a simple echo server and client. The client sends a message, the server receives the message and sends the message back to the client, then the client receives the message it sent.

import Control.Concurrent        (forkIO, threadDelay)
import Control.Monad             (unless)
import qualified Data.ByteString       as BS
import qualified Data.ByteString.Char8 as C
import Network.Socket hiding     (recv)
import Network.Socket.ByteString (recv, sendAll)

runTCPEchoServer :: IO ()
runTCPEchoServer = do 
  addrinfos <- getAddrInfo Nothing (Just "127.0.0.1") (Just "7000")
  let serveraddr = head addrinfos

It is important to remember to use Stream to receive data and respond via TCP. Then we can bind the socket to the address, wait to receive data and then respond to the client.

  sock <- socket (addrFamily serveraddr) Stream defaultProtocol
  bind sock (addrAddress serveraddr)
  listen sock 1
  (conn, _) <- accept sock
  print "TCP server is waiting for a message..."
  msg <- recv conn 1024
  unless (BS.null msg) $ do 
    print ("TCP server received: " ++ C.unpack msg)
    print "TCP server is now sending a message to the client"
    sendAll conn msg
  print "TCP server socket is closing now."
  close conn
  close sock

The client code is similar. We need to sned data via Datagram or the server will ignore it. Then we run sendAll with a ByteString and the server will receive the message.

sendMessage :: String -> IO ()
sendMessage s = do
  addrinfos <- getAddrInfo Nothing (Just "127.0.0.1") (Just "7000")
  let serveraddr = head addrinfos
  sock <- socket (addrFamily serveraddr) Stream defaultProtocol
  connect sock (addrAddress serveraddr)
  sendAll sock $ C.pack s
  msg <- recv sock 1024
  close sock
  -- delay thread to avoid client and server from printing at the same time
  threadDelay 1000000
  print ("TCP client received: " ++ C.unpack msg)

We run the server in a separate thread because recv is blocking. We add a few threadDelays to make sure that the server has started up and that the prints from different threads do not occur at the same time. Otherwise, the messages might print at the same time and be illegible.

main :: IO ()
main = do
  _ <- forkIO $ runTCPEchoServer
  threadDelay 1000000 -- wait one second
  sendMessage "Hello, world!"
  threadDelay 1000000 -- wait one second

When main terminates all of the other threads in the program will terminate as well [2].

A real world TCP server will likely need to constantly receive requests and make responses. We can make the request/response code an infinite loop.

runTCPEchoServerForever :: IO ()
runTCPEchoServerForever = do 
  addrinfos <- getAddrInfo Nothing (Just "127.0.0.1") (Just "7000")
  let serveraddr = head addrinfos
  sock <- socket (addrFamily serveraddr) Stream defaultProtocol
  bind sock (addrAddress serveraddr)
  listen sock 1
  (conn, _) <- accept sock
  print "TCP server is waiting for a message..."
  rrLoop conn
  print "TCP server socket is closing now."
  close conn
  close sock
  
  where
    rrLoop conn = do
      msg <- recv conn 1024      
      unless (BS.null msg) $ do 
        print ("TCP server received: " ++ C.unpack msg)
        print "TCP server is now sending a message to the client"
        sendAll conn msg            

References