The communication between the client and server is done using ITC messages. Fundamentally, the messages are blocks of memory that are put into the linked list (mailbox) belonging to the thread to receive the message.
If the receiving thread is blocked waiting for a message, it will be awakened and find the new message in its mailbox.
While this describes the fundamental how of the implementation, the ITC framework is designed to support even higher level notions including:
There are several issues with these mechanisms.
Message queues of this type also require that data be copied into the queue during the send operation and out of the queue during the receive operation. This is inefficient, but it does allow these types of mechanisms to be used across address spaces (e.g. TCP/IP sockets, UNIX fifos and pipes.)
For communication between threads in the same address space, the amount of copying can be reduced by only copying a pointer to the data into the message queue itself.
This is as efficient as the placement of pointers in traditional message queues, without the need to wory about queue full conditions.
It is this linked-list method that is used for message passing
in the OSCL ITC framework.
The OSCL ITC framework is designed such that all messages contain
a link field such they can be linked into the mailbox
of the receiving thread. The OSCL provides
oscl/queue/queue.h
a type-safe singly linked list queue implementation, used to implement the
ITC mailboxes implemented by
oscl/mt/itc/mbox/mbox.h
and
oscl/mt/itc/mbox/mbox.cpp
.
Any element linked into an Oscl::Queue
must have a public
link field member with the following signature:
void* __qitemlink;
This field is usually supplied by inheriting the Oscl::QueueItem
type defined in oscl/queue/queueitem.h.
Thus, the most simple message is simply an instance of Oscl::QueueItem
.
Of course such a message is not very useful, and indeed in reality cannot
even be used with the Oscl::Mt::Itc::Mailbox
since its linked list
is not publicly accessible as it must be shared by multiple threads and
access to the list must be protected by a mutual exclusion mechanism.
The Oscl::Mt::Itc::Mailbox
accepts messages which are
of the type Oscl::Mt::Itc::Msg
.
oscl/mt/itc/mbox/msg.h
inherits the required queue link field from Oscl::QueueItem
and defines a polymorphic operation (pure virtual) called process()
.
The receiver of a message must examine the message to determine its type, and ultimately what to do with the message. The type indicates to the receiver what the sender intends the receiver to do with the message, and the types of data that accompany the message.
Traditionally, this is done by defining a field common to all messages
that contains an integer that is mapped to the message type. The receiver
would examine the type using a switch
statement, and
take the appropriate action with the remainder of the message, which
might include casting data to the appropriate type.
Seeing the switch-on-type construct, the object oriented practioner
can clearly see an oportunity for improvement. Thus the purpose
of the process()
operation, to polymorphically dispatch
the appropriate message handling based on its type.
process()
operation, lets first consider the different needs of clients and
servers.
Generally, interfaces define a set of requests that a server is expected to satisfy. Requests are ITC messages in the active object ITC framework.
It is usually the case in a client-server relationship that the client needs to know when the server has completed the request, either to obtain the resulting data from the request or to simply know that the server is finished with the client resources used for the communication (e.g. the ITC message.)
There are many methods the server might use to indicate that it has completed the request. For example, the server might set a semaphore on which the client is blocked, or the server might send a reply message to the client.
It is desirable to leave this choice of acknowledgment mechanism to the client, and thus there is a need to hide the mechanism from the server.
This is done using another polymorphic operation called
returnToSender()
as illustrated by the
Oscl::Mt::Itc::SrvMsg
as defined in the file
oscl/mt/itc/mbox/srvmsg.h.
returnToSender()
operation is invoked by the server
when it completes completes the request.
For reasons of symmetry and type-safety the ITC framework defines
Oscl::Mt::Itc::CliMsg
another message subclass to
differentiate messages received by clients (responses) from those
received by servers (requests.)
Notice that clients have no need for a returnToSender()
operation
since they are the source of the original request.
returnToSender
operation on a server request
message.
There are two return handler implementations that
correspond to the two primary acknowledgement mechanisms:
It is possible to create other types of return handlers, for very specialized uses, but my experience to this point has proven that to be unnecessary. As a result, it is strongly recommended that any decision to create a new type of return handler be undertaken only after considering other alternatives.
An important property of the synchronous and asynchronous return handlers is that both methods notify the client when the server has completed processing the request. This promotes and simplifies the principle of the client being responsible for the memory used in the communications with other active objects.
The message memory is considered busy or in-use until the server invokes the request message return to sender operation. At that point, the memory used for the message may be reclaimed and reused by the client.
To avoid unnecessary coupling, however, we do not want the server to know its clients.
The process
operation is invoked by the receiving
thread when the message is received from the thread's mailbox.
Invoking the process()
operation polymorphically
invokes the code responsible for handling the received message.
This mechanism replaces traditional systems that use switch
constructs to dispatch received messages.
Notice that the SrvMsg
(server message) has an additional
operation called returnToSender()
which is invoked by
the server implementation when it has completed the action requested
by the message. This will be covered in more detail below.
To simplify the usage of client and server messages, and to capture common implementation details, two template classes are used define request and response messages.
SrvRequest
) are
server messages (SrvMsg
)
defined by an interface instantiated by a client, and sent
to a server.
Server messages have a return to sender
operation, distinguishing them from client messages,
that is invoked by the server when it has completed the request.
Request messages are defined in terms of the
Oscl::Mt::Itc::SrvRequest
template as defined in the "oscl/mt/itc/mbox/srvreq.h"
header file.
ReqApi
and Payload
are template parameters. The ReqApi
type must have a member
operation that matches the signature:
void request(SrvRequest& msg) throw();
where SrvRequest
is the same type as this
particular server request message.
The Payload
type may be a class
or struct
, which may optionally
have no members.
CliResponse
) are
client messages (CliMsg
)
that are defined in terms of a specific request message
(SrvRequest
.) In fact, the CliResponse
template
is defined such that it actually contains (is composed of) the
corresponding SrvRequest
message.
The Oscl::Mt::Itc::CliResponse
template captures the
common behavior of response messages . The template
is defined in the "oscl/mt/itc/mbox/clirsp.h"
header file.
A response message is purely a client concept used
when a client sends a request asynchronously to a server.
As such, the CliResponse
template class contains an
AsyncReturnHandler.
Notice that servers know nothing about the existence of a client response message.
Response messages are sent from the server to the client during the execution of the corresponding request message's return to sender operation, which is dispatched polymorphically.
returnToSender()
operation, the
implementation of which simply signals the semaphore releasing the client
from its blocked condition. The result is semantically indistinguishable
from a simple function call from the client point-of-view.
Payloads contain any data transferred between
the client and server as the result of the
request messasge and may be structures or classes.
Payloads may also contain the result
of a request. However, the payload may
be an empty type (struct
or class
) when there
is no data transfer.
When a client posts a request synchronously, the message is placed into the server's mailbox, and the client thread blocks until the server has fulfilled the request and invokes the return to sender operation of the request message.
When a client posts a request asynchronously, the message is placed into the server's mailbox, and the client thread continues to execute according to its priority. In this case, when the server finishes processing the client's request by invoking the return to sender operation on the request message, the response message is placed into the client's mailbox.
To obtain this property, there is a difference between synchronous and asynchronous mesasges.
The difference is a parameter used when creating a request message known as the return handler.
It is possible to create other types of return handlers, for very specialized uses, but my experience to this point has proven that to be unnecessary. As a result, it is strongly recommended that any decision to create a new type of return handler be undertaken only after considering other alternatives.
An important property of the synchronous and asynchronous return handlers is that both methods notify the client when the server has completed processing the request. This promotes and simplifies the principle of the client being responsible for the memory used in the communications with other active objects.
The message memory is considered busy or in-use until the server invokes the request message return to sender operation. At that point, the memory used for the message may be reclaimed and reused by the client.