Transport: WSE 3.0 TCP Interoperability

The WSE 3.0 TCP Interoperability Transport sample demonstrates how to implement a TCP duplex session as a custom Windows Communication Foundation (WCF) transport. It also demonstrates how you can use the extensibility of the channel layer to interface over the wire with existing deployed systems. The following steps show how to build this custom WCF transport:

  1. Starting with a TCP socket, create client and server implementations of IDuplexSessionChannel that use DIME Framing to delineate message boundaries.

  2. Create a channel factory that connects to a WSE TCP service and sends framed messages over the client IDuplexSessionChannels.

  3. Create a channel listener to accept incoming TCP connections and produce corresponding channels.

  4. Ensure that any network-specific exceptions are normalized to the appropriate derived class of CommunicationException.

  5. Add a binding element that adds the custom transport to a channel stack. For more information, see [Adding a Binding Element].

Creating IDuplexSessionChannel

The first step in writing the WSE 3.0 TCP Interoperability Transport is to create an implementation of IDuplexSessionChannel on top of a Socket. WseTcpDuplexSessionChannel derives from ChannelBase. The logic of sending a message consists of two main pieces: (1) Encoding the message into bytes, and (2) framing those bytes and sending them on the wire.

ArraySegment<byte> encodedBytes = EncodeMessage(message);

WriteData(encodedBytes);

In addition, a lock is taken so that the Send() calls preserve the IDuplexSessionChannel in-order guarantee, and so that calls to the underlying socket are synchronized correctly.

WseTcpDuplexSessionChannel uses a MessageEncoder for translating a Message to and from byte[]. Because it is a transport, WseTcpDuplexSessionChannel is also responsible for applying the remote address that the channel was configured with. EncodeMessage encapsulates the logic for this conversion.

this.RemoteAddress.ApplyTo(message);

return encoder.WriteMessage(message, maxBufferSize, bufferManager);

Once the Message is encoded into bytes, it must be transmitted on the wire. This requires a system for defining message boundaries. WSE 3.0 uses a version of DIME as its framing protocol. WriteData encapsulates the framing logic to wrap a byte[] into a set of DIME records.

The logic for receiving messages is similar. The main complexity is handling the fact that a socket read can return fewer bytes than were requested. To receive a message, WseTcpDuplexSessionChannel reads bytes off the wire, decodes the DIME framing, and then uses the MessageEncoder for turning the byte[] into a Message.

The base WseTcpDuplexSessionChannel assumes that it receives a connected socket. The base class handles socket shutdown. There are three places that interface with socket closure:

  • OnAbort -- close the socket ungracefully (hard close).

  • On[Begin]Close -- close the socket gracefully (soft close).

  • session.CloseOutputSession -- shut down the outbound data stream (half close).

Channel Factory

The next step in writing the TCP transport is to create an implementation of IChannelFactory for client channels.

  • WseTcpChannelFactory derives from ChannelFactoryBase<IDuplexSessionChannel>. It is a factory that overrides OnCreateChannel to produce client channels.

protected override IDuplexSessionChannel OnCreateChannel(EndpointAddress remoteAddress, Uri via)

{

return new ClientWseTcpDuplexSessionChannel(encoderFactory, bufferManager, remoteAddress, via, this);

}

  • ClientWseTcpDuplexSessionChannel adds logic to the base WseTcpDuplexSessionChannel to connect to a TCP server at channel.Open time. First the hostname is resolved to an IP address, as shown in the following code.

hostEntry = Dns.GetHostEntry(Via.Host);

  • Then the hostname is connected to the first available IP address in a loop, as shown in the following code.

IPAddress address = hostEntry.AddressList[i];

socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

socket.Connect(new IPEndPoint(address, port));

  • As part of the channel contract, any domain-specific exceptions are wrapped, such as SocketException in CommunicationException.

Channel Listener

The next step in writing the TCP transport is to create an implementation of IChannelListener for accepting server channels.

  • WseTcpChannelListener derives from ChannelListenerBase<IDuplexSessionChannel> and overrides On[Begin]Open and On[Begin]Close to control the lifetime of its listen socket. In OnOpen, a socket is created to listen on IP_ANY. More advanced implementations can create a second socket to listen on IPv6 as well. They can also allow the IP address to be specified in the hostname.

