Canal de fragmentación
El ejemplo de canal de fragmentación muestra cómo un protocolo personalizado o un canal en capas se pueden utilizar para la fragmentación y desfragmentación de mensajes arbitrariamente grandes.
Al enviar mensajes de gran tamaño mediante Windows Communication Foundation (WCF), a menudo es conveniente limitar la cantidad de memoria usada para almacenar en búfer esos mensajes. Una posible solución es transmitir en secuencias el cuerpo del mensaje (suponiendo que la mayor parte de los datos se encuentra en el cuerpo). Sin embargo, algunos protocolos requieren almacenado en búfer del mensaje completo. La mensajería de confianza y la seguridad son dos ejemplos de lo anterior. Otra posible solución es dividir el mensaje grande en mensajes menores llamados fragmentos, enviar uno por uno esos fragmentos y reconstituir el mensaje entero en el lado receptor. La propia aplicación podría hacer esta fragmentación y desfragmentación, o podría utilizar un canal personalizado para hacerlo.
Siempre se debería emplear la fragmentación solo una vez construido el mensaje completo que se va a enviar. Un canal de fragmentación siempre se debería disponer en niveles debajo de un canal de seguridad y un canal de sesión confiable.
Nota
El procedimiento de instalación y las instrucciones de compilación de este ejemplo se encuentran al final de este tema.
Supuestos y limitaciones del canal de fragmentación
Estructura de mensaje
El canal de fragmentación supone la estructura de mensaje siguiente para que los mensajes estén fragmentados:
<soap:Envelope>
<!-- headers -->
<soap:Body>
<operationElement>
<paramElement>data to be chunked</paramElement>
</operationElement>
</soap:Body>
</soap:Envelope>
Al utilizar ServiceModel, las operaciones de contrato que tienen 1 parámetro de entrada cumplen con esta forma de mensaje para su mensaje de entrada. De manera similar, las operaciones de contrato que tienen 1 parámetro de salida o valor devuelto cumplen con esta forma de mensaje para su mensaje de salida. A continuación, se muestran ejemplos de tales operaciones:
[ServiceContract]
interface ITestService
{
[OperationContract]
Stream EchoStream(Stream stream);
[OperationContract]
Stream DownloadStream();
[OperationContract(IsOneWay = true)]
void UploadStream(Stream stream);
}
Sesiones
El canal de fragmentación requiere que los mensajes se entreguen exactamente una vez, en entrega ordenada de mensajes (fragmentos). Esto significa que la pila del canal subyacente debe tener sesión. Las sesiones se pueden proporcionar por el transporte (por ejemplo, transporte TCP) o por un canal protocolar con sesión (por ejemplo, ReliableSession acanalan).
Enviós y recepciones asincrónicos
Los métodos de envíos y recepciones asincrónicos no se implementan en esta versión de ejemplo del canal de fragmentación.
Protocolo de fragmentación
El canal de fragmentación define un protocolo que indica el inicio y fin de una serie de fragmentos, así como el número de secuencia de cada fragmento. Los tres mensajes del ejemplo siguientes muestran los mensajes de inicio, fragmento y fin con comentarios que describen los aspectos clave de cada uno.
Mensaje de comienzo
<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>
Mensaje fragmento
<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>
Mensaje final
<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>
Arquitectura del canal de fragmentación
El canal de fragmentación es un IDuplexSessionChannel
que, a un nivel alto, sigue la arquitectura de canal típica. Hay un ChunkingBindingElement
que puede generar un ChunkingChannelFactory
y un ChunkingChannelListener
. ChunkingChannelFactory
crea instancias de ChunkingChannel
cuando se le pide. ChunkingChannelListener
crea instancias de ChunkingChannel
cuando se acepta un nuevo canal interno. El propio ChunkingChannel
es responsable de enviar y recibir los mensajes.
En el siguiente nivel inferior, ChunkingChannel
confia en varios componentes para implementar el protocolo de fragmentación. En el lado emisor, el canal usa un XmlDictionaryWriter personalizado denominado ChunkingWriter
que realiza la fragmentación real. ChunkingWriter
usa el canal interno directamente para enviar fragmentos. Utilizar un XmlDictionaryWriter
personalizado nos permite mandar fragmentos mientras se escribe el cuerpo grande del mensaje original. Esto significa que no almacenamos en búfer el mensaje original completo.
En el lado receptor, ChunkingChannel
extrae mensajes del canal interno y los ofrece a un XmlDictionaryReader personalizado denominado ChunkingReader
, que reconstituye el mensaje original a partir de los fragmentos de entrada. ChunkingChannel
ajusta este ChunkingReader
en una implementación de Message
ChunkingMessage
personalizada y devuelve este mensaje al nivel anterior. Esta combinación de ChunkingReader
y ChunkingMessage
nos permite al anular la fragmentación del cuerpo del mensaje original mientras la lee el nivel anterior en lugar de tener que almacenar en búfer todo el cuerpo del mensaje original. ChunkingReader
tiene una cola donde almacena en búfer fragmentos de entrada hasta un número máximo configurable de fragmentos almacenados en búfer. Cuando se alcanza este límite máximo, el lector espera que la capa superior recoja los mensajes de la cola (es decir, simplemente leyendo el cuerpo del mensaje original) o hasta que se alcance el tiempo de espera máximo de recepción.
Modelo de programación de fragmentación
Los programadores del servicio pueden especificar qué mensajes deben ser fragmentados, aplicando el atributo ChunkingBehavior
a las operaciones dentro del contrato. El atributo expone una propiedad AppliesTo
que permite al desarrollador especificar si la desfragmentación se aplica al mensaje de entrada, el mensaje de salida o ambos. En el ejemplo siguiente se muestra el uso del atributo ChunkingBehavior
:
[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);
}
De este modelo de programación, ChunkingBindingElement
compila una lista de URI de acción que identifica los mensajes que se deben fragmentar. La acción de cada mensaje saliente se compara contra esta lista para determinar si el mensaje se debería fragmentar o enviar directamente.
Implementar la operación Send
A un nivel alto, la operación Send comprueba primero si el mensaje saliente debe fragmentarse y, si no, envía el mensaje directamente mediante el canal interno.
Si el mensaje debe fragmentarse, Send crea un nuevo ChunkingWriter
y llama WriteBodyContents
en el mensaje saliente pasándole este ChunkingWriter
. ChunkingWriter
realiza a continuación la fragmentación del mensaje (incluyendo copiar los encabezados originales del mensaje al fragmento inicial del mensaje) y envía fragmentos mediante el canal interno.
Detalles que se han de tener en cuenta:
Send primero llama a
ThrowIfDisposedOrNotOpened
para asegurar que se abreCommunicationState
.El envío se sincroniza, para que solo un mensaje se pueda enviar a la vez para cada sesión. Hay un
ManualResetEvent
denominadosendingDone
que se restablece cuando se envía un mensaje fragmentado. Una vez enviado el fragmento del mensaje final, se establece este evento. El método Send espera a que este evento esté establecido antes de intentar enviar el mensaje saliente.Send bloquea el
CommunicationObject.ThisLock
para evitar cambios de estado sincronizados mientras se realiza el envío. Consulte la documentación CommunicationObject para obtener más información sobre los estados y el equipo de estado CommunicationObject.El tiempo de espera pasado a Send se utiliza como tiempo de espera para la operación de envío al completo, la cual incluye el envío de todos los fragmentos.
El diseño XmlDictionaryWriter personalizado fue elegido para evitar almacenar en búfer el cuerpo del mensaje original completo. Si deseáramos obtener un XmlDictionaryReader en el cuerpo utilizando
message.GetReaderAtBodyContents
el cuerpo completo se almacenaría en búfer. En su lugar, tenemos un XmlDictionaryWriter personalizado que se pasa amessage.WriteBodyContents
. Mientras el mensaje llama a WriteBase64 en el sistema de escritura, el sistema de escritura empaqueta los fragmentos en los mensajes y los envía utilizando el canal interno. WriteBase64 se bloquea hasta que se envía el fragmento.
Implementar la operación Receive
A un nivel alto, la operación Receive comprueba primero que el mensaje entrante no es null
y que su acción es ChunkingAction
. Si no cumple ambos criterios, el mensaje se devuelve sin modificar desde Receive. De lo contrario, Receive crea un nuevo ChunkingReader
y un nuevo ChunkingMessage
ajustado alrededor de él (llamando a GetNewChunkingMessage
). Antes de devolver ese nuevo ChunkingMessage
, Receive utiliza un subproceso de un conjunto de subprocesos para ejecutar ReceiveChunkLoop
, que llama innerChannel.Receive
en un bucle y entrega fragmentos a ChunkingReader
hasta que se reciba el fragmento de mensaje final o se alcance el tiempo de espera de recepción.
Detalles que se han de tener en cuenta:
Al igual que Send, Receive primero llama
ThrowIfDisposedOrNotOpened
para asegurar que se abreCommunicationState
.Receive también se sincroniza para que solo se pueda recibir un mensaje de la sesión al mismo tiempo. Esto es especialmente importante porque una vez se recibe un fragmento de mensaje de inicio, se espera que todos los mensajes siguientes recibidos sean fragmentos de esta nueva secuencia de fragmentos hasta que se reciba un fragmento de mensaje final. Receive no puede incorporar los cambios de los mensajes del canal interno hasta que no se reciban todos los fragmentos que pertenecen al mensaje que se está desfragmentando. Para lograr esto, Receive utiliza un
ManualResetEvent
denominadocurrentMessageCompleted
, que se establece cuando el fragmento de mensaje final se recibe y se restablece cuando se recibe un nuevo fragmento de mensaje de inicio.A diferencia de Send, Receive no evita las transiciones de estado sincronizadas mientras recibe. Por ejemplo, se puede llamar a Close mientras se recibe y se espera hasta que las recepciones pendientes del mensaje original se completen o se alcance el valor de tiempo de espera especificado.
El tiempo de espera pasado a Receive se utiliza como tiempo de espera para la operación de recepción completa, que incluye la recepción todos los fragmentos.
Si la capa que consume el mensaje está consumiendo el cuerpo del mensaje a una tasa menor que la tasa de llegadas de fragmentos de mensajes, el
ChunkingReader
almacena en búfer esos fragmentos de entrada hasta el límite especificado porChunkingBindingElement.MaxBufferedChunks
. Una vez alcanzado ese límite, ningún fragmento más se extrae de la capa inferior hasta que un fragmento almacenado en búfer se utiliza o se alcanza el tiempo de espera para la recepción.
Invalidaciones de CommunicationObject
OnOpen
OnOpen
llama innerChannel.Open
para abrir el canal interno.
OnClose
OnClose
establece primero stopReceive
a true
para señalar el ReceiveChunkLoop
pendiente para detenerse. Después, este espera a receiveStopped
ManualResetEvent, que se establece cuando ReceiveChunkLoop
se detiene. Suponiendo las pausas ReceiveChunkLoop
dentro del tiempo de espera especificado, OnClose
llama innerChannel.Close
con el tiempo de espera restante.
OnAbort
OnAbort
llama innerChannel.Abort
para anular el canal interno. Si hay un ReceiveChunkLoop
pendiente, recibe a una excepción de la llamada innerChannel.Receive
pendiente.
OnFaulted
ChunkingChannel
no requiere comportamiento especial cuando el canal tiene errores, por lo que no se invalida OnFaulted
.
Implementar el generador de canales
ChunkingChannelFactory
es responsable de crear instancias de ChunkingDuplexSessionChannel
y de colocar en cascada las transiciones de estado al generador de canales interno.
OnCreateChannel
utiliza el generador de canales interno para crear un canal interno IDuplexSessionChannel
. Crea a continuación un nuevo ChunkingDuplexSessionChannel
que le pasa este canal interno junto con la lista de acciones de mensaje para ser fragmentada y el número máximo de fragmentos para almacenar en búfer cuando se produce la recepción. La lista de acciones de mensaje para ser fragmentada y el número máximo de fragmentos para almacenar en búfer son dos parámetros pasados a ChunkingChannelFactory
en su constructor. La sección en ChunkingBindingElement
describe de dónde proceden estos valores.
OnOpen
, OnClose
, OnAbort
y sus equivalentes asincrónicos llaman al método de transición de estado correspondiente en el generador de canales interno.
Implementar el escuchado del canal
ChunkingChannelListener
es un contenedor alrededor de una escucha de canal interno. Su función principal, además de delegar llamadas a esa escucha del canal interno, es ajustar nuevo ChunkingDuplexSessionChannels
alrededor de los canales aceptados por la escucha del canal interno. Esto se hace en OnAcceptChannel
y OnEndAcceptChannel
. El ChunkingDuplexSessionChannel
recientemente creado se pasa al canal interno junto con los otros parámetros descritos previamente.
Implementar elemento de enlace y enlace
ChunkingBindingElement
es el responsable de crear el ChunkingChannelFactory
y ChunkingChannelListener
. ChunkingBindingElement
comprueba si T en CanBuildChannelFactory
<T> y CanBuildChannelListener
<T> es de tipo IDuplexSessionChannel
(el único canal que admite el canal de fragmentación) y el resto de elementos de enlace en la compatibilidad con enlaces para este tipo de canal.
BuildChannelFactory
<T> comprueba primero que el tipo de canal solicitado pueda generarse y después obtiene una lista de acciones de mensaje que deben fragmentarse. Para más información, consulte la sección siguiente. Crea a continuación un nuevo ChunkingChannelFactory
que le pasa el generador de canales interno (tal y como se devolvió desde context.BuildInnerChannelFactory<IDuplexSessionChannel>
), la lista de acciones de mensaje, y el número máximo de fragmentos para almacenar en búfer. El número máximo de fragmentos procede de una propiedad llamada MaxBufferedChunks
expuesta por el ChunkingBindingElement
.
BuildChannelListener<T>
tiene una implementación similar para crear ChunkingChannelListener
y pasarle el agente de escucha de canal interno.
Hay un enlace de ejemplo incluido en este ejemplo denominado TcpChunkingBinding
. Este enlace está compuesto de dos elementos de enlace: TcpTransportBindingElement
y ChunkingBindingElement
. Además de exponer la propiedad MaxBufferedChunks
, el enlace también establece algunas de las propiedades TcpTransportBindingElement
como MaxReceivedMessageSize
(lo establece a ChunkingUtils.ChunkSize
+ 100KB bytes para los encabezados).
TcpChunkingBinding
también implementa IBindingRuntimePreferences
y devuelve true desde el método ReceiveSynchronously
que indica que solo las llamadas sincrónicas Receive se implementan.
Determinar qué mensajes se fragmentan
El canal de fragmentación solo fragmenta los mensajes identificados a través del atributo ChunkingBehavior
. La clase ChunkingBehavior
implementa IOperationBehavior
y se implementa llamando al método AddBindingParameter
. En este método,el ChunkingBehavior
examina el valor de su propiedad (AppliesTo
, InMessage
o ambos) OutMessage
para determinar qué mensajes deberían fragmentarse. Obtiene a continuación la acción de cada uno de esos mensajes (de la colección Messages en OperationDescription
) y lo agrega a una colección de cadenas contenida dentro de una instancia de ChunkingBindingParameter
. Agrega a continuación ChunkingBindingParameter
al BindingParameterCollection
proporcionado.
Esta BindingParameterCollection
se pasa dentro del BindingContext
a cada elemento de enlace en el enlace cuando ese elemento de enlace compila el generador de canales o la escucha del canal. La implementación de ChunkingBindingElement
de BuildChannelFactory<T>
y BuildChannelListener<T>
extrae este ChunkingBindingParameter
de la BindingParameterCollection
de BindingContext'
. La colección de acciones contenida dentro de ChunkingBindingParameter
se pasa a continuación al ChunkingChannelFactory
o ChunkingChannelListener
, que a su vez lo pasa al ChunkingDuplexSessionChannel
.
Ejecutar el ejemplo
Configurar, compilar y ejecutar el ejemplo
Instale ASP.NET 4.0 con el siguiente comando.
%windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
Asegúrese de que ha realizado el procedimiento de instalación única para los ejemplos de Windows Communication Foundation.
Para compilar la solución, siga las instrucciones que se indican en Compilación de los ejemplos de Windows Communication Foundation.
Para ejecutar el ejemplo en una configuración de una sola máquina o de varias máquinas, siga las instrucciones que se indican en Ejecución de los ejemplos de Windows Communication Foundation.
Ejecute primero Service.exe, a continuación, ejecute Client.exe e inspeccione ambas ventanas de la consola para ver el resultado.
Al ejecutar el ejemplo, se espera el resultado siguiente.
Cliente:
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
Servidor:
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