Remote Procedure Calls &Web Services
Case studies
Paul Krzyzanowski
September 18, 2023
In this section, we will cover the highlights of a few RPC frameworks. The goal is not to dive into them deeply or chart their evolution but rather to understand what problems each was trying to solve and the approach they took. We’ll see that all RPC systems follow the same principle: client stub functions and a server stub (skeleton) that calls the user-provided server functions.
The first generation of RPCs: functional access
Remote procedure calls were created to allow functions to be called in another address space, often on another computer on a shared network. Prior to the popularity of the internet, these functions were generally provided by and hosted within the organization’s network. RPC provided a method for executing procedures (subroutines) on a remote server. RPCs were tightly coupled, meaning the client and server had to understand the exact procedure calls.
Sun (ONC) RPC
Sun’s RPC, formally called ONC (Open Network Computing) RPC was one of the first RPC systems to achieve widespread use, thanks to the early popularity of Sun workstations, servers, and the Network File System (NFS). It is still in use on virtually all UNIX-derived systems (Linux, macOS, *BSD, SunOS). It uses an RPC compiler called rpcgen that takes input from an interface definition language (IDL) file. This is a file that defines the interfaces to the remote procedures. The interfaces are a set of functions that could be called by clients, including their parameters and return types. The programmer can also define multiple versions of an interface. This is useful since services may evolve over time but one cannot always count on all clients getting updated; some clients may still call functions on the old interfaces, which might take different parameters or have different names. From this IDL file, rpcgen creates client stub functions and a server stub program. These can be compiled and linked with the client and server functions, respectively.
A programmer must assign a program number to each interface, that is, each set of server functions in the IDL file. This is a 32-bit number that must be a unique ID on that server. When the server starts up, it binds a socket to any available port and registers that port number and interface’s program number with a name server, known as the portmapper, running on the same machine. A client, before it can invoke any remote procedure calls, contacts the portmapper on the specified server with the program number to find the port to which it needs to send its requests.
The choice of transport protocol, UDP or TCP, can be specified at run-time. All incoming and return parameters are marshalled into a standard format called XDR, or eXternal Data Representation.
DCE RPC
The Distributed Computing Environment, defined by the Open Group, created its own flavor of RPC which was similar in concept to ONC RPC. The programmer defines the set of remotely accessible functions as an interface using an interface definition language, an IDL, which they called the Interface Definition Notation (IDN).
To avoid the problem of picking a unique 32-bit identifier to identify the interface, DCE RPC created the concept of a **unique universal ID **(UUID) – a 128-bit number that is a function of the current time and ethernet address. UUID generation is formally defined in RFC 4122. A program called uuidgen generates this identifier.
The Distributed Computing Environment also introduced the concept of a cell, which is an administrative grouping of machines. Each cell has a cell directory server that maintains information about the services available within the cell.
Each computer in the cell knows how to contact its cell directory server. When a server program starts up under DCE RPC, it registers its port and the interface’s UUID with a local name server (the DCE host dæmon, dced, which is similar to the portmapper we find on Linux and BSD systems). It also registers the UUID-to-host mapping with the cell directory server. This allows for location transparency for services: a client does not need to know what machine a service lives on a priori. The client queries the cell server with the UUID of the interface to find the system on which the desired service is running. Then it contacts the local name server to get the port number and transport type on which to make requests.
A standard way of encapsulating data (marshalling) is crucial since encodings may differ between different machines. Sun defined a standard format called XDR (eXternal Data Representation). Every participating system must convert (marshal) data into this format. DCE defined a format called NDR (Network Data Representation). However, instead of creating a single set of definitions, NDR defines a set of data representations that can be used. The hope is that the client can and server can find a representation that will require minimal or no data conversion, hence, providing less overhead). It avoids the situation where the client and server may both have to convert data to an intermediate format only for the other side to convert it back to what it was initially. If the client uses the same data representation as the server, it will not need to convert data. In the worst case, only one side will need to convert data. This support for multiple representations is known as a multi-canonical approach to data conversion.
As object oriented languages gained popularity in the late 1980s and 1990s, RPC systems like Sun’s and DCE’s proved incapable of handling some object-oriented constructs, such as object instantiation or polymorphism (different functions sharing the same name, with the function distinguished by the incoming parameters). Creating objects, in particular, requires a need for memory allocation on the server and cleanup when these remote objects are no longer needed. This is called distributed garbage collection. A new generation of RPC systems dealt with these issues.
The second generation of RPCs: object-oriented access – Distributed Object Architecture (DOA)
The main concept of the first generation of RPC frameworks is to enable a procedure (function) call on one machine to get transparently executed on another machine. The main characteristics of this first generation of remote procedure calls were:
- Procedural:
- The interface is based on calling procedures remotely.
- Marshalled data:
- Arguments are serialized (marshalled) into a format suitable for transport, and then deserialized (“unmarshalled”) on the other side.
- Client and server stubs:
- Automatically-generated client stubs marshal data that is sent to a server stub (skeleton) that unmarshals it to call the server function.
- Stateless:
- Typically, each call is stateless. The server doesn’t retain any context between calls. A server function may choose to store static data but that data would then be present whenever any client invokes the function. There is no programmatic partitioning among different callers.
As object-oriented programming (OOP) became the dominant paradigm, it was natural to extend its principles to distributed systems. Instead of thinking in terms of remote procedures, distributed object systems revolve around the idea of invoking methods on remote objects. These methods could modify the state of the object, and subsequent calls would see that changed state. An object is an instance of a class that is created on demand by a program. All objects in a class share the same methods (functions) but each object has its own pool of memory for storing data on a per-object basis.
Remote procedure calls in the object-oriented environment continue to operate essentially the same way: stubs for marshalling and unmarshalling data. However, there are some key distinctions. They are:
- Object-Oriented:
- These are essentially remote object invocations. An object on one machine can be used as if it was a local object, but its methods run on another machine.
- Stateful:
- Unlike stateless RPC, these systems typically maintain object state across method calls. This means you can set some attributes of an object in one call and access them in a later call.
- Garbage Collection:
- Eventually, an object is no longer needed by the client and must be deleted at the server. This can be because the process deletes the object, the process has no more references to it, or the process exits. It can also be because the process crashed, the entire client crashed, or network connectivity was lost. Some distributed object systems, like Java RMI, integrate with the garbage collection of the platform (Java in this case) to ensure remote references are properly cleaned up.
- Interfaces, Inheritance, and Polymorphism:
- These systems often support object-oriented features like inheritance. For instance, a remote object can be an instance of a subclass and the client can treat it as an instance of the superclass. Polymorphism isn’t specific to object-oriented programming but is often present in object-oriented languages. It refers to the ability to have different methods sharing the same name because they take different parameters. For instance,
add(int, int)
can have a distinctly different implementation fromadd(String, String)
.
Microsoft COM+/DCOM & ORPC (MS-RPC)
Microsoft already had a mechanism in place for dynamically loading software modules, called components, into a process. This was known as COM, the Component Object Model and provided a well-defined mechanism for a process to identify and access interfaces within the component. The same model was extended to invoke remotely-located components and became the Distributed Component Object Model (DCOM), later fully merged with COM and called COM+. Because remote components cannot be loaded into the local process space, they have to be loaded by some process on the remote system. This process is known as a surrogate process. It runs on the server (under the name dllhost.exe), accepting remote requests for loading components and invoking operations on them.
COM+ is implemented through remote procedure calls. The local COM object simply makes RPC requests to the remote implementation. Microsoft enhanced the DCE RPC protocol slightly to create what they called Object RPC (ORPC). For confusion, it is also called MSRPC, Microsoft RPC.
This is essentially DCE RPC with the addition of support for an interface pointer identifier (IPID). The IPID provides the ability to identify a specific instance of a remote class. Interfaces are defined via the Microsoft Interface Definition Language (MIDL) and compiled into client and server side stubs. The client-side stub becomes the local COM object that is loaded on the client when the object is activated by the client program. Like DCE, ORPC supports multi-canonical data representation. The remote, server-side, COM object is loaded by the server’s surrogate process when first requested by the client.
ORPC became a core part of Windows operating systems, starting with Windows NT. It allowed for various services to be accessed remotely, such as file services, printer and remote management services. ORPC was used for implementing the Messaging Application Programming Interface (MAPI) for communicating with Exchange, Microsoft’s email server. Tasks like user authentication, user management, group policy application, and other Active Directory-related functions often use MSRPC.
DCE RPC is completely compatible with Microsoft ORPC. Because of their ability to interoperate with Windows-based services, both Linux and macOS provide libraries for DCE RPC.
Since objects can be instantiated and deleted remotely, the surrogate process needs to ensure that there isn’t a build-up of objects that are no longer needed by any client. COM+ accomplishes this via remote reference counting. This is an explicit action on the part of the client where the client sends requests to increment or decrement a reference count on the server. When the reference count for an object drops to zero, the surrogate process deletes that object. To guard against programming errors or processes that terminated abnormally, a secondary mechanism exists, called pinging. The client must periodically send the server a ping set – a list of all the remote objects that are currently active. If the server does not receive this information within a certain period, it deletes the objects. This is a form of leasing, where the object expires if a lease is not renewed periodically. However, there is a subtle difference. With leasing, the lifetime of an object is generally renewed whenever an object is accessed, so there is no need for the client to ping the server to renew a lease unless it has not accessed the object for the duration of the lease.
Java RMI
When Java was created at Sun Microsystems, it was designed to be a language for deploying downloadable applets and offered minimal networking support and no mechanism for RPC. Sun later extended Java to support Remote Method Invocation (RMI). It allows a Java object on one JVM to invoke methods on an object in another JVM, possibly on a different machine.
Since RMI is designed for Java, there is no need for OS,
language, or architecture interoperability. This allows RMI to have a simple
and clean design.
Classes that interact with RMI must simply play by a couple of rules.
All parameters to remote methods must implement the
serializable interface.
This ensures that the data can be serialized into a byte stream (marshalled) for
transport over the network.
Serialization is a core aspect of marshalling: converting
data into a stream of bytes so that it can be sent over a
network or stored in a file or database.
All remote classes
must extend the remote interface. A remote interface
is one whose methods may be invoked from a different Java virtual machine.
Any class that is defined as extends Remote
can be a remote object.
Remote methods within that class must
be capable of throwing a java.rmi.RemoteException
.
This is an exception that the client RMI library will throw
if there is a communication error in calling the remote method.
RMI provides a naming service called rmiregistry to allow clients to locate remote object references. These objects are given symbolic names and looked up via a URI naming scheme (e.g., rmi://cs.rutgers.edu:2311/testinterface).
Java’s distributed garbage collection is somewhat simpler than Microsoft’s COM+. Instead of reference counting, it uses a form of leased-based garbage collection. There are two operations that a client can send to the server: dirty and clean. When the first reference to a remote object is made, the client JVM sends a dirty message to the server for that object. As long as local references exist for the object, the client will periodically send dirty messages to the server to renew the lease on the object. When the client’s JVM’s garbage collector detects that there are no more references to the object, it sends a clean message for that object to the server. Should the client exit abnormally, it will not renew its lease by refreshing the dirty call, so the lease will expire on the server and the server will destroy the object.
Python: implementing RPC via reflection
Modern languages, such as Python, provide opportunities for more transparency in generating remote interfaces by using the language’s reflection capability. Reflection, in programming, refers to the ability of a program to inspect and potentially modify its own structure and behavior during runtime.
Reflection may be used in remote procedure calls in several ways:
(1) Function/Method Registration:
- RPC servers often need to register functions or methods that should be accessible remotely. Reflection allows frameworks to dynamically identify these functions or methods, perhaps based on their names, annotations, or other metadata.
- For instance, in the
xmlrpc.server.SimpleXMLRPCServer
module, theregister_function
method can be used to register a function for remote access.
(2) Dynamic Invocation:
- When an RPC request arrives at the server, the server needs to dynamically find and invoke the corresponding function or method. Reflection enables this by letting the framework search for the function by name and then invoke it.
- For example, using Python’s
getattr
function, an RPC framework can retrieve a reference to a method of an object and then call it with provided arguments.
(3) Argument Handling:
- Reflection can be used to inspect the expected arguments of a function or method. This can be useful for type-checking, argument validation, or default value handling in some RPC implementations.
(4) Serialization and Deserialization:
- Some RPC frameworks can automatically serialize and deserialize custom Python objects. Reflection can help in this process by inspecting the attributes and methods of objects to determine how they should be represented in serialized form.
(5) Service Introspection:
- Some RPC systems offer clients the ability to query the server about available methods, their arguments, and return types. This introspection is possible because of reflection, where the server examines its own functions and provides metadata about them to the client.
Not all RPC frameworks in Python and other languages rely on reflection to the same degree. While frameworks like XML-RPC
and RPyC
use reflection more heavily, others, like gRPC
, rely more on predefined interfaces (defined using Protocol Buffers in the case of gRPC
) and generated code, minimizing the need for runtime reflection.
Reflection is not the only mechanism to implement RPC in Python, it is a powerful tool that many Python RPC frameworks leverage to provide dynamic, flexible, and introspective capabilities.
Python RPyC
RPyC (short for Remote Python Call) is one of several RPC frameworks available for Python. Some other RPC frameworks include yRO, PyInvoke, RPyC, and ZeroRPC. RPyC is designed to operate in a trusted environment.
The design of RPyC had several key goals:
- Transparent RPC interface:
- No need for interface definition files, stub compilers, name servers, HTTP servers, or special syntax.
- Symmetric
- Both sides can invoke RPCs on each other. This enables callbacks – for the called process to later notify the calling process of some event.
- Server incorporated with the server functions
- RPyC binds to a default port (18812) or you specify the host’s IP address and port explicitly
- Client connects to the server
- Once the client connects to the server, it can perform remote operations through the
modules
property, which exposes the server module’s namespace.
What helps Python achieve transparency is that the language enables the inspection of live objects (modules, classes, methods, functions) through the inspect module. You can examine the contents of a class, retrieve the source code for a method, and extract the argument list for a function. The general idea behind the use of RPyC is that you create a connection using an rpyc
object and then simply invoke methods via that object.
Unlike the other RPC systems we examined, RPyC supports passing objects by value or by reference. Immutable data types (such as strings, integers, tuples) are passed by value: they are serialized and sent to the other side. Other objects are passed by reference. A reference becomes a special proxy object called a netref that behaves just like the actual object but the proxy sends operations to the remote process – effectively a remote procedure call in the other direction. This feature enables a process to pass location-sensitive objects, like files or other OS resources. For instance, a process can write to the stdout (standard output) of a local process by getting its sys.stdout
.
On the client side, the client creates local proxy objects for remote modules. Just like with other RPC systems, this enables transparent access. Any operation on the proxy object is delivered to the remote side.
RPyC supports both synchronous and asynchronous calls. With synchronous calls, the code that issues the operation waits for a return. With asynchronous calls, the call returns immediately but the process later receives a notification when the remote operation is complete. Calls can be made asynchronous by wrapping the proxy with an asynchronous wrapper.
RPyC is built around exposing remote services. Each process exposes a service that is responsible for the policy – the set of supported operations. Services are simply classes that derive from rpyc.core.service.Service
and define exposed
methods. These methods are either names that explicitly begin with exposed_
or have the @rpyc.exposed
decorator applied to them. All exposed members of a service class will be available to the other side