[Published in Open Source For You (OSFY) magazine, May 2015 edition.]
In this article we shall explore network programming in Haskell.
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 under:
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.
Firstly, 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 ...
You can then run the TCP client in another terminal:
$ ./tcp-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 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 the same 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:
$ cabal install whois
The serverFor function returns a whois server that can be queried for obtaining 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 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 Domain Name System 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 under:
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 nameservers 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 the Network.DNS hackage web page https://hackage.haskell.org/package/dns
for more information.