|
Index
- 1. Introduction
- 2. Developing a simple client / server
- 3. CORBA advantages & disadvantages
- 4. References
1. Introduction
1.1 A brief overview of CORBA
CORBA is a specification of OMG (Object Management Group) formed in 1989, with the intention of adopting distributed object systems to use the advantages of object-oriented programming to develop distributed software for distributed systems. To achieve this goal, the OMG advocated the use of open systems based on standard object-oriented interfaces. The advantage of this issue was any distributed object could be implemented in any programming language and could communicate with one another. Therefore, applications could work in heterogeneous hardware, networks, operating systems, and programming languages. The OMG introduced the ORB (Object Request Broker) whose role is to help a client to invoke a method on an object transparently.
In 1991, a group of companies created a new specification for ORB called CORBA, which stands for Common Object Request Architecture. The CORBA 2.0 specification was defined in 1996, which allows implementations made by different developers to communicate with one another. This standard, called General Inter-ORB protocol or GIOP, that was intended to be implemented over any transport layer using TCP/IP, is called Internet Inter-ORB protocol or IIOP.
1.2 CORBA architecture
The architecture was designed to support the ORB that allows clients to invoke remote methods in other objects, where the clients and servers can be implemented in a variety of programming languages such as: Ada, C, C++, Java, SmallTalk, and Python. The main layout of the CORBA architecture is shown in the image below:
There are two kinds of invocations in CORBA: static and dynamic. Static invocations are used when the remote interface of the remote object is known at compile time, so the stub for the client and the skeleton can be used. However, if the remote interface for the object is not known at compile time, a dynamic invocation must be used. Most programmers use static invocation because it is a more natural and smart programming model.
The elements shown above are:
- ORB Core: It's the base of the CORBA architecture, it enables to communicate through a request / reply protocol between the server and the client side. Requests and responses between objects are delivered in a standard format defined by the Internet Inter-ORB Protocol (IIOP).
- Portable Object Adapter (POA): It has several roles like: creating remote object references for CORBA objects, dispatching RMI via a skeleton to the suitable servant, and activating objects. An object adapter gives each CORBA object a unique name, which forms part of its remote object reference. Each object adapter has its own name, which also forms part of the remote object references of all the CORBA objects it manages. The CORBA 2.2 standard for object adapters is called the Portable Object Adapter. It is called portable because it allows applications and servants to be run in any ORB produced by different developers.
- Skeleton: The skeleton classes are generated in the server language by the IDL compiler. The remote method invocation (RMI) are dispatched through the skeleton for the suitable servant, and unmarshals arguments in the request messages and marshals exceptions and results in the reply messages.
- Client stub/proxies: Are implemented in the client language and generated with the IDL compiler. As before, the client stub marshals the arguments in the invocation request and unmarshals exceptions and results in the replies.
- Implementation repository (IMR): Handles the automated start and restart of servers and realizes object persistency.
- Interface repository (IFR): The role of the IFR is to provide information about registered IDL interfaces to clients and servers that require it. It can provide the name of the methods, and for each method, its argument names and exceptions.
- Dynamic Skeleton Interface (DSI): This allows a CORBA object to accept invocations on an interface for which it has no skeleton because the type of its interface was not known at compile time.
- Dynamic Interface Invocation (DII): In some applications, a client without a proxy class needs to invoke a method in a remote object. It is used when it is not possible to use stubs. The client can obtain the required information about the available remote methods in the CORBA object through the Interface repository (IFR).
1.3 Reference to remote objects
CORBA 2.0 specifies a format for remote object reference. The references that use this format are called interoperable remote object references (IOR, interoperable object reference). The next block diagram shows the IOR details:
IDL Interface type name |
Protocol and address detail |
Object key | ||||
Interface repository identifier | IIOP | Host domain name | Port number | Adapter name | Object name |
- The first field specifies the type name of the IDL interface of the CORBA object. If the ORB has an interface repository, this type name is also the IDL interface identifier in the interface repository.
- The second field specifies the transport protocol and the required details in order that the transport protocol identifies the server. The Internet Inter-ORB protocol (IIOP) uses TCP/IP, in which the server address contains the host domain name and a port number.
- The third field is used for the ORB to identify a CORBA object. It consists of the object adapter in the server and the object name of a CORBA object specified by the object adapter.
There are two kinds of IORs: Transients and Persistents. Transient IORs last only as long as the process that host them, whereas Persistent IORs last between activations of the CORBA objects. A transient IOR contains the address details of the server containing the CORBA object, whereas a persistent IOR will contain the implementation repository addresses.
1.4 IDL basics
The IDL (Interface Definition Language) is designed to allow objects implemented in other languages to invoke one another. The IDL uses a notation to define interfaces with methods and input and output parameters. The IDL is a neutral language, and is later converted to a skeleton and stub by the IDL compiler, usually typed as idl (in Orbacus ORB) or idl2cpp (in VisiBroker and Orbit), for example. This allows a Java client to communicate with C++ server methods. The IDL language grammar is very similar to C++, and is a subset of C++ ANSI. The IDL provides facilities to define the follwing elements:
1.4.1 Modules
The module
construct allows interfaces and other IDL type definitions to be grouped in logical units. The keyword is module
, and avoids name collisions of others outside it.
module Computer { interface Mouse {}; interface Keyboard {}; };
1.4.2 Interfaces and their methods
As seen before, an IDL interface describes the available methods in the CORBA object that implements that interface through the skeleton.
module Computer { interface Mouse { void setCursor(in long x,in long y); void getCursor(out long x,out long y); }; interface Keyboard {}; };
The IDL interface methods are specified with the keywords in
, out
, or inout
depending on if the arguments are for input, output, or both. A method specified as oneway
indicates that the client that invokes the method will not be locked while the target object performs the method. For example:
oneway void docall(in long data);
The optional expression raises
indicates user defined exceptions that can be thrown to finish the method execution.
interface Network { Computer getHost(in long host) raises (NetworkException); }; exception NetworkException {string error_code}
Interfaces can have multiple inheritance. For example:
interface A {}; interface B : A{}; interface C{}; interface Z : B, C{};
1.4.3 Constructed types
sequence
: Defines a type to a variable length sequence. The size can be fixed or undefined.Collapse Copy Codetypedef sequence<Computer,100> Network; typedef sequence<Computer> Network;
string
: Defines a character sequence, null terminated. Can be unlimited or fixed. Similar to an STL string.Collapse Copy Codetypedef string <8> Computername; string Computername;
array
: Defines a type for a multidimensional sequence of fixed size of elements.Collapse Copy Codetypedef long ports[12]; typedef Network Matrix[10][8];
record
: Defines a type containing a set of related entities. Very similar to C/C++.Collapse Copy Codestruct information{ string type; Computer cluster[200]; long size; };
enumerated
: Relates a name type with a small set of integer values.Collapse Copy Codeenum Platform (PC, MAC, IBM, WorkStation);
union
: The IDL discriminated union allows one of a given set of types to be passed as an argument.Collapse Copy Codeunion Exp switch(Platform) case PC: string motherboard; case MAC: string osversion; case IBM: string provider; case WorkStation: string multiprocessor_number; };
1.4.4 Primitive types
Type | Signification |
---|---|
short | 16 bit signed integer |
unsigned short | 16 bit unsigned integer |
long | 32 bit signed integer |
unsigned long | 32 bit unsigned integer |
long long | 64 bit signed integer |
unsigned long long | 64 bit unsigned integer |
float | 32 bit IEEE float |
double | 64 bit IEEE float |
long double | 128 bit float |
boolean | boolean value: TRUE or FALSE |
octet | 8 bit byte |
char | 8 bit character (ISO latin-1) |
wchar | international character format |
string | character string based on char |
wstring | character string based on wchar |
1.4.5 Type any
The type any
is an universal container. It can be used when we don't know at compile time what IDL types you will need to transmit between the client and the server. The any
type can represent any of the primitive and constructed types previously seen.
1.4.6 Attributes
IDL interfaces can have as many attributes as methods. The attributes can be defined as readonly
. The attributes are private to CORBA objects, but two access methods are generated for each one automatically by the IDL compiler, i.e, get
and set
. The readonly
attributes have only the get method.
attribute string number_of_keys;
1.4.7 Parameter passing
- CORBA objects: Any parameter of an
interface
type is passed by reference. All CORBA objects that implements theinterface
type inherits theObject
base type which represents a reference to a remote object. - Primitive and constructed types: They are copied and passed by value.
1.5 CORBA basic services
- Naming service: It allows names to be bound to remote object references of CORBA objects within naming contexts. A naming context is the scope within a set of names - each name must be unique. A name can be associated with either an object reference for a CORBA object in an application or with another context in the naming service. Contexts can be nested to provide a hierarchical name space.
- Event service and notification service: The CORBA event services define interfaces, allowing objects of interest, called suppliers, to communicate notifications to subscribers, called consumers, in a push and pull way. The notification can be pushed by the suppliers to the consumers as pulled by the consumer from the supplier.
- Security service: It allows authentication of servers and users, and controls access to CORBA objects when an RMI is produced. The access rights can be defined with ACLs, etc.
- Trading service: In contrast to the Naming service which allows CORBA objects to be located by name, the Trading service allows to locate them by attributes like a directory service.
- Transaction service and concurrency control service: The transaction service allows distributed CORBA objects to participate in either flat or nested transactions. The concurrency service allows concurrency access to CORBA objects.
- Persistent object service: A CORBA object can be stored in a passive way, while they are not in use, and then can be recovered and activated on demand. The Persistent object service provides a persistent object store of CORBA objects.
2. Developing a simple client / server
For the development of a basic client/server in CORBA, I have used the Orbacus ORB that you can find here. It's a free ORB for non-commercial purposes, with an unlimited time of use. The package is available for both Java and C++, and the specification is up-to-date CORBA compliant.
2.1 The business logic domain
The demo application is inspired by a cryptographic service where the client makes a request for encrypting and decrypting a plaint text to a server which performs the operations. The encryption algorithm is based on the Caesar cipher, which is one of the simplest encryption techniques. It's a type of substitution cipher in which each letter in the plain text is replaced by a letter, some fixed number of positions down the alphabet. Besides this technique, I added a XOR operation with an encryption key after the shift operation to obscure the relationship. In the image below, you can see this technique:
2.2 Writing the IDL
The first step before implementing the C++ application is to write the IDL file with the specific syntax defining the interfaces, structures, sequences, and methods. The IDL file created for the demo application is the following:
// Crypt.idl interface CaesarAlgorithm { typedef sequence<char> charsequence; charsequence encrypt(in string info,in unsigned long k,in unsigned long shift); string decrypt(in charsequence info,in unsigned long k,in unsigned long shift); };
There is only one interface that will represent the remote CORBA object which will perform the encryption operations. This interface defines an ANSI charsequence
and two methods.
2.3 Generating the skeleton and the stub
After writing the IDL file, we will proceed to compile it by calling idl.exe crypt.idl. This little compiler will generate the skeleton and the stub, with the following names:
Skeleton | Stub |
---|---|
crypt_skel.h | crypt.h |
crypt_skel.cpp | crypt.cpp |
Note: Try to configure your system environment variables to the Orbacus installation package, for libraries and binaries.
2.4 Implementing the sevant class
The next step is to implement the servant class. For this purpose, we will need to create a class for the servant that inherits from the skeleton class, as shown below:
#include "OB/CORBA.h" #include "crypt_skel.h" #include <iostream> #include <string> class CryptographicImpl : virtual public ::POA_CaesarAlgorithm, virtual public PortableServer::RefCountServantBase { public: CryptographicImpl() {} // Caesar text encryption algorithm ::CaesarAlgorithm::charsequence* encrypt(const char* info, ::CORBA::ULong k,::CORBA::ULong shift) throw(::CORBA::SystemException) { std::string msg = info; int len = msg.length(); ::CaesarAlgorithm::charsequence* outseq = new ::CaesarAlgorithm::charsequence; outseq->length(len + 1); std::string::iterator i = msg.begin(); std::string::iterator end = msg.end(); int j = 0; while (i != end) { *i+= shift; *i ^= k; (*outseq)[j++] = *i++; } (*outseq)[len] = '\0'; return outseq; } // Caesar text decryption algorithm char* decrypt(const ::CaesarAlgorithm::charsequence& info, ::CORBA::ULong k,::CORBA::ULong shift) throw(::CORBA::SystemException) { char* r = CORBA::string_alloc(info.length()); for (int i = 0;i < info.length() - 1;i++) { r[i] = info[i]; r[i] ^= k; r[i] -= shift; } r[info.length() - 1] = '\0'; return r; } };
The servant implements the CORBA object methods functionality. The servant is called by the skeleton when a client calls a CORBA object method implemented by the servant. The Orbacus IDL compiler generates an empty implementation of the servant, called cryptimpl.h.
2.5 Creating the server
Before ending the server side implementation, it's necessary to create an entry point that instantiates and activates the servant class. We will have to implement a server with the CORBA initialization. The following code explains this issue:
#include <iostream> #include "OB/CORBA.h" #include <OB/Cosnaming.h> #include "crypt.h" #include "cryptimpl.h" using namespace std; int main(int argc, char** argv) { try { // Initialize the ORB. 1 CORBA::ORB_var orb = CORBA::ORB_init(argc, argv); // Get a reference to the root POA 2 CORBA::Object_var rootPOAObj = orb->resolve_initial_references("RootPOA"); // Narrow it to the correct type PortableServer::POA_var rootPOA = PortableServer::POA::_narrow(rootPOAObj.in()); // Create POA policies CORBA::PolicyList policies; policies.length(1); 3 policies[0] = rootPOA->create_thread_policy(PortableServer::SINGLE_THREAD_MODEL); // Get the POA manager object 4 PortableServer::POAManager_var manager = rootPOA->the_POAManager(); // Create a new POA with specified policies PortableServer::POA_var myPOA = rootPOA->create_POA("myPOA", manager, policies); // Get a reference to the Naming Service root_context 5 CORBA::Object_var rootContextObj = orb->resolve_initial_references("NameService"); // Narrow to the correct type CosNaming::NamingContext_var nc = CosNaming::NamingContext::_narrow(rootContextObj.in()); // Create a reference to the servant 6 CryptographicImpl* CrypImpl = new CryptographicImpl(); // Activate object PortableServer::ObjectId_var myObjID = myPOA->activate_object(CrypImpl); // Get a CORBA reference with the POA through the servant 7 CORBA::Object_var o = myPOA->servant_to_reference(CrypImpl); // Get a reference to the CaesarAlgorithm interface 8 CaesarAlgorithm_var cav = CaesarAlgorithm::_narrow(o); // The reference is converted to a character string 9 CORBA::String_var s = orb->object_to_string(cav); cout << "The IOR of the object is: " << s << endl; CosNaming::Name name; name.length(1); name[0].id = (const char *) "CryptographicService"; name[0].kind = (const char *) ""; // Bind the object into the name service 10 nc->rebind(name,o); // Activate the POA manager->activate(); cout << "The server is ready. Awaiting for incoming requests..." << endl; // Start the ORB 11 orb -> run(); } catch(const CORBA::Exception& e) { // Handles CORBA exceptions cerr << e << endl; return 1; } return 0; }
- Initializes the CORBA ORB.
- Gets the root POA. The server object must be registered using an object adapter. In this case, we register the object in the root POA. In most cases, this object adapter is enough.
- Set the POA policies to
SINGLE_THREAD_MODEL
. This POA policy manages one request at a time, instead of multithreading, so we get it multithread-aware. If you want to use multithread capabilities, read a CORBA book about JTCThreads. This approach is safe and clear, but has bad performance. - A POA Manager is obtained. The Manager controls a set of object adapters, allowing them to work.
- A reference to the Name Service is obtained. It will allow to register an object reference in a naming context.
- A servant
CryptographicImpl
object is dynamically created. - The POA method
servant_to_reference
is used to obtain a CORBA reference from a servant. - The reference is converted to a
CaesarAlgorithm
interface reference. - The reference is converted to a character string in order to display it.
- The CORBA reference from a servant is registered in the Name Service using a naming context, through a call to
rebind
. - Finally, the ORB starts and keeps awaiting requests.
2.6 Implementing the client
The client side is a program that allows to communicate with the remote CORBA object instantiated in a server. The following code is a sample of a client invoking the remote object:
#include <iostream> #include <string> #include "OB/CORBA.h" #include "OB/Cosnaming.h" #include "crypt.h" // Include the stub using namespace std; int main(int argc, char** argv) { try { // Initialize the ORB 1 CORBA::ORB_var orb = CORBA::ORB_init(argc, argv); // Get a reference to the Naming Service 2 CORBA::Object_var rootContextObj = orb->resolve_initial_references("NameService"); CosNaming::NamingContext_var nc = CosNaming::NamingContext::_narrow(rootContextObj.in()); CosNaming::Name name; name.length(1); name[0].id = (const char *) "CryptographicService"; name[0].kind = (const char *) ""; // Invoke the root context to retrieve the object reference 3 CORBA::Object_var managerObj = nc->resolve(name); // Narrow the previous object to obtain the correct type 4 ::CaesarAlgorithm_ptr manager = ::CaesarAlgorithm::_narrow(managerObj.in()); string info_in,info_out,exit,dummy; ::CaesarAlgorithm::charsequence* inseq; unsigned long key,shift; try{ do{ cout << "\nCryptographic service client" << endl; cout << "----------------------------" << endl; do{ // Get the cryptographic key if (cin.fail()) { cin.clear(); cin >> dummy; } cout << "Enter encryption key: "; cin >> key; } while (cin.fail()); do{ // Get the shift if (cin.fail()) { cin.clear(); cin >> dummy; } cout << "Enter a shift: "; cin >> shift; } while (cin.fail()); getline(cin,dummy); // Get the text to encrypt cout << "Enter a plain text to encrypt: "; getline(cin,info_in); // Invoke first remote method 5 inseq = manager->encrypt(info_in.c_str(),key,shift); cout << "----------------------------------------------" << endl; cout << "Encrypted text is: " << inseq->get_buffer() << endl; // Invoke second remote method 6 info_out = manager->decrypt(*inseq,key,shift); cout << "Decrypted text is: " << info_out << endl; cout << "----------------------------------------------" << endl; cout << "Exit? (y/n): "; cin >> exit; } while (exit!="y"); } catch(const std::exception& std_e){ cerr << std_e.what() << endl; return 1; } }catch(const CORBA::Exception& e) { // Handles CORBA exceptions cerr << e << endl; return 1; } return 0; }
- Initialize the CORBA ORB.
- Get a reference to the Name Service object.
- Invoke the root context to retrieve the object reference.
- Get a pointer to the
CaesarAlgorithm
interface from the CORBA object reference. - Invoke the first remote method, passing three input arguments, and return a CORBA sequence.
- Invoke the second remote method, passing the previous returned sequence by value.
2.7 Working all together
Once we have implemented the client and the server, it's time to connect them. Let's start with the server. Previously, to run it, we had to call the Orbacus Name Service application by searching for nameserv.exe. Therefore, the call must be done in this form:
nameserv -OAhost localhost -OAport 8140
Instead of using the localhost, we can use an external LAN or Internet IP address by replacing localhost with the IP.
After this, we can execute the server with these parameters:
server -ORBInitRef NameService=corbaloc:iiop:localhost:8140/NameService
It's also possible to replace localhost with an external IP.
We will get the following result:
Finally, we can proceed to call the client by prompting the following:
client -ORBInitRef NameService=corbaloc:iiop:localhost:8140/NameService
and we will get a client application running and connecting with the server through the Internet:
3. CORBA advantages & disadvantages
Advantages:
- Multiple OS support and platforms.
- Multiple language support.
- Great variety of services: messaging, events, transactions, persistence, concurrence, etc.
- Managed by the OMG and what is the same: standard compliance.
Disadvantages:
- Complexity: Programming CORBA application is hard and tedious. The learning curve is too steep.
- Bureaucracy: The evolution of CORBA specifications requires many steps of bureaucracy, so the platform evolution is slow.
- Few free solutions: As a consequence of its complexity and the slow evolution.
4. References
- Distributed Systems. Concepts and Design (Third edition). George Coulouris, Jean Dollimore, Tim Kindberg.
- Advanced CORBA programming with C++ (First edition). Michi Henning and Steve Vinoski.
- Notes of the Distributed System subject from UNED - National University of Distance Education.