DCE Exercise

Overview of an RPC application

Client/server model for distributed applications embodies a client program (client) and a server program (server). They can running on different machines. The client makes a request to the server, which is a daemon process, and the server sends a response back to the client.

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.

Interface

Interface definition contains some identifying information and a few facts about remote procedures. Each procedure declaration includes the name of the procedure, tha data type of the value it returns (it can be void), and the order of data types of its parameters. An interface definition contains a set of procedure declarations and data types.

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.

Universal Unique Identifiers

When you write a new interface, you must first generate a universal unique identifier (UUID) with uuidgen. A UUID is simple a number that is generated using time and network adress information so that no matter when and where it is generated, it is guaranteed to be unique.

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.

Interface definition

Now we put data type definitions and procedure declarations that need to be shared between server and client. The interface definition includes syntax elements called attributes which specify features needed for distributed applications. Attributes define information about the whole interface or items in the interface including data types, arrays, pointers, structure members, union cases, procedures, and procedure parameters. For example the out attribute specifies an output parameter for a remote procedure. Attributes eclosed in squere brackets.

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.

[in]
A value is passed in to the remote procedure when it is called from the client.
[out]
A value is passed back from the server to the calling procedure on the client when the procedure returns. A parameter with the out direction must be a pointer or array so that the parameter can be passed to the client stub by reference.

When the interface definition is complete you compile it with the IDL compiler which creates

The IDL compiler uses two phases during the compilation. In the preprocessing phase it creates the header file and intermediate C language stub files. In the compilation phase it generates stub object files. To invoke compiler, type:
% idl simple.idl
If 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.

Client

Coding the client is so simple. You will not be able to detect any difference between client and traditional, single-system program. Thanks to DCE, it hides the networking complexity from the client developer. To use all the capabilities of RPC, you must known the RPC runtime routines. This very simple client requires no RPC runtime routines.
/* 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

Remote procedure implementation

The programmer who writes a server must develop all procedures that are declared in the interface definition. Refer to the interface definition (simple.idl) and the header file generated by the IDL compiler (simple.h) for the procedure's parameters and data types. The following example shows the code of the remote procedure of the simple application:
/* 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.

Distributed application environment

When a client make a remote procedure call a binding relationship is established with a server. Binding information is network communacation and location information for a particular server. In the simple application, the client stub and the RPC runtime library automatically find a server during the remote procedure call. Binding information includes the following:
  1. Protocol sequence

    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.

  2. Server host

    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.

  3. Endpoint

    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

    When the server has completed initialization, a client can find it by obtaning its binding information. A remote procedure call in the client application code transfers execution to the client stub. The client stub looks up the information in the name service database to find the server system. The RPC runtime library finds the server process endpoint by looking up the information in the server system's endpoint map. The RPC runtime library uses the binding information to complete the binding of the client to the server.

    Figure 4: Client finding a server

    As shown in the next picture (figure 5), the remote procedure executes after the client finds the server. The client stub puts arguments and other calling information into an internal RPC format that the runtime library tarnsmits over the network. The server runtime library receives the data and transfers it to the stub, which converts it back to a format the application can use. When the remote procedure completes, the conversion precess is reversed. The server stub puts the return arguments into the internal RPC format, and the server runtime library transmits the data back to the client system over the network. The client runtime library receives the data and gives it to the client stub, which converts the data back for use by the application.

    Figure 5: Completing a remote procedure call

    Server program

    /* 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);
    }   
    
    1. Register the interface. Register the interface with the RPC runtime library by using the rpc_server_register_if routine. The simple_v0_0_s_ifspec variable is called an interface handle. It is produced by the IDL compiler and refers to information that applications need, such as the UUID. The CHECK_STATUS macro is defined in the check_status.h header file. It used to interpret status codes from runtime calls:
      /* 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();
      
    2. Create binding information. To create binding information, you must choose one or more network protocol sequences. This application calls rpc_server_use_all_protseqs so that clients can use all available protocols. During this call, the RPC runtime library gathers together information about available protocols, your host, and endpoints to create binding information. The system allocates a buffer for each endpoint, to hold incoming call information. DCE sets the buffer size when you use the rpc_c_protseq_max_calls_default argument.

    3. Obtain the binding information. When creating binding information, the RPC runtime library stores binding information for each protocol sequence. A binding handle is a reference in application code to the information for one poosible binding. A set of server binding handles is called a binding vector. You must obtain this information through the rpc_server_inq_bindings routine to pass the information to other DCE services with other runtime routines.

    4. Advertise the server location in the name service database. In this application the server places (exports) all its binding information in the name service database using the rpc_ns_binding_export runtime routine.

      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.

    5. Register the endpoints in the local endpoint map. The RPC runtime library assigns andpoints to the server as part of creating binding information. The rpc_ep_register runtime routine lets the endpoint map on the local host known that the process running at these endpoints is associated with this interface.

    6. Free the set of binding handles. Memory for the binding handles was allocated with a call to the rpc_server_inq_bindings routine. When you have finished passing binding information to other parts of DCE, release the memory using the rpc_binding_vector_free routine.

    7. Listen for remote calls. Finally, the server must wait for calls to arrive. Each system has a default for the maximum number of calls that a server can accept at one time. DCE sets this maximum when you use the rpc_c_listen_max_calls_default argument.

    Compile, link, and run programs

    1. Compile the client C language source file on the client system (represented by the shell prompt, C>) to generate the client object file.
      C> cc -c client.c
    2. Link the client object file and client stub file with the DCE library to create the executable client file.
      C> cc -o client client.o simple_cstub.o -ldce -lcma
    3. Compile the server C language source file on the server system (represented by the shell prompt S>), including the remote procedure implementation and the server initialization, to create the server object files.
      S> cc -c server.c procedure.c
    4. Link the server object files and server stub file with the DCE library to create the excutable server file.
      S> cc -o server server.o procedure.o simple_sstub.o -ldce -lcma
      
    The client stub obtains the binding information exported by the server to the name service database, and the client RPC runtime library completes the remote procedure call. This automatic binding method requires you to set the RPC-specific environment variable, RPC_DEFAULT_ENTRY, on the client system, so the client stub has an entry name with which to begin looking for the binding information. More advanced applications can use binding methods not dependent on the login environment.

    To run the distributed application, follow these steps:

    1. This server exports binding information to a name service database. Exporting requires read and write access permission to the name service.
    2. Execute the server. For this example, the application-specific environment variable, SIMPLE_SERVER_ENTRY, is set. This variable represents a name for the entry that this server uses when exporting the binding information to the name service database. The usual convention for entry names is to concatenate the interface name and host names. We use an environment variable here because the name can vary depending on which host you use to invoke the server. If you do not supply a valid name, the binding information will not be placed in the name service database, and the program will fail. The prefix /.:/ is required to represent the global portion of a name in the hierachy of the name service database. For this example, assume that the server runs on the system ind01:
      ind01> setenv SIMPLE_SERVER_ENTRY /.:/simple_ind01
      ind01> server
      
    3. For the client system (represented by the C> prompt), set the RPC environment variable RPC_DEFAULT_ENTRY to the name of the server's entry name in the name service database. The client stub can then automatically begin its search to find the server.
      C> setenv RPC_DEFAULT_ENTRY /.:/simple_ind01
    4. After the server is running, execute the client on the client system.
      C> client
      muls:
      1000
      2000
      3450
      230
      670
      650
      370
      730
      920
      400
      
    5. The server is still running and sould be terminated with a kill command or by typing ^C (Ctrl-C).

    Back to my home page.