This month, the more advanced techniques of naming and event services are discussed.
In our last article, we introduced the concept of distributed programming with CORBA from a high-level point of view. In order to further flush out the CORBA infrastructure, we need to detail some of the standard services that the OMG (Object Management Group) has defined that should be supplied at least in part by most ORB vendors. Among these are the Trader Service, the Naming Service, the Event Service, the Interface Repository and the Implementation Repository.
The OMG has defined only the interface to each service while not attempting to provide an implementation. This means an OMG Service is actually nothing more than a CORBA interface written in IDL (Interface Definition Language). If a particular service is not available within a particular ORB or is not well-implemented, the developer always has the option of writing a custom implementation for the interface. In fact, if a vendor is truly CORBA-compliant, one vendor's implementation of a service can be used with another vendor's implementation of the ORB. This ability to mix and match CORBA-compliant implementations allows for flexible approaches to CORBA solutions. In this article, we will describe two of the most commonly provided OMG Services: the Naming Service and the Event Service. Our sample code is written using the feature-rich and GNU-licensed MICO CORBA implementation and demonstrates how to use both the Naming and Event Services in C++.
Last month, we introduced the concept of an IOR (Interoperable Object Reference), which we said was like a phone number or mailing address for the remote object. The client application can use the IOR to locate the remote object and establish communication. In that article, we handed the client application the IOR by writing it to a file and passing the file to the server application at startup. In practice, this is an inconvenient way to design a system. One of the most common approaches to solving the problem of locating objects at runtime is to use the OMG Naming Service. The Naming Service is an interface to a database where an object's name is associated with its IOR.
In order to understand the Naming Service, it is often helpful to think in terms of the UNIX directory structure. The Naming Service is comprised of objects called naming contexts. A naming context can be thought of as a directory within a file system, ultimately deriving from a common root directory (the “root” context). Each name within a naming context must be unique. Since naming contexts are actually objects, a naming context can be registered with another naming context. In effect, this is analogous to creating a subdirectory within another directory in a file system. The hierarchical structure created by this method is called a naming graph. In order to simplify finding objects within a naming graph, the Naming Service allows objects to be referred to by compound names, which are similar to an absolute path name in UNIX.
The name under which an object is registered in the Naming Service is completely discretional and not required to even describe the actual object. In the Naming Service, the object's name is defined by a NameComponent object. These NameComponent objects are then stored in a particular naming context. The NameComponent object actually consists of two parts, an “identifier” and a “kind”. The NameComponent is represented in IDL as:
struct NameComponent { Istring id; Istring kind; };
Returning to the UNIX file system analogy, a UNIX file called Consumer.C would have an identifier of Consumer and a kind of C. In the same manner, an object may be stored in a naming context with an identifier of BusinessObject and a kind of java. The developer can thus use any naming standard he wishes when defining objects using the Naming Service.
In order for a CORBA client object to use the Naming Service to find other objects, it must know where to find the naming service. The preferred method of finding the Naming Service is to use the OMG method resolve_initial_references. Under most ORB solutions, resolve_initial_references will return the IOR of the “root naming context”, or in effect, the root directory node.
In simplest terms, when a server application is launched, it registers or “binds” objects it wishes to expose with the Naming Service using compound names. This is accomplished through the bind and rebind methods. The client application can then look up a particular object's IOR simply by resolving the object's compound name, which the client must know. The client application uses the resolve method to find an IOR from a given compound name. Once the name has been resolved and the IOR obtained, the application can narrow (narrowing an object is CORBA terminology for downcasting) the object reference to resolve the actual object implementation; from that point on, the object can be used as usual. Later, our example demonstrates how you might use the Naming Service to register and locate object implementations.
Another service with an OMG-defined interface is the Event Service. The OMG Event Service specification provides for decoupled message transfer between CORBA objects. The decoupling of communication provided by the Event Service allows for flexibility in terms of communication modes and methods. Specifically, it allows one object (Supplier) the ability to send messages to another object (Consumer) that is interested in receiving those messages without having to know where the receiver is or even whether the receiver is listening. This decoupling provides several important benefits:
Suppliers and Consumers do not have to physically handle the communication and do not need any specific knowledge of each other. They simply connect to the Event Service, which mediates their communication.
Message passing between the Supplier and Consumer takes place asynchronously. Message delivery does not need to entail blocking (although a pull Consumer may choose to block if it wishes—see below).
Event Channels can be set up to be either typed or untyped (not all ORB implementations support typed events).
Event Channels will automatically buffer received events until a suitable Consumer expresses interest in the events. Note that this does not imply either persistence or store and forward capabilities. Generally, an independent queue in the Event Channel will be devoted to each Consumer. These internal queues are generally based on a LIFO (last-in first-out) basis, with older messages disposed when the buffer is full and new messages arrive, without a Consumer extracting the messages fast enough. Most ORBs will allow you to set the maximum queue length.
Events can be confirmed and can have their delivery guaranteed, if the vendor has implemented this capability.
Suppliers can choose to either push events onto the channel (push) or have the channel request events from them (pull). Similarly, a Consumer may request to either synchronously (pull) or asynchronously (try_pull) obtain events from the channel, or have the channel deliver events to them (push).
A one-to-one correspondence between Suppliers and Consumers is not necessary. There can be multiple Suppliers connected to a single Consumer via the Event Service, as well as a single Supplier connected to one or more Consumers.
Two primary styles of interaction exist between Suppliers and Consumers and the Event Channel: Push and Pull.
In the Push Method, a Supplier will connect to the Event Channel and initiate a push of an event onto the Event Channel whenever it is ready to do so. It is the Event Channel's responsibility to buffer those events until they are delivered to one or more interested Consumers. In the Push Model, it is the Supplier that initiates the flow of events to the Event Channel. When a Supplier wants to connect to an Event Channel, it needs an object within the Event Channel to “pretend” it is a Consumer. This allows the Supplier to simply deliver events to its “Consumer”, when in reality, its Consumer is simply a proxy for the actual Consumer, which is outside the Event Channel. It is to this proxy “Consumer” that the Push Supplier pushes events. Thus, the Proxy object is not a real Consumer, but merely an object within the Event Channel that provides a delivery mechanism through which the Supplier can deliver messages.
A Push Consumer will likewise connect to a “proxy” object, a proxy that represents the Push Supplier. When the Event Channel has a message available, the Push Supplier proxy will deliver (push) the message to the actual Consumer object. The message path is from the actual Push Supplier, through its Proxy Push Consumer, to the Proxy Push Supplier and finally to the Push Consumer itself. There are other variations of this, as our later example will show.
In the Pull Method, the Event Channel will pull data from the Supplier. In the Pull Model, it is the Consumer that drives the delivery of messages. A Pull Supplier will connect to a Proxy Pull Consumer. Again, as far as the Pull Supplier is concerned, it can consider this proxy object as a real Consumer that will request events periodically. An interested Pull Consumer object will then connect on the other end of the Event Channel to a Proxy Pull Supplier. When a Pull Consumer is ready to receive an event, it will initiate either a pull or try_pull call on its Proxy Pull Supplier, which will in turn query the Proxy Pull Consumer connected to the actual Pull Supplier, to request another event be delivered. In this way, the Consumer drives the data, when it is ready to process another message. Some implementations of the Pull Method will allow the Proxy Pull Supplier to periodically pull events from the Supplier at regular intervals in an attempt to keep a buffer full of events for consumers when they request delivery.
The nice thing about the Event Channel abstraction is a communication does not need to be either entirely Push Model or Pull Model. A Push Supplier may indirectly connect to one or more Pull Consumers, and several Pull Suppliers may connect to one or more Push Consumers. It is the Event Channel logic that allows such interrelationships disproportionality among objects. It is the application design that drives the decisions concerning suppliers, consumers and their numbers.
Regardless of the relationship among suppliers and consumers, to establish a connection and deliver events through the Event Channel the following five steps must be taken:
The client (Supplier or Consumer) must bind to the Event Channel, which must already have been created by someone, perhaps the client.
The client must get an Admin object from the Event Channel.
The client must obtain a proxy object from the Admin object—a Consumer Proxy for a Supplier client and a Supplier Proxy for a Consumer client.
Add the Supplier or Consumer to the event channel via a connect call.
Transfer data between the client and the Event Channel via a push, pull or try_pull call.
When messages are delivered through the Event Channel, they can be either “typed” or “untyped”. Typed messages are those defined in an IDL which are type-checked at compile time. Untyped events, the most common, adhere to the standard Event Services interfaces and are packaged as type CORBA::Any, which is a wrapper around all known CORBA types. It is this “Any” type that is actually sent from a Supplier Object to a Consumer Object. The Supplier will construct an Any, and the Consumer, upon receipt of the message, will derive the true value from the Any wrapper. This allows for great flexibility in delivering messages, as a Supplier may pass a string first, a long value second and an array third, all through packaging the values into an Any. The example code shows how to create, embed and extract values from Any types.
Our example incorporates both the Naming Service lookup as well as an implementation of a Supplier and a Consumer interacting through the use of the Event Service. The Supplier implements the Push Supplier Model and the Consumer implements the Pull Consumer Model, thus illustrating that the models do not have to be all of one type. Listing 1 shows Consumer.C, and Listing 2 shows Supplier.C. These listings are available by anonymous download in the file ftp://ftp.linuxjournal.com/pub/lj/listings/issue62/3213.tgz.
The first step the Consumer must take is to find the root naming context. This is accomplished by calling resolve_initial_references and then narrowing the returned IOR. The resulting object is the root naming context we can then use to resolve our Event Service.
CORBA::Object_var nsobj = orb->resolve_initial_references("NameService"); assert(! CORBA::is_nil(nsobj)); CosNaming::NamingContext_var context = CosNaming::NamingContext::_narrow(nsobj); assert(! CORBA::is_nil(context));
As we turn to the Event Service sections of the code, we notice that the first thing the Consumer does after obtaining the initial context from the Naming Service is resolve and narrow the EventChannelFactory.
CosNaming::Name name; name.length(1); name[0].id = CORBA::string_dup("EventChannelFactory"); name[0].kind = CORBA::string_dup("factory"); CORBA::Object_var obj; obj = context->resolve(name);MICO uses the factory referenced above as a generic CORBA::Object to create a new Event Channel object by first narrowing the generic reference, then calling the factory's create_eventchannel function:
SimpleEventChannelAdmin::EventChannelFactory_var factory; CosEventChannelAdmin::EventChannel_var event_channel; factory = SimpleEventChannelAdmin::EventChannelFactory::_narrow(obj); event_channel = factory->create_eventchannel();We then use the Naming Service to bind this newly created Event Channel object to the name TestEventChannel via the Naming Service's bind method. This is done so that the Supplier will be able to locate this particular Event Channel by the name TestEventChannel when needed.
name.length(1); name[0].id = CORBA::string_dup("TestEventChannel"); name[0].kind = CORBA::string_dup(""); context->bind(name,<\n> CosEventChannelAdmin::EventChannel:: _duplicate(event_channel));Once the Event Channel has been created and named, the Event Channel object (event_channel) is used to obtain a reference to a ConsumerAdmin object through the for_consumers function. The ConsumerAdmin object provides the proxies for the Consumer clients of the Event Channel. It allows the Consumer to obtain the appropriate Supplier Proxy. In our case, we use the ConsumerAdmin object to provide us (a Pull Consumer) with a proxy Pull Supplier. This allows our Consumer object to act as if it were communicating directly with a Supplier that expects us to be “pulling” events from it. Of course, that's not actually the case. Our Supplier is really a Push Supplier that pushes events onto the Event Channel. The proxies decouple the Consumer and Supplier objects and allow them to function as if they were directly connected, when in fact, their connection is indirect. Once we have the ConsumerAdmin, we use it to create our Push Consumer proxy:
CosEventChannelAdmin::ConsumerAdmin_var Consumer_admin; Consumer_admin = event_channel->for_consumers(); ... CosEventChannelAdmin::ProxyPullSupplier_var proxy_Supplier; proxy_Supplier = Consumer_admin->obtain_pull_Supplier();Once the Consumer has obtained a reference to its Supplier Proxy, it then notifies the Event Channel of its interest in receiving events from it through a call to the Proxy's connect_pull_Consumer method. An implementation of the Event Service's Pull Consumer interface is passed into the proxy_Supplier to make the connection.
proxy_Supplier->connect_pull_Consumer (CosEventComm::PullConsumer::_duplicate(Consumer));Once connected, calls can be made on the Proxy Pull Supplier's pull or try_pull functions. The PullSupplier interface is:
interface PullSupplier { any pull() raises(Disconnected); any try_pull(out boolean has_event) raises(Disconnected); void disconnect_pull_Supplier(); };In our case, we have the Consumer spawn a worker thread, and we pass the Pull Supplier Proxy reference to that thread, the one that actually makes the try_pull call. The try_pull call is an asynchronous polling mechanism allowing the Consumer to contact the Event Channel and “check for mail”. If there is a message in the Event Channel, that message will be returned as a CORBA::Any value, and the try_pull's CORBA::Boolean flag has_event will be set to true. The try_pull call is thus made from within the thread's “start” function in this way:
CORBA::Any* anyval; CORBA::Boolean has_event = 0; anyval = proxy_Supplier->try_pull(has_event);If no event is waiting, the has_event flag is set to false and no value is returned; but the call does not block (as the pull function does), so it returns to the client immediately. This allows the client to continue doing other work while periodically checking to see if a new event message is waiting in the Event Channel's queue.
Once the has_event value is true and an Any value is retrieved, the Consumer must decide first what type it is, then extract that value from the Any wrapper in order to use it. The code to do that uses the Any's overloaded >>= operator. This strange-looking beast will attempt to extract the Any into the destination type. If the type contained in the Any is compatible with the destination type, the value is extracted from the Any; if not, null is returned. The usual way to check for the value is to do something like the following:
if( *anyval >>= shortval ) { cerr << "Consumer: thread pulled short: " << shortval << endl; } else if( *anyval >>= doubleval ) { cerr << setiosflags(ios::fixed); cerr << "Consumer: thread pulled double: " << doubleval << endl; }
In our case, when we extract the correct type from the Any, we print it out and immediately begin checking again for events through our try_pull call.
Our Supplier implementation is a bit simpler. After binding to the ORB, it creates an implementation of a class that implements the CORBA PushSupplier IDL:
class PushSupplierImpl : virtual public CosEventComm::PushSupplier_skel { public: PushSupplierImpl() { } void disconnect_push_Supplier(); }; ... PushSupplierImpl * Supplier = new PushSupplierImpl();
This class implements the IDL PushSupplier interface, which has only a single function to implement: disconnect_push_Supplier. The implementation object, PushSupplierImpl * Supplier, will be used later to connect to the Event Channel and register our interest in supplying events to the Channel.
Just as the Consumer started by finding the root Naming Context, our Supplier begins by calling resolve_initial_references. Using the IOR returned by resolve_initial_references, the Supplier can then narrow to the naming context object.
CORBA::Object_var nsobj = orb->resolve_initial_references("NameService"); assert(! CORBA::is_nil(nsobj)); cerr << "Supplier: successful call to \ resolve_initial_references()" << endl; CosNaming::NamingContext_var context = CosNaming::NamingContext::_narrow(nsobj); assert(! CORBA::is_nil(context));
Once the name is resolved and narrowed, the Supplier attempts to retrieve a SupplierAdmin object through a call to the event channel's for_suppliers function.
CosNaming::Name name; name.length(1); name[0].id = CORBA::string_dup("TestEventChannel"); name[0].kind = CORBA::string_dup(""); CORBA::Object_var obj; ... obj = context->resolve(name); ... CosEventChannelAdmin::EventChannel_var event_channel; CosEventChannelAdmin::SupplierAdmin_var Supplier_admin; ... event_channel = CosEventChannelAdmin::EventChannel::_narrow(obj); Supplier_admin = event_channel->for_suppliers();Once the SupplierAdmin object is retrieved, its obtain_push_Consumer function is called in order for the Supplier to obtain a Proxy PushConsumer with which to communicate.
CosEventChannelAdmin::ProxyPushConsumer_var proxy_Consumer; ... proxy_Consumer = Supplier_admin->obtain_push_Consumer();Once a proxy is obtained, we then need to connect the Supplier to the proxy through this call:
proxy_Consumer->connect_push_Supplier( CosEventComm::PushSupplier::_duplicate(Supplier));This call registers our interest in providing the Event Channel with events. The IDL interface for the PushConsumer (the implementation of which ProxyPushConsumer inherits) is:
interface PushConsumer { void push(in any data) raises(Disconnected); void disconnect_push_Consumer(); };Once a proxy push Consumer has been obtained, calls may be made on its push function, passing in a CORBA::Any value. This is done quite simply:
CORBA::Any any; any <<=(CORBA::ULong) 555555555; proxy_Consumer->push(any);At this point, the Any value is delivered to the Event Channel, which is responsible for making that event message available to the try_pull calls of the Consumer, described above. Thus, we have come full circle in our discussion of the Supplier/Consumer roles in interacting with the Event Service.
Our example was built using the egcs 1.1b C++ compiler and MICO 2.2.1. In order to build and run the example, once you have unpacked the tar file, you simply need to update the variable MICO_BASEDIR in the Makefile to point to your base Mico installation, then type make. This will build both the Supplier and Consumer. To run the application, we've provided a simple script that starts the rather lengthy MICO naming and event services for you automatically, then starts the Consumer (which creates the Event Channel), then the Supplier. To run the script, simply type runit. You will see the progress of the Supplier writing messages to the Event Channel, and the Consumer extracting them from the Event Channel; as it does so, it prints them out. Our Supplier will push, in succession, a long, a short, a double, a string, and finally another long (the number 13), which signals to the Consumer that it is finished. At that point, the Consumer thread exits and the applications are killed by the runit script.
Our next article will discuss an implementation of VisiBroker for Java that can be made available for development of clients and servers completely on Linux using Sun's JDK.
Resources
Home page for the Object Management Group: http://www.omg.org/
Introduction to CORBA: http://www.omg.org/news/begin.htm
The Free CORBA Page: http://adams.patriot.net/~tvalesky/freecorba.html
Java port for Linux: http://java.blackdown.org/
The CORBA FAQ: http://www.cerfnet.com/~mpcline/Corba-FAQ