The promise of distributed object systems revolves around different software components, spread all over the network, talking to each other. The reality of such systems, however, involves everything from different byte ordering on different machines and different system API calls, to complications ranging from low-level socket programming to the staggering overhead of systems like CORBA. Life isn't easy for programmers who want to write distributed object-based systems that don't require all the features of heavyweight middleware technologies. In fact, there is a whole class of distributed systems that would benefit from a lightweight, portable library for interobject communication that is technically somewhere between low-level socket programming and high-level middleware infrastructures.
The YAMI (Yet Another Messaging Infrastructure) project (http://www.mobczak.com/prog/yami/) fills the gap between brute-force interobject communication and the expansive middleware technologies. YAMI is an open project targeting different platforms currently Windows, Linux, FreeBSD, and SunOS and different programming languages C, C++, Python, and Tcl, for the time being. The library is free, and its use is limited only by minimal "please do not remove copyright notes from the source code."
The YAMI world consists of objects exchanging messages. Every software component capable of sending messages is a first-class YAMI object. The objects that are addressees of any message are grouped in domains. A single domain is managed by the agent. Actually, all of the network data transfer is performed by the graph of connected agents each operating in its own domain. Figure 1 illustrates this concept. The domain has a concrete (explicit) network address and port on which the agent listens to messages coming from remote objects. An object also uses the agent to send messages to other objects.
As Figure 1 indicates, a single agent can send/receive messages for all objects within a domain. The message's address consists of the remote domain's network address and port (actually, the network address and port on which the domain's agent is listening), and the name of the object within the domain that will receive the message. Once the message is received, it is stored in the agent's internal data structures for retrieval by the receiving object. The object can then read the message and reply to it if necessary.
A set of parameters is passed with each message. To effectively implement the varying types of the parameters, two levels of type conformance are defined:
Compared to COM, CORBA, and Java RMI, this model is simplified because there is no notion of the interface or reference to the object. The message can be sent to some remote object based on the knowledge that there is a domain with some network address in which there is a registered object with some name that will understand the message. No compile-time checking is performed, resulting in the possibility of raising runtime exceptions when some domain or object rejects the incoming message. Thanks to this lack of static type safety, YAMI programming does not involve writing interface definitions (although static interface definitions are supported; see later examples) and the library can be easily plugged into a script interpreter. Another characteristic of this messaging model is that it is completely asynchronous. A message is not obligatorily mapped to the function call semantics. Instead, when some object sends a message with the help of an agent, the agent returns a message token, which can be used for later reference. This means that many messages can be sent without waiting for their results. From the remote end (where another object receives the message), it is similar. The receiving object is not obligated to immediately respond to the message once it is received. This asynchronous model is very flexible and is a functional superset of many other communication strategies, including one-way messages (when there is no reply to the message) as well as the well-known synchronous model (where the sender explicitly waits for the response from the remote object). To facilitate this latter model, it is possible to map incoming messages to normal function calls on the server side. If interface definitions are used, both client and server can operate with the method-call semantics.
The best way to describe the YAMI communication library is to trace the development of a distributed client/server system. In this article, I present a calculator example, equivalent to those in books devoted to other communication infrastructures.
The YAMI library exists in versions for different programming languages and, in each language, implements similar concepts. Thanks to this, the development is consistent; so once you understand how the library works for C++, you won't have a problem writing parts of your system in other languages. This, of course, minimizes the learning curve if you want to use YAMI for developing distributed applications in many languages. The library was written with object-based concepts in mind, so even C programmers find the library consistent and should have no problem porting the C++ code. Here, I concentrate on C++ support, but I will also show the equivalent client code written in Python and Tcl (they have built-in support for lists, which seamlessly integrate with the notion of parameter lists in YAMI). The calculator example assumes that both the client and server reside on the same machine, but it is easy to make the code work across the network.
YAMI provides extensive support for server-side development. The server is created as a process that runs forever, waiting for invocations from the clients. The typical steps on the server side are:
These steps are similar to other messaging infrastructures, such as CORBA and Java RMI. In YAMI, two types of servants are possible:
This example assumes the passive model, which is more object oriented. Listing 1 is the interface that servants need to implement to be able to register with the agent. When the message arrives, the call method is called. IncomingMsg references all the information that is associated with the message, such as its name and a set of parameters. Listing 2 is the complete server code.
At the top, the appropriate header is included. All the names defined in the YAMI library reside in their own namespace. Later, the class Calculator is defined as a class derived from the PassiveObject interface. The call method implements the server's logic:
Later, the result of the requested operation is computed and the new set of parameters is constructed to contain the resulting integer value, which is passed to the reply method.
In the main function, these operations are performed:
That's all. In this example, only one servant is used on the server, but the number of objects registered at the same time with one agent is not limited (however, they will be registered with names that are unique in the scope of a single domain).
In Listing 3, the client, assumes that the server runs on the same machine (127.0.0.1 is used as an address of the server's domain), but this is only for presentation. In real systems, use the address of the machine on which the server was started.
After creating its own agent, the client registers a remote domain in its own address book. Later, the client prepares a set of parameters, which will consist of two integers. The set of parameters is used in the call to the agent's send method, which returns a handle to the message object. From then on, the agent takes care of message delivery and the client can proceed.
In this particular example, there is nothing to do until the reply arrives from the server, so the client decides to wait. The status of the message lets the client decide how to continue its work. If the message was replied to properly, the returning set of parameters is extracted and the result displayed on the console.
For comparison, Listings 4 and 5 present clients written in Python and Tcl, respectively. From the point of view of the server, all clients presented so far are identical.
Listings 2 and 3 operate on messages explicitly in the sense that the notions of parameter set and message name are visible to the programmer. The advantage of this approach is that it is possible to write a system that is fully dynamic with no compile-time or runtime constraint on the names and possible parameters that can be sent between objects.
The disadvantage is that operating on this level is overly verbose in systems where objects exchange messages that are supposed to express some predefined interfaces.
YDL is a language and a compiler, letting you design the system in terms of static interfaces. The name intentionally references IDL used in other infrastructures.
Listing 6 presents a definition of the calculator server's interface. YDL, as a language, is not built on any existing syntax because there is no need to do so. The interface definition can be compiled using a special compiler, resulting in additional source files that need to be used in server or client programs.
YDL is similar to CORBA in that you need to do the following:
Listing 7 is the calculator server built using the YDL interface. Only the servant's definition has changed with regard to Listing 2. The preparation for messaging remains the same. Also, throwing an exception in the servant automatically results in a message rejection. Listing 8 is client code that now uses a special object, which is a local representative of the server. Both listings contain additional include directives that pull what was produced by the YDL compiler.
The YAMI library lets you do much more than is indicated in these simple examples. One interesting feature is the ability to forward messages without changing the sender's address. With this feature, it is easy to implement transparent load balancing across many servers. It is possible to register the default servant, which can accept messages sent to objects whose names are not registered (and would be rejected by the agent otherwise). This feature lets you implement servers where objects are physically created only when necessary. Moreover, agents can be created with a set of policies that influence parameters such as the size of the connection pool, the number of threads used for message dispatching, and more.
It is possible to compile the library without the threading support. This may be convenient if you're working on systems where there are no threads, or if you need extremely small builds (about 50 KB).
Apart from the core library functionality, the set of standard services is prepared to provide a consistent environment for distributed-systems development. At this writing, the Property Service (it can be used as a Registry or Name Service) and Event Service are implemented as standalone applications, ready for use as building blocks in bigger systems.
The library was written entirely in C and the support for other languages is implemented in terms of wrappers in each particular language. The concepts that lie beneath the communication model (for example, no notion of the object reference and communication session, and so on) influence the performance. Yet, when it comes to performance, the library is comparable to Java RMI and CORBA, depending on the particular hardware and software configuration. Refer to http://www.msobczak.com/prog/yami/ for performance tests results, numbers, charts, and comments.