Chunking Channel

The ChunkingChannel sample shows how a custom protocol or layered channel can be used to do chunking and dechunking of arbitrarily large messages.

When sending large messages using Windows Communication Foundation (WCF), it is often desirable to limit the amount of memory used to buffer those messages. One possible solution is to stream the message body (assuming the bulk of the data is in the body). However some protocols require buffering of the entire message. Reliable messaging and security are two such examples. Another possible solution is to divide up the large message into smaller messages called chunks, send those chunks one chunk at a time, and reconstitute the large message on the receiving side. The application itself could do this chunking and de-chunking or it could use a custom channel to do it.

Chunking should always be employed only after the entire message to be sent has been constructed. A chunking channel should always be layered below a security channel and a reliable session channel.

Note

The setup procedure and build instructions for this sample are located at the end of this topic.

Chunking Channel Assumptions and Limitations

Message Structure

The chunking channel assumes the following message structure for messages to be chunked:

<soap:Envelope>
  <!-- headers -->
  <soap:Body>
    <operationElement>
      <paramElement>data to be chunked</paramElement>
    </operationElement>
  </soap:Body>
</soap:Envelope>

When using the ServiceModel, contract operations that have 1 input parameter comply with this shape of message for their input message. Similarly, contract operations that have 1 output parameter or return value comply with this shape of message for their output message. The following are examples of such operations:

[ServiceContract]
interface ITestService
{
    [OperationContract]
    Stream EchoStream(Stream stream);

    [OperationContract]
    Stream DownloadStream();

    [OperationContract(IsOneWay = true)]
    void UploadStream(Stream stream);
}

Sessions

The chunking channel requires messages to be delivered exactly once, in ordered delivery of messages (chunks). This means the underlying channel stack must be sessionful. Sessions can be provided by the transport (for example, TCP transport) or by a sessionful protocol channel (for example, ReliableSession channel).

Asynchronous Send and Receive

Asynchronous send and receive methods are not implemented in this version of the chunking channel sample.

Chunking Protocol

The chunking channel defines a protocol that indicates the start and end of a series of chunks as well as the sequence number of each chunk. The following three example messages demonstrate the start, chunk and end messages with comments that describe the key aspects of each.

Start Message

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
<!--Original message action is replaced with a chunking-specific action. -->
    <a:Action s:mustUnderstand="1">http://samples.microsoft.com/chunkingAction</a:Action>
<!--
Original message is assigned a unique id that is transmitted
in a MessageId header. Note that this is different from the WS-Addressing MessageId header.
-->
    <MessageId s:mustUnderstand="1" xmlns="http://samples.microsoft.com/chunking">
53f183ee-04aa-44a0-b8d3-e45224563109
</MessageId>
<!--
ChunkingStart header signals the start of a chunked message.
-->
    <ChunkingStart s:mustUnderstand="1" i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://samples.microsoft.com/chunking" />
<!--
Original message action is transmitted in OriginalAction.
This is required to re-create the original message on the other side.
-->
    <OriginalAction xmlns="http://samples.microsoft.com/chunking">
http://tempuri.org/ITestService/EchoStream
    </OriginalAction>
   <!--
    All original message headers are included here.
   -->
  </s:Header>
  <s:Body>
<!--
Chunking assumes this structure of Body content:
<element>
  <childelement>large data to be chunked<childelement>
</element>
The start message contains just <element> and <childelement> without
the data to be chunked.
-->
    <EchoStream xmlns="http://tempuri.org/">
      <stream />
    </EchoStream>
  </s:Body>
</s:Envelope>

Chunk Message

<s:Envelope
  xmlns:a="http://www.w3.org/2005/08/addressing"
  xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
   <!--
    All chunking protocol messages have this action.
   -->
    <a:Action s:mustUnderstand="1">
      http://samples.microsoft.com/chunkingAction
    </a:Action>
<!--
Same as MessageId in the start message. The GUID indicates which original message this chunk belongs to.
-->
    <MessageId s:mustUnderstand="1"
               xmlns="http://samples.microsoft.com/chunking">
      53f183ee-04aa-44a0-b8d3-e45224563109
    </MessageId>
