Architecture & DesignDeveloping Efficient Network and Distributed Applications with ACE, Part 2

Developing Efficient Network and Distributed Applications with ACE, Part 2

The first installment of this article series introduced and took an overview of Adaptive Communication Environment (ACE) framework. Now that we are familiar with the structure of ACE framework and its major components, we can move towards understanding how ACE works under the hood and how it can be implemented to develop efficient communication software.

This article explores the ACE framework components necessary for application development and how they work in tandem to provide flexible, efficient, reliable, and portable communication solutions for application. This article also contains an example of a simple distributed application developed using ACE, comprised of a blog server and a desktop-based blogging client, to help explain the implementation and working of ACE components, along with the communication taking place between them.

Note: The example application, which is GUI-based, presented in this article was built and tested using MS VC++ and MFC on a Windows 2000 platform. The GUI, being OS-dependent and in this particular case slightly tight-coupled with the application layer, is not portable; yet the underlying implementation of ACE is portable. Readers can model their own GUI-based applications on the example for different platforms while using the ACE implementation code as it is, thanks to the high portability that ACE provides.

How ACE Works

Before we take a look at the example application, we need to understand how communication between components of an ACE-based distributed application takes place.

Communication in a distributed application that implements ACE follows the usual client/server model (refined into a new Acceptor-Connector pattern by ACE). It involves an active client that initiates a communication and requests for a connection for data exchange; and a passive server that waits for a needy client to initiate communication and request for a connection to arrive. Upon arrival of such a request(s), the server accepts the request by establishing a connection and exchanges data with the client. Here, both the client and the server are components of the distributed application, between whom the communication takes place.

Note: ACE allows the reversal of client and server roles anytime during communication, once a connection between the two is established. Therefore, the server can become a client and request information from the client-turned-server and vise-versa.

The ACE framework contains two useful components, namely Acceptor and Connector, that carry out the communication for the client and server. ACE also provides other useful components, such as Dispatcher and Service Handler, to aid different aspects of this communication.

Connector and Acceptor both are factories. Connector resides on the client side (of the application), while Acceptor lies on the server side, and there is one singleton Dispatcher each for both client and server.

Connector initializes a connection by actively sending out connection requests to a passive Acceptor, which is listening on a designated port for a connection request to arrive. Connector can initialize connections both synchronously and asynchronously, depending on the application’s requirement.

Both Connector and Acceptor are registered with Dispatchers on their own sides. Depending on the type of connection establishment used by the Connector, the appropriate type of Dispatcher is used:

  • Reactor for synchronously established connections
  • Proactor for asynchronously established connections

Here, we’ll explore Reactor because our example application requires a simple synchronous connection establishment.

The Reactor watches out for relevant events occurring; in other words, incoming connection requests and confirmation of connection establishment. When the former event occurs, Reactor informs Acceptor. Acceptor in turn accepts the requests, establishes a connection with Connector, and creates a Service Handler to handle the processing of data exchanged over the connection.

Meanwhile, the Reactor at the client side informs the Connector of confirmation that the connection was established. Now, Connector also initializes the appropriate Service Handler on its own side. Then, the Service Handlers take over and exchange data with their corresponding remote peer Service Handler. When the data exchange is finished on a connection, the peer Service Handlers are shut down and any resources involved in the data exchange or processing are released. Figure 1 explains how ACE components work.

Figure 1: Working of ACE components

ACE at Work: Example Application

To build applications, ACE can be downloaded for free from the ACE homepage. This application uses version 5.4 of ACE. After downloading the compressed source of ACE for the appropriate platform (here, ACE-5.4.zip), the source can be uncompressed, compiled, and thus installed on the desired machine.

As described earlier, the example application consists of a blog server and a desktop-based blog client (analogous an to an e-mail client such as MS Outlook Express), both of which may or may not reside on different machines. Both the client and the server are GUI based. The complete source code of the application and its executable files are available for download.

Figure 2: The blog server’s GUI

First, executing blogserver.exe starts the server’s GUI. In the resulting dialog box, the Start button is clicked to start the server. When started, the server creates two Acceptor objects of type CNewUserAcceptor and CServiceAcceptor respectively and registers them with the singleton Reactor on its own side. These Acceptor objects are assigned to and listen at two different ports.

Code: Source code from file BlogServer.h

#define NEWUSER_PORT   10001
#define SERVICE_PORT   10002

Code: Source code from file BlogServer.cpp

   static const char *const SERVER_HOST = ACE_DEFAULT_SERVER_HOST;
... ... ....
CNewUserAcceptor newUserAcceptor;
CServiceAcceptor srvAcceptor;
if (newUserAcceptor.open (ACE_INET_Addr(NEWUSER_PORT),
    ACE_Reactor::instance()) == -1)
{
   AfxMessageBox("Can not start new user service");
   return -1;
}
if (srvAcceptor.open (ACE_INET_Addr(SERVICE_PORT),
    ACE_Reactor::instance()) == -1)
{
   newUserAcceptor.close();
   AfxMessageBox("Can not start blog service");
   return -1;
}
... ... ... ...

