2.2. Accepting Connection Requests with Acceptor

assa-logd accepts connection from client applications with the help of Acceptor class. For in-depth discussion of Acceptor pattern, please, refer to the article "Acceptor and Connector: A Family of Object Creational Patterns for Initializing Communication Services" by Douglas C. Schmidt [MartinBuschmannRiehle97].

An abstract class Acceptor implements the generic strategy for passively initializing communication services. This parameterized class separates cleanly data communication code from connection acceptance code.

Example 2-1. Class Acceptor definition.


template<class SERVICE_HANDLER, class PEER_ACCEPTOR>
class Acceptor : public EventHandler
{
public:
    Acceptor (Reactor* r_);
    virtual ~Acceptor ();

    virtual int open (const Address& local_addr_);

    // ... other methods ...

private:
    PEER_ACCEPTOR m_listenSocket;
};
	  

Acceptor is parameterized both by a particular type of SERVICE_HANDLER and PEER_ACCEPTOR. Parameterized types are used to decouple the connection establishment strategy from the type of service handler, network programming interface, and transport layer connection initiation protocol.

The PEER_ACCEPTOR provides the transport mechanism used by the Acceptor to passively establish the connection. In case of TCP/IP communication, the value of PEER_ACCEPTOR is IPv4Socket.

2.2.1. Class ServiceHandler

This class defines a generic interface for a service. The ServiceHandler contains a communication endpoint (m_peerStream) that encapsulates an I/O handle (i.e. BSD socket file descriptor). This endpoint can be initialized by either Acceptor or Connector and is subsequently used by the ServiceHandler to exchange data with the peer. Both Acceptor and Connector activate ServiceHandler by calling its open() hook when connection is established. Once a ServiceHandler is completely initialized, it typically does not interact with its initializer.

		
001 template<class PEER_STREAM>
002 class ServiceHandler : public EventHandler
003 {
004 public:	  
005    ServiceHandler (PEER_STREAM* ps_);
006    ServiceHandler ();                
007
008    virtual ~ServiceHandler ();
009
010    virtual int open (void) = 0;
011    virtual void close (void);
012
013    operator PEER_STREAM& ();
014    PEER_STREAM& get_stream ();
015    // ... 
016
017 protected:
018    PEER_STREAM* m_peerStream;
019 };
	  

Parameter PEER_STREAM can be any class derived from the base class Socket. In particular, for TCP/IP communication, it takes the form of IPv4Socket class for communicating with TCP peers over IP.

To communicate with a peer, you are expected to derive your class from ServiceHandler, overload pure virual function open(). The derived class can be used by either Acceptor or Connector to establish a connection with a peers.

The client-side constructor instantiates an object of type PEER_STREAM on the heap. The PEER_STREAM object is passed as a parameter to the server-side constructor. Either way, ServiceHandler owns the PEER_STREAM object, and its destructor deletes the object. Never delete PEER_STREAM object yourself!

Warning

Deleting twice in C++ is a serious error; the behavior is undefined and most likely disastrous.

2.2.2. Adding Conn Handler

We are going to introduce a new class, Conn, to represent a connection with the peer. After connection establishment by Acceptor, handling actual data transfer is delegated to this class.


// logserver/server/Conn.h

0023 #include <assa/ServiceHandler.h>
0024 #include <assa/IPv4Socket.h>
0025
0026 using ASSA::ServiceHandler;
0027 using ASSA::IPv4Socket;
0028 
0029 class Conn : public ServiceHandler<IPv4Socket>
0030 {
0031 public:
0032     Conn (IPv4Socket* stream_) : 
0033         ServiceHandler<IPv4Socket> (stream_) { /* no-op */ }
0034
0035     ~Conn () { /* no-op */ }
0036 
0037     virtual int open ();
0038 
0039     virtual int handle_read (int fd_);
0040     virtual int handle_close (int fd_);
0041 };
	  

Our Conn class is derived from ServiceHandler, which in its own turn is derived from EventHandler.

2.2.3. Connection Handler Implementation

First, we are going to show how class Conn is used in our logserver application. Then we will examine step-by-step its implementation broken down by functionality phases.

2.2.3.1. Endpoint Initialization Phase