<!--
The sequence number of the chunk.
This number restarts at 1 with each new sequence of chunks.
-->
    <ChunkNumber s:mustUnderstand="1"
                 xmlns="http://samples.microsoft.com/chunking">
      1096
    </ChunkNumber>
  </s:Header>
  <s:Body>
<!--
The chunked data is wrapped in a chunk element.
The encoding of this data (and the entire message)
depends on the encoder used. The chunking channel does not mandate an encoding.
-->
    <chunk xmlns="http://samples.microsoft.com/chunking">
kfSr2QcBlkHTvQ==
    </chunk>
  </s:Body>
</s:Envelope>

End Message

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">
      http://samples.microsoft.com/chunkingAction
    </a:Action>
<!--
Same as MessageId in the start message. The GUID indicates which original message this chunk belongs to.
-->
    <MessageId s:mustUnderstand="1"
               xmlns="http://samples.microsoft.com/chunking">
      53f183ee-04aa-44a0-b8d3-e45224563109
    </MessageId>
<!--
ChunkingEnd header signals the end of a chunk sequence.
-->
    <ChunkingEnd s:mustUnderstand="1" i:nil="true"
                 xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns="http://samples.microsoft.com/chunking" />
<!--
ChunkingEnd messages have a sequence number.
-->
    <ChunkNumber s:mustUnderstand="1"
                 xmlns="http://samples.microsoft.com/chunking">
      79
    </ChunkNumber>
  </s:Header>
  <s:Body>
<!--
The ChunkingEnd message has the same <element><childelement> structure
as the ChunkingStart message.
-->
    <EchoStream xmlns="http://tempuri.org/">
      <stream />
    </EchoStream>
  </s:Body>
</s:Envelope>

Chunking Channel Architecture

The chunking channel is an IDuplexSessionChannel that, at a high level, follows the typical channel architecture. There is a ChunkingBindingElement that can build a ChunkingChannelFactory and a ChunkingChannelListener. The ChunkingChannelFactory creates instances of ChunkingChannel when it is asked to. The ChunkingChannelListener creates instances of ChunkingChannel when a new inner channel is accepted. The ChunkingChannel itself is responsible for sending and receiving messages.

At the next level down, ChunkingChannel relies on several components to implement the chunking protocol. On the send side, the channel uses a custom XmlDictionaryWriter called ChunkingWriter that does the actual chunking. ChunkingWriter uses the inner channel directly to send chunks. Using a custom XmlDictionaryWriter allows us to send out chunks as the large body of the original message is being written. This means we do not buffer the entire original message.

Diagram that shows the chunking channel send architecture.

On the receive side, ChunkingChannel pulls messages from the inner channel and hands them to a custom XmlDictionaryReader called ChunkingReader, which reconstitutes the original message from the incoming chunks. ChunkingChannel wraps this ChunkingReader in a custom Message implementation called ChunkingMessage and returns this message to the layer above. This combination of ChunkingReader and ChunkingMessage allows us to de-chunk the original message body as it is being read by the layer above instead of having to buffer the entire original message body. ChunkingReader has a queue where it buffers incoming chunks up to a maximum configurable number of buffered chunks. When this maximum limit is reached, the reader waits for messages to be drained from the queue by the layer above (that is, by just reading from the original message body) or until the maximum receive timeout is reached.

Diagram that shows the chunking channel receive architecture.

Chunking Programming Model

Service developers can specify which messages are to be chunked by applying the ChunkingBehavior attribute to operations within the contract. The attribute exposes an AppliesTo property that allows the developer to specify whether chunking applies to the input message, the output message or both. The following example shows the usage of ChunkingBehavior attribute:

[ServiceContract]
interface ITestService
{
    [OperationContract]
    [ChunkingBehavior(ChunkingAppliesTo.Both)]
    Stream EchoStream(Stream stream);

    [OperationContract]
    [ChunkingBehavior(ChunkingAppliesTo.OutMessage)]
    Stream DownloadStream();

    [OperationContract(IsOneWay=true)]
    [ChunkingBehavior(ChunkingAppliesTo.InMessage)]
    void UploadStream(Stream stream);

}

From this programming model, the ChunkingBindingElement compiles a list of action URIs that identify messages to be chunked. The action of each outgoing message is compared against this list to determine if the message should be chunked or sent directly.

Implementing the Send Operation

