An Indigo Service is a program that can receive and process, and may also send, Indigo Messages. A Service communicates with the world through Endpoints. A Client is a program that exchanges messages with one or more Endpoints.
Each Endpoint represents a portal at a specific network address that is used to receive and send certain Messages. The set of messages that an Endpoint communicates is defined by a contract. The set of protocols that an Endpoint uses in communicating those messages is defined by a binding.
Note that an application may simultaneously expose services and act as a client for other services.
Indigo Endpoints communicate with the world using a communication stack called the channel stack. It’s interesting to compare the channel stack with other communication stacks e.g. TCP/IP (see figure 1).
Figure 1: Comparing the Indigo Channel Stack and the TCP stack.
First, the similarities: In both cases, each layer of the stack provides some abstraction of the world below this layer and exposed this abstraction to the layer directly above it. Each layer uses the abstraction of the layer directly below it. Also in both cases, when two stacks engage in a communication, each layer communicates with the corresponding layer in the other stack e.g. the IP layer communicates with the IP layer and the TCP layer with the TCP layer etc.
Now, the differences: While the TCP stack was designed to provide an abstraction of the physical network, the channel stack is designed to provide an abstraction of the transport and everything below it. This abstraction is achieved by requiring the bottom channel in the stack to adapt the underlying transport protocol to the channel stack architecture and then relying on channels further up in the stack to provide communication features such as reliability guarantees and security.
Messages flow through the communication stack as Message objects. As shown in figure 1, the bottom channel is called a transport channel. It is the channel responsible for sending/receiving messages to/from other parties. This includes the responsibility of transforming the Message object to and from the format used to communicate with other parties. Above the transport channel there can be any number of protocol channels each responsible for providing a communication function such as reliable delivery guarantees. Protocol channels operate on messages flowing through them in the form of the Message object. They typically either transform the message e.g. by adding headers or encrypting the body, and/or send and receive their own protocol control messages e.g. receipt acknowledgements.
Channel stacks are typically created using a factory pattern where a binding creates the channel stack. On the send side, a binding is used to build a ChannelFactory which in turn builds a channel stack and returns a reference to the top channel in the stack. The application can then use this channel to send messages. Similarly, on the receive side a binding is used to build a ChannelListener which listens for incoming messages. The ChannelListener provides messages to the listening application by creating channel stacks and handing the application a reference to the top channel. The application then uses this channel to receive incoming messages. Let’s walk through the code for this process.
The first step in listening for and receiving messages is creating a binding. The code in listing 1 creates an instance of CustomBinding and adds a TcpTransportBindingElement to its Elements collection. This Elements collection contains a binding elements that contribute channels to the channel stack. In this example, because the elements collection has only the TcpTransportBindingElement, the resulting channel stack will have only the TCP transport channel.
After creating a binding, we call BuildChannelListener
Once a listener is created, we call SetUri() on it to set the network address on which it will listen. Each transport channel will support one (or possibly several) address schemes. In this example we’re using the TCP transport channel which supports only the address scheme net.tcp. Then we call Open() on the listener and start accepting channels. We now have an endpoint located at net.tco://localhost/channelapp and ready to receive messages. The Open() and Close() methods you see in this code are part of a common state machine that all channels, channel factories and channel listeners implement. This state machine is defined by ICommunicationObject and will be discussed in detail in chapter 2.
The listener returns a channel that implements IInputChannel. To receive messages on this channel we first call Open() on it to open it then we call Receive() which will block until a message arrives.
When Receive() returns a Message, we write out the message’s action and body content (which we assume is a string). We then close the message, the channel and the listener.[1]
using System;
using System.ServiceModel;
namespace ProgrammingChannels
{
class Service
{
static void RunService()
{
CustomBinding binding = new CustomBinding();
binding.Elements.Add(new TcpTransportBindingElement());
BindingParameterCollection bpcol = new
BindingParameterCollection();
using (IChannelListener<IInputChannel> listener =
binding.BuildChannelListener<IInputChannel>(bpcol))
{
listener.SetUri(
new Uri("net.tcp://localhost/channelapp"));
listener.Open();
IInputChannel channel = listener.AcceptChannel();
Console.WriteLine("Listening for messages");
channel.Open();
Message message = channel.Receive();
Console.WriteLine("Message received");
Console.WriteLine("Message action: {0}",
message.Headers.Action);
Console.WriteLine("Message content: {0}",
message.GetBody
message.Close();
channel.Close();
listener.Close();
}
}
public static void Main()
{
Service.RunService();
Console.WriteLine("Press enter to exit");
Console.ReadLine();
}
}
}
Similarly, sending messages starts by creating a binding and adding a TcpTransportBindingElement to its Elements collection. Instead of creating a channel listener, this time we create a channel factory by calling BuildChannelFactory
After opening the factory, we call CreateChannel on it to create a channel to the service’s endpoint specifying the same endpoint address we specified before opening the channel listener on the service side.
We then create a new message and call Send on the IOutputChannel to send the message. The TCP transport channel will convert[2] the Message object to a byte array and send that over a TCP connection to the service endpoint. The TCP transport on the service side will receive the bytes from the TCP connection and create a Message object from them then return that message to the service application.
using System;
using System.ServiceModel;
namespace ProgrammingChannels
{
class Client
{
public static void Main()
{
RunClient();
}
static void RunClient()
{
CustomBinding binding = new CustomBinding();
binding.Elements.Add(new TcpTransportBindingElement());
BindingParameterCollection bpcol =
new BindingParameterCollection();
using (IChannelFactory<IOutputChannel> factory =
binding.BuildChannelFactory<IOutputChannel>(bpcol))
{
factory.Open();
IOutputChannel channel=factory.CreateChannel(
new Uri("net.tcp://localhost/channelapp"));
channel.Open();
Message message = Message.CreateMessage(
"some action", "This is the body data");
channel.Send(message);
channel.Close();
factory.Close();
}
}
}
}
The WCF channel stack is a layered communication stack with one or more channels that process messages. At the bottom of the stack is a transport channel that is responsible for adapting the channel stack to the underlying transport (e.g. TCP, HTTP, SMTP etc.). Channels provide a low-level programming model for sending and receiving messages. This programming model relies on several interfaces and other types collectively known as the WCF Channel Model. The rest of this book explores the Channel Model in detail with a focus on creating custom channels.