The remote procedure call mechanism is a simple way to implement client-server applications. The programmer on the client side makes a call, and the programmer on the server side writes a procedure to carry out the desired function. It makes an illusion that you are working in a single address space, some hidden code has to handle all networking.
In client application code a remote procedure call looks like a local procedure call because it is a call to client stub. The stub is supplementary code that supports remote procedure calls. The client stub communicates with server stub using the RPC runtime library which is a set of standard runtime routines that supports all DCE RPC applications.
The server's RPC runtime library services the remote procedure call and hands client information to the server stub. The server stub invokes the remote procedure in the server application. When it finishes the executing the remote procedure its stub communicates output to the client stub. Finally the client stub returns to the client application code.
Developing the distributed application requires three steps. First step is developing interfece which is a collection of the remote procedure declarations. Next steps are developing the client and the server application.
Client application writers use interface definitions to determine how to call remote procedures. Server application writers use interface definetion to determine the data type of the remote procedure's return value, and the number, order, and data types of the argumets.
Figure 1: Simple application: interface development
You can write the interface definition in the Interface Definition Language (IDL). The IDL closely resembles the declaration syntax and semantics of C, but attributes have been added, and these allow information to be sent over a network. When the interface definition is complete, compile it with the IDL compiler (idl) to generate stubs and a C header file that you use to develop the client and server programs.
When client makes a remote procedure call, its UUID has to match that of the server. The RPC runtime library performs this check. More complicated applications use UUIDs for other reasons than identifying an interface.
To generate a UUID in a template for an inrterface definition, type the following command:
% uuidgen -i [ uuid(A0DF7780-4C98-329C-A6B7-070120BDECF1), version(1.0) ] interface INTERFACENAME { }The output goes to the terminal but you can save it in a file with extension .idl and replace the template name INTERFACENAME with a name you choose for the new interface.
Following example shows a simple interface definition. The text consists of a header and a body. The header contains a uuid attribute and the name assigned to the interface. The body specifies all procedures for the interface. There is only one procedure declared in this example. That procedure multiplies every items of the input array by a constant number passed in the other input parameter and returns the result in the output array.
/* FILE NAME: simple.idl */ [ uuid(C985A380-255B-11C9-A50B-08002B0ECEF1) /* Universal Unique ID */ ] interface simple /* interface name is simple */ { const unsigned short ARRAY_SIZE= 10; /* an unsigned integer constant */ typedef long long_array[ARRAY_SIZE]; /* an array type of long integers */ void mularrayi /* The mularrayi multiplies array items by i */ ( [in] long_array a, /* 1st parameter is passed in */ [in] int b, /* 2nd parameter is passed in */ [out] long_array c /* 3rd parameter is passed out */ ); }You can define constants for type definitions and application code, like ARRAY_SIZE in this example. You can define data types for use in other type definitions and procedure declarations. long_array is an example in this interface. It is the type of an array of ten long integers. The indexes of arrys begin at 0, so the index value for this array range from 0 to 9.
The reminder of this interface definition is a procedure declaration. A procedure of type void does not return a value. The in and out attributes are necessary so the IDL compiler knows which direction the data nedds to be sent over the network.
When the interface definition is complete you compile it with the IDL compiler which creates
% idl simple.idlIf you develop the client and server on diferent systems, copies of the interface definition and IDL compiler must exist on both the client and server sides. To generate object code correctly for different systems, compile the interface definition for the client stub on the client system, and the server stub on the server system.
/* FILE NAME: client.c */ /* This is the client module of the simple example. */ #include <stdio.h> #include "simple.h" /* header file created by IDL compiler */ long_array a= {100,200,345,23,67,65,37,73,92,40}; main () { long_array result; int i; mularrayi(a, 10, result); /* A Remote Procedure Call */ puts("muls:"); for(i = 0; i < ARRAY_SIZE; i++) printf("%ld\n", result[i]); }The following picture (figure 2) shows the development of the client program:
Figure 2: Simple application: client development
/* FILE NAME: procedure.c */ /* An implementation of the procedure defined in the simple interface. */ #include <stdio.h> #include "arithmetic.h" /* header file produced by IDL compiler */ void mularrayi(a, b, c) /* implementation of the mularrayi procedure */ long_array a; int b; long_array c; { int i; for(i = 0; i < ARRAY_SIZE; i++) c[i] = a[i] * b; /* array elements are each mul by b */ }You can compile and link the client and remote procedures, and run the resulting program as a local test.
A protocol sequence is an RPC-specific name containing a combination of communication protocols that describe the network communication used between a client and server.
The client needs to identify the server system. The server host is the name or network address of the host on which the server is running.
The client needs to identify a server process on the server host. An endpoint is a number representing a specific server process running on a system. It is typically a port number for TCP or UDP. The help clients to find servers, DCE provides a name service to store binding information. Using the name service the server can store binding information that a client on another system can retreive later. The name service offered with DCE is called Cell Directory Service (CDS). The RPC runtime library contains a set of functions called name service independent (NSI) routines. To store binding information, your server calls an NSI routine. This routine internally communicates with DCS in order to put information into the database.
Distributed applications do not require the name service database. Alternatives to using name service to manage binding information directly to the client and server code, or to create your own application specific method of searching for servers. These alternatives causes more programming problems so we recommend to use name server routines.
Server must make certain information available to clients. A server first registers the interface with RPC runtime library, so the client later know wheter they are compatible with the server. The runtime library creates binding information to identify the server process. The server places the binding information in appropriate databases so that clients can find it. The server places communication and host information in the name service database. The server also places process information (endpoints) in a special database on the server systemn called local endpoint map, which is a database used to store endpoints for servers running on a given system. In the final initalization step, a server waits while listening for remote procedure calls from clients.
Figure 3: Server initializing
Figure 4: Client finding a server
Figure 5: Completing a remote procedure call
/* FILE NAME: server.c */ #include <stdio.h> #include "simple.h" /* header created by the idl compiler */ #include "check_status.h" /* header with the CHECK_STATUS macro */ main () { unsigned32 status; /* error status (nbase.h) */ rpc_binding_vector_t *binding_vector; /*set of binding handles(rpcbase.h)*/ unsigned_char_t *entry_name; /*entry name for name service (lbase.h)*/ char *getenv(); rpc_server_register_if( /* register interface with the RPC runtime */ simple_v0_0_s_ifspec, /* interface specification (simple.h) */ NULL, NULL, &status /* error status */ ); CHECK_STATUS(status, "Can't register interface\n", ABORT); rpc_server_use_all_protseqs( /* create binding information */ rpc_c_protseq_max_reqs_default, /* queue size for calls (rpcbase.h) */ &status ); CHECK_STATUS(status, "Can't create binding information\n", ABORT); rpc_server_inq_bindings( /* obtain this server's binding information */ &binding_vector, &status ); CHECK_STATUS(status, "Can't get binding information\n", ABORT); entry_name = (unsigned_char_t *)getenv("SIMPLE_SERVER_ENTRY"); rpc_ns_binding_export( /* export entry to name service database */ rpc_c_ns_syntax_default, /* syntax of the entry name (rpcbase.h) */ entry_name, /* entry name for name service */ simple_v0_0_s_ifspec, /* interface specification (simple.h) */ binding_vector, /* the set of server binding handles */ NULL, &status ); CHECK_STATUS(status, "Can't export to name service database\n", ABORT); rpc_ep_register( /* register endpoints in local endpoint map */ simple_v0_0_s_ifspec, /* interface specification (simple.h) */ binding_vector, /* the set of server binding handles */ NULL, NULL, &status ); CHECK_STATUS(status, "Can't add address to the endpoint map\n", ABORT); rpc_binding_vector_free( /* free set of server binding handles */ &binding_vector, &status ); CHECK_STATUS(status, "Can't free binding handles and vector\n", ABORT); puts("Listening for remote procedure calls..."); rpc_server_listen( /* listen for remote calls */ rpc_c_listen_max_calls_default, /*concurrent calls to server (rpcbase.h)*/ &status ); CHECK_STATUS(status, "rpc listen failed\n", ABORT); }
/* FILE NAME: check_status.h */ #include <stdio.h> #include <dce/dce_error.h> /* required to call dce_error_inq_text routine */ #include <dce/pthread.h> /* needed if application uses threads */ #include <dce/rpcexc.h> /* needed if application uses exception handlers */ #define RESUME 0 #define ABORT 1 #define CHECK_STATUS(input_status, comment, action) \ { \ if (input_status != rpc_s_ok) \ { \ dce_error_inq_text(input_status, error_string, &error_stat); \ fprintf(stderr, "%s %s\n", comment, error_string); \ if (action == ABORT) exit(1); \ } \ } static int error_stat; static unsigned char error_string[dce_c_error_string_len]; void exit();
The rpc_c_ns_syntax_default argument tells the routine how to interpret an antry name. The entry_name is a string obtained in this example from an environment variable set by the user specifically for this application, SIMPLE_SERVER_ENTRY. The interface handle, simple_v0_0_s_ifspec, associates interface information with the entry name in the name service database. The client later uses name serveice routines to obtain binding information by comparing interface information in the name service database with information about its own interface.
C> cc -c client.c
C> cc -o client client.o simple_cstub.o -ldce -lcma
S> cc -c server.c procedure.c
S> cc -o server server.o procedure.o simple_sstub.o -ldce -lcma
To run the distributed application, follow these steps:
ind01> setenv SIMPLE_SERVER_ENTRY /.:/simple_ind01 ind01> server
C> setenv RPC_DEFAULT_ENTRY /.:/simple_ind01
C> client muls: 1000 2000 3450 230 670 650 370 730 920 400