At a high level, the Send operation first checks whether the outgoing message must be chunked and, if not, sends the message directly using the inner channel.

If the message must be chunked, Send creates a new ChunkingWriter and calls WriteBodyContents on the outgoing message passing it this ChunkingWriter. The ChunkingWriter then does the message chunking (including copying original message headers to the start chunk message) and sends chunks using the inner channel.

A few details worth noting:

  • Send first calls ThrowIfDisposedOrNotOpened to ensure the CommunicationState is opened.

  • Sending is synchronized so that only one message can be sent at a time for each session. There is a ManualResetEvent named sendingDone that is reset when a chunked message is being sent. Once the end chunk message is sent, this event is set. The Send method waits for this event to be set before it tries to send the outgoing message.

  • Send locks the CommunicationObject.ThisLock to prevent synchronized state changes while sending. See the CommunicationObject documentation for more information about CommunicationObject states and state machine.

  • The timeout passed to Send is used as the timeout for the entire send operation which includes sending all of the chunks.

  • The custom XmlDictionaryWriter design was chosen to avoid buffering the entire original message body. If we were to get an XmlDictionaryReader on the body using message.GetReaderAtBodyContents the entire body would be buffered. Instead, we have a custom XmlDictionaryWriter that is passed to message.WriteBodyContents. As the message calls WriteBase64 on the writer, the writer packages up chunks into messages and sends them using the inner channel. WriteBase64 blocks until the chunk is sent.

Implementing the Receive Operation

At a high level, the Receive operation first checks that the incoming message is not null and that its action is the ChunkingAction. If it does not meet both criteria, the message is returned unchanged from Receive. Otherwise, Receive creates a new ChunkingReader and a new ChunkingMessage wrapped around it (by calling GetNewChunkingMessage). Before returning that new ChunkingMessage, Receive uses a threadpool thread to execute ReceiveChunkLoop, which calls innerChannel.Receive in a loop and hands off chunks to the ChunkingReader until the end chunk message is received or the receive timeout is hit.

A few details worth noting:

  • Like Send, Receive first calls ThrowIfDisposedOrNotOpened to ensure the CommunicationState is Opened.

  • Receive is also synchronized so that only one message can be received at a time from the session. This is especially important because once a start chunk message is received, all subsequent received messages are expected to be chunks within this new chunk sequence until an end chunk message is received. Receive cannot pull messages from the inner channel until all chunks that belong to the message currently being de-chunked are received. To accomplish this, Receive uses a ManualResetEvent named currentMessageCompleted, which is set when the end chunk message is received and reset when a new start chunk message is received.

  • Unlike Send, Receive does not prevent synchronized state transitions while receiving. For example, Close can be called while receiving and waits until the pending receive of the original message is completed or the specified timeout value is reached.

  • The timeout passed to Receive is used as the timeout for the entire receive operation, which includes receiving all of the chunks.

  • If the layer that consumes the message is consuming the message body at a rate lower than the rate of incoming chunk messages, the ChunkingReader buffers those incoming chunks up to the limit specified by ChunkingBindingElement.MaxBufferedChunks. Once that limit is reached, no more chunks are pulled from the lower layer until either a buffered chunk is consumed or the receive timeout is reached.

CommunicationObject Overrides

OnOpen

OnOpen calls innerChannel.Open to open the inner channel.

OnClose

OnClose first sets stopReceive to true to signal the pending ReceiveChunkLoop to stop. It then waits for the receiveStopped ManualResetEvent, which is set when ReceiveChunkLoop stops. Assuming the ReceiveChunkLoop stops within the specified timeout, OnClose calls innerChannel.Close with the remaining timeout.

OnAbort

OnAbort calls innerChannel.Abort to abort the inner channel. If there is a pending ReceiveChunkLoop it gets an exception from the pending innerChannel.Receive call.

OnFaulted

The ChunkingChannel does not require special behavior when the channel is faulted so OnFaulted is not overridden.

Implementing Channel Factory

The ChunkingChannelFactory is responsible for creating instances of ChunkingDuplexSessionChannel and for cascading state transitions to the inner channel factory.

