26 Typed sockets for client/server applications

In this section, we will see how fudgets can be suitable for other kinds of I/O than graphical user interfaces. We will write client/server applications, where a fudget program acts as a server on one computer. The clients are also fudget programs, and they can be run on other computers if desired.

The server is an example of a fudget program which may not have the need for a graphical user interface. However, the server should be capable of handling many clients simultaneously. One way of organising the server is to have a client handler for each connected client. Each client handler communicates with its client via a connection (a socket), but it may also need to interact with other parts of the server. This is a situation where fudgets come in handy. The server will dynamically create fudgets as client handlers for each new client that connects.

We will also see how the type system of Haskell can be used to associate the address (a host name and a port number) of a server with the type of the messages that the server can send and receive. If the client is also written in Haskell, and imports the same specification of the typed address as the server, we know that the client and the server will agree on the types of the messages, or the compiler will catch a type error.

The type of sockets that we consider here are Internet stream sockets. They provide a reliable, two-way connection, similar to Unix pipes, between any two hosts on the Internet. They are used in Unix tools like telnet, ftp, finger, mail, Usenet and also in the World Wide Web.

26.1 Clients

To be able to communicate with a server, a client must know where the server is located. The location is determined by the name of the host (a computer on the network) and a port number. A typical host name is www.cs.chalmers.se. The port number distinguishes different servers running on the same host. Standard services have standard port numbers. For example, WWW servers are usually located on port 80.

The Fudget library uses the following types:

type Host = String
type Port = Int
The fudget
socketTransceiverF :: Host -> Port -> F String String
allows a client to connect to a server and communicate with it.(Footnote: The library also provides combinators that give more control over error handling and the opening and closing of connections.) Chunks of characters appear in the output stream as soon as they are received from the server (compare this with stdinF in Section 14.1).

The simplest possible client we can write is perhaps a telnet client:

telnetF host port = stdoutF >==< 
                    socketTransceiverF host port >==< 
This simple program does not do the option negotiations required by the standard telnet protocol [RFC854,855], so it does not work well when connected to the standard telnet server (on port 23). However, it can be used to talk to many other standard servers, e.g., mail and news servers.

26.2 Servers

Whereas clients actively connect to a specific server, servers passively wait for clients to connect. When a client connects, a new communication channel is established, but the server typically continues to accept connections from other clients as well.

A simple fudget to create servers is

simpleSocketServerF :: Port -> F (Int,String) (Int,String)
The server allows clients to connect to the argument port on the host where the server is running. A client is assigned a unique number when it connects to the server. The messages to and from simpleSocketServerF are strings tagged with such client numbers. Empty strings in the input and output streams mean that a connection should be closed or has been closed, respectively.

This simple server fudget does not directly support a program structure with one handler fudget per client. A better combinator is shown in the next section.

26.3 Typed sockets

Many Internet protocols use messages that are human readable text. When implementing these, the natural type to use for messages is String. However, when we write both clients and severs in Haskell, we may want to use an appropriate data type for messages sent between clients and server, as we would do if the client and server were fudgets in the same program. In this section we show how to abstract away from the actual representation of messages on the network.

We introduce two abstract types for typed port numbers and typed server addresses. These types will be parameterised on the type of messages that we can transmit and receive on the sockets. First, we have the typed port numbers:

data TPort c s
The client program needs to know the typed address of the server:
data TServerAddress c s
In these types, c and s stand for the type of messages that the client and server transmit, respectively.

To make a typed port, we apply the function tPort on a port number:

tPort :: (Show c, Read c, Show s, Read s) => Port -> TPort c s
The Show and Read contexts in the signature tells us that not all types can be used as message types. Values will be converted into text strings before they are transmitted as a message on the socket. This is clearly not very efficient, but it is a simple way to implement a machine independent protocol.

Given a typed port, we can form a typed server address by specifying a computer as a host name:

tServerAddress :: TPort c s -> Host -> TServerAddress c s
For example, suppose we want to write a server that will run on the host animal, listening on port 8888. The clients transmit integer messages to the server, which in turn sends strings to the clients. This can be specified by
thePort :: TPort Int String
thePort = tPort 8888
theServerAddr = tServerAddress thePort "animal"
A typed server address can be used in the client program to open a socket to the server by means of tSocketTransceiverF:
tSocketTransceiverF :: (Show c, Read s) => 
       TServerAddress c s -> F c (Maybe s)
Again, the Show and Read contexts appear, since this is where the actual conversion from and to text strings occurs. The fudget tSocketTransceiverF will output an incoming message m from the server as Just m, and if the connection is closed by the other side, it will output Nothing.

In the server, we will wait for connections, and create client handlers when new clients connect. This is accomplished with tSocketServerF:

tSocketServerF :: (Read c, Show s) => 
       TPort c s 
   ->  (F s (Maybe c) -> F a (Maybe b)) 
   ->  F (Int,a) (Int,Maybe b)
So tSocketServerF takes two arguments, the first one is the port number to listen on for new clients. The second argument is the client handler function. Whenever a new client connects, a socket transceiver fudget is created and given to the client handler function, which yields a client handler fudget. The client handler is then spawned inside tSocketServerF. From the outside of tSocketServerF, the different client handlers are distinguished by unique integer tags. When a client handler emits Nothing, tSocketServerF will interpret this as the end of a connection, and kill the handler.

