In this chapter, we will go through some sample powerRPC programs.
In this tutorial, we show you how to write a file server, and then write a client that understands some simple commands similar to those in FTP, such as GET, PUT, LS and CD. Since you are completely relieved from the task of writing networking code by using powerRPC, this assignment become rather trivial - once you get familiar with how RPC works.
Our file server should provide a set of RPC functions that allow a client to access files located at the server machine. The server would open the file on client's behalf, and subsequently, read from or write to the file when such operations are requested from the client.
It will be convenient to make our RPC functions look like existing file I/O functions. For example, we could make our RPC interface resembles the UNIX file I/O system calls, read(), write(), and chdir(), etc. For our demo, let's mimic the C stdio library functions, such as fread() and fwrite(). To distinguish our RPC from the C library functions, we prefix our functions with a lower case 'r', so our RPCes will be rfread(), rfwrite(), etc , we will define a type called rFILE corresponding to the FILE type. So the rfread() function is of the following prototype,
int rfread(void * buf, int size, int nmemb, rFILE* stream);
of course, we should also have a rfopen() function to open a remote file.
Assuming an RPC connection has been established, a client could read from a file "foo" sitting on the server's machine like this,
rFILE * fp; char buf[1024]; fp = rfopen("foo", "r"); rfread(buf, 1, 1024, fp);
Having decide the interface functions, we need to decide the transport protocol. UDP is unreliable and the size of datagrams are usually limited to 8K, so let's choose TCP.
Another decision we need to make is the statefulness of the server. A stateful server maintains the state for the client, whereas in a stateless server, the client supplies all the information (such as current file offset) at every RPC. In our demo, we choose to use a stateful implementation.
The stateful server works as follows. The server maintains a table of opened files, when the client makes a rfopen() call, the server uses fopen() to open the file and record the FILE* pointer in the table, the index to that table entry is return to the client. The client then use the index to reference the file when it makes rfread(), rfwrite() and rfclose() calls later. Obviously, This index must be a field of our rFILE structure.
To implement our client program (simple FTP), we also need an RPC to list the contents of the remote directory. Thus we have the rlistdir() function, which returns a linked list of directory entries in its argument.
Combining these ideas we come up with the following interface declaration,
typedef char [size=strlen(*this)+1, 1024] str1024; typedef char c_arr1024 [size=strlen(*this)+1, 1024]; typedef unsigned long size_T; typedef struct fHandle { int fd; // identify the file on server }rFILE; typedef struct dentry { c_arr1024 name; struct dentry *next; }DENTRY; interface rfile { property TRANSPORT_PROTOCOL= tcp; property INIT_BEFORE_REGISTER= init_fdtable; rFILE* rfopen(in const str1024 filename, in const str1024 mode) { property TIMEOUT_VALUE = 2; }; int rfread (out void [maxsize=size*nm, size=return>0?return:0] ptr, size_T size, size_T nm, in rFILE* stream ); int rfwrite (in const void [maxsize=size*nm, size=nm*size] ptr, size_T size, size_T nm, in rFILE* stream); int rfclose(rFILE*stream) ; int rlistdir(in str1024 path, out DENTRY * pent); int rchdir(in str1024 path) ; } 0x5555 ;
Note we defined the property INIT_BEFORE_REGISTER, which is the function to initialize the table of FILE* pointers to 0 on the server.
You need to write the server implementation of the rfopen(), rfread(), etc. The rfopen() functions merely fopen()s the file, and record the FILE* pointer in the global fd_table and return the index to the client.
The rfread() function is listed below,
int rfread(void *ptr, int size, int nm, rFILE * stream) { FILE *fp; int index = stream->fd; /* first check if the index is valid, if true, get the FILE pointer */ if (index < 0 || index >= MAXFILE || (fp = fd_table[index]) == 0) { fprintf(stderr, "Invalid rFILE pointer!\n"); return -1; } return fread(ptr, size, nm, fp); }
That is it! The rfwrirte() function is defined by replacing the word read in the above with write.
The rchdir() function is even simpler.
int rchdir(char *path) { return chdir(path); }
The rlistdir() function is a bit more complicated, but probaly you can make it simpler by writing a more elegant linked list.
Now the server code is complete. When it is compiled and executed, it makes the six RPC functions to be callable from anywhere in a network. A programmer can make use use of these functions and write whatever applications he/she wants. Given the available RPC functions, we can easily write a file transfer client program which supports GET, PUT, LS and CD commands.
The get_file() function, which reads a remote file src and saves it in file local, is listed below,
int get_file(char *src, char *local) { rFILE *fp = 0; FILE *localfp; char buf[1024]; int cnt; localfp = fopen(local, "w"); if (!localfp) { perror(local); return -1; } fp = rfopen(src, "r"); if (fp == 0) { fprintf(stderr, "Fail to open remote file!\n"); fclose(localfp); return -1; } while ((cnt = rfread(buf, 1, 1024, fp)) > 0) { fwrite(buf, 1, cnt, localfp); } fclose(localfp); rfclose(fp); free(fp); return 0; }
The code above is almost exactly what one would do to copy one local file to another using the fread(), fwrite() functions. The only difference is in the free(fp) call. PowerRPC always allocates the memory for a return value of reference type, it is the caller's responsibility to free that memory.
In this example, we will write a talk program that allow two users to send messages to each other.
A true talk program must have some daemon to notify a user that someone else is trying to initiate talk with him/her. We can certainly write such as server using powerRPC, however, this is not the purpose of this demo, since you can already write such a server after learning the powerRPC from the material above.
Our talk system will be just one program named talk2. One user starts talk2 first, and another user executes the same program to communicate with the first user, knowing he/she is there waiting. Talk2 must be both a server and a client of the same RPC interface, when sending message to the peer, it is a client, when receiving message, the role is reversed, and it becomes the server.
What we are going to do requires more from both powerRPC and the programmer.
The talk RPC interface contains a single function: send_msg().
interface talk2 { property TRANSPORT_PROTOCOL = udp; property GEN_MAIN_FUNC = false; property SERV_CALL_PREFIX = s_; void send_msg( unsigned long sender_program_no, char [size = strlen(sender_host) + 1, 1024] sender_host, char [size = strlen(sender_name) + 1, 1024] sender_name, char [size = strlen(msg) + 1, 4096] msg ) = 1; } = 0x9999;
The send_msg() RPC takes four arguments, the first three identifies the sender, the last one is the message being sent. Since talk2 is both a server and a client, we must define the property SERV_CALL_PREFIX, so the server implementation of the send_msg is actually named s_send_msg. We also need to write our main() function, therefore the GEN_MAIN_FUNC property is set to false.
Our code for s_send_msg() is just a little more than a few printfs.
void s_send_msg(u_long sno, char *host, char *sender, char *msg) { printf("\n...............%s@%d@%s................\n%s", sender, sno, host, msg); printf("....................over..................\n"); talk2_unbind(0); talk2_bind(host, sno, 0, 0); }Besides writing the message from the client onto the terminal, we also establish an RPC connection to the sender.
To receive messages from a peer, the Talk2 program needs to be an RPC server, to send messages it must read stdin and acts as an RPC client. This requires the Talk2 program to do I/O multiplexing, in our case, the Talk2 program must handle the input from both the network communication channel for RPCes and the terminal input. powerRPC accommodates this easily by providing a set of server library functions to set up I/O handlers for a particular file descriptor. Although we could use the INIT_AFTER_REGISTER property to insert all of the code, we take this chance to write the server main() function ourselves using the powerRPC generated code and libraries.
Thus we have the following code,
void handle_stdin(int fd) { char msg[1024]; int cnt; cnt = read(fd, msg, 1023); if (cnt <= 0) exit(1); msg[cnt] = '\0'; send_msg(myprog, myhost, myname, msg); } int main(int argc, char **argv) { fd_set fds; SVCXPRT *mytxp; int serv_sock; int dtsz; char msg[4096]; if (argc < 2) { printf("Usage: %s -n talker -p program# [-c peer -P perr_prog#]\n", argv[0]); exit(0); } getoption(argc, argv); gethostname(myhost, 1023); if (strlen(peer_host)) { if (!talk2_bind(peer_host, peer_no, 0, 0)) { printf("Can not find peer to talk.\n"); exit(1); } } pw_serv_init(); if (!talk2_1_reg(RPC_ANYSOCK, myprog, 1, IPPROTO_UDP)) exit(1); myprog = myprog == 0 ? TALK : myprog; printf("My program number is %d\n", myprog); signal(SIGINT, unset_myprog); pw_serv_input_handler(0, handle_stdin); pw_serv_mainloop(0, 0, 0, 0); }
As you can the see, the handle_stdin() function simply
reads something from stdin and call the send_msg() RPC to
deliver it to a connected peer.
The main server function is more interesting. First, it reads some options. To allow two talk2 program to sit on the same machine, we let them to use whatever program number the user gives (if none is given at the command line option then the one defined in the IDL will be used). The first instance of Talk2 has no one to talk with, however, a subsequent Talk2 can talk to it by supplying the peer hostname and program number, through the ``-c" and ``-P" options respectively. So when the peer_host variable is set, we try to make connection to another Talk2. Then we initialize the server code by calling pw_serv_init() function. Then we register our server by calling the generated talk2_1_reg() function, with the program number defined in myprog variable. We then set up the input handler for stdin. Finally, Talk2 enters its mainloop by calling pw_serv_mainloop() with default arguments.
The first Talk2 program would be started like this
% talk2 -n Mike -p 1234It will announce ``My program number is 1234".
Suppose the first one is on machine host1, we can talk with it by
%talk2 -n Dave -p 2345 -P 1234 -c host1
Now both Mike and Dave can type in messages on their terminal and talk to each other.
Our 100 line Talk2 is not intended to be a replacement for the existing talk program. But you can make it comparable in functionality with talk by writing more code. How good it can be is only limited by your C/C++ skills, not by your knowledge of networking or things like that.
Sometimes, we want an RPC to return immediately, and let the server return the results to the client asynchronously. For example, if it takes the quote server a long time to query a database to get the data, for reasons such as the large number of requests, we should let the client to continue to do other things and get the result later when it is ready.
With powerRPC this can be achieved easily. By setting the NON_BLOCKING
property to true, an RPC call would return before the server function gets
called. To receive the result later, the client can register a collection
service, which is an RPC interface. When the server get the result, it calls
upon this RPC registered by the client
to send the result back.
In this example, we demonstrate how to make our quote RPC asynchronous. We have two RPC interfaces defined in back.idl and over.idl. The client main function looks like this,
#include "over.h" #include "back.h" #include <signal.h> #include <rpc/pmap_clnt.h> main(int argc, char **argv) { char myhost[1024]; if (argc < 3) { printf("usage: %s hostname ticker\n", argv[0]); exit(1); } pw_serv_init(); back_1_unmap(0, 0); if (!back_1_reg(RPC_ANYSOCK, BACK, BACK_1, IPPROTO_TCP)) { printf("fail registering \n"); exit(0); } pw_serv_async_enable(); if (!over_bind(argv[1], 0, 0, 0)) { printf("fail connect to OVER server.\n"); exit(0); } gethostname(myhost, 1023); getQuote(myhost, argv[2]); printf("Returned from 1st getQuote... now do something else ..\n"); getQuote(myhost, argv[2]); printf("Returned from 2st getQuote... now do something else ..\n"); getQuote(myhost, argv[2]); printf("Returned from 3rd getQuote... now do something else ..\n"); while (1) { printf("happily doing ....\n"); sleep(10); } }
In the above, the client set up an asynchronous RPC back.
Then it makes two calls of getQuote(), both return
immediately. This getQuote() call is different from the one we studied
earlier: it has an additional argument myhost.
When the server receives the call,
it gets the quote, and calls back myhost to send the results.
The client enters a while loop, pretending to do other work,
when the server's callback arrives, the client will be interrupted,
and the returnQuote() implementation is called.
The asynchronous behavior of the client's RPC is enabled
by the pw_serv_async_enable() powerRPC library call
{.
Below is the server's code.
#include "back.h" #include <stdlib.h> /* This is an asynchronous call, the client sends over its hostname, after receiving the request, the server replies back to the client immediately. Then client and server reverse their roles, the server make the RPC returnQuote() to send the result to the client, who is supposed to acting as a BACK server waiting for the result. */ void getQuote(char *caller_host, char Ticker[8]) { stkQuote quote; printf("Called by %s\n", caller_host); printf("sleep for a while ....\n"); sleep(6); if (!back_bind(caller_host, 0, 0, 0)) { printf("client is not there!\n"); return; } strcpy(quote.Ticker, Ticker); /* find the quotes */ quote.Low = rand() % 100; quote.High = rand() % 100; quote.Close = rand() % 100; printf("now sending back quote ....\n"); printf("%s %f %f %f\n", quote.Ticker, quote.Low, quote.High, quote.Close); returnQuote("e); back_unbind(0); }
The powerRPC distribution conatins other sample programs for demonstration purposes.