OnCreateChannel uses the inner channel factory to create an IDuplexSessionChannel inner channel. It then creates a new ChunkingDuplexSessionChannel passing it this inner channel along with the list of message actions to be chunked and the maximum number of chunks to buffer upon receive. The list of message actions to be chunked and the maximum number of chunks to buffer are two parameters passed to ChunkingChannelFactory in its constructor. The section on ChunkingBindingElement describes where these values come from.

The OnOpen, OnClose, OnAbort and their asynchronous equivalents call the corresponding state transition method on the inner channel factory.

Implementing Channel Listener

The ChunkingChannelListener is a wrapper around an inner channel listener. Its main function, besides delegate calls to that inner channel listener, is to wrap new ChunkingDuplexSessionChannels around channels accepted from the inner channel listener. This is done in OnAcceptChannel and OnEndAcceptChannel. The newly created ChunkingDuplexSessionChannel is passed the inner channel along with the other parameters previously described.

Implementing Binding Element and Binding

ChunkingBindingElement is responsible for creating the ChunkingChannelFactory and ChunkingChannelListener. The ChunkingBindingElement checks whether T in CanBuildChannelFactory<T> and CanBuildChannelListener<T> is of type IDuplexSessionChannel (the only channel supported by the chunking channel) and that the other binding elements in the binding support this channel type.

BuildChannelFactory<T> first checks that the requested channel type can be built and then gets a list of message actions to be chunked. For more information, see the following section. It then creates a new ChunkingChannelFactory passing it the inner channel factory (as returned from context.BuildInnerChannelFactory<IDuplexSessionChannel>), the list of message actions, and the maximum number of chunks to buffer. The maximum number of chunks comes from a property called MaxBufferedChunks exposed by the ChunkingBindingElement.

BuildChannelListener<T> has a similar implementation for creating ChunkingChannelListener and passing it the inner channel listener.

There is an example binding included in this sample named TcpChunkingBinding. This binding consists of two binding elements: TcpTransportBindingElement and ChunkingBindingElement. In addition to exposing the MaxBufferedChunks property, the binding also sets some of the TcpTransportBindingElement properties such as MaxReceivedMessageSize (sets it to ChunkingUtils.ChunkSize + 100KB bytes for headers).

TcpChunkingBinding also implements IBindingRuntimePreferences and returns true from the ReceiveSynchronously method indicating that only the synchronous Receive calls are implemented.

Determining Which Messages To Chunk

The chunking channel chunks only the messages identified through the ChunkingBehavior attribute. The ChunkingBehavior class implements IOperationBehavior and is implemented by calling the AddBindingParameter method. In this method, the ChunkingBehavior examines the value of its AppliesTo property (InMessage, OutMessage or both) to determine which messages should be chunked. It then gets the action of each of those messages (from the Messages collection on OperationDescription) and adds it to a string collection contained within an instance of ChunkingBindingParameter. It then adds this ChunkingBindingParameter to the provided BindingParameterCollection.

This BindingParameterCollection is passed inside the BindingContext to each binding element in the binding when that binding element builds the channel factory or the channel listener. The ChunkingBindingElement's implementation of BuildChannelFactory<T> and BuildChannelListener<T> pull this ChunkingBindingParameter out of the BindingContext's BindingParameterCollection. The collection of actions contained within the ChunkingBindingParameter is then passed to the ChunkingChannelFactory or ChunkingChannelListener, which in turn passes it to the ChunkingDuplexSessionChannel.

Running the Sample

To set up, build, and run the sample

  1. Install ASP.NET 4.0 using the following command.

    %windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
    
  2. Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.

  3. To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  4. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.

  5. Run Service.exe first, then run Client.exe and watch both console windows for output.

When running the sample, the following output is expected.

Client:

Press enter when service is available

 > Sent chunk 1 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 2 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 3 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 4 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 5 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 6 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 7 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 8 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 9 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 10 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 1 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 2 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 3 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 4 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 5 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 6 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 7 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 8 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 9 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 10 of message 5b226ad5-c088-4988-b737-6a565e0563dd

Server:

Service started, press enter to exit
 < Received chunk 1 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 2 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 3 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 4 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 5 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 6 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 7 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 8 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 9 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 10 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 1 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 2 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 3 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 4 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 5 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 6 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 7 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 8 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 9 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 10 of message 5b226ad5-c088-4988-b737-6a565e0563dd