Now, the Reactor constantly watches out for a relevant connection request (sent by Connectors on the client side) events to occur and notifies the appropriate Acceptor object about it.

Code: Source code from file BlogServer.cpp

//The application calls the handle_events() function to
//continuously watch the events in a loop.
      ACE_Reactor::instance()->handle_events ();

Now, the blogclient.exe is executed to start the client application, which in turn starts the event-handling loop of the client-side Reactor.

Code: Source code from file BlogClient.cpp

//The application calls the handle_events() function to
//continuously watch the events in a loop.
ACE_Reactor::instance()->handle_events();

The blog client’s GUI appears and accepts input. First is a Login dialog box that asks for the username and password and also provides an option for new user registration.

Figure 3: The blog client GUI

Registering a new user

When the button for new user registration is clicked, the application GUI asks for new username and password entries. When appropriate input for the same is submitted, the application creates a Connector object of type CLogConnector.

Figure 4: New user registration Dialog

Then CLogConnector actively establishes a connection with CNewUserAcceptor on the server side. Both CLogConnector and CNewUserAcceptor create service handlers CNewUserLogHandler and CNewUserHandler, respectively.

Code: Source code from file NewUserDlg.cpp

typedef ACE_Connector<CNewUserLogHandler,ACE_SOCK_CONNECTOR>
        CLogConnector;
static  const char *const SERVER_HOST = ACE_DEFAULT_SERVER_HOST;
   ----
   ---
   ACE_INET_Addr addr(NEWUSER_PORT,SERVER_HOST);
   CNewUserLogHandler *pSrvHandler = new CNewUserLogHandler(this);
   CLogConnector logCon;
   logCon.open();
   if (logCon.connect(pSrvHandler,addr) == -1)
   {
      AfxMessageBox("Error: connection failed");
      return;
   }

Once the connection is established, the Service Handler objects on both sides start communicating with each other. The CNewUserLogHandler sends the username and password to CNewUserHandler.

Code: Source code from file BlogClient.h

#define USERNAME_LENGTH     255

Code: Source code from file NewUserDlg.h

#define NEWUSER_DATA_LENGTH 512

Code: Source code from file NewUserDlg.cpp

   char* pstrBuffer = NULL;
   pstrBuffer = new char[NEWUSER_DATA_LENGTH];
   strcpy(pstrBuffer,m_strUser.operator LPCTSTR());
   pstrBuffer+=USERNAME_LENGTH;
   strcpy(pstrBuffer,m_strPsw.operator LPCTSTR());
   pstrBuffer-=USERNAME_LENGTH;
   pSrvHandler->peer().send_n (pstrBuffer,NEWUSER_DATA_LENGTH);
   delete []pstrBuffer;

CNewUserHandler checks for the availability of the username and sends the status message back to CNewUserLogHandler and closes the communication by calling its close() function.

Code: Source code from file NewUserHandler.cpp

char strNewUserData[NEWUSER_DATA_LENGTH];
   char strStatus[STATUS_DATA_LENGTH];
   CLogin LogObj;

   peer().recv_n(strNewUserData,NEWUSER_DATA_LENGTH);
   if (LogObj.AddNewUser(strNewUserData))
   {
      strcpy(strStatus,STATUS_SUCCESS);
   }
   else
   {
      strcpy(strStatus,STATUS_FAIL);
   }
   this->peer().send_n(strStatus, STATUS_DATA_LENGTH);
   this->close();

The login process

To log in, the user supplies the username and password to a login dialog box, which appears when the client application is executed (refer to Figure 3: The blog client login GUI). When the user submits these entries and presses the login button, the blog client creates a connector object of type CServiceConnector. This connector object in turn creates a Service Handler object of type CSrvHandler.

Code: Source code from file UserLoginDlg.cpp

   ACE_INET_Addr addr(SERVICE_PORT,SERVER_HOST);
   CBlogClientDoc *pDoc = (CBlogClientDoc*)
                          ((CFrameWnd*)AfxGetApp()->GetMainWnd())
                          ->GetActiveDocument();
   if(!pDoc)
      return;

   pDoc->m_pSrvHandler= new CSrvHandler(pDoc);
   CServiceConnector svrCon;
   svrCon.open();
   if (svrCon.connect(pDoc->m_pSrvHandler,addr) == -1)
   {
      AfxMessageBox("Error: connection failed");
      pDoc->m_pSrvHandler = NULL;
      svrCon.close();
      return;
   }

On the server side, when CServiceAcceptor receives the connection request, it creates a Service Handler of the CServiceHandler class. Once the connection is established, this Service Handler object communicates with CSrvHandler, its remote peer on the client side. CSrvHandler sends the username and password along with a LogRequest command to CServiceHandler.

Code: Source code from file UserLoginDlg.cpp

