Examples of Client Server Systems
These are all common:
- Finger
- Telnet
- ftp
- NFS (Network File System)
- X Window System
- Gopher
- World Wide Web
TCP/IP
The OSI model was devised using a committee process wherein the
standard was set up and then implemented. Some parts of the OSI standard are
obscure, some parts cannot easily be implemented, some parts have not been
implemented.
The TCP/IP protocol was devised through a long-running DARPA project. This
worked by implementation followed by RFCs (Request For Comment). TCP/IP is the
principal Unix networking protocol. TCP/IP = Transmission Control
Protocol/Internet Protocol.
TCP/IP stack
The TCP/IP stack is shorter than the OSI one:
TCP is a connection-oriented protocol, UDP (User Datagram Protocol) is a
connectionless protocol.
IP datagrams
The IP layer provides a connectionless and unreliable
delivery system. It considers each datagram independently of the others. Any
association between datagrams must be supplied by the higher layers.
The IP layer supplies a checksum that includes its own header. The header
includes the source and destination addresses.
The IP layer handles routing through an Internet. It is also responsible for
breaking up large datagrams into smaller ones for transmission and reassembling
them at the other end.
UDP
UDP is also connectionless and unreliable. What it adds to IP is a
checksum for the contents of the datagram and port numbers. These are
used to give a client/server model - see later.
TCP
TCP supplies logic to give a reliable connection-oriented protocol
above IP. It provides a virtual circuit that two processes can use to
communicate.
Addressing
Internet adddresses
In order to use a service you must be able to find
it. The Internet uses an address scheme for machines so that they can be
located.
The address is a 32 bit integer which gives the IP address. This encodes a
network ID and more addressing. The network ID falls into various classes
according to the size of the network address.
Network address
Class A use 8 bits for the network address with 24 bits
left over for other addressing. Class B uses 16 bit network addressing. Class C
uses 24 bit network addressing and class D uses all 32.
The University of Canberra is registered as a Class B network, so we have a
16 bit network address with 16 bits left to identify each machine.
Subnet address
Internally, the Uni network is divided into subnetworks.
8 bits are used for this. Building 11 is currently on one subnetwork.
Host address
8 bits are finally used for host addresses within out
subnet. This places a limit of 256 machines that can be on the subnet.
Total address
The 32 bit address is usually written as 4 integers
separated by dots
Symbolic names
Each host has a name. This can be found from the user
level command hostname
A symbolic name for the network also exists. For our network it is
``canberra.edu.au''. The the symbolic network name for any host is formed from
the two: birch.canberra.edu.au
Programming interface
The BSD library provides some functions for
finding names. char *gethostname(char *name,
int size)
finds the ordinary hostname. struct hostent
*gethostbyname(char *name)
returns a pointer to a structure with two important fields:``char *
h_name'' which is the ``official'' network name of the host and ``char
**h_addr_list'' which is a list of TCP/IP addresses. The following program
prints these: #include
#include
#include
#include
#include
#include
#define SIZE (MAXHOSTNAMELEN+1)
int main(void)
{
char name[SIZE];
struct hostent *entry;
if (gethostname(name, SIZE)
!= 0) {
fprintf(stderr,
"unknown name\n");
exit(1);
}
printf("host name is %s\n",
name);
if ((entry =
gethostbyname(name)) ==
NULL) {
fprintf(stderr,
"no host name info\n");
exit(2);
}
printf("offic. name: %s\n",
entry->h_name);
printf("address: %s\n",
inet_ntoa(
(struct in_addr *)entry->h_addr_list[0]));
exit(0);
}
This programming interface uses a number of standard files: /etc/hostname
to find the name, /etc/rhosts to find the network address (or a name server) if
it can't find it there.
Port addresses
A service exists on a host, and is identified by its
port. This is a 16 bit number. To send a message to a server you send it to the
port for that service of the host that it is running on. This is not
location transparency!
Certain of these ports are ``well known''. They are listed in the file
/etc/services. For example,
- ftp: 21/tcp
- time: 37/tcp
- time: 37/udp
- finger 79/tcp
Ports in the region 1-255 are reserved by TCP/IP.
The system may reserve more. User processes may have their own ports above 1023.
The function ``getservbyname'' can be used to find the port for a service
that is registered in /etc/services.
Berkeley sockets
When you know how to reach a service via its network
and port IDs, what then? If you are a client you need an API that will allow you
to send messages to that service and read replies from it.
If you are a server, you need to be able to create a port and listen at it.
When a message comes in you need to be able to read and write to it.
Berkeley sockets are the BSD Unix system calls for this. They are part of the
BSD Unix kernel. They have also been adopted by the PC world. They form the
lowest practical level of doing client/server on both DOS and Unix. They form
the highest common denominator for DOS and Unix (changing rapidly).
Data representation
Some computers are ``big endian''. This refers to
the representation of objects such as integers within a word. A big endian
machine stores them in the expected way: the high byte of an integer is stored
in the leftmost byte, while the low byte of an integer is stored in the
rightmost byte. A Sun Sparc is big endian. So the number 5 + 6 * 256 would be
stored as
A
``little endian'' machine stores them the other way. The 386 is little endian.
If a Sparc sends an integer to a 386, what happens? The 386 sees 5 + 6 * 256
as 5 *16777216 + 6 * 65536
To avoid this, two communicating machines must agree on data
representation.
The Sun RPC uses a format known as XDR, which just happens to be the format
that doesn't require any conversions for Suns. However, if two 386s are
communicating then each of them will have to keep swapping bytes both on receipt
and send.
The OSF DCE uses native format, with the receiving machine swapping
bytes if needed. This section describes the Unix BSD networking API for IP as in
WR Stevens ``Unix Network Programming.''
Byte ordering
To handle byte ordering for non-standard size integers
there are conversion functions
- htonl - host to network, long int
- htons - host to network, short int
- ntohl - network to host, long int
- ntohs - network to host, short int
Address conversion
These functions convert to and from the ``dotted''
addresses as in 137.92.11.1 to 32 bit integer addresses: #include
#include
#include
#include
unsigned long
inet_addr(char *ptr)
char *
inet_ntoa(struct in_addr in)
(The structure in_addr has only one field which is the 32 bit IP address.)
Addresses
The address of an IP service given is using a structure #include
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
}
Example: The finger service (port 79) on machine 137.92.11.1 is
given by struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(79);
addr.sin_addr.s_addr =
inet_addr("137.9.2.11.1");
Sockets
A socket is a data structure maintained by a BSD-Unix system to
handle network connections.
A socket is created using the call ``socket''. It returns an integer that is
like a file descriptor: it is an index into a table and ``reads'' and ``writes''
to the network use this ``socket file descriptor''.
#include
#include
int socket(int family,
int type,
int protocol);
Here ``family'' will be AF_INET for IP communications, ``protocol'' will
be zero, and ``type'' will depend on whether TCP or UDP is used.
Two processes wishing to communicate over a network create a socket each.
These are similar to two ends of a pipe - but the actual pipe does not yet
exist.
Connection oriented (TCP)
One process (server) makes its socket known
to the system using ``bind''. This will allow other sockets to find it.
It then ``listens'' on this socket to ``accept'' any incoming messages.
The other process (client) establishes a network connection to it, and then
the two exchange messages.
As many messages as needed may be sent along this channel, in either
direction.
TCP time client
Each machine runs a TCP server on port 13 that returns
in readable form the time on that particular machine. All that a client has to
do is to connect to that machine and then read the time from that
machine. /* TCP client that finds the
time from a server */
#include
#include
#include
#include
#define SIZE 1024
char buf[SIZE];
#define TIME_PORT 13
int main(int argc,
char *argv[])
{
int sockfd;
int nread;
struct sockaddr_in serv_addr;
if (argc != 2) {
fprintf(stderr,
"usage: %s IPaddr\n",
argv[0]);
exit(1);
}
if ((sockfd =
socket(AF_INET,
SOCK_STREAM, 0))
< 0) {
perror(NULL);
exit(2);
}
serv_addr.sin_family =
AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(argv[1]);
serv_addr.sin_port =
htons(TIME_PORT);
if (connect(sockfd,
&serv_addr,
sizeof(serv_addr))
< 0) {
perror(NULL);
exit(3);
}
nread = read(sockfd, buf,
SIZE);
write(1, buf, nread);
close(sockfd);
exit(0);
}
Example: If the program is compiled to ``tcptime'', find the time
in various places by
- tcptime 139.130.4.6
- tcptime 192.76.144.75
- tcptime 146.169.22.37
TCP time server
The real time server can only be started by the system
supervisor (usually at boot time) asss the time port is reserved. To run the
following code yourself, change the time port to say 2013. int main(int argc,
char *argv[])
{
int sockfd;
int nread;
struct sockaddr_in serv_addr,
client_addr;
time_t t;
if ((sockfd =
socket(AF_INET,
SOCK_STREAM, 0))
< 0) {
perror(NULL);
exit(2);
}
serv_addr.sin_family =
AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(INADDR_ANY);
serv_addr.sin_port =
htons(TIME_PORT);
listen(sockfd, 5);
for (;;;) {
client_sockfd =
accept(sockfd,
&client_addr,
&len);
time(&t);
sprintf(buf, "%s",
asctime(localtime(t)));
len = strlen(buf) + 1;
write(1, buf, len);
close(client_sockfd);
}
}
Connectionless (UDP)
In a connectionless protocol both sockets have to
make their existence known to the system using ``bind''. This is because each
message is treated separately, so the client has to find the server each time it
sends a message and vice versa.
When bind is called it binds to a new port - it cannot bind to one already in
use. If you specify the port as zero the system gives you a currently unused
port.
Because of this extra task on each message send, the processes do not use
read/write but recvfrom/sendto. These functions take as parameters the socket to
write to, and the address of the service on the remote machine.
Time client (UDP)
The UDP time server requires a datagram to be sent to
it. It ignores the contents of the message but uses the return address to
send back a datagram containing the time. /* UDP client for time */
#include
#include
#include
#include
#include
#define SIZE 1400
char buf[SIZE];
#define TIME_PORT 13
int main(int argc,
char *argv[])
{
int sockfd;
int nread;
struct sockaddr_in serv_addr,
client_addr;
int len;
if (argc != 2) {
fprintf(stderr,
"usage: %s IPaddr\n",
argv[0]);
exit(1);
}
if ((sockfd =
socket(AF_INET,
SOCK_DGRAM, 0))
< 0) {
perror(NULL);
exit(2);
}
client_addr.sin_family =
AF_INET;
client_addr.sin_addr.s_addr =
htonl(INADDR_ANY);
client_addr.sin_port =
htons(0);
serv_addr.sin_family =
AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(argv[1]);
serv_addr.sin_port =
htons(TIME_PORT);
if (bind(sockfd,
&client_addr,
sizeof(client_addr))
< 0) {
perror(NULL);
close(sockfd);
exit(3);
}
len = sizeof(serv_addr);
sendto(sockfd, buf, 1, 0,
&serv_addr, len);
nread = recvfrom(sockfd, buf,
SIZE, 0,
&client_addr,
&len);
write(1, buf, nread);
close(sockfd);
exit(0);
}
Socket controls
Sockets are treated by the O/S as devices and so there
are a variety of device driver controls that can be used (see later). For
example, the command ``fcntl'' can be used to make a socket non-blocking, and
``select'' can be used to test if a socket (device) has input or output pending.
In addition, ``getsockopt'' and ``setsockopt'' can be used for more specific
socket control:
- broadcast - allowed for datagrams
- keepalive - periodic transmits on connected sockets
If a read or
write does not return, it should timeout. What should the time limit be? On your
own machine or on your local network it should be in milliseconds. To Melbourne
in seconds, whereas to Scandinavia it should probably be minutes.
Timeout algorithms should adjust the time according to the curent trip time
in some manner. They can be implemented using timer signals.
Protocol Design
Some parameters are
- Broadcast vs point to point.
Broadcast must be UDP or the more
experimental MBONE. Point to point could be either TCP or UDP.
- Stateful vs stateless.
Is it reasonable for one side to maintain state
about the other side? It is often simpler to do so, but what happens if
something crashes?
- Reliable vs unreliable.
- Replies needed.
If a reply is needed, how do you handle a lost reply?
Timeouts may be used.
- Data format
MIME or byte level.
- Bursty or steady stream.
Ethernet and the Internet are best at bursty
traffic. Steady stream is needed for video streams. ATM will solve this.
- Synchronisation required. Does the data need to be synchronised with
anything? e.g. video and voice.
The Common Client Interface
CCI is a new interface with Mosaic 2.5 for
X. It is designed to allow control of a Mosaic browser so that it can be told to
fetch documents, and also so that Mosaic can tell an application what documents
it has fetched.
An example use of this is for a teacher/student environment, where a teacher
is using an instance of Mosaic, and the student instances are all mirroring the
master instance. One possible architecture for this is where the master is told
about the students (somehow) and then sends information to each of them whenever
it does anything. The student instances in turn will have to listen and obey the
master. This is a case of point to multipoint.
While it solves the problem, it has a lot of ``special case'' feel to it. It
means that the master must be able to maintain a list of clients which have to
be instances of Mosaic for it to work (why must it be limited in this way?).
Similarly, the student instances must be listening to something that must be
Mosaic again. How should they find each other given these types of constraints.
The requirements also rule out another related use: an instance of Mosaic
displaying documents as told to by a script file, for automated demonstrations.
Should this mechanism also know about files as well as networks?
A more general solution is for an instance of Mosaic to be able to tell an
arbitrary application which documents it is fetching, and for an arbitrary
application to be able to tell Mosaic to fetch some documents. The
teacher/student will then be solved by a special-purpose application sitting
between teacher and student instances of Mosaic.
The protocol is an ASCII based one, compliant with RFC 822. Data is passed in
compliance with the MIME 1.0 specification. Command lines are terminated with a
CR-LF. The browser (Mosaic) is regarded as the server, any application talking
to it is the client. The primary components are
GET <url>
SEND ANCHOR [STOP]
DISCONNECT
Each command sent is acknowledged. The acknowledgements are of the form 1xx - informative message
2xx - command ok
3xx - command ok + additional output
4xx - client error
5xx - server error
Whenever the user selects an anchor for Mosaic to display, it will also
send this information to all clients that have sent SEND ANCHOR. The format of
these messages is 301 ANCHOR <url>
The xwebteach application sends a SEND ANCHOR to the teacher Mosaic. Each
time it receives an anchor by a 301 ANCHOR <url>, it sends this on to each
student Mosaic that is connected by GET <url>.
The changes made to Mosaic are quite general. It needs to maintain a list of
clients that it is connected to and that are active in wanting URL information
sent. Each time a URL is selected, it uses this list to send the anchor. In
addition, it must also receive messages from each port that used by an
application.
There is no registration part to the protocol. It is assumed that the user of
xwebteach and all servers will set up the common ports and machines that are
used. Connections are set up within the CCI library and all error checking is
done internally to this library.
The security side of CCI at present is very weak - almost non-existent.
Mosaic by default starts off by not listening on any port. The user selects by
menu which port to listen on. It will only listen on one port so can be sent
messages from only one client. Because the user has choice of port number and
whether listening is enabled, the chances are low that an intruder will get in.
At worst though, it will only be able to monitor your choice of URL's, or select
its own.
The X Session Manager
The X Session Manager is a new protocol
introduced in X11R6. It allows one to capture the entire state of your current X
Windows environment, so that it can be restored to the same point later. The
intent is that you can save session state before logging off, and have it
restored when logging on.
The heart of this protocol is the SaveYourself command sent from the session
manager (SM) to any client (application) that it is managing. It can also send a
Die command to terminate any client, which it will do when the session is
finishing. It must also be able to query the client for how to start it up again
later, for its RestartCommand.
Clients are expected to locate the server in some unspecified manner. The
sample implementation uses a Unix environment variable set by the session
manager, and then read by each client that is forked by the session manager.
When a client finds the session manager it informs it that it wants to be
managed by sending a RegisterClient. The SM replies with a RegisterClientReply.
There are actually two possibilities by the time we get to this point: the
client is starting for the first time, or is being restarted. How they tell the
difference is that the client is expected to maintain state: it keeps an ID that
will be null on first startup but non-null afterwards. The server sends this ID
as part of the RegisterClientReply. This ID is expected to be part of the
RestartCommand.
When a client is started the first time, no state has been saved about it, so
the SM sends it a SaveYourself message straight away.
When a client receives a SaveYourself message, it is expected to save state
in such a way that it can be restored. Suppose it is an editor, with an unnamed
file in the buffer. It should be able to perform an interaction with the user to
name this file first. Many applications may want to do this. To organise the
order of these requests, the SM will tell a client to have an interaction, but
only if the client has asked for it.
Thus there may be an InteractRequest from the client to SM, and an Interact
message from SM to client. Only after the client sends InteractDone will the SM
continue. Finally, a client should say that its save is complete by
SaveYourselfDone.
This is all complex enough that a state transition diagram is useful on both
client and SM sides. On the client side it is
start:
ICE protocol setup complete ->
register
register:
send RegisterClient ->
collect-id
collect-id:
receive RegisterClientReply ->
idle
idle:
receive Die -> die
receive SaveYourself ->
freeze-interaction
freeze-interaction:
freeze user interaction ->
save-yourself
save-yourself:
send InteractRequest ->
interact-request
send SaveYourselfDone ->
idle
interact-request:
receive Interact ->
interact
interact:
send InteractDone ->
save-yourself
There is a byte-level encoding of the protocol, with a fixed range of data
types, such as one-byte, two-byte, four-byte unsigned integers, and sequences of
integers.
Security is managed by the ICE library (see later). This is able to implement
any security scheme, but the default is the MIT MAGIC-COOKIE scheme.
Libraries and callbacks
A protocol may be designed for one-off use,
where there is only one possible client, and only one possible server.
Alternatively, it may be designed as a reusable library.
Examples of one-off are telnet, ftp. In these cases the protocol is a quite
visible part of the applications, and there will be loops that poll the
connection port as part of the applications.
For a library, there will need to be some interface between the library and
the application in which it used, so that when certain protocol events occur
appropriate application-specific code can be called.
In the X Window system all of the network interface is hidden behind a call
XNextEvent(). This returns each time a message is received, with a data
structure filled in with information about the message. The different data
structures all have a first field set to the type of message. The application
sets up a loop around XNextEvent() and within that loop branches to application
code on the type of event. This is similar to the Microsoft Windows event loop,
but of course that does not use a client-server architecture.
while XNextEvent(&event)
switch (event.type) {
...
...
}
An alternative, that goes well with an object-oriented approach, is to use
callback functions. An application registers a function to a message handler.
Each time the message comes in, the library sorts out which message handler to
call internally, and the message handler then calls each function on its
callback list. this reduces the visibility of the protocol details, and just
means that the library interface is reduced to registering callbacks for each
message type.
XtAddCallback(XmNactivateCallback,
my_callback_func,
client_data)
The ICE library
The socket interface is standardised enough so that the
real work - designing and implementing the protocol for any system - should be
all that needs to be done. The ICE (Inter Client Exchange) library is an X
Consortium standard devised for this. This handles the common matters of
authentication, version negotiation, data typing and connection management.
ICE gives connection setup, consisting of byte order exchange, authentication
and connection information exchange. Then protocol negotiation is done. This
agrees on the protocol used, and may also include authentication (multiple
protocols may share a connection). This also registers a callback function that
is used to handle any of the messages of that protocol. When a message arrives
this callback function, belonging to the application, is called to process the
message. There are a variety of functions to read message data of various sizes.
Overheads
For two processes to communicate over the wire, a message has
to be constructed, sent, and recognised at the other end. This overhead may be
quite substantial. It is proposed that this explains why an X Window application
has a long startup time compared to a Microsoft Windows application. It is
suggested that X Windows can be made much quicker on local systems by mapping
the X server into an application's address space and making local procedure
calls.
Remote Procedure Call
Ordinary procedure call
The imperative languages use the procedure as a
means of structuring the language. The language will have conditionals, loops
and procedure calls.
When a procedure is called, it usually makes use of the stack, pushing
parameters onto the stack and reserving space for local variables:
Parameter types
Value parameters
When a parameter is called by value, the actual
value of the parameter is placed on the stack. This can then be used and
modified by the procedure without any change to any original variable.
Reference parameters
The address of the parameter is passed into the
procedure. Any use of the parameter within the procedure uses the address to
access/change the value.
C does not have call by reference, but only call by value. Most other
procedural languages have both.
Remote procedure call
The socket method of network use is a
message-based system, in which one process writes a message to another. This is
a long way from the procedural model.
The remote procedure call is intended to act like a procedure call, but to
act across the network transparently.
The process makes a remote procedure call by pushing its parameters and a
return address onto the stack, and jumping to the start of the procedure. The
procedure itself is responsible for accessing and using the network.
After the remote execution is over, the procedure jumps back to the return
address. The calling process then continues.
Without RPC
Consider how you would implement a procedure to find the
time on a remote machine as a string, using the IP socket calls: int remote_time(char *machine,
char *time_buf)
{ struct sockaddr_in serv_addr;
int sockfd;
int nread;
if (sockfd =
socket(AF_INET,
SOCK_STREAM, 0))
< 0)
return 1;
serv_addr.sin_family =
AF_INET;
serv_addr.sin_addr.s_addr =
inet_addr(machine);
serv_addr.sin_port =
htons(13);
if (connect(sockfd,
&serv_addr,
sizeof(serv_addr))
< 0)
return 2;
nread = read(sockfd,
time_buf,
sizeof(time_buf));
time_buf[nread] = '\0';
close(sockfd);
return 0;
}
This very obviously uses the network.
What RPC should look like?
The network needs to be made invisible, so
that everything looks just like ordinary procedure calls. The calling process
would execute remote_time(machine, time_buf);
All networking should be done by the RPC implementation, such as
connecting to the remote machine. On the remote machine this simple function
gets executed: int remote_time(char *time_buf)
{ struct tm *time;
time_t t;
time(&t);
time = localtime(&t);
strcpy(time_buf,
asctime(time));
return 0;
}
Stubs
When the calling process calls a procedure, the action performed
by that procedure will not be the actual code as written, but code that begins
network communication. It has to conenct to the remote machine, send all the
parameters down to it, wait for replies, do the right thing to the stack and
return. This is the client side stub.
The server side stub has to wait for messages asking for a procedure to run.
It has to read the parameters, and present them in a suitable form to execute
the procedure locally. After execution,it has to send the results back to the
calling process.
- The client calls the local stub procedure. The stub packages up the
parameters into a network message. This is called marshalling.
- Networking functions in the O/S kernel are called by the stub to send the
message.
- The kernel sends the message(s) to the remote system. This may be
connection-oriented or connectionless.
- A server stub unmarshals the arguments from the network message.
- The server stub executes a local procedure call.
- The procedure completes, returning execution to the server stub.
- The server stub marshals the return values into a network message.
- The return messages are sent back.
- The client stub reads the messages using the network functions.
- The message is unmarshalled. and the return values are set on the stack
for the local process.
Data representation
A procedure, for example, may have a short int, a
string and an ordinary int as parameters. How is it to be marshalled so that it
can be correctly unmarshalled at the other end?
For example, the short int could use the first two bytes with the next two
blank, or the other way round. The string could be prefixed by its length or be
terminated by a sentinel value. If the length is sent, should it be an int? A
short int? The ordinary int could be big-endian or little-endian.
The Sun RPC uses a standard format called XDR. The ordering is big-endian and
the minimum size of any field is 32 bits. DCE uses a different format, as does
Xerox Courier.
The message could be formed using implicit typing. That is, only the
values are sent, and it is assumed that both the client and the server know what
the types are meant to be.
Alternatively, there is a type specification ISO language called ASN.1
(Abstract Syntax Notation). This increases message sizes, but is more reliable.
Valid data types
Can you send a pointer value to a remote procedure?
A pointer would refer to an address in the calling procedure's address space.
The remote procedure could not assign a meaning to this as it would not have
access to that address space. So passing pointers is usually not possible.
How about fixed size arrays? Variable sized arrays? Variant records? Floating
point numbers?
Each RPC method must have a list of acceptable data types that can be passed
across the network.
Generating stubs
Common RPC methods use implicit typing. This means
that both the server stub and the client stub must agree exactly on what the
parameter types are for any remote call.
If this was done by hand, then obscure errors would result. So it must be
done automatically.
For a normal procedure call, the compiler is able to look at the
specification of the procedure and do two things: generate the correct code for
placing arguments on the stack when a procedure is called, and generate correct
code for using these parameters within the procedure.
In RPC, this is more complex. The compiler must generate separate
stubs, one for the client stub embedded in the application, and one for the
server stub for the remote machine.
The compiler must know which parameters are in parameters and which
are out. In parameters are sent from the client to server, out parameters
are sent back.
Languages like C have no concept of in or out parameters.
Therefore the compiler cannot be a standard C compiler, and the specification of
the procedures cannot be done in C.
A typical specification might be
int max(in int x,
in int y,
out int z);
A stub compiler would use this to generate the two stubs.
Errors
An ordinary procedure may cause an error by executing an illegal
instruction such as divide by zero or illegal memory reference.
What errors can occur in a remote procedure call?
Can't find the server
If the server is not there, an error indication
should be returned.
In C, it may be possible to return an error value for some functions, but not
for all. Anyway, in Ada, if you have to use a function then you can't use the
parameters like you can with procedures.
In Ada you can raise an exception, or in C generate a signal. However, Pascal
has neither of these concepts.
There is no language-independant solution.
Request to server is lost
This is easy: the client stub sets a timer
that expires if no reply is received. Send the message again.
Unfortunately, what if the server has in fact received the message, but is
just being slow. The request may end up being executed twice or more. This can
be avoided by including an identifier in the message to stop it being retried if
it has already been received.
Reply from server is lost
This is the same type of problem.
Server crashes
In this case, when the server comes back up, it will
probably have no record of having received the message, and will probably do it
again. This can be okay. If the message was a funds transfer message then it
probably won't be.
Preventing this is the at most once problem.
One solution is to not resend messages. In this case you hit the at least
once problem.
Client crashes
This can be guarded against be keeping a record on disk of each RPC message
sent. This slows things down a bit though.
Sun RPC
This is a common RPC mechanism, available on lots of platforms.
it consists of a data represnetation, a set of low-level calls to execute the
procedure remotely, and a higher-level mechanism using a program
rpcgen to gnerate much of the networking code from a specification
file.
XDR
Valid data types supported by XDR include
- int
- unsigned int
- long
- structure
- fixed array
- string (null terminated char *)
RPC specification
A file with a ``.x'' suffix acts as a remote
procedure specification file. It defines functions that will be remotely
executed functions. Functions are restricted: they may take at most one
in parameter, and return at most one out parameter as
the function result.
If you want to use more than one in parameter, you have to wrap
them up in a single structure, and similarly with theout values.
Multiple functions may be defined at once. They are numbered from one
upwards, and any of these may be remotely executed.
The specification defines a program that will run remotely, made up of the
functions. The program has a name, a version number and a unique identifying
number (chosen by you).
For example, a program may have two local functions to find the date on a
machine. The local definitions could be
long bin_date(void);
char *str_date(long);
The program with these specified as remote procedures for a remote machine
would define the two functions bin_date and str_date
in file rdate.x: program RDATE_PROG {
version RDATE_VERS {
long BIN_DATE(void) = 1;
string STR_DATE(long) = 2;
} = 1;
} = 1234567;
Each of these could have one argument.
rpcgen
rpcgen is a program that takes a specification file
as command line parameter and generates C source files that can be used as
client and server stubs.
rpcgen run on rdate.x would generate files
- rdate.h - a header file for both client and server sides.
- rdate_svc.c - a set of stub functions for use on the server side. This
also defines a full a main function that will allow the server side to run as
a server program i.e. it can run and handle requests across the network.
- rdate_clnt.c - a set of stub functions for use on the client side that
handles the remote call.
Functions are generated from the
specification as follows:
- The function name is all lower-case, with ``_1'' appended.
- On the client side the function generated has two parameters, on the
server side it has the same number as in the specification.
- The client side function has either the one parameter of the spec, or a
dummy void * pointer (use NULL) as first parameter.
- On the client side, the second parameter is a ``handle'' created by the C
function
clnt_create().
- On both sides, the function return value is replaced by a pointer to that
function value.
In this example, the rdate_clnt.c would define long *bin_date_1(void *, CLIENT *);
char **str_date_1(long, CLIENT *);
On the server side, rdate_svc.c would define long *bin_date_1(void);
char **str_date_1(long);
Note that the function returns is in terms of a pointer to the original data
type. You are expected to write versions of the functions which use a variable
to store the pointer value returned, and dereference this variable.
On the client side this is
extern CLIENT *handle;
long bin_date(void)
{
long *p;
p = bin_date_1(NULL, handle);
return *p;
}
char *str_date(long l)
{
char **p;
p = str_date_1(l, handle);
return *p;
}
On the server side a static variable must be used to ensure that a valid
address is returned. This is long *bin_date_1(void)
{
static long l;
l = bin_date();
return &l;
}
char **str_date_1(long l)
{
static char *s;
s = str_date(l);
return &s;
}
Finally, the ``handle'' variable on the client side is set by a call
#define RMACHINE "localhost"
CLIENT *handle;
handle = clnt_create(RMACHINE,
RDATE_PROG, RDATE_VERS,
"tcp");
which would be added to the main function before any of the rpc calls.