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.
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.
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 theCommunicationState
is opened.Sending is synchronized so that only one message can be sent at a time for each session. There is a
ManualResetEvent
namedsendingDone
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 tomessage.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 theCommunicationState
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
namedcurrentMessageCompleted
, 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 byChunkingBindingElement.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
Install ASP.NET 4.0 using the following command.
%windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.
To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.
To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.
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