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
stdinF
in 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)
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.
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
number:
ThetPort :: (Show c, Read c, Show s, Read s) => Port -> TPort c s
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:
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 byA 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"
tSocketTransceiverF
:Again, thetSocketTransceiverF :: (Show c, Read s) => TServerAddress c s -> F c (Maybe s)
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
:
SotSocketServerF :: (Read c, Show s) => TPort c s -> (F s (Maybe c) -> F a (Maybe b)) -> F (Int,a) (Int,Maybe b)
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
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 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