This step creates a passive mode endpoint (listening socket) that is bound to the network address (such as an IP address and a port number). This passive-mode endpoint is then listens for new connection requests from the peers.


// logserver/server/LogServer.h

#include <assa/Acceptor.h>
#include <assa/IPv4Socket.h>

class Conn;

class LogServer
{
    ...

private:
    ASSA::Acceptor<Conn, ASSA::IPv4Socket>* m_acceptor;
};
		

We added necessary header files and a data member, m_acceptor to hold a pointer to the Acceptor object.

Acceptor object is instantiated and registered with Reactor for event notification during the server's initialization phase.


// logserver/server/LogServer.cpp

0020 #include <assa/INETAddress.h>
0021 #include "Conn.h"
...
0033 LogServer::
0034 LogServer () :
0035     m_exit_value (0),
0036     m_acceptor (NULL)
0037 {
0038    
....
0070 }
...
0072 void
0073 LogServer::
0074 initServer ()
0075 {
0076     trace("LogServer::initServer");
0077 
0078     m_acceptor = new ASSA::Acceptor<Conn, ASSA::IPv4Socket> (getReactor ());
0079     ASSA::INETAddress lport (getPortName ().c_str ());
0080     Assert_exit (!lport.bad ());
0081     Assert_exit (m_acceptor->open (lport) == 0);
0082 
0083     DL((ASSA::APP,"Service has been initialized\n"));
0084 }
		

  • Lines 20 and 21 add appropriate header files.

  • Line 36 in the constructor initalizes the pointer to Acceptor.

  • Line 78 instantiates an Acceptor object. Its first template parameter is class Conn which would handle connection once it has been established.

  • We create listening port address with INETAddress and test its validity on lines 79-80.

  • Line 81 opens listening socket for incoming connection requests from peers. We are ready to accept connection requests.

Warning

Always instantiate Acceptor<> object on a heap with operator new() and never delete it yourself. Reactor object owns all of your Acceptor<> objects.

The collaboration diagram illustrates the message passing between classes involved in initServer().

Figure 2-2. Acceptor Initializations

2.2.3.2. Service Initialization Phase

Service initialization phase activates ServiceHandler associated with the passive-mode endpoint. When new connection is requested, the Reactor notifies Acceptor about incoming connection request. Acceptor performs the strategy for initializing ServiceHandler. The default steps involved are:

  • Instantiate new concrete ServiceHandler object.

  • Accept new connection.

  • Activate ServiceHandler object by calling its open() hook to let it execute service-specific initialization.

Graphically these steps can be represented as:

Figure 2-3. Accept new connection from peer

We provide implementation of open() function according the diagram.

Example 2-2. Registering EventHandler with Reactor


// logserver/server/Conn.cpp

0005 #include "LogServer.h"
...
0010 int Conn::open ()
0011 {
0012     trace("Conn::open");
0013
0014     ASSA::IPv4Socket& s = *this;
0015     REACTOR->registerIOHandler (this, s.getHandler (), ASSA::READ_EVENT);
0016
0017     return 0;
0018 }
		  

  • Line 5 includes LogServer header file to get definition of REACTOR.

  • Line 14 presents libassa idiom of getting a hold of PEER_STREAM object of ServiceHandler (you can also use get_stream() instead).

  • On line 15, Conn object registers itself with Reactor to be notified of READ_EVENT events (bytes arrived and ready to be read) on the socket PEER_STREAM.

  • Line 17 returns 0 to the fuction caller indicating that Conn accepted connection. You can reject connection acceptance by returning -1 here as well.

2.2.3.3. Service Processing Phase

Once the passive connection has been established and service has been initialized, service processing phase begins. In this phase, ServiceHandler communicates with its peer by exchanging chunks of bytes that hopefully carry meaningful data.

As was mentioned before, during initialization phase Conn object registers with Reactor for notification of READ_EVENT event. When data is detected on the endpoint, Conn::handle_read() function is called by Reactor. We have more to say on the subject of data exchange between peers in the next chapter. For now, our evolving logging server gets a message from its peer and writes it to the log file.


// logserver/server/Conn.cpp

