Network Programming in Haskell

0
7608
haskell

haskell

Haskell, named after logician Haskell Curry, is a standardised, general-purpose, purely functional programming language, with non-strict semantics and strong static typing. This eleventh article on Haskell covers network programming.

Let us begin with a simple TCP (transmission control protocol) client and server example. The network package provides a high-level interface for communication. You can install the same in Fedora, for example, using the following command:

$ sudo yum install ghc-network

Consider the following simple TCP client code:

-- tcp-client.hs

import Network
import System.IO

main :: IO ()
main = withSocketsDo $ do
handle <- connectTo “localhost” (PortNumber 3001)
hPutStr handle “Hello, world!”
hClose handle

After importing the required libraries, the main function connects to a localhost server running on Port 3001, sends a string ‘Hello, world!’ and closes the connection.
The connectTo function defined in the Network module accepts a hostname, port number and returns a handle that can be used to transfer or receive data.
The type signatures of the withSocketsdo and connectTo functions are as follows:

ghci> :t withSocketsDo
withSocketsDo :: IO a -> IO a

ghci> :t connectTo
connectTo :: HostName -> PortID -> IO GHC.IO.Handle.Types.Handle

The simple TCP server code is illustrated below:

-- tcp-server.hs
import Network
import System.IO

main :: IO ()
main = withSocketsDo $ do
sock <- listenOn $ PortNumber 3001
putStrLn “Starting server ...”
handleConnections sock

handleConnections :: Socket -> IO ()
handleConnections sock = do
(handle, host, port) <- accept sock
output <- hGetLine handle
putStrLn output
handleConnections sock

The main function starts a server on port 3001 and transfers the socket handler to a handleConnections function. It accepts any connection requests, reads the data, prints it to the server log, and waits for more clients.
First, you need to compile the tcp-server.hs and tcp-client.hs files using GHC:

$ ghc --make tcp-server.hs
[1 of 1] Compiling Main ( tcp-server.hs, tcp-server.o )
Linking tcp-server ...

$ ghc --make tcp-client.hs
[1 of 1] Compiling Main ( tcp-client.hs, tcp-client.o )
Linking tcp-client ...

You can now start the TCP server in a terminal:

$ ./tcp-server
Starting server ...

Then run the TCP client in another terminal:

./udp-client

You will now observe the ‘Hello, world’ message printed in the terminal where the server is running:

$ ./tcp-server
Starting server ...
Hello, world!

The Network.Socket package exposes more low-level socket functionality for Haskell and can be used if you need finer access and control. For example, consider the following UDP (user datagram protocol) client code:

-- udp-client.hs
import Network.Socket

main :: IO ()
main = withSocketsDo $ do
(server:_) <- getAddrInfo Nothing (Just “localhost”) (Just “3000”)
s <- socket (addrFamily server) Datagram defaultProtocol
connect s (addrAddress server)
send s “Hello, world!”
sClose s

The getAddrInfo function resolves a host or service name to a network address. A UDP client connection is then requested for the server address, a message is sent, and the connection is closed. The type signatures of getAddrInfo, addrFamily and addrAddress are given below:

ghci> :t getAddrInfo
getAddrInfo
:: Maybe AddrInfo
-> Maybe HostName -> Maybe ServiceName -> IO [AddrInfo]

ghci> :t addrFamily
addrFamily :: AddrInfo -> Family

ghci> :t addrAddress
addrAddress :: AddrInfo -> SockAddr

The corresponding UDP server code is as follows:

-- udp-server.hs

import Network.Socket

main :: IO ()
main = withSocketsDo $ do
(server:_) <- getAddrInfo Nothing (Just “localhost”) (Just “3000”)
s <- socket (addrFamily server) Datagram defaultProtocol
bindSocket s (addrAddress server) >> return s
putStrLn “Server started ...”
handleConnections s

handleConnections :: Socket -> IO ()
handleConnections conn = do
(text, _, _) <- recvFrom conn 1024
putStrLn text
handleConnections conn

The UDP server binds to localhost and starts to listen on Port 3000. When a client connects, it reads a maximum of 1024 bytes of data, prints it to stdout, and waits to accept more connections. You can compile the udp-server.hs and udp-client.hs files using the following commands:

$ ghc --make udp-server.hs
[1 of 1] Compiling Main ( udp-server.hs, udp-server.o )
Linking udp-server ...

$ ghc --make udp-client.hs
[1 of 1] Compiling Main ( udp-client.hs, udp-client.o )
Linking udp-client ...

You can start the UDP server in one terminal:

$ ./udp-server
Server started ...

You can then run the UDP client in another terminal:

$ ./tcp-client

You will now see the ‘Hello, world!’ message is printed in the terminal where the server is running:

$ ./udp-server
Server started ...
Hello, world!

The network-uri module has many useful URI (uniform resource identifier) parsing and test functions. You can install them on Fedora using the following command:

$ cabal install network-uri

The parseURI function takes a string and attempts to convert it into a URI. It returns ‘Nothing’ if the input is not a valid URI, and returns the URI, otherwise. For example:

ghci> :m + Network.URI

