This exercise should be done in groups of two or three. You are required to demonstrate working programs and to submit well-commented code.
This sheet contains a set of exercises that are intended to lead you through the steps necessary to master the use of UNIX datagram sockets to build a form of remote procedure call. You should consult CDK3 Sections 4.2, 4.3, 4.4 and 4.6 whilst doing these exercises. The following Notes on Sockets in Unix may be also be useful - they are available in a downloadable PDF file . You should use either C++ or ANSI standard C function prototypes. Definitions for use with for C++ and definitions for use with C are given as appendices at the end of this page.
Before working on the coursework itself, you should perform the following exercise, which is not examined. It involves compiling and running an existing sockets program in UDPsock.c . Study it carefully. Like the subsequent programs you are asked to write for this exercise, it uses UDP (not TCP) sockets. You may borrow any code you feel necessary from this program, such as MakeLocalSA, MakeDestSA, MakeReceiverSA, printSA, anyThingThere and use them as utilities in the following exercises. Also note the `include' files needed and the function prototype for gethostbyname.
Warm-up exercise. Take a copy of the file
UDPsock.c .
This file comprises a C program containing the examples given in the Notes on Sockets in Unix. It can be run either as a "sender" - a process that calls the sender procedure or as a "receiver" - a process that calls the receiver procedure. You should compile it and then run it as follows:
Log in to two computers. On one of them, run the program as a "receiver" by giving it the argument "r". On the other one, run the program as a sender by giving it the four arguments: "s", the name of the computer where you are running the receiver and two messages. The program prints out the socket addresss used and the messages received. |
You are required to produce client and server programs based on the procedures DoOperation , GetRequest and SendReply (see CDK3 Section 4.4). These operations have been simplified so that client and server exchange messages consisting of strings. In Exercise 2, you will put the arguments of DoOperation (such as the procedure identifier) into a request message.
The client and server behave as follows:
Client: this takes the name of the server computer as an argument. It repeatedly requests a string to be entered by the user, and uses DoOperation to send the string to the server, awaiting a reply. Each reply should be printed out.
Server: repeatedly receives a string using GetRequest, prints it on the screen and replies with SendReply. The server exits when the string consists of the single character `q'.
Use the following (or equivalent C) definitions for Status and SocketAddress , which are in C++.
enum Status { Ok, // operation successful Bad, // unrecoverable error Wronglength // bad message length supplied }; typedef sockaddr_in SocketAddress;
sends a given request message to a given socket address and blocks until it returns with a reply message. | |
Each procedure returns a value of type Status which reports on the success of its execution. For example, if the sendto or recvfrom system calls return negative values, your procedures should return a Status value of Bad.
sends a given message through a socket to a given socket address. | |
receives a message and the socket address of the sender into two arguments. |
Use the recommended definitions for SocketAddress and Message and the recommended prototypes for UDPsend and UDPreceive. In the C++ defintions, the latter are to be found in class Socket.
You will want to run server processes that can coexist with other people's processes in the same computer. You need to select an agreed port number for the server to receive messages from clients. Two servers on the same computer cannot use the same local port number. You will therefore want to choose a port number that is sure to be different from other people's port numbers. If everybody takes the first unreserved port number and adds their uid, there should be no such clashes - i.e. :
aPort = IPPORT_RESERVED + getuid() ;
As part of this exercise, you should design an experiment to find out whether you can cause datagrams to be dropped. Describe the experiment and discuss its results in a comment in your client program.
In this exercise you will create an adaptation of Exercise 1, so that the strings that users type to the clients are arithmetic expressions; and the server evaluates each arithmetic expression and returns the results. Communication is to be by RPC - implemented by you. See CDK3 Section 5.3 for a discussion of RPC. The two operations Stop and Ping are for all services.
Client and server should provide the ability to use Stop and Ping and in addition do arithmetic
Client: Each line typed at the client is to be interpreted as a simple arithmetic operation (e.g. 34+67, 89*54, etc). This requires the expression to be separated into an operation (+, -, *, /) and two non-negative integer arguments.
Server: This must implement an "Arithmetic Service" consisting of a dispatcher and the four operations add, subtract, multiply and divide which should have identical prototypes:
Status op( int , int , int *) ; // the last argument is for the result
The Status value should be extended with extra values required for this service, e.g DivZero.
The code supporting the specific service (The Arithmetic Service) should be implemented as a separate part so that it could be replaced by the code for another service.
You will need to define a new class (in C++) or a struct in C for RPC messages as suggested in the corresponding Appendices.
This exercise requires you to write marshalling and unmarshalling function members, which take network ordering into account, by using the functions htonl and ntohl . The recommended prototypes for marshal and unmarshal are given in the definitions. In C++ they are function members of the class RPCMessage.
Two other matters that should be addressed:
Add a timeout in your client program. This should have the effect that if there is no response from the server for several seconds after sending the request message, the client resends the request for up to a small, fixed number of times. This is in case a message was dropped, or the server has crashed. The client should report on its behaviour in these circumstances. Extend Status to allow for the time out.
Timing-out can be done by using the select system call to test whether there is any outstanding input on the socket before calling UDPreceive. The procedure anyThingThere in the example program shows how to do this. You can test your time-out by running the client when the server is not running.
You must demonstrate that your programs work correctly by running two clients and a server on three different computers. You should also report whether you managed to make the server drop any messages, and what experiment you performed to test this.
The result should be an arithmetic server that performs arithmetic operations for several clients, and which behaves sensibly when dealing with exceptional conditions.
Demonstrate that after a time out, your client program re-tries a few times and then reports that it cannot contact the server.
The client and server programs should have separate code (unlike the demonstration program UDPsock.c).
Provide the main program for the client and the server. The code for the client should include a comment containing a short write-up describing the experiment for testing the reliability of datagrams.
Please supply the print outs in the following order:
class Message { public: Message(unsigned char *, unsigned int ); // message and length supplied Message(unsigned int ); // length supplied private: unsigned char * data; unsigned int length; };
class Socket { public: Socket(); Socket(int); // port given as argument status UDPsend(UDPMessage *m, SocketAddress * destination); status UDPreceive(UDPMessage **m, SocketAddress * origin); private: int s; SocketAddress * socketAddress; };
class Client: public Socket { public: Client(); // calls constructor Socket() status DoOperation (UDPMessage *callMessage, UDPMessage *replyMessage, SocketAddress * server); };class Server: public Socket { public: Server(int); // calls constructor Socket(int); status GetRequest (UDPMessage *callMessage, SocketAddress * client); status SendReply (UDPMessage *replyMessage, SocketAddress * client); };
enum MessageType { Request, Reply}; class RPCMessage{ public: RPCMessage(MessageType); RPCMessage(MessageType, int, int, int); void marshall( Message ** ); // marshalls self to Message argument void unmarshall(Message *); // unmarshalls from given message to self private: MessageType type; unsigned int requestId; unsigned int procedureId; // e.g.(1,2,3,4) for (+, -, *, /) int arg1, arg2; // arguments/ return parameters };
This class has data members corrseponding to the structure given in CDK3 Figure 4.13, but without the remote object reference which is not needed for RPC. The fields arg1 and arg2 can be used for operation arguments or returned value and status. RPCMessage has two constructors: the first takes a message type as argument and the second takes values of all 4 data members as arguments. The two function members deal with marshalling and unmarshalling. You may add other function members, e.g. a function that executes a dispatcher given as a function argument. You could consider using a static data member for the next requestId.
Note also that you will need to include a definition of the prototypes of the socket system calls in an extern "C" statement in any ".C" file that uses them. This can be achieved by an include line such as the following:#include "/import/GCC/lib/g++-include/sys/socket.h"You may also need the following:
extern "C" { char * inet_ntoa(struct in_addr); }
#define SIZE 1000 typedef struct { unsigned int length; unsigned char data[SIZE]; } Message; typedef enum { OK, /* operation successful */ BAD, /* unrecoverable error */ WRONGLENGTH /* bad message length supplied */ } Status; typedef struct sockaddr_in SocketAddress ;
The prototypes for DoOperation, GetRequest and SendReply (to which you must adhere) are as follows:
Status DoOperation (Message *message, Message *reply, int s, SocketAddress serverSA); Status GetRequest (Message *callMessage, int s, SocketAddress *clientSA); Status SendReply (Message *replyMessage, int s, SocketAddress clientSA);
The prototypes for UDPsend and UDPreceive are as follows:
Status UDPsend(int s, Message *m, SocketAddress destination); Status UDPreceive(int s, Message *m, SocketAddress *origin);
You should use the following definition of an RPC message:
typedef struct { enum {Request, Reply} messageType; /* same size as an unsigned int */ unsigned int RPCId; /* unique identifier */ unsigned int procedureId; /* e.g.(1,2,3,4) for (+, -, *, /) */ int arg1; /* argument/ return parameter */ int arg2; /* argument/ return parameter */ } RPCMessage; /* each int (and unsigned int) is 32 bits = 4 bytes */
The fields arg1 and arg2 can be used for operation arguments or returned value and status.
The prototypes of the marshalling and unmarshalling procedures should be as follows:void marshal(RPCmessage *rm, Message *message); void unMarshal(RPCmessage *rm, Message *message);