0048 int Conn::handle_read (int fd_)
0049 {
0050     trace("Conn::handle_read");
0051     
0052     ASSA::IPv4Socket& s = *this;
0053     if (s.getHandler () != fd_) { return (-1); }
0054 
0055     int ret = 0;
0056     char buf [256];
0057 
0058     if ((ret = s.read (buf, 256)) < 0) {
0059         DL((ASSA::ERROR,"Error reading from socket\n"));
0060         return (-1);
0061     }
0062 
0063     if (ret > 0) {
0064         /* 
0065          * Process data received
0066          */
0067         ASSA::MemDump::dump_to_log (ASSA::APP, "=> Got new message", buf, ret);
0068     }
0069 
0070     return s.eof () ? -1 : s.in_avail ();
0071 }  
        

  • Line 53 checks to see if we are serving the right socket file descriptor.

  • Line 55 reads up to 256 bytes from the socket. If an error occurs on read, we report an error here and bail out.

  • Lines 63 through 68 dump data we've received to the log file both in ASCII and HEX format.

  • Line 70 is an idiom for proper returning from handle_read(). We always test for EOF condition of the socket and also report number of unread bytes still available in the internal socket buffer left.

UML diagram illustrates these steps:

Figure 2-4. Reading Data From Socket (simple)

As would be explained later (see Figure 2-10 for details), N >= M.

2.2.3.4. Termination Phase

When remote peer drops connection, the process goes in reverse, and ServiceHandler object eventually destroys itself. This condition occurs when handle_read() returns 0 bytes read. Reactor marks ServiceHandler for destruction by calling its handle_close() function.


// logserver/server/Conn.cpp

0075 int Conn::handle_close (int /* fd */)
0076 {
0077     trace("Conn::handle_close");
0078 
0079     delete (this);
0080     return 0;
0081 }
        

  • Line 79 self-destructs the objet Conn.

To illustrate with the diagram:

Figure 2-5. Peer dropped connection (simple)

2.2.3.5. Summary

We have finished writing the minimal amount of code to get data from the peer. Our logging server can now handle multiple peers, each represented by Conn object that desroys itself upon peer disconnect. The inner class dependency is illustrated by the class diagram:

Figure 2-6. LogServer Class Diagram (simple)

We are ready to build and test our first version of assa-logd. To test, have two xterm windows open: one for monitoring the server; another for the client. We haven't written a client yet, so we are going to use telnet to connect to our server and send ASCII messages instead.

Before you attempt to run the daemon, make sure your /etc/services has appropriate service names bound to the listening port numbers. We use ports 1000 and 1001, but they can be any other numbers so long that they are not used by some other daemons and their values is greater then 1024.


// /etc/services

# Local services
assalogd    10000/tcp       # libASSA log server (clients)
assalmon    10001/tcp       # libASSA log server (monitors)
		

In one window, start the daemon and monitor its log file:


% assa-logd --daemon --port=assalogd
% tail -f LogServer.log

[GenServer::initInternals] ||         Server configuration settings        ||
[GenServer::initInternals] ==================================================
[GenServer::initInternals]  cmd_line_name = 'assa-logd'
[GenServer::initInternals]  name = 'lt-assa-logd'
[GenServer::initInternals]  std cfg fname = '/home/vlg/.assa-logd.conf'
[GenServer::initInternals]  alt cfg fname = ''
[GenServer::initInternals]  debug_mask = 0x22
[GenServer::initInternals] ==================================================
[GenServer::initInternals] 
[LogServer::initServer] Service has been initialized

		

From another terminal window, connect to the server and send it an ASCII string:


% telnet dedalus assalogd

Trying 192.168.1.2...
Connected to dedalus.
Escape character is '^]'.

It works!
		

At this point, you should see in your first terminal window something along these lines:


|  |  |  |  [Conn::handle_read] (11 bytes) => Got new message
|  |  |  |  [Conn::handle_read] 

4974 2077 6f72 6b73 210d 0a              It works!\r\n

        

To terminate the server:


% kill `cat ~/.assa-logd.pid`
        

You can alleviate the pain of typing repetevive commands to start and stop server with appropriate shell scripts. See (Section 4.15) for details.