I work in a fairly large
office building with a central mailroom. The existence of this mailroom greatly simplifies
life for both me and the postal service employee who delivers mail to my building.
I have a centralized place to pick up my mail, and the mail carrier doesn’t
have to hunt around the entire building looking for every individual office. Instead,
there’s one guy at my company whose job is to take the bundle of incoming mail
and distribute it to the mailboxes of each person in the building. In this way, large
numbers of messages can be delivered to their appropriate recipients quickly, with
minimal work on everyone’s part.
There’s an analogy between an office mailroom and the messaging internals of
WSE2. Both systems, in effect, solve similar problems, and by looking at how one functions
it is possible to understand the other.
My office building houses many message destinations (people) within a single physical
address. The WSE2 analogy of my office building is an application that hosts a set
of services. Although this application might host a large number of services, it only
needs one transport address. For example, many different services can be hosted over
the same TCP port. One address – many destinations. This is a new notion in
WSE2, made possible by WS-Addressing. In ASMX web services, the concept of “service
identity” and “transport address” are bound up into a single URI.
In other words, the problem of logically identifying a service is inextricably linked
with the problem of delivering messages to it. In WSE2, services have an “identity
URI” which uniquely identifies the service for things like policy application
as well as a “transport address”, which tells the infrastructure how to
get messages from point A to point B. Keeping these two ideas separate is important
when you start thinking about exposing a service over a variety of network transports.
WSE addresses this separation through the EndpointReference.Via property – the
details of which can be found
over
on Hervey’s blog. For the purposes of this analogy, though, it’s sufficient
to say that many services can be hosted on the same transport address in the same
way that many people can have mail delivered to the same office building.
By the time a letter arrives at my office building, it’s already traveled through
a complicated infrastructure in order to get from its original sender to my building.
From the perspective of my office mail clerk, though, this process “just happens”
and he doesn’t have to be concerned about its internals. All the mail clerk
has to do is be familiar enough with the protocols of the network to transfer messages
on and off of it – in the real world, this means signing USPS delivery confirmations
and filling out shipping labels. In the WSE2 analogy, the infrastructure for exchanging
messages with the outside world is implemented using some sort of standardized message
exchange technology (e.g. TCP, UDP, SMTP, HTTP, MSMQ, etc). The WSE 2.0 “mail
clerk” must implement enough of the network protocols to move messages on and
off of the underlying network.
In WSE2, the “mail clerk” is a concrete implementation of the SoapTransport
class. Each particular network technology has its own SoapTransport implementation
– WSE2 ships with implementations for TCP and HTTP, and it’s possible
to extend the architecture to other transports by implementing custom derivations
of SoapTransport. A SoapTransport implementation is responsible for maintaining a
pool of network resources over which messages might arrive. The TCP transport, for
example, maintains a set of TCP sockets in “listen” mode. Similarly, an
MSMQ transport might maintain a pool of MessageQueue objects. As such, the transport
serves as an abstraction barrier between the network and the rest of the messaging
architecture. It encapsulates the details of the underlying network so that they are
hidden from higher layers of the messaging stack.
In my office, every person has a mailbox that stores incoming mail until the recipient
gets around to picking it up. When the mailman arrives at my office building, he leaves
a large bundle of incoming mail with the mail clerk. The mail clerk is responsible
for looking at the address of each letter and delivering it to the proper mailbox.
In WSE2, the analogy to “mailbox” is an ISoapInputChannel implementation.
An InputChannel is like a mailbox for services – it stores messages that are
addressed to a specific service until that service can come and pick them up.
The transport class is responsible for the creation and maintenance of input channels.
Every SoapTransport must implement ISoapTransport.GetInputChannel( EndpointReference
epr, SoapChannelCapabilities c ). This method opens an input channel on a specific
endpoint. As part of the GetInputChannel() operation, the transport class must open
a new network resource (exactly which network resource is derived from the EndpointReference.TransportAddress
property, according to the semantics of the transport). For example, as part of opening
an InputChannel on the “soap.tcp://localhost:2323” URI, the TCP transport
might open a TCP socket on port 2323 – but in general it’s up to each
individual transport implementation to determine how to map transport addresses into
network resources. The transport must also store the newly created input channel in
an internal collection (conveniently implemented by the SoapTransport.InputChannels
collection), so it can deliver messages to that channel when they arrive later.
Just like the mail clerk, the SoapTransport implementation must look at the address
of each incoming message and deliver it to the appropriate InputChannel. This operation
can be done generically for all transport (thanks to the transport-independent addressing
mechanism that WS-Addressing provides). As such, dispatch logic is implemented in
a single place – SoapTransport.DispatchMessage(). Thus, when a message arrives
on a the network, the transport is responsible for deserializing that message from
the network and calling SoapTransport.DispatchMessage(). DispatchMessage() then looks
at the addressing headers of the message and matches them against the all of the active
InputChannels the transport maintains in its InputChannels collection. If a match
is found, dispatch message calls InputChannel.Enqueue to store the message in the
InputChannel.
When a message arrives in my office mailbox, it stays there until I pick it up. Similarly,
messages stay buffered in an InputChannel until the service that opened the channel
can come along and process it. Realistically, I don’t think that messages really
accumulate inside of the InputChannel because the service infrastructure picks them
up almost as soon as they come in – but conceptually, the InputChannel offers
the same buffering capabilities as a real mailbox.
The problem with my office mailroom is that I periodically have to poll my mailbox
for new messages. Being the lazy programmer that I am, what I’d really like
is for an intern to watch my inbox for me and run over to my office with the new message
every time one arrives. WSE2 allows services a similar degree of laziness; the service
class (whatever concrete subtype of SoapReceiver that happens to be) is not responsible
for monitoring the status of its input channels directly. Instead, there’s an
intermediary – an intern, if you will, that takes care of delivering the message
from the input channel to the SoapReceiver automatically. This “intern”
is the SoapReceivers collection.
In order to register a listening service, you must register it with the SoapReceivers
collection by calling SoapReceivers.Add( EndpointReference epr, SoapReceiver receiver).
This binds a service instance to an endpoint and starts the messaging infrastructure
listening for messages on that endpoint. Internally, this is accomplished by finding
the appropriate instance of SoapTransport based upon the URI scheme of the endpoint’s
TransportAddress, and then calling GetInputChannel() on that transport. The SoapReceivers
collection can then call BeginReceive() on that InputChannel, registering a generic
dispatch function as a callback. Thus, whenever a message arrives on that InputChannel,
the callback will be invoked and the SoapReceivers collection can deliver that message
from the InputChannel to the waiting service. It’s exactly the same effect as
having someone run up to your office every time a message arrives in your mailbox,
only it’s accomplished with async callbacks and no interns.
In summary, delivering a message to your office is a series of the following asynchronous
operations:
1) Postal
carrier delivers letter to mailroom.
2) Mail
clerk delivers it to your mailbox.
3) Intern
runs it to your office.
The corresponding sequence
of asynchronous events in WSE2 is as follows:
1) Message
arrives on network resource
2) Transport
dispatches it to an InputChannel
3) The
SoapReceivers collection dispatches it to the service class
I’ll talk more about
what I see as the benefits of this architecture later. I’ll also be talking
more about the mechanics of accomplishing all of this via a custom transport implementation
in future posts.