Examples of Client Server Systems

These are all common:

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 <stdio.h> #include <sys/param.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include <netinet/in.h> #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,

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

Address conversion

These functions convert to and from the ``dotted'' addresses as in 137.92.11.1 to 32 bit integer addresses: #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> 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 <netinet/in.h> 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 <sys/types.h> #include <sys/socket.h> 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 <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #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

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 <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #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:

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

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 &lt;url&gt; 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 &lt;url&gt;

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.

  1. The client calls the local stub procedure. The stub packages up the parameters into a network message. This is called marshalling.
  2. Networking functions in the O/S kernel are called by the stub to send the message.
  3. The kernel sends the message(s) to the remote system. This may be connection-oriented or connectionless.
  4. A server stub unmarshals the arguments from the network message.
  5. The server stub executes a local procedure call.
  6. The procedure completes, returning execution to the server stub.
  7. The server stub marshals the return values into a network message.
  8. The return messages are sent back.
  9. The client stub reads the messages using the network functions.
  10. 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

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

Functions are generated from the specification as follows: 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.
Hosted by www.Geocities.ws

1