DEV Community

Cover image for A CRUD journey in Haskell, part II, Socket programming
Leandro Proença
Leandro Proença

Posted on

A CRUD journey in Haskell, part II, Socket programming

Assuming that we've got some introduction to Haskell, let's start doing some Socket programming in order to build a simple TCP server.

Requirements

For the purpose of this article, I'll expect that you have some knowledge in client-server architecture, basic networking protocols and TCP/IP.

You can get a deeper understanding on what's a client-server architecture by reading this article, where we explained some networking fundamentals and created a simple TCP server in Ruby.

TCP server

First things first, we're going to use the network package which exposes the module Network.Socket that provides full control over TCP sockets.

Server.hs

import Network.Socket

main :: IO ()
main = do
  ...
Enter fullscreen mode Exit fullscreen mode

This will pretty much provide some essential functions for Socket programming:

  • sock: creates a new socket given the address family, socket type and protocol number
  • bind: binds the socket to an address
  • listen: listen for connections on the socket specifying the max number of queued connections
  • accept: accepts a connection and returns a connection socket
  • socketToHandle: turns a connection socket into a handle, ready to be read/written

However, the module Network.Socket is not enough. It does not provide functions to manipulate the handle. Then, we have to import the module System.IO that comes with the base package.

import Network.Socket
import System.IO

main :: IO ()
main = do
  ...
Enter fullscreen mode Exit fullscreen mode

The System.IO module provides a lot of functions to manipulate I/O, but for now we will need:

  • hGetLine: reads a line from the handle
  • hPutStrLn: writes the String to the handle
  • hClose: closes the handle

Once we presented all the needed functions for our simple TCP server, let's dig into its implementation.

Setup and accepting connections

First, let's create a new socket:

sock <- socket AF_INET Stream 0
Enter fullscreen mode Exit fullscreen mode

Bind the socket to the port 4000:

bind sock (SockAddrInet 4000 0)
Enter fullscreen mode Exit fullscreen mode

Listen for connections:

listen sock 2
Enter fullscreen mode Exit fullscreen mode

And, last but not least, accept a connection:

-- the server will keep blocked on this line until a new TCP connection arrives
(conn, _) <- accept sock
Enter fullscreen mode Exit fullscreen mode

The accept function returns a pair (conn, address), but at this moment we will ignore the client address and use only the conn socket

Now we have our server ready to accept incoming TCP connections. But yet we can't read/write messages through the socket.

Manupulating the client socket

Turn a socket into a handle on read/write mode:

handleSock <- socketToHandle conn ReadWriteMode
Enter fullscreen mode Exit fullscreen mode

Read a line from the handle (read request):

line <- hGetLine handleSock
putStrLn $ "Request received: " ++ line
Enter fullscreen mode Exit fullscreen mode

Send some response back to the handle, of course:

hPutStrLn handleSock $ "Hey, client!"
Enter fullscreen mode Exit fullscreen mode

Now, we can safely close that client socket connection:

hClose handleSock
Enter fullscreen mode Exit fullscreen mode

The full implementation

Our TCP server is so simple that its implementation looks like this:

import Network.Socket                            
import System.IO                                 

main :: IO ()                                    
main = do                                        
  sock <- socket AF_INET Stream 0                
  bind sock (SockAddrInet 4000 0)                
  listen sock 2                                  
  putStrLn "Listening on port 4000..."           

  (conn, _) <- accept sock                       
  putStrLn "New connection accepted"             

  handleSock <- socketToHandle conn ReadWriteMode

  line <- hGetLine handleSock                    
  putStrLn $ "Request received: " ++ line        

  hPutStrLn handleSock $ "Hey, client!"          
  hClose handleSock                              
Enter fullscreen mode Exit fullscreen mode

In order to test the TCP server, let's use a simple TCP client in Ruby that will connect to the server and send a message:

client.rb

require 'socket'                              

server = TCPSocket.open('localhost', 4000)

server.puts 'Hey Server!'                     

response = ''                                 

while line = server.gets                      
  response += line                            
end                                           

puts "Response received: #{response}"         

server.close                                  
Enter fullscreen mode Exit fullscreen mode

Start the server:

ghc app/Server.hs -e main

# Listening on port 4000...
Enter fullscreen mode Exit fullscreen mode

Run the client:

ruby client.rb
Enter fullscreen mode Exit fullscreen mode

Server output:

Listening on port 4000...    
New connection accepted      
Request received: Hey Server!
Enter fullscreen mode Exit fullscreen mode

Client output:

Response received: Hey, client!
Enter fullscreen mode Exit fullscreen mode

A note on TCP client-server architecture

Note that our TCP server accepts a new connection, reads/writes to the socket, then closes the connection, exiting the program.

A more reliable server should return back to the listening connections after every client socket is closed. It means that it must loop forever.

In functional programming, we can loop over a function using recursion:

loopForever sock = do                            
  (conn, _) <- accept sock                       
  handleSock <- socketToHandle conn ReadWriteMode

  line <- hGetLine handleSock                    
  putStrLn $ "Request received: " ++ line        

  hPutStrLn handleSock $ "Hey, client!"          
  hClose handleSock                              
  loopForever sock                               
Enter fullscreen mode Exit fullscreen mode
  • We moved all the code related to a specific client connection to a new function called loopForever
  • In the last line, we call the function recursively

Final implementation:

main :: IO ()                                    
main = do                                        
  sock <- socket AF_INET Stream 0                
  bind sock (SockAddrInet 4000 0)                
  listen sock 2                                  
  putStrLn "Listening on port 4000..."           

  loopForever sock                               

loopForever :: Socket -> IO ()                   
loopForever sock = do                            
  (conn, _) <- accept sock                       
  handleSock <- socketToHandle conn ReadWriteMode

  line <- hGetLine handleSock                    
  putStrLn $ "Request received: " ++ line        

  hPutStrLn handleSock $ "Hey, client!"          
  hClose handleSock                              
  loopForever sock                               
Enter fullscreen mode Exit fullscreen mode

As for now, our TCP server won't exit after closing each client connection. Long live the Server! 🎉

Conclusion

In this article we learned how to do some basic Socket programming in Haskell.

This lesson will be the ground for the upcoming posts, which will cover a more sophisticated kind of message over TCP: the Hypertext Transfer Protocol (HTTP).

Discussion (0)