Saturday, February 12, 2011

RPC with Java

The goal of RPC in general is make distributed programming as easy as possible by creating the illusion that an exchange of data over the network that results in some processing (on another machine than the one the application is running on) is nothing more than a special kind of procedure call — one that has the special attribute of being “remote”.

Stating the obvious, there is a clear separation of roles in this scenario: the application invoking the procedure (the caller) is called the client, the one executing the procedure is the server. (In more advanced applications, a single program may assume both roles during the course of its execution.)



Unsurprisingly, RPC is the foundation of client/server computing. While RPC started out as a way to provide a transparent programming experience for traditional, procedural models, it was quickly followed by technologies that aimed to do the same for object-oriented programming. Examples include DCOM, CORBA, and RMI.

The first popular RPC solution was introduced by Sun (Sun RPC, later called ONC RPC) together with NFS, the Network File System. It featured all the important parts of more modern incarnations: An interface description with an associated stub/skeleton generator, marshaling, even a (albeit very simple) registry (portmap).

The other important RPC standard is DCE RPC, which is also used as the foundation of Microsoft’s DCOM.

The Sun RPC compiler is called rpcgen. As input, it takes a list of remote procedures (interfaces) defined in an interface definition language (IDL).

The output from rpcgen is a set of files that include...

1. Server code: Main function that sets up a socket, registers the port with a name server, listens for and accepts connections, receives messages, unmarshals parameters, calls the user-written server function, marshals the return value, and sends back a network message.

2. Client stub: The code with the interface of the remote function that marshals parameters, sends the message to the server, and unmarshals the return value.

3. Header: Contains definitions of symbols used by client and server as well as function prototypes

4. Data conversion functions: A separate file may be generated if special functions need to be called to convert between local data types and their marshaled forms.

The rpcgen tool generates code in C - from a given IDL.

Netbula JRPC is a complete port of the C version of ONC RPC to the Java(tm) platform - which generates code in JAVA. In this blog post we'll be using Netbula JRPC.

Also Distinct ONC RPC/XDR for Java - is another vendor who provides an ONC RPC toolkit in java.

Let's first have a look at how a sample IDL file looks like.
type definitions

program identifier { 
   version version_id {
       procedure list
    } = value;
} = value;
type definitions block is used to define structs - for example,
struct person {
string name<>;
int age;
};
Then the program block - a given program block can have different versions of the same interface - and program has a program number to identify it uniquely. In the following example, the program number is 1234567.
program msgserv {
    .................
} = 1234567;
Now let's have a look at the version block. As I mentioned before there can be multiple version blocks inside a given program and each version is identified by the version number. In the following example - the version number is - 1.
program msgserv {
    version MSGSERV_V1 {
     .................... 
     }= 1;
} = 1234567;
Next the procedure list. Inside a one version - there can be set of procedures defined. And there also each procedure is identified with a unique number. In the following example sendmsg() is identified by number - 2 and addperson() is identified by number - 3.
program msgserv {
    version MSGSERV_V1 {
                string sendmsg(string)=2; 
                void addperson(person)=3;     
    }= 1;
} = 1234567;
RPC Call Message
Each remote procedure call message contains the following unsigned integer fields to uniquely identify the remote procedure.
- Program number
- Program version number
- Procedure number

RPC Reply Message
The RPC protocol for a reply message varies depending on whether the call message is accepted or rejected by the network server. The reply message to a request contains information to distinguish the following conditions.
- RPC executed the call message successfully.
- The remote implementation of RPC is not protocol version 2. The lowest and highest supported RPC version numbers are returned.
- The remote program is not available on the remote system.
- The remote program does not support the requested version number. The lowest and highest supported remote program version numbers are returned.
- The requested procedure number does not exist. This is usually a caller-side protocol or programming error.

Let's get our hands dirty.. Following is the complete IDL of our example used here - which is in the file msg.x.
struct person {
string name<>;
int age;
};

program msgserv {
    version MSGSERV_V1 {
      string sendmsg(string)=2; 
      void addperson(person)=3; 
     }= 1;
} = 1234567;
Now, you need to download Netbula JRPC from here. You can find the jrpcgen tool inside the bin folder.

Let's use jrpcgen to generate Java code.
$ jrpcgen msg.x
This generates four files.

1. msgserv.java : The RPC program interface definition, including constant definition such as program number.

2. msgserv_cln.java : The client stub class. This class implements the RPC interface defined above. An RPC client program instantiate an instance of this class and call its methods (remote call).

3. msgserv_svcb.java : The RPC service. This class inherits the RPC interface and is abstract, the programmer needs to extend this class and supply the implmentation for the interface.

4. person.java : The corresponding Java file for the struct person.

Compile all four java files - make sure you have orpc.jar in the classpath. You can find orpc.jar inside the lib directory of Netbula distribution you downloaded.
$ javac msgserv.java person.java msgserv_cln.java msgserv_svcb.java -classpath orpc.dev\lib\orpc.jar
Next let's write the actual service and start it. Following is the code for it - msgsvc.java.
import netbula.ORPC.*;