ghci> parseURI “http://www.shakthimaan.com”
Just http://www.shakthimaan.com

ghci> parseURI “shakthimaan.com”
Nothing

The type signature of the parseURI function is given below:

ghci> :t parseURI
parseURI :: String -> Maybe URI

A number of functions are available for testing the input URI as illustrated in the following examples:

ghci> isURI “shakthimaan.com”
False

ghci> isURI “http://www.shakthimaan.com”
True

ghci> isRelativeReference “http://shakthimaan.com”
False

ghci> isRelativeReference “../about.html”
True

ghci> isAbsoluteURI “http://www.shakthimaan.com”
True

ghci> isAbsoluteURI “shakthimaan.com”
False

ghci> isIPv4address “192.168.100.2”
True

ghci> isIPv6address “2001:0db8:0a0b:12f0:0000:0000:0000:0001”
True

ghci> isIPv6address “192.168.100.2”
False

ghci> isIPv4address “2001:0db8:0a0b:12f0:0000:0000:0000:0001”
False

The type signatures of the above functions are as follows:

ghci> :t isURI
isURI :: String -> Bool

ghci> :t isRelativeReference
isRelativeReference :: String -> Bool

ghci> :t isAbsoluteURI
isAbsoluteURI :: String -> Bool

ghci> :t isIPv4address
isIPv4address :: String -> Bool

ghci> :t isIPv6address
isIPv6address :: String -> Bool

You can make a GET request for a URL and retrieve its contents. For example:

import Network
import System.IO

main = withSocketsDo $ do
h <- connectTo “www.shakthimaan.com” (PortNumber 80)
hSetBuffering h LineBuffering
hPutStr h “GET / HTTP/1.1\nhost: www.shakthimaan.com\n\n”
contents <- hGetContents h
putStrLn contents
hClose h

You can now compile and execute the above code, and it returns the index.html contents as shown below:

$ ghc --make get-network-uri.hs
[1 of 1] Compiling Main (get-network-uri.hs, get-network-uri.o )
Linking get-network-uri ...

$ ./get-network-uri
HTTP/1.1 200 OK
Date: Sun, 05 Apr 2015 01:37:19 GMT
Server: Apache
Last-Modified: Tue, 08 Jul 2014 04:01:16 GMT
Accept-Ranges: bytes
Content-Length: 4604
Content-Type: text/html
...

You can refer to the network-URI package documentation at https://hackage.haskell.org/package/network-uri-2.6.0.1/docs/Network-URI.html for more detailed information.

The whois Haskell package allows you to query for information about hosting servers and domain names. You can install the package on Ubuntu, for example, using the following command:

$ cabal install whois

The serverFor function returns a whois server that can be queried for more information regarding an IP or domain name. For example:

ghci> :m + Network.Whois

ghci> serverFor “shakthimaan.com”
Loading package array-0.4.0.1 ... linking ... done.
Loading package deepseq-1.3.0.1 ... linking ... done.
Loading package bytestring-0.10.0.2 ... linking ... done.
Loading package old-locale-1.0.0.5 ... linking ... done.
Loading package time-1.4.0.1 ... linking ... done.
Loading package unix-2.6.0.1 ... linking ... done.
Loading package network-2.6.0.2 ... linking ... done.
Loading package transformers-0.4.3.0 ... linking ... done.
Loading package mtl-2.2.1 ... linking ... done.
Loading package text-1.2.0.4 ... linking ... done.
Loading package parsec-3.1.9 ... linking ... done.
Loading package network-uri-2.6.0.1 ... linking ... done.
Loading package split-0.2.2 ... linking ... done.
Loading package whois-1.2.2 ... linking ... done.

Just (WhoisServer {hostname = “com.whois-servers.net”, port = 43, query = “domain “})

You can use the above specific information with the whois1 function to make a DNS (domain name system) query:

ghci> whois1 “shakthimaan.com” WhoisServer {hostname = “com.whois-servers.net”, port = 43, query = “domain “}
Just “\nWhois Server Version 2.0\n\nDomain names in the .com and .net domains can now be registered\n
...

You can also use the whois function to return information on the server as shown below:

ghci> whois “shakthimaan.com”
Just “\nWhois Server Version 2.0\n\nDomain names in the .com and .net domains can now be registered\n
...

The type signatures of the serverFor, whois1 and whois functions are as follows:

ghc> :t serverFor
serverFor :: String -> Maybe WhoisServer

ghci> :t whois1
whois1 :: String -> WhoisServer -> IO (Maybe String)

ghci> :t whois
whois :: String -> IO (Maybe String, Maybe String)

The dns package provides a number of useful functions to make DNS queries and handle the responses. You can install the same on Ubuntu, for example, using the following commands:

$ sudo apt-get install zlib1g-dev
$ cabal install dns

A simple example of finding the IP addresses for the haskell.org domain is shown below:

ghci> import Network.DNS.Lookup
ghci> import Network.DNS.Resolver

ghci> let hostname = Data.ByteString.Char8.pack “www.haskell.org”

ghci> rs <- makeResolvSeed defaultResolvConf

ghci> withResolver rs $ \resolver -> lookupA resolver hostname
Right [108.162.203.60,108.162.204.60]

