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.
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:
The fudgettype Host = String type Port = Int
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 withsocketTransceiverF :: Host -> Port -> F String String
stdinFin Section 14.1).
The simplest possible client we can write is perhaps a telnet client:
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.telnetF host port = stdoutF >==< socketTransceiverF host port >==< stdinF
A simple fudget to create servers is
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 fromsimpleSocketServerF :: Port -> F (Int,String) (Int,String)
simpleSocketServerFare 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.
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:
The client program needs to know the typed address of the server:data TPort c s
In these types, c and s stand for the type of messages that the client and server transmit, respectively.data TServerAddress c s
To make a typed port, we apply the function
tPort on a port
ThetPort :: (Show c, Read c, Show s, Read s) => Port -> TPort c s
Readcontexts 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:
For example, suppose we want to write a server that will run on the hosttServerAddress :: TPort c s -> Host -> TServerAddress c s
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
A typed server address can be used in the client program to open a socket to the server by means ofthePort :: TPort Int String thePort = tPort 8888 theServerAddr = tServerAddress thePort "animal"
Again, thetSocketTransceiverF :: (Show c, Read s) => TServerAddress c s -> F c (Maybe s)
Readcontexts appear, since this is where the actual conversion from and to text strings occurs. The fudget
tSocketTransceiverFwill output an incoming message m from the server as
Just m, and if the connection is closed by the other side, it will output
In the server, we will wait for connections, and create client
handlers when new clients connect. This is accomplished with
SotSocketServerF :: (Read c, Show s) => TPort c s -> (F s (Maybe c) -> F a (Maybe b)) -> F (Int,a) (Int,Maybe b)
tSocketServerFtakes 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
tSocketServerFwill 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
simpleTSocketServerF :: (Read c, Show s) => TPort c s -> F (Int,s) (Int,Maybe c) simpleTSocketServerF port = tSocketServerF port id
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:
We have picked an arbitrary port number. Now, if the client is as follows:module MyPort where myPort :: TPort Int String myPort = tPort 9000
and the servermodule Main where -- Client import MyPort ... main = fudlogue (... tSocketTransceiverF 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.module Main where -- Server import MyPort ... main = fudlogue (... tSocketServerF myPort ... )
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:
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).module MyPort where myPort :: (Read a, Show a) => TPort a String -- Wrong! myPort = tPort 9000
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.
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
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
module MyPort where import Fudgets type SymTPort a = TPort a a myPort :: SymTPort ((String,Int),String) -- e.g. (("Torsdag",13),"Doktorandkurs:") port = tPort 8888