IPEndPoint localEndpoint = new IPEndPoint(IPAddress.Any, uri.Port);

this.listenSocket = new Socket(localEndpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

this.listenSocket.Bind(localEndpoint);

this.listenSocket.Listen(10);

When a new socket is accepted, a server channel is initialized with this socket. All the input and output is already implemented in the base class, so this channel is responsible for initializing the socket.

Adding a Binding Element

Now that the factories and channels are built, they must be exposed to the ServiceModel runtime through a binding. A binding is a collection of binding elements that represents the communication stack associated with a service address. Each element in the stack is represented by a binding element.

In the sample, the binding element is WseTcpTransportBindingElement, which derives from TransportBindingElement. It supports IDuplexSessionChannel and overrides the following methods to build the factories associated with our binding.

public IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)

{

return (IChannelFactory<TChannel>)(object)new WseTcpChannelFactory(this, context);

}

public IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)

{

return (IChannelListener<TChannel>)(object)new WseTcpChannelListener(this, context);

}

It also contains members for cloning the BindingElement and returning our scheme (wse.tcp).

The WSE TCP Test Console

Test code for using this sample transport is available in TestCode.cs. The following instructions show how to set up the WSE TcpSyncStockService sample.

The test code creates a custom binding that uses MTOM as the encoding and WseTcpTransport as the transport. It also sets up the AddressingVersion to be conformant with WSE 3.0, as shown in the following code.

CustomBinding binding = new CustomBinding();

MtomMessageEncodingBindingElement mtomBindingElement = new MtomMessageEncodingBindingElement();

mtomBindingElement.MessageVersion = MessageVersion.Soap11WSAddressingAugust2004;

binding.Elements.Add(mtomBindingElement);

binding.Elements.Add(new WseTcpTransportBindingElement());

It consists of two tests—one test sets up a typed client using code generated from the WSE 3.0 WSDL. The second test uses WCF as both the client and the server by sending messages directly on top of the channel APIs.

When running the sample, the following output is expected.

Client:

Calling soap://stockservice.contoso.com/wse/samples/2003/06/TcpSyncStockService

Symbol: FABRIKAM
        Name: Fabrikam, Inc.
        Last Price: 120

Symbol: CONTOSO
        Name: Contoso Corp.
        Last Price: 50.07
Press enter.

Received Action: http://SayHello
Received Body: to you.
Hello to you.
Press enter.

Received Action: http://NotHello
Received Body: to me.
Press enter.

Server:

Listening for messages at soap://stockservice.contoso.com/wse/samples/2003/06/TcpSyncStockService

Press any key to exit when done...

Request received.
Symbols:
        FABRIKAM
        CONTOSO

Set up, build, and run the sample

  1. To run this sample, you must have Web Services Enhancements (WSE) 3.0 for Microsoft .NET and the WSE TcpSyncStockService sample installed.

Note

Because WSE 3.0 is not supported on Windows Server 2008, you cannot install or run the TcpSyncStockService sample on that operating system.

  1. Once you install the TcpSyncStockService sample, do the following:

    1. Open the TcpSyncStockService in Visual Studio. (The TcpSyncStockService sample is installed with WSE 3.0. It is not part of this sample's code.)

    2. Set the StockService project as the start-up project.

    3. Open StockService.cs in the StockService project and comment out the [Policy] attribute on the StockService class. This disables security from the sample. While WCF can interoperate with WSE 3.0 secure endpoints, security is disabled to keep this sample focused on the custom TCP transport.

    4. press F5 to start the TcpSyncStockService. The service starts in a new console window.

    5. Open this TCP transport sample in Visual Studio.

    6. Update the "hostname" variable in TestCode.cs to match the machine name running the TcpSyncStockService.

    7. press F5 to start the TCP transport sample.

    8. The TCP transport test client starts in a new console. The client requests stock quotes from the service and then displays the results in its console window.