The defaultResolvConf is of type ResolvConf and consists of the following default values:

-- * ‘resolvInfo’ is ‘RCFilePath’ \”\/etc\/resolv.conf\”.
--
-- * ‘resolvTimeout’ is 3,000,000 micro seconds.
--
-- * ‘resolvRetry’ is 3.

The makeResolvSeed and withResolver functions assist in making the actual DNS resolution. The lookupA function obtains all the A records for the DNS entry. Their type signatures are shown below:

ghci> :t makeResolvSeed
makeResolvSeed :: ResolvConf -> IO ResolvSeed

ghci> :t withResolver
withResolver :: ResolvSeed -> (Resolver -> IO a) -> IO a

ghci> :t lookupA
lookupA
:: Resolver
-> dns-1.4.5:Network.DNS.Internal.Domain
-> IO
(Either
dns-1.4.5:Network.DNS.Internal.DNSError
[iproute-1.4.0:Data.IP.Addr.IPv4])

The lookupAAAA function returns all the IPv6 ‘AAAA’ records for the domain. For example:

ghci> withResolver rs $ \resolver -> lookupAAAA resolver hostname
Right [2400:cb00:2048:1::6ca2:cc3c,2400:cb00:2048:1::6ca2:cb3c]

Its type signature is shown below:

lookupAAAA
:: Resolver
-> dns-1.4.5:Network.DNS.Internal.Domain
-> IO
(Either
dns-1.4.5:Network.DNS.Internal.DNSError
[iproute-1.4.0:Data.IP.Addr.IPv6])

The MX records for the hostname can be returned using the lookupMX function. An example for the shakthimaan.com website is as follows:

ghci> import Network.DNS.Lookup
ghci> import Network.DNS.Resolver

ghci> let hostname = Data.ByteString.Char8.pack “www.shakthimaan.com”

ghci> rs <- makeResolvSeed defaultResolvConf

ghci> withResolver rs $ \resolver -> lookupMX resolver hostname
Right [(“shakthimaan.com.”,0)]

The type signature of the lookupMX function is as follows:

ghci> :t lookupMX
lookupMX
:: Resolver
-> dns-1.4.5:Network.DNS.Internal.Domain
-> IO
(Either
dns-1.4.5:Network.DNS.Internal.DNSError
[(dns-1.4.5:Network.DNS.Internal.Domain, Int)])

The name servers for the domain can be returned using the lookupNS function. For example:

ghci> withResolver rs $ \resolver -> lookupNS resolver hostname
Right [“ns22.webhostfreaks.com.”,”ns21.webhostfreaks.com.”]

The type signature of the lookupNS function is shown below:

ghci> :t lookupNS
lookupNS
:: Resolver
-> dns-1.4.5:Network.DNS.Internal.Domain
-> IO
(Either
dns-1.4.5:Network.DNS.Internal.DNSError
[dns-1.4.5:Network.DNS.Internal.Domain])

You can also return the entire DNS response using the lookupRaw function as illustrated below:

ghci> :m + Network.DNS.Types

ghci> let hostname = Data.ByteString.Char8.pack “www.ubuntu.com”

ghci> rs <- makeResolvSeed defaultResolvConf

ghci> withResolver rs $ \resolver -> lookupRaw resolver hostname A

Right (DNSFormat
{header = DNSHeader
{identifier = 29504,
flags = DNSFlags
{qOrR = QR_Response,
opcode = OP_STD,
authAnswer = False,
trunCation = False,
recDesired = True,
recAvailable = True,
rcode = NoErr},
qdCount = 1,
anCount = 1,
nsCount = 3,
arCount = 3},
question = [
Question
{qname = “www.ubuntu.com.”,
qtype = A}],
answer = [
ResourceRecord
{rrname = “www.ubuntu.com.”,
rrtype = A,
rrttl = 61,
rdlen = 4,
rdata = 91.189.89.103}],
authority = [
ResourceRecord
{rrname = “ubuntu.com.”,
rrtype = NS,
rrttl = 141593,
rdlen = 16,
rdata = ns2.canonical.com.},
ResourceRecord
{rrname = “ubuntu.com.”,
rrtype = NS,
rrttl = 141593,
rdlen = 6,
rdata = ns1.canonical.com.},
ResourceRecord
{rrname = “ubuntu.com.”,
rrtype = NS,
rrttl = 141593,
rdlen = 6,
rdata = ns3.canonical.com.}],
additional = [
ResourceRecord
{rrname = “ns2.canonical.com.”,
rrtype = A,
rrttl = 88683,
rdlen = 4,
rdata = 91.189.95.3},
ResourceRecord
{rrname = “ns3.canonical.com.”,
rrtype = A,
rrttl = 88683,
rdlen = 4,
rdata = 91.189.91.139},
ResourceRecord
{rrname = “ns1.canonical.com.”,
rrtype = A,
rrttl = 88683,
rdlen = 4,
rdata = 91.189.94.173}]})

Please refer to the Network.DNS hackage Web page at https://hackage.haskell.org/package/dns for more information.

LEAVE A REPLY

Please enter your comment!
Please enter your name here