public class msgsvc extends msgserv_svcb {
        
//implement the server function, 
//let's just echo the msg back
public String sendmsg(String msg) {
 System.out.println("got msg from client "+ msg);
 return msg;
}

public void addperson(person psn) {
 System.out.println("got msg from client");
}

//main function runs the server
public static void main(String srgv[]) {

//let's run the server using the run() method in Svc
//For more flexibility, one could use the TCPServer and UDPServer directly

   new Thread(new PortMapper()).start();

   try {  
     new msgsvc().run(); 
   } catch(netbula.ORPC.rpc_err ex){
      System.out.println("error occured");
   }
  
}

}
class PortMapper implements Runnable {

@Override
public void run() {

 try {
   new Pmapsvc().run();
 } catch (rpc_err e1) {
 }
}
}
Now compile and start the server - msgsvc.java.
$ javac msgsvc.java -classpath .;orpc.dev\lib\orpc.jar
$ java msgsvc  -classpath .;orpc.dev\lib\orpc.jar
Next the client... Following is the code for client.
import netbula.ORPC.*;
               
import java.net.*;
 
public class ClientTest {  
    
public ClientTest () {}
                              
static public void main(String args[]) {
               
try {
               
     msgserv_cln cl = new msgserv_cln(args[0], "udp");            
     String msg = "hello world\n";
               
     System.out.println("sending.. ");
               
     for(int i=0; i<5; i++){
           String reply = cl.sendmsg(msg);
           System.out.println("got " + reply +"\n");
     }
}catch (rpc_err e) {
     System.out.println("rpc: " + e.toString());
}
}
}
Here, we constructed the client which connects to the server on host args[0] with UDP protocol, send a message, and print out the reply.
$ javac ClientTest.java -classpath .;orpc.dev\lib\orpc.jar
$ java ClientTest localhost  -classpath .;orpc.dev\lib\orpc.jar
If you get here - means your sample works fine.. :-) Let's get in to the internals.

You may recall - that I mentioned jrpcgen produces four java classes. One of them is the interface, msgserv.java - which is been implemented by both the client and server side stubs.

Let's have a look at this interface.
public interface msgserv{

 public static final int _def_pno = 1234567;
 public static final int _def_vno = 1;

 public static final int _sendmsg_proc = 2;
 public static final int _addperson_proc = 3;
 
 public String sendmsg(String in_arg) throws netbula.ORPC.rpc_err;
 public void addperson(person in_arg) throws netbula.ORPC.rpc_err;
}
Can you notice that this defined four constants. _def_pno is the program number. _def_vno version number of the interface. Other two are the numbers corresponding to each procedure defined in the IDL.

Now let's have a look at the constructor of the client side stub, instantiated by our ClientTest.java above.
/**
Construct an RPC client object connected to a server
on the specified host with the specified protocol

@param host server hostname, or URL of the RPC proxy if http is used
@param proto protocol, can be "tcp", "udp" or "http"
*/
public msgserv_cln(String host, String proto) throws rpc_err {
 super(host, msgserv._def_pno, msgserv._def_vno, proto);
}
See here in this constructor it passes the program number[_def_pno] and the version number[_def_vno] to construct the instance. So - this particular instance is created for a given version of the given program.

Now you got a very valid question. What if we have multiple versions inside the same program. How does this constructor look like. Well.. in case we have multiple versions - then for each version separate client/server stubs as well as an interface will be generated.

Similarly if you look at the sendmessage() method of client side stub, you will find out - the procedure number is used to invoke the method.

The above emphasizes on the value of having program number, version number and procedure numbers in RPC. Still not happy? Then let's have a look at the server side stub.
public XDT proc_call (int proc, XDR inXDR) throws XDRError {
  switch(proc) {
 case 0:  return new XDTvoid();

 case 2:
         String _out_arg2;
  try {
   XDTString _in_arg = new XDTString();
   _in_arg.xdr(inXDR);
   _out_arg2 = this.sendmsg(_in_arg.value);
  } catch (XDRError e) {
   throw e;
  }
  return new XDTString(_out_arg2);

 case 3:
  try {
   person _in_arg = new person();
   _in_arg.xdr(inXDR);
   this.addperson(_in_arg);
  } catch (XDRError e) {
   throw e;
  }
  return new XDTvoid();

  default: return null;
  }
 }
Here, this the method invoked at the server side when a service method been invoked from the client end.

Here you will see - based on the procedure number, procedure been invoked.

References :

[1]: http://www.innoq.com/blog/st/2005/05/18/rpcstyle_web_services.html
[2]: http://netbula.com/javarpc/msgsamp.html
[3]: http://www.javvin.com/protocol/rfc1831.pdf : RPC: Remote Procedure Call Protocol Specification Version 2 (ONC version)
[4]: http://www.javvin.com/protocol/rfc1057.pdf : RPC: Remote Procedure Call Protocol Specification Version 2 (Sun version)

3 comments:

Pavithra Gunasekara said...

Simply the best article on RPC... :-)

Pavithra Gunasekara said...

Thanks again for this fabulous post... Should't we start portmapper before running the rpc server? I faced with a small problem when running it probably because it is an evaluation copy?

දෞපදී said...

Thanks for the valuable post....