char *pStrCommand = new char[COMMAND_LENGTH];
ZeroMemory(pStrCommand,COMMAND_LENGTH);
strcpy(pStrCommand,LOG_REQUEST_COMMAND);
pStrCommand = pStrCommand + BLOGSIZE_COMMAND_LENGTH;
sprintf(pStrCommand,"%d",USERNAME_LENGTH + PASSWRD_LENGTH);
pStrCommand = pStrCommand - BLOGSIZE_COMMAND_LENGTH;
pDoc->m_pSrvHandler->peer().send_n(pStrCommand,COMMAND_LENGTH);
delete[] pStrCommand;
char *pStrLogInfoBuf = new char[USERNAME_LENGTH + PASSWRD_LENGTH];
strcpy(pStrLogInfoBuf,m_strLogId);
pStrLogInfoBuf = pStrLogInfoBuf + USERNAME_LENGTH;
strcpy(pStrLogInfoBuf,m_strPassword);
pStrLogInfoBuf = pStrLogInfoBuf -  USERNAME_LENGTH;
pDoc->m_pSrvHandler->peer().send_n(pStrLogInfoBuf,USERNAME_LENGTH +
                                   PASSWRD_LENGTH);
delete[] pStrLogInfoBuf;

When CServiceHandler receives LogRequest command, it checks whether such a user exists or not. If not, it sends a Terminate command back to CSrvHandler and closes the connection by calling its close() function.

Code: Source code from file ServiceHandler.cpp

if(strcmp(strCommand,LOG_REQUEST_COMMAND) == 0)
{
   char *pUserBuffer = new char[nDataLen];
   peer().recv_n(pUserBuffer,nDataLen);
   //pUserBuffer = pUserBuffer + COMMAND_LENGTH;
   CLogin userLogInfo;
   char *pBlogFileName = NULL;
   pBlogFileName       = userLogInfo.CheckUser(pUserBuffer);
   m_strBlogFileName   = pBlogFileName;
   if(pBlogFileName    == NULL)
   {
      char strCommand[COMMAND_LENGTH];
      strcpy(strCommand,TERMINATE_COMMAND);
      peer().send_n(strCommand,COMMAND_LENGTH);
      close();
      return 0;
   }
}

If such a user exists, the CServiceHandler sends the blog data to CSrvHandler.

Code: Source code from file ServiceHandler.cpp

char *pBlogBuffer = NULL;
char *pCmdBuffer = new char[COMMAND_LENGTH];
char strBolgCmd[BLOG_DATA_COMMAND_LENGTH];
char strBlogLen[BLOGSIZE_COMMAND_LENGTH];
CBlog blogObj;
int nBlogLen = blogObj.ReadBuffer(pBlogFileName,pBlogBuffer);
strcpy(strBolgCmd,BLOG_DATA);
sprintf(strBlogLen,"%d",nBlogLen);
strcpy(pCmdBuffer,strBolgCmd);
pCmdBuffer = pCmdBuffer + BLOG_DATA_COMMAND_LENGTH;
strcpy(pCmdBuffer,strBlogLen);
pCmdBuffer = pCmdBuffer - BLOG_DATA_COMMAND_LENGTH;
peer().send_n(pCmdBuffer,COMMAND_LENGTH);
peer().send_n(pBlogBuffer,nBlogLen);
delete[] pBlogBuffer; 
delete[] pCmdBuffer;

On receiving blog data, the client displays it in a blog window, in which the user can make new entries or edit/delete old entries. Once the desired operation is finished, the user can save the blog entries by clicking the Save button.

Figure 5: Blog window for existing logged-in user

After that, data exchange between peer service handler objects continues as long as the user remains logged in. The user can discard changes and log off at any time, simply by closing the window. This is followed by the shutting down the peer Service Handlers and resource release.

This is how ACE enables communication between the components of a distributed application.

Gains from ACE

Now that we’ve learned how to implement ACE, we are ready to answer the most important question: “Why should one implement ACE at all, when there are other ways, such as sockets and TLI, are available for the same purpose?” The answer is simple: Implementing ACE for developing communication software is more advantageous than other methods. Some of the advantages are listed here:

  • The biggest gains from implementing ACE are in terms of increased portability for the application. It’s possible to port an application from one platform, with minimum efforts, resources, and time, thanks to ACE’s support for various platforms.
  • Another important advantage is that communication software developers need not be aware of OS communication internals for different OS platforms (or any OS platform, to be precise). ACE can be easily ported to most widely used OS platforms available today and hides complexities of the communication handling mechanism of the underlying OS by providing a standard framework for developing communication software.
  • ACE internally implements the best practices, design patterns, and strategies to enhance communication software efficiency.
  • ACE helps increase the reusability and extensibility of implementing application because it clearly decouples the connection establishment from the subsequent service initialization. This approach also makes adding new services and providing new service implementations easy.

About the Author

Mugdha Chauhan is a senior IT consultant and author. An open source supporter, she frequently writes articles and tutorials on useful emerging open source projects. Major tech portals including developer.com, IBM developerWorks, CNET Networks, Slashdot, and many eZines regularly publish her work. Her other expertise and interests include Java, Linux, XML, and wireless application development.

Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.
Get the Free Newsletter!
Subscribe to Developer Insider for top news, trends & analysis
This email address is invalid.

Latest Posts

Related Stories