The idea is that the client handler functions should use the transceiver argument for the communication with the client. Complex handlers can be written with a loopThroughRightF around the transceiver, if desired. In many cases though, the supplied socket transceiver is good enough as a client handler directly. A simple socket server can therefore be defined by:

simpleTSocketServerF :: (Read c, Show s) =>
       TPort c s -> F (Int,s) (Int,Maybe c)
simpleTSocketServerF port = tSocketServerF port id

26.4 Avoiding type errors between client and server

By using the following style for developing a client and a server, we can detect when the client and the server disagree on the message types.

First, we define a typed port to be used by both the client and the server. We put this definition in a module of its own. Suppose that the client sends integers to the server, which in turn can send strings:

module MyPort where
  myPort :: TPort Int String
  myPort = tPort 9000
We have picked an arbitrary port number. Now, if the client is as follows:
module Main where -- Client
  import MyPort
  main = fudlogue (... tSocketTransceiverF myPort ...)
and the server
module Main where -- Server
  import MyPort
  main = fudlogue (... tSocketServerF myPort ... )
then the compiler can check that we do not try to send messages of the wrong type. Of course, this is not foolproof. There is always the problem of having inconsistent compiled versions of the client and the server, for example. Or one could use different port declarations in the client and the server.

Now, what happens if we forget to put a type signature on myPort? Is it not possible then that we get inconsistent message types, since the client and the server could instantiate myPort to different types? The immediate answer is no, and this is because of a subtle property of Haskell, namely the monomorphism restriction. A consequence of this restriction is that the type of myPort cannot contain any type variables. If we forget the type signature, this would be the case, and the compiler would complain. It is possible to circumvent the restriction by explicitly expressing the context in the type signature, though. If we do this when defining typed ports, we shoot ourselves in the foot:

module MyPort where
  myPort ::  (Read a, Show a) => TPort a String -- Wrong!
  myPort = tPort 9000
We said that this was the immediate answer. The real answer is that if the programmer uses HBC, we might get inconsistent message types, since it is possible to give a compiler flag that turns off the monomorphism restriction, which circumvents our check. This is a feature that we have used a lot (see also Section 40.1).

26.5 Example: a group calendar

Outside the lunch room in our department, there is a whiteboard where the week's activities are registered. We will look at an electronic version of this calendar, where people can get a view like this on their workstation (Figure 60).

Figure 60. The calendar client.

The entries in the calendar can be edited by everyone. When that happens, all calendar clients should be updated immediately.

The calendar consists of a server maintaining a database, and the clients, running on the workstations.

26.5.1 The calendar server

The server's job is to maintain a database with all the entries on the whiteboard, to receive update messages from clients and then update the other connected clients. The server consists of the stream processor databaseSP, and a tSocketServerF, where the output from the stream processor goes to tSocketServerF, and vice versa (Figure 61). The program appears in Figure 62.

Figure 61. The structure of server. The small fudgets are client handlers created inside the socket server.

module Main where -- Server

  import Fudgets
  import MyPort(myPort)

  main = fudlogue (server myPort)

  data HandlerMsg a = NewHandler | HandlerMsg a

  server port = loopF (databaseSP [] [] >^^=< 
                       tSocketServerF port clienthandler)

  clienthandler transceiver = 
     putSP (Just NewHandler) (mapSP (map HandlerMsg))
     >^^=< transceiver

  databaseSP cl db = 
       getSP $ \(i,e) ->
       let clbuti = filter (/= i) cl
       in case e of
             Just handlermsg -> case handlermsg of
                NewHandler -> 
                  -- A new client, send the database to it,
                  -- and add to client list.
                  putsSP [(i,d) | d <- db] $
                  databaseSP (i:cl) db
                HandlerMsg s -> 
                  -- Tell the other clients,
                  putsSP [(i',s) | i' <- clbuti] $
                  -- and update database. 
                  databaseSP cl (replace s db)
             Nothing  -> 
               -- A client disconnected, remove it from 
               -- the client list.
               databaseSP clbuti db

  replace :: (Eq a) => (a,b) -> [(a,b)] -> [(a,b)]
  replace = ...

Figure 62. The calendar server.

The stream processor databaseSP maintains two values: the client list cl, which is a list of the tags of the connected clients, and the simple database db, organised as a list of (key,value) pairs. This database is sent to newly connected clients. When a user changes an entry in her client, it will send that entry to the server, which will update the database and use the client list to broadcast the new entry to all the other connected clients. When a client disconnects, it is removed from the client list. The client handlers ( clienthandler) initially announce themselves with NewHandler, then they apply HandlerMsg to incoming messages.The type of the (key,value) pairs in the database is the same as the type of the messages received and sent, and is defined in the module MyPort:

module MyPort where
  import Fudgets
  type SymTPort a = TPort a a
  myPort :: SymTPort ((String,Int),String)
    --          e.g. (("Torsdag",13),"Doktorandkurs:")
  port = tPort 8888