Distinct ONC RPC/XDR for
.NET Toolkit
Programming
Guide
1.
Introduction This introduction
is addressed to programmers who wish to write their own distributed
applications in .NET using the Distinct® ONC RPC/XDR Toolkit for
.NET. The sections in this programming guide provide an
overview of the following topics.
This document assumes that you
are already familiar with .NET, the basic concepts of RPC
client/server computing and ONC RPC and XDR in particular. It also
assumes a familiarity with the basics of the C binding of ONC RPC.
If you wish to write .NET ONC RPC applications and are not yet
familiar with these concepts you should do further reading before
using the toolkit.
You may need to refer to the reference
part of this manual to understand parts of the examples given in
this programming guide.
This toolkit may be used to
help you out in several different scenarios. Here are
some:
-
Whenever you wish to write
an ONC RPC client in any .NET-language to communicate with existing ONC RPC/XDR
servers.
-
Whenever you wish to
preserve the existing interface of an application written using
the XDR specifications.
-
Whenever you wish to write
C# code that can read and write XDR streams. Other (for example
C, C++ ) applications often use this platform independent encoding
format for serializing data.
-
Whenever you wish to write
simple client/server applications in .NET.
In all these scenarios
Distinct ONC RPC/XDR for .NET will certainly help you to write
portable, high-quality C# code in a minimum amount of
time that can be used from any .NET-language.
This guide discusses how to write client server
applications using the Distinct ONC RPC/XDR Toolkit for .NET. It
first explains how to write an ONC RPC client and then illustrates
how a server is built in .NET. It then explains how to pass RPCs
through Internet firewalls and then describes some other concepts
the understanding of which are fundamental to using this
toolkit.
2. What comes with the Distinct ONC RPC/XDR for
.NET Toolkit? The
Distinct ONC RPC/XDR Toolkit for .NET is a set of tools and
libraries that enables you to write pure .NET ONC RPC clients and
servers. It consists of:
-
The Distinct ONC RPC/XDR
for .NET assembly that contains the ONC RPC/XDR run time
libraries that conform to RFC 1831 (RPC: Remote Procedure Call
Protocol Specification Version 2) and RFC 1832 (XDR: External Data
Representation Standard). The API consists of classes that allow
you to write .NET clients, for standard RPC servers, that can
be embedded in applets and be run by a standard Web browser. It
also allows you to develop ONC RPC stand-alone servers. The
package allows connections over TCP and UDP.
-
An RPCBIND
application that implements the RPCBIND protocol versions 2 (portmapper),
3 and 4.
-
The Rpcgen.NET compiler
that translates standard RPC/XDR interface definition files into
C# classes that implement the client-and server
side stubs and the XDR conversion routines for the described interface.
This means Rpcgen.NET implements a C# language mapping for
.x IDL files.
-
The RPCInfo utility
displays a list of all the services registered with the RPCBIND
on a system. This utility can be used to query the RPCBIND on the local host (by specifying "localhost" or no host
name at all on the command line) or on a remote host (by
specifying the name of the host as a command line argument).
Source code for the RPCInfo utility is included for
reference.
-
A set of demo applications
that
consist of an XDR file which describes an interface of a very
simple server, the server implementation, and demo
applications that invokes the server written in both Visual
Basic.NET and C#.
3. Building a Client with Distinct ONC
RPC/XDR for .NET In this
section we will illustrate how to use this toolkit to build a .NET client
in C#. For our example we will use the scenario of an application
programmer who wants to write a .NET-based front end for an existing ONC RPC
service.
When writing an ONC RPC
application the first thing to do is to write an XDR interface
definition file. This file describes the data types and the
signature of your interface. Distinct ONC RPC/XDR for .NET understands the XDR language as described in RFC 1832 (XDR: External
Data Representation Standard) and many of the extension that have
been added by various vendors over the last decade (like multiple
arguments in one procedure and "in"/"out"/"inout" parameters). By common programming convention XDR
files have a .x extension.
3.1. The XDR Interface Definition
Language File For the
remainder of this guide we will work with the XDR interface
definition language (IDL) file demo.x as shown below. It defines a
simple service that returns a sequence of consecutive lines from a
text:
%/*****************************************
% * Distinct ONC RPC/XDR for .NET Example *
% ****************************************/
struct request {
int from;
int to;
};
struct result {
int number;
string line<>;
struct result *next;
};
typedef result *res_list;
program DEMO_SERVER {
version DEMO_VERSION {
res_list get_line(request) = 1;
} = 1;
} = 0x20000023;
This IDL file has 5 sections:
-
A comment with leading '%' characters.
-
A struct request type declaration.
-
A struct result type declaration.
-
A typedef res_list type declaration.
-
A program definition for the server interface DEMO_SERVER.
The program definition contains
only one procedure (function get_line()). It was a limitation of the
original ONC
RPC that each procedure may have only one input parameter and one
return value. But as these parameters may be of arbitrary complex
type (e.g. a structure), the restriction is only a syntactical one (with
Distinct ONC RPC/XDR for .NET this limitation is gone and you can
also specify multiple arguments per procedure like you would do in
C).
In our example the input type of get_line() (named request) contains
two integers specifying a range of lines (named from and to). The
output type res_list is a pointer to a linked list of result
structures. Each element of this list describes one line of the
result (line number and content string). In this example it is
important to define the typedef result because with Rpcgen.NET a
procedure can have only plain type names in its signature (e.g.
result *get_line(request) is not allowed). But since you can use any
level of typedefs this is not really a semantic
restriction.
3.2 The Server Implementation in
C Although the
functionality of the service might be obvious at this point, we show
here its implementation in C to clarify any questions you may have.
In this simple version the lines of text that can be returned are
hard coded and there is no error processing done (e.g. if a client
requests a non-existing range of lines).
#include <stdio.h>
#include <rpc/rpc.h>
#include "demo.h"
char *text[] = {
"This is the first line of text",
"This is the second line of text",
"This is the third line.",
"This is the 4th line",
"This is the 5th line",
"This is the 6th line.",
"This is the 7th line.",
"This is the 8th line."
};
res_list *get_line_1(request *req)
{
static res_list rl;
result *p, *q;
result **n;
int c;
/* free any dynamically allocated memory from previous requests */
p = rl;
while (p != NULL) {
q = p;
p = p->next;
free(q);
}
/* the handling of the new request starts here*/
rl = NULL;
/* n points to the pointer where the next list element is inserted */
n = &rl;
for (c = req->from; c < req->to; c++) {
/* allocate the next element of the list */
*n = malloc(sizeof(result));
/* assign the line number */
(*n)->number = c;
/* assign the line (string) */
(*n)->line = text[c];
n = &((*n)->next);
*n = NULL;
}
return &rl;
}
To build the server binary with just the demo.x IDL
file and the demo_server.c file above, run the standard ONC RPC
rpcgen (for C) on demo.x and compile and link the generated files demo_svc.c, demo_xdr.c, demo.h, and demo_server.c into one native
binary.
Once you have built the above server and have
it up and running with the ONC RPC RPCBIND, you can proceed to
build a .NET stand-alone client that calls this
server.
3.3 The .NET Client The following pages will take you through the
steps needed to build the .NET client. The first thing to do is to
run the Rpcgen.NET provided with the Distinct ONC RPC/XDR for .NET Toolkit on the XDR file. The
Rpcgen.NET application translates XDR
files into .NET stubs. For a complete description of the Rpcgen.NET
command please refer to the reference portion of this
manual.
You run Rpcgen.NET on demo.x by
typing:
>Rpcgen.NET demo.x
Rpcgen.NET V5.0, Copyright 1997 - 2002 by Distinct Corporation
demo.x
writing: request.cs
writing: result.cs
writing: res_list.cs
writing: demo.cs
The execution of Rpcgen.NET creates
four .NET classes in four files, that implement the
client stub for calling the demo service described: There is one
file per type definition for request.cs, result.cs, and
res_list.cs, and one main stub file called demo.cs.
3.3.1 The
Main Stub We will now take a
look at the demo.cs main stub file that has been
generated.
/****************************************
* Distinct ONC RPC/XDR for .NET Example *
****************************************/
using
com.distinct.rpc;
namespace
demoRPC
{
///
<summary>
/// This class was automatically
generated by Rpcgen.NET from the RPC/XDR file "demo.x" .
///
It defines the client interface to a server implementing the "DEMO_SERVER"
interface.
///
</summary>
public
class demo : NetRPCClient {
///
<summary>
Program ID of the interface.
</summary>
public
const int
DEMO_SERVER = 0x20000023;
///
<summary>
/// Creates and
connects an RPC client for a server that implements the "demo"
interface.
///
Calls the remote Portmapper in order to get the port of the server.
</summary>
///
<param name = "host">
The host on which the server lives.
</param>
///
<param name = "stream">
true for a TCP connection, false for UDP.
</param>
public
demo(System.NET.IPAddress host, bool
stream) :
base(host,
DEMO_SERVER, DEMO_VERSION, stream) { }
///
<summary>
/// Creates and
connects an RPC client for a server that implements the "demo"
interface.
///
The client is connected it to a server with a known port. (No
interaction with a portmapper)
</summary>
///
<param name = "host">
The host on which the server lives.
</param>
///
<param name = "port">
The port on which the server listens.
</param>
///
<param name = "stream">
true for a TCP connection, false for UDP.
</param>
public
demo(System.NET.IPAddress host, int
port, bool stream) :
base(host,
DEMO_SERVER, DEMO_VERSION, port, stream) { }
///
<summary>
/// Creates an RPC
client for a server that implements the "demo" interface.
///
It initializes it with a externally created protocol client object.
</summary>
///
<param name = "protocol">
The protocol object that implements the client connection.
/// </param>
public demo(ClientGeneric protocol)
:
base(protocol)
{ }
///
<summary>
Version ID of the interface.
</summary>
public
const int
DEMO_VERSION = 1;
public
const int
get_line = 1;
///
<summary>
/// Stub method that
invokes the server function "get_line" (version 1).
</summary>
///
<param name = "arg1">
input parameter of the RPC.
</param>
///
<returns> The
return value of the RPC.
</returns>
public
res_list get_line_1(request arg1) {
res_list retval =
new res_list();
GetClient().Call(get_line, arg1, retval);
return
retval;
}
}
}
An instance of this demo stub class represents a
client to the demo server. It maintains the complete necessary
connection status. Without going into all details we can easily see
that this class consists of a number of constants, three
constructors, and a public method get_line_1() that has the same
signature as get_line() in the XDR file. You can also see that the
lines from the beginning of the XDR file (the three comment lines)
were copied over to the .NET source generated, without the leading %
characters. Also Rpcgen.NET generates XML-style comments that are
understood by the C#-compiler auto documentation feature (option
/doc).
Like
all main client-stub classes in Distinct ONC RPC/XDR for .NET, the
demo class is derived from the base class NetRPCClient. This class
provides the framework for calling RPC servers using the various
possible protocols. Similar to the C binding of ONC RPC, the
constants are the program's number and version as well as an ordinal
number for each procedure. Of the three constructors the first (public
demo(System.NET.IPAddress host, bool stream)) is
probably the most commonly used one. We will use it in our
application example. Finally, the get_line_1() method is what we
have to invoke when we want to interact with the server. The "_1"
extension results from the fact that this is the implementation of
version 1 of this RPC program. As with the C binding the version
number from the XDR file is always appended with an underscore in
front of it.
3.3.2
The Type Definition Files We now take a look into the result.cs file, which
is one of the type definition files that were
generated.
/****************************************
* Distinct ONC RPC/XDR for .NET Example *
****************************************/
using
System;
using
com.distinct.rpc;
namespace demoRPC
{
///
<summary>
///
This class was automatically generated by Rpcgen.NET from the RPC/XDR
file "demo.x".
///
result: was struct
///
</summary>
[Serializable()]
public
class
result : XDRType {
public
int number;
public System.String
line;
public result next;
///
<summary>
/// Default
constructor for objects of class result.
</summary>
public result()
{}
///
<summary>
/// Creates
an object of class result.
</summary>
///
<param name = "arg_number">
The value of the number component.</param>
///
<param name = "arg_line">
The value of the line component.</param>
///
<param name = "arg_next">
The value of the next component.</param>
public result(int
arg_number, System.String arg_line, result arg_next)
{
number = arg_number;
line = arg_line;
next = arg_next;
}
///
<summary>
/// Encodes
an object of class result in compliance to RFC 1832 (XDR).
</summary>
///
<param name = "xdrs">
The XDR output stream.
</param>
public
void
xdr_encode(XDRStream xdrs)
{
xdrs.xdr_encode_int(number);
xdrs.xdr_encode_string(line);
xdrs.xdr_encode_bool(next !=
null);
if (next !=
null)
next.xdr_encode(xdrs);
}
///
<summary>
/// Decodes
an object of class result in compliance to RFC 1832 (XDR).
</summary>
///
<param name = "xdrs">
The XDR input stream.</param>
public
void
xdr_decode(XDRStream xdrs)
{
number = xdrs.xdr_decode_int();
line = xdrs.xdr_decode_string();
next = null;
if
(xdrs.xdr_decode_bool()) {
next = new
result();
next.xdr_decode(xdrs);
}
}
}
}
Once again you can see that the lines from the
beginning of the XDR file (the three comment lines) were copied over
to the C#-file generated. This happens for all files generated.
The data members of the result class mirror the C like definition of
the struct result in the XDR file. The linked-list structure of
the result type is mapped in an XDR-typical manner to a recursive
invocation guided by a boolean value that indicates a NULL pointer
value (and thus, the end of the list). The constructor allows to
create a result-object and initilize its data members.
Like all classes that are created by Rpcgen.NET from type definitions, result implements the XDRType
interface. This interface defines the two methods xdr_encode() and
xdr_decode() that are used by the stub implementation for
marshalling and unmarshalling the parameter into and from an XDRStream object.
Rpcgen.NET generates the necessary code to implement
these methods for the given XDR type.
Typically, if you
are using the RPC functionality of the Distinct ONC RPC/XDR for .NET Toolkit, you will not have to bother with these methods. As with the
C binding, the stub implementation hides all details of their usage.
You just have to construct objects of the defined input parameter
class and pass them to the stub method (in our case e.g. of class
request to demo.get_line()) and you will receive a newly created
reply object from the stub (in our case of class res_list). However,
if you use the Distinct ONC RPC/XDR Toolkit just for
encoding/decoding XDR data (e.g. from a file) you will have to call
these methods directly in conjunction with an appropriate XDRStream
object.
The other two type definition classes,
request.cs and res_list.cs, are very similar and have exactly
the same structure.
3.3.3 The Client Application Now we are ready to write our first Distinct ONC RPC/XDR for
.NET application. Below we show the listing of a
simple console C# application that uses the generated stubs to
make some invocations of the demo server:
using
System;
namespace
demoRPC
{
///
<summary>
///
The main program of the client sample.
///
</summary>
public
class
RPCClient
{
static
public
void Main (String[]
args)
{
demo client;
// this is the RPC client object
request req =
new
request();
try {
client =
new
demo(
System.NET.Dns.GetHostByName(args[0]).AddressList[0],
// the host
true);
// use TCP
for
(int
i = 0; i < 8; i++)
for
(int
j = i + 1; j < 8; j++) {
req.from = i;
req.to = j;
System.Console.Out.WriteLine("from " + i + " to "
+ j);
res_list rl = client.get_line_1(req);
result res = rl.value;
while (res !=
null)
{
System.Console.Out.WriteLine(res.number+":"+res.line);
res = res.next;
}
}
client.CloseClient();
}
catch
(Exception e) {
System.Console.Out.WriteLine(e);
}
}
}
}
Here we take a step by step look at this
application:
- In
this example we
assume that the generated demo stub class is in the same
namespace (in this case demoRPC). So we have no special using
directive here.
- We then define a very simple class RPCClient with just
the static Main() method. Main() interprets the first string
parameter from the command line as the name of the host where the
demo server is running.
Now we declare an object of class
demo. This is the client object we use every time we call the demo
server.
- Next we open an exception context. Most of the
Distinct ONC RPC/XDR for .NET methods throw either a RPCError or an
System.Net.Sockets.SocketException. Please note that every time this occurs, the status of
the used client object is undefined. If this happens, you should
reestablish the connection with a new client object if you want to
call the server again. In our sample program we just catch every
exception and print the message on the screen.
- Now, we
establish the connection to the server by calling the simplest
constructor of the demo client class. Parameters are the hostname of
the server (from the command line) and a boolean that indicates
whether we want to set up a TCP or a UDP connection. A value of true
means TCP.
- If this call does not throw an exception we are
connected. Depending on whether the addressed host really has a ONC
RPC RPCBIND and a demo server running, it may take several
seconds before the success or the failure of this call are
reported.
- Once we are connected we call the server several
times in a loop. We initialize the input object req, invoke the
server with res_list rl = client.get_line_1(req); and process the
output object rl. Please note the handling of the value member of rl
that represents the original type (* result) of the typedef
res_list. The invocation of client.get_line_1() may again result in
an asynchronous exception of type RPCError,
System.Net.Sockets.SocketException, or System.IO.IOException if any
problem occurs in the RPC runtime (typically, when the server
becomes unreachable).
- Finally, we close the connection to
the server and free all resources with client.CloseClient();.
Here we are. This is our first RPC
client written in C#. Now we only have to compile the stubs and
our RPCClient.cs with the C#-compiler (Don't forget to add a
reference to the assembly RPC_NET.dll that contains the ONC RPC
runtime library).
If you have
compiled the files and you run the program, the output will look
like this:
>RPCClient localhost
from 0 to
1 0:This is the first line of text from 0 to
2 0:This is the first line of text 1:This is the
second line of text from 0 to 3 0:This is the
first line of text 1:This is the second line of
text 2:This is the third line. from 0 to
4 0:This is the first line of text 1:This is the
second line of text 2:This is the third
line. 3:This is the 4th line from 0 to
5 ...
If
the output looks like this:
>RPCClient localhost
System.NET.Sockets.SocketException: No connection could be made
because the target machine actively refused it at
System.NET.Sockets.Socket.Connect(EndPoint remoteEP)
...
This
means that our program has caught an exception because we have most
probably forgotten to start the demo server (in this case on localhost, our local machine).
4. Building a Server with Distinct ONC
RPC/XDR for .NET
4.1 The .NET Server This section illustrates how to build a .NET server. To build
a server, again we have to run Rpcgen.NET from
the Distinct ONC RPC/XDR Toolkit for .NET on the XDR file. This time
you run Rpcgen.NET on demo.x with the -S option, telling it that it has
to create the server stub as well. Thus, you
type:
>Rpcgen.NET -S demo.x
Rpcgen.NET V5.0, Copyright 1997 - 2002 by Distinct Corporation
demo.x
writing: request.cs
writing: result.cs
writing: res_list.cs
writing: demo.cs
writing: demoServer.cs
The execution of Rpcgen.NET creates the same four C#-classes as before (even if we do not need the client part demo.cs
this time) plus the additional server stub class named demoServer.cs.
4.2
The Main Stub We will now take
a look into the demoServer.cs main stub file that was
generated.
/****************************************
* Distinct ONC RPC/XDR for .NET Example *
****************************************/
using
com.distinct.rpc;
namespace
demoRPC {
///
<summary>
///
DEMO_SERVER Server Stub
///
This class was automatically generated by Rpcgen.NET from the RPC/XDR
file "demo.x".
///
DEMO_SERVER: was interface
///
</summary>
public
abstract
class
demoServer : NetRPCServer {
///
<summary>
Program ID of the interface.
</summary>
public
const
int
DEMO_SERVER = 0x20000023;
///
<summary>
Version ID of the interface.
</summary>
public
const
int
DEMO_VERSION = 1;
public
const
int
get_line = 1;
///
<summary>
///
Constructor that creates the RPC server that implements the "demo"
interface.
///
</summary>
public demoServer() :
base(DEMO_SERVER,
DEMO_VERSION, true)
{
UnregisterServer();
RegisterServer(StartUDP(0),
false);
RegisterServer(StartTCP(0),
true);
}
///
<summary>
///
Constructor that creates the RPC server that implements the "demo"
interface.
</summary>
///
<param name = "port">
The port on which the server listens (if 0 a random port will be
///
chosen).
</param>
public demoServer(int
port) :
base(DEMO_SERVER,
DEMO_VERSION, true)
{
UnregisterServer();
RegisterServer(StartUDP(port),
false);
RegisterServer(StartTCP(port),
true);
}
///
<summary>
///
Constructor that creates the RPC server that implements the "demo"
interface.
</summary>
///
<param name = "port">
The port on which the server listens (if 0 a random port will be
///
chosen).
</param>
///
<param name = "do_tcp">
True, if a TCP server should be started.
</param>
///
<param name = "do_udp">
True, if a UDP server should be started.
</param>
///
<param name = "do_rpcb">
True, if a RPCBind nameserver should be started
///
(only if there isn't already one running).
</param>
public demoServer(int
port, bool
do_tcp, bool
do_udp, bool
do_rpcb) :
base(DEMO_SERVER,
DEMO_VERSION, do_rpcb) {
UnregisterServer();
if (do_udp)
RegisterServer(StartUDP(port),
false);
if (do_tcp)
RegisterServer(StartTCP(port),
true);
}
///
<summary>
///
Dispatcher Routine that interpretes the call requests.
</summary>
///
<param name = "proc"
The index of the requested function. </param>
///
<param name = "xin"
read the input-parameter from this XDR stream. </param>
///
<param name = "xout"
write the return-parameter to this XDR stream. </param>
///
<returns>
true, if the function with the index proc can be served, false
otherwise.
</returns>
override
public
bool
DoCall(int
proc, XDRStream xin, XDRStream xout) {
try
{
switch (proc) {
case
get_line: {
request arg =
new
request();
arg.xdr_decode(xin);
res_list ret = get_line_1(arg);
ret.xdr_encode(xout);
return
true;
}
default:
break;
}
return (proc==0)?true:false;
}
catch
(System.Exception) {
return
false;
}
}
// Overwrite these abstract server methods for implementing the
server's functionality.
public
abstract
res_list get_line_1(request arg1);
}
}
This stub class demoServer represents the
frame for a server that implements the DEMO_SERVER interface
described in demo.x. Similar to the client stub we see, this
class consists of a number of constants, two constructors, and an
abstract method get_line_1() that has the same signature as get_line() in the XDR file. You can also see that the lines from the
beginning of the XDR file (the three comment lines) were copied over
to the generated .NET source without the leading % characters.
Like all server stub
classes in Distinct ONC RPC/XDR for .NET, the class demoServer is
derived from the base class NetRPCServer. This class provides the
framework for implementing RPC servers using the various possible
protocols. Again, the constants are the program's number and version
and ordinal numbers for the remote procedure. The method DoCall() is
the core of the server. It implements dispatching of the remotely
called procedures depending on the given ordinal number and it calls
marshalling and unmarshalling of the parameters. Finally, the
abstract get_line_1() method is the procedure we have to implement
for our demo server by overriding it in a derived
class.
4.3
Synchronization In Distinct ONC RPC/XDR
for .NET DoCall() and, consequently, all implemented
remote procedures can be called concurrently by different server
threads. In the Distinct ONC RPC/XDR for .NET implementation of an ONC RPC server, each TCP client is served by its own thread.
AN
additional server thread pool handles the UDP
requests.
4.4 The Server
Application Now we are ready
to write our first RPC server in .NET. Below is the listing of a
simple .NET application that uses the generated stub to implement
the demo server:
using
System;
using
com.distinct.rpc;
namespace
demoRPC
{
///
<summary>
///
The main program of the server sample.
///
</summary>
public
class
RPCServer : demoServer
{
static
String[] text = {
"This is the first
line of text",
"This is the second line of text",
"This is the third line.",
"This is the 4th line",
"This is the 5th line",
"This is the 6th line.",
"This is the 7th line.",
"This is the 8th line."
};
static
public
void Main
(String[] args)
{
try
{
new RPCServer ();
}
catch (RPCError e) {
System.Console.Out.WriteLine(e);
}
}
public RPCServer() :
base()
{}
// here
we override and implement the real remote procedure
public
override
res_list get_line_1(request arg)
{
res_list rl =
new res_list();
result n = new
result();
for (int
c = arg.from; c < arg.to; c++) {
// allocate the
next element in the list
n.next =
new
result();
n = n.next;
if
(c == arg.from)
rl.value
= n;
// assign the
line number
n.number = c;
// assign the line
(string)
n.line = text[c];
}
return rl;
}
}
}
This C# code implements the same simple server
functionality as the C version shown earlier. It defines a new class
named RPCServer that is derived from the demoServer class and that
finally implements the remote procedure get_line_1() in a
straightforward manner. Compared to the implementation in C there
are two differences worth noting:
- The Main() method is
not defined by the automatically generated stub but by the
implementing class. The only thing Main() does, is create a
RPCServer
server object and handle the possible RPCError
exception.
- Compared to the C version all the ugly memory
management has gone from get_line_1().
If you now compile
this program and run it as a stand alone program together with
the RPC client as shown above you will see that our first C# ONC
RPC server behaves exactly like the initial C
version.
4.5 RPCBIND If you are not using fixed server ports (see
below) you will need an instance of the RPCBIND running on each machine that hosts ONC RPC servers. It is required
to bind RPC clients to servers. Basically, RPCBIND is a name server.
Server ports of ONC RPC servers are chosen randomly. It is the
responsibility of RPCBIND to tell the clients the port number of a
server with a given program/version number. RPCBIND itself is an RPC
server that listens on a well-known port (111) for these requests.
Typically, RPCBIND is running as a demon in the
background and it is usually provided by the standard ONC RPC
infrastructure that is part of e.g. any network connected UNIX host
(e.g. NFS relies on ONC RPC). However, if you are starting your RPC
server on a machine that does not provide this infrastructure by
default (e.g. all flavors of MS Windows) you have to make sure that RPCBIND is present to enable clients to connect to your new
server.
The Distinct ONC RPC/XDR for .NET Toolkit
provides its own implementation of the RPCBIND. This native implementation of the RPCBIND implements the RPCBIND protocol
versions 3 and 4 as well as the portmap interface version 2. As RPCBIND uses a fixed port number there can be only one RPCBIND instance per node. Therefore, any server that uses the Rpcgen.NET generated stub will first check, whether the system already
provides RPCBIND, and if not, it will start its own RPCBIND. Please
note, that all ONC RPC servers that are subsequently started on that
node will now also use this instance of RPCBIND. This means, if you
shut down the Distinct ONC RPC/XDR for .NET server that started RPCBIND, you will also loose the ability to bind new clients to the
other, still running servers, as the RPCBIND that stores the
binding information is gone. Connections that were established
before RPCBIND went down are not affected, as the binding is
established only once during the connection setup. You can avoid
this by starting the RPCBIND manually before
starting the server.
To check whether RPCBIND is running you can use the standard ONC RPC rpcinfo
tool with the -p <hostname> option. This should enumerate the
servers registered with RPCBIND of this host. If this tool reports
an error or does not show the servers you were expecting, there is
obviously a problem with RPCBIND.
If
you have to disable the RPCBIND autostart feature of Rpcgen.NET
generated server stubs for some reason, you currently have to edit
the xxxServer class. Simply change the third parameter of the
invocation super(DEMO_SERVER, DEMO_VERSION, true); in the
constructors to false and the server will not try to start RPCBIND.
5. RPC Programs in Visual Basic .NET
One of the main
features of .NET is the Common Language Runtime (CLR). Among various
other features it allows for interoperability of code modules
written in different .NET-languages. Especially it allows to use
libraries (assemblies) written in C# from Visual Basic .NET and
vice-versa. This means, with the Distinct ONC RPC/XDR for .NET you
can create the stub code for accessing ONC RPC server in C# and use
these stubs from Visual Basic .NET. Of course this also applies for
implementing servers in Visual Basic .NET and for using the methods
from the toolkit directly in your code (e.g. for XDR-Encoding/Decoding).
As an example here
is the Visual Basic code of a client accessing the demo sever.
This code
basically does the same as C# client above
(To keep it short, it uses directly the loopback address to access a
local server and it makes just one call instead of some kind of loop):
Imports
demoRPC
Module
RPCClient
Sub Main()
Dim cl
As demo
cl =
New
demo(System.NET.IPAddress.Loopback,
True)
Dim req
As request
Dim res
As res_list
req =
New
request()
req.from = 1
req.to = 5
res = cl.get_line_1(req)
Dim r As
result
r = res.value
While (Not
r Is
Nothing)
System.Console.Write(r.number)
System.Console.WriteLine(". " + r.line)
r = r.next
End
While
End
Sub
End
Module
As C# and Visual
Basic cannot be mixed directly at code level, the following project
structure is required to get the program running:

The C# stub code
generated by Rpcgen.NET is put into a class library - in this
example the library named "demoRPCLib". This code is then compiled
to a DLL (a .NET assembly) and used as a reference in the Visual
Basic project. Both projects, the library and the Visual Basic
client also need a reference to the assembly RPC_NET.dll that contains the ONC RPC
runtime library.
6. XDR
to .NET Type Mapping In
most cases the mapping of XDR types to .NET types and classes is
quite obvious. For an overview of the complete mapping please have a
look at the following table:
| XDR
Type |
.NET
Type |
| int |
int |
| unsigned int |
uint |
| long |
int |
| unsigned long |
uint |
| short |
short |
| unsigned short |
ushort |
| hyper |
long |
| unsigned hyper |
ulong |
| char |
sbyte |
| unsigned char |
byte |
| float |
float |
| double |
double |
| bool |
boolean |
| string |
String |
| opaque |
byte
array |
| fixed length
array |
.NET array |
| variable
length array |
.NET array |
| optional
data (pointer like *x) |
Reference to
an object of class x. If x is a basic type a special wrapper
class XDRx that implements XDRType is used
instead. |
| enum x |
Class x
implements XDRType with member variable int value and one
constant int per enum constant. |
| struct x |
Class x
implements XDRType with member variables for each struct
member. |
| union
x |
Class x
implements XDRType with member variables for each union member
including discriminant. No overlaying of members is supported
(neither for type conversion nor for saving
space). |
| typedef x
y |
Class x
implements XDRType with member variable value of the redefined
type y. |
The most notable
difficulty in the .NET mapping is the handling of typedefs. As .NET
has no notion of type name aliasing, the only viable solution that
preserves the introduced names is the definition of a new class. In
the .NET mapping of typedef this new class has always only one data
member, called value, that is a variable of the redefined type (for
an illustration please refer to the generated file res_list.cs).
7. How To This section discusses how to do certain
things in the Distinct ONC RPC/XDR for .NET environment. It includes
information on how to authenticate, how to broadcast RPCs, how to
run RPCs in batch mode and discusses server data flow, using XDR
streams and error and timeout handling.
7.1
AUTHENTICATION By
default, Distinct ONC RPC/XDR for .NET uses NULL authentication.
However, this default can be changed at the client object by using setCredential() and setVerifier(). In the other direction the
returned verifier from the server can be obtained after each call
with getReturnedVerifier(). Please note that Distinct ONC RPC/XDR
for .NET (like the C binding) provides the data structures for
transferring authentication data and the methods for setting,
retrieving and checking this data. It does not provide protection
against forged authentication data. Any security and access control
features have to be implemented by your application. Below is a code
sample that creates a Unix authentication (sometimes also called
System authentication) and sets it as the new credential of this
client. From now on each request will be sent with this
authentication.
using
System;
using
com.distinct.rpc;
namespace
demoRPC
{
...
try {
client =
new
demo(
System.NET.Dns.GetHostByName(args[0]).AddressList[0],
// the host
true);
// use TCP
// a new Unix authentication is created (UID = GID = 0
client.GetClient().setCredential(new AuthUnix(
(int)(System.DateTime.Now.Ticks/10000000),
"myhostname",
0, 0, new
int[0]));
res_list rl = client.get_line_1(req);
...
// Here it is assumed that the server returned an authentication
// (typically of type short) that is now used for any subsequent calls
client.GetClient().setCredential(client.GetClient().getReturnedVerifier());
}
...
The class
com.distinct.rpc.NETRPCServer provides the DoAuth() method for
handling authentication at the server side. Override this method if
you want your server to check authentication (the default is no
check). DoAuth() receives the credential and verifier provided by
the client as well as the call identifier and returns the verifier
that should be sent back with the reply to the calling client
(simply return null for NULL authentication). It throws a com.distinct.rpc.RPCAuthError if it wants to signal an unsuccessful
authentication. The following code fragment illustrates how DoAuth()
can be used in the server
code.
...
public
override
Auth DoAuth(int
proc, Auth cred, Auth verf)
{
if
(cred.Flavor != NetRPC.kAUTH_UNIX)
throw
new
RPCAuthError(NetRPC.kAUTH_TOOWEAK);
// convert it to the correct type
AuthUnix unix_cred =
new
AuthUnix(cred);
if
(unix_cred.Uid != 0)
throw
new
RPCAuthError(NetRPC.kAUTH_BADCRED);
// uses the same
authenticator as returned verifier
// usually you would use some shortcut here
return
unix_cred;
} ...
This example
implementation of DoAuth() checks for each call to see if it has a
credential of type AuthUnix and if so if the user id is 0. If either
condition is not fulfilled it throws a com.distinct.rpc.RPCAuthError
exception in order to terminate any further processing of this call.
The remote procedure itself is not called at all in this
case.
If you need to use other types of authentication
(e.g Kerberos style) you can implement it easily by deriving a new
authentication class from the com.distinct.rpc.Auth base
type.
7.3 RPC
BROADCAST RPC broadcast
allows your application to broadcast a message to all computers on a
network. Typically you would use it if you were trying to identify
which servers are available. RPC broadcast runs over UDP only and
always uses NULL authentication. RPC broadcast can handle many
responses from all responding servers. It filters out all
unsuccessful responses, therefore, if a version mismatch exists
between the broadcaster and a remote service, the user of RPC
broadcast will never know this. When using RPC broadcast keep in
mind that all broadcast messages are sent to RPCBIND. Thus,
only services that register themselves with their RPCBIND are
accessible via RPC broadcast.
In Distinct ONC RPC/XDR
for .NET broadcast calls are implemented by the static member
function NetRPCClient.BroadcastCall(). Syntax and semantics are very
similar to the C binding. Here is an example of how this function
may be used calling the demo server we used
earlier.
using
System;
using
com.distinct.rpc;
namespace
demoRPC
{
///
<summary>
///
The main program of the client sample.
///
</summary>
public
class
RPCClient : BroadcastHandler
{
// "BroadcastHandler.onReply()" is the handler called for each reply
packet.
public
bool
onReply(XDRType retval, System.Net.IPAddress addr)
{
result res =
((res_list)retval).value;
while (res !=
null)
{
System.Console.Out.WriteLine(res.number+":"+res.line);
res = res.next;
}
return
true;
// return
with the first answer
}
static
public
void Main (String[]
args)
{
request req =
new request();
res_list rl =
new res_list();
RPCClient app =
new RPCClient();
try {
for (int
i = 0; i < 8; i++)
for (int
j = i + 1; j < 8; j++) {
req.from = i; req.to = j;
System.Console.Out.WriteLine("from "
+ i + " to " + j);
demo.BroadcastCall( demo.DEMO_SERVER,
demo.DEMO_VERSION,
demo.get_line,
req, rl,
app);
}
}
catch
(Exception e) {
System.Console.Out.WriteLine(e);
}
}
}
}
The changes to the original
client program are:
-
The
com.distinct.rpc.BroadcastHandler and com.distinct.rpc.XDRType have
to be imported.
-
One class has to implement the com.distinct.rpc.BroadcastHandler interface. This interface has only
one method onReply() that is called by Distinct ONC RPC/XDR for .NET
for each reply packet (as long as onReply() returns true). In our
example onReply() just prints the reply and returns true in order to
stop the handling of further reply messages.
-
There is no
longer a client object. It is not required as the BroadcastCall
method is static (a class-method) and not dependent on a special
connection between a client and a server.
-
The return object
is created as an empty object before the call is done and it is then
passed to the call as an input parameter. The request is
made via the BroadcastCall() method. This method is not implemented
in the client class but directly in com.distinct.rpc.NETRPCClient.
This means the call is generic and takes the server number, the
server version, and the call identifier (all taken from the .x file
and defined in the client class) as well as references to the
argument and the return class as parameters.
-
The last parameter is
the object that implements the reply handler onReply(). If this
parameter is null all possible replies are
discarded.
7.4 HOW TO
BATCH RPCs Usually RPC is
synchronous, meaning that clients send a call message and wait for
the server to reply by indicating that the call succeeded. But this
implies that clients may be sitting idle while servers process
calls. Therefore, in cases where the client does not require an
acknowledgement for every message sent, RPC messages can be placed
in a pipeline of calls to the desired server. This process is known
as batching. When operating in batch mode, RPC is simply acting as a
message passing system. It is possible to batch RPC calls
when:
No RPC call in the pipeline requires a response
from the server, and the server does not send a response
message. The pipeline of calls is transported via the
reliable stream transport protocol TCP. Because the server
does not respond to every call, the client can generate new calls in
parallel with the server executing previous calls. This overlapped
execution greatly decreases the total elapsed time of a series of
calls. Because the batched calls may be buffered, the client must
eventually do a nonbatched call to flush the
pipeline.
Distinct ONC RPC/XDR for .NET provides two
additional methods for the use of batched calls, one for the client
and one for the server side. A client simply has to set the timeout
value of the client object to the value of -1 in order to force all RPC calls to return immediately after sending the request message
(by throwing a RPCTimeoutError exception). Use the
com.distinct.rpc.setTimeout() method to modify the timeout value.
The com.distinct.rpc.NETRPCServer class provides
the IsBatched() method for handling batched calls at the server.
Override this method if you want your server not to send any reply
to a request. IsBatched() receives the call identifier and returns a
bool value. Depending on the call identifier it decides whether
this call is one that uses the batched mode and if so it simply
returns true. This tells Distinct ONC RPC/XDR for .NET that there is
no need to send any reply to the
client.
7.5 CONTROL
AND DATA FLOW ON THE SERVER As we have seen so
far, there are three methods that can be overloaded to implement an
ONC RPC server's functionality: NetRPCServer.DoAuth(),
NetRPCServer.DoCall(), and NetRPCServer.IsBatched(). The server calls
these functions in exactly this order to serve each call, meaning
first it calls DoAuth() to check authentication, then it calls
DoCall() to perform the remote procedure call itself, and finally,
it checks with IsBatch() whether there is a need to send back a
reply message. Simple RPC servers only override DoCall() and use
defaults for DoAuth() and IsBatched(). You only have to override the
latter two methods if you want to change this
default.
In some cases you might want to pass data
between these three server-specific methods, e.g. if you need
authentication data when processing the remote procedure call
itself. Distinct ONC RPC/XDR allows for this data flow by providing
the NetRPCServer.SetClientData() and NetRPCServer.SetClientData()
methods. With SetClientData() you can register any .NET object and
retrieve it later by calling getClientData(). The data is stored
per-thread. This means one server thread (the entity that serves one
client request) can store any data that has to be transferred
between the three methods
(NetRPCServer.DoAuth(),NetRPCServer.DoCall(),NetRPCServer.IsBatched())
here. The data will no longer be available after the call to
IsBatched().
7.6 USING XDR STREAMS XDRStream implements
all the encoding/decoding methods that are required for
encoding/decoding the .NET basic types according to RFC 1832 (XDR).
It also provides a simple memory management for the streamed data.
XDRStream implements a dynamically growing buffer of bytes (in fact
a queue). The constructors allow for the creation of an empty
XDRStream (with default allocation size 1024), an empty XDRStream
with user defined allocation size (just a possible optimization), or
an XDRStream initialized with the content of an existing byte array
(usually the start of the decoding procedure). The put_byte(),
put_bytes(), and all xdr_encode_xxx() methods add bytes to the head
of the queue, while the get_byte(), get_bytes(), and all
xdr_decode_xxx() methods consume bytes from the tail. With
get_length() and get_data() you can request the current length and
content of the queue without changing its status. The reset()
methods reset the XDRStream to an empty queue. dump() just prints
the content of the queue in hex and ASCII to System.out (typically
used for debugging).
When you want to use Distinct ONC
RPC/XDR for .NET just for reading or writing XDR encoded data you
might want to use an XDRStream object and just manipulate the
contents of its buffer. Alternatively you can derive your own
XDRStream class that handles I/O
automatically.
7.7 TIMEOUT
HANDLING Whenever asynchronous network protocols are
designed, timeouts play an important role. Since setting a timeout
for a response is the only way to determine whether your server is
still alive, it is crucial to choose the right timeout value. If it
is too short, you might think your server has crashed when it may
just be working slowly under a heavy load. If the timeout is too
long, you might waste important time waiting for something that will
never happen.
RPC maintains timeout values for both
types of communication protocols (UDP and TCP). Typically, it is
more important to have a timeout in UDP sessions, as the protocol
itself does not provide any error detection. In addition, as UDP
does not guarantee message delivery at all, a retransmission feature
is desirable for UDP clients. RPC implements both. TCP provides
reliable streams and usually it is a good idea to rely on the TCP
connection management to determine whether your server is still
alive. But, as this does not protect your client from endlessly
looping (or totally overloaded) servers, RPC also has a time-out
mechanism for TCP connections.
By default, the RPC time
out is set to 25 seconds (UDP retransmission time out is 5 seconds).
When a client does not receive any byte of the reply message for
more than 25 seconds it assumes the server has crashed and throws a
timeout RPCError exception. You can change this time-out value by
calling NetRPCClient.SetTimeout() on your client object, specifying
the new time-out value in milliseconds (a value of 0 means no
timeout at all). For clients running over the UDP protocol, the
retransmission time can be changed by calling
NetRPCClient.SetResend.
7.8 ERROR HANDLING Whenever the Distinct ONC RPC/XDR for .NET runtime detects a
non-recoverable error in a client call it will throw an exception.
Usually this will be an System.Net.Sockets.SocketException when the problem is directly
related to low-level socket I/O or an RPCError when the problem has a close
relation to the RPC protocol. For details about the problem please
look into the message string of the particular exception. Once you
have received an exception, the status of the client object is
undefined. Therefore, it is important to reestablish the connection
by creating a new client object before trying to call the server
again.
| Exception |
Reason
|
| RPCAuthError
|
An
authentication error occurred |
| RPCDecodeError |
A problem
such as an unexpected EOF has occurred during XDR decoding
|
| RPCServerError |
An RPC server
is not available or not registered with RPCBIND |
| RPCTimeoutError |
An RPC
blocked longer than the timeout value of the client
|
| RPCError
|
For all
other errors |
7.9 Connecting Through Firewalls Most RPC applications are designed for Intranets, and
although RPC can be made to run over the Internet, the original
specifi |