Canal Chunking
O exemplo ChunkingChannel mostra como um protocolo personalizado ou canal em camadas pode ser usado para fazer chunking e dechunking de mensagens arbitrariamente grandes.
Ao enviar mensagens grandes usando o Windows Communication Foundation (WCF), geralmente é desejável limitar a quantidade de memória usada para armazenar essas mensagens em buffer. Uma solução possível é transmitir o corpo da mensagem (assumindo que a maior parte dos dados está no corpo). No entanto, alguns protocolos exigem buffering de toda a mensagem. Mensagens confiáveis e segurança são dois desses exemplos. Outra solução possível é dividir a mensagem grande em mensagens menores chamadas blocos, enviar esses blocos um pedaço de cada vez e reconstituir a mensagem grande no lado recetor. O próprio aplicativo poderia fazer esse chunking e de-chunking ou poderia usar um canal personalizado para fazê-lo.
A fragmentação deve ser sempre empregada somente depois que toda a mensagem a ser enviada tiver sido construída. Um canal de fragmentação deve estar sempre em camadas abaixo de um canal de segurança e um canal de sessão confiável.
Nota
O procedimento de configuração e as instruções de compilação para este exemplo estão localizados no final deste tópico.
Pressupostos e limitações do canal de fragmentação
Estrutura da mensagem
O canal de fragmentação assume a seguinte estrutura de mensagens para que as mensagens sejam fragmentadas:
<soap:Envelope>
<!-- headers -->
<soap:Body>
<operationElement>
<paramElement>data to be chunked</paramElement>
</operationElement>
</soap:Body>
</soap:Envelope>
Ao usar o ServiceModel, as operações de contrato que têm 1 parâmetro de entrada estão em conformidade com essa forma de mensagem para sua mensagem de entrada. Da mesma forma, as operações de contrato que têm 1 parâmetro de saída ou valor de retorno estão em conformidade com essa forma de mensagem para sua mensagem de saída. Seguem-se exemplos de tais operações:
[ServiceContract]
interface ITestService
{
[OperationContract]
Stream EchoStream(Stream stream);
[OperationContract]
Stream DownloadStream();
[OperationContract(IsOneWay = true)]
void UploadStream(Stream stream);
}
Sessões
O canal de fragmentação requer que as mensagens sejam entregues exatamente uma vez, na entrega ordenada de mensagens (blocos). Isso significa que a pilha de canais subjacente deve ser sessionful. As sessões podem ser fornecidas pelo transporte (por exemplo, transporte TCP) ou por um canal de protocolo de sessão (por exemplo, canal ReliableSession).
Envio e recebimento assíncronos
Os métodos assíncronos de envio e recebimento não são implementados nesta versão do exemplo de canal de fragmentação.
Protocolo de Fragmentação
O canal de fragmentação define um protocolo que indica o início e o fim de uma série de blocos, bem como o número de sequência de cada bloco. As três mensagens de exemplo a seguir demonstram as mensagens de início, parte e fim com comentários que descrevem os principais aspetos de cada uma.
Mensagem de início
<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>
Mensagem Chunk
<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>
Mensagem 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>
Arquitetura do canal Chunking
O canal chunking é um IDuplexSessionChannel
que, em um alto nível, segue a arquitetura típica do canal. Há um ChunkingBindingElement
que pode construir um ChunkingChannelFactory
e um ChunkingChannelListener
. O ChunkingChannelFactory
cria instâncias de ChunkingChannel
quando é solicitado. O ChunkingChannelListener
cria instâncias de ChunkingChannel
quando um novo canal interno é aceito. O ChunkingChannel
próprio é responsável por enviar e receber mensagens.
No próximo nível abaixo, ChunkingChannel
depende de vários componentes para implementar o protocolo de fragmentação. No lado de envio, o canal usa um personalizado XmlDictionaryWriter chamado ChunkingWriter
que faz o chunking real. ChunkingWriter
usa o canal interno diretamente para enviar partes. O uso de um personalizado XmlDictionaryWriter
nos permite enviar partes à medida que o corpo grande da mensagem original está sendo escrito. Isso significa que não armazenamos em buffer toda a mensagem original.
No lado de receção, ChunkingChannel
puxa mensagens do canal interno e entrega-as a um personalizado XmlDictionaryReader chamado ChunkingReader
, que reconstitui a mensagem original dos blocos recebidos. ChunkingChannel
Encapsula isso ChunkingReader
em uma implementação personalizada Message
chamada ChunkingMessage
e retorna essa mensagem para a camada acima. Essa combinação de ChunkingReader
e ChunkingMessage
nos permite desfragmentar o corpo da mensagem original à medida que ela está sendo lida pela camada acima, em vez de ter que armazenar em buffer todo o corpo da mensagem original. ChunkingReader
tem uma fila onde armazena em buffer os blocos de entrada até um número máximo configurável de blocos em buffer. Quando esse limite máximo é atingido, o leitor espera que as mensagens sejam drenadas da fila pela camada acima (ou seja, apenas lendo o corpo da mensagem original) ou até que o tempo limite máximo de recebimento seja atingido.
Modelo de programação de fragmentação
Os desenvolvedores de serviços podem especificar quais mensagens devem ser fragmentadas aplicando o ChunkingBehavior
atributo às operações dentro do contrato. O atributo expõe uma AppliesTo
propriedade que permite ao desenvolvedor especificar se o chunking se aplica à mensagem de entrada, à mensagem de saída ou a ambas. O exemplo a seguir mostra o uso do ChunkingBehavior
atributo:
[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);
}
A partir desse modelo de programação, o ChunkingBindingElement
compila uma lista de URIs de ação que identificam mensagens a serem fragmentadas. A ação de cada mensagem de saída é comparada com essa lista para determinar se a mensagem deve ser fragmentada ou enviada diretamente.
Implementando a operação de envio
Em um nível alto, a operação Send primeiro verifica se a mensagem de saída deve ser fragmentada e, se não, envia a mensagem diretamente usando o canal interno.
Se a mensagem precisar ser fragmentada, Enviar criará uma nova ChunkingWriter
e chamará WriteBodyContents
a mensagem de saída passando-a assim ChunkingWriter
. O ChunkingWriter
then faz o fragmentamento da mensagem (incluindo a cópia dos cabeçalhos da mensagem original para a mensagem do bloco inicial) e envia partes usando o canal interno.
Alguns detalhes que merecem destaque:
Envie as primeiras chamadas
ThrowIfDisposedOrNotOpened
para garantir que oCommunicationState
está aberto.O envio é sincronizado para que apenas uma mensagem possa ser enviada de cada vez para cada sessão. Há um
ManualResetEvent
nomesendingDone
que é redefinido quando uma mensagem em partes está sendo enviada. Uma vez que a mensagem de bloco final é enviada, esse evento é definido. O método Send aguarda que esse evento seja definido antes de tentar enviar a mensagem de saída.Enviar bloqueia o para evitar alterações de estado sincronizadas durante o
CommunicationObject.ThisLock
envio. Consulte a CommunicationObject documentação para obter mais informações sobre CommunicationObject estados e máquina de estado.O tempo limite passado para Enviar é usado como o tempo limite para toda a operação de envio, que inclui o envio de todos os blocos.
O design personalizado XmlDictionaryWriter foi escolhido para evitar o buffer de todo o corpo da mensagem original. Se fôssemos obter um XmlDictionaryReader no corpo usando
message.GetReaderAtBodyContents
o corpo inteiro seria tamponado. Em vez disso, temos um costume XmlDictionaryWriter que é passado paramessage.WriteBodyContents
. À medida que a mensagem chama WriteBase64 no gravador, o gravador empacota partes em mensagens e as envia usando o canal interno. WriteBase64 bloqueia até que o bloco seja enviado.
Implementando a operação de recebimento
Em um nível alto, a operação Receber primeiro verifica se a mensagem recebida não null
é e se sua ação é o ChunkingAction
. Se não atender aos dois critérios, a mensagem será retornada inalterada de Receber. Caso contrário, Receive criará um novo ChunkingReader
e um novo ChunkingMessage
embrulhado em torno dele (chamando GetNewChunkingMessage
). Antes de retornar esse novo ChunkingMessage
, Receive usa um thread threadpool para executar ReceiveChunkLoop
, que chama innerChannel.Receive
em um loop e entrega blocos para a ChunkingReader
mensagem de bloco até que o final seja recebido ou o tempo limite de recebimento seja atingido.
Alguns detalhes que merecem destaque:
Como o Enviar, receba as primeiras chamadas
ThrowIfDisposedOrNotOpened
para garantir que oCommunicationState
seja Aberto.O recebimento também é sincronizado para que apenas uma mensagem possa ser recebida por vez da sessão. Isso é especialmente importante porque, uma vez que uma mensagem de bloco inicial é recebida, espera-se que todas as mensagens recebidas subsequentes sejam partes dentro dessa nova sequência de blocos até que uma mensagem de bloco final seja recebida. Receber não pode extrair mensagens do canal interno até que todas as partes que pertencem à mensagem que está sendo desfragmentada no momento sejam recebidas. Para fazer isso, Receive usa um
ManualResetEvent
nomecurrentMessageCompleted
, que é definido quando a mensagem do bloco final é recebida e redefinido quando uma nova mensagem do bloco inicial é recebida.Ao contrário do Envio, o Recebimento não impede transições de estado sincronizadas durante o recebimento. Por exemplo, Close pode ser chamado durante o recebimento e aguarda até que o recebimento pendente da mensagem original seja concluído ou o valor de tempo limite especificado seja atingido.
O tempo limite passado para Receber é usado como o tempo limite para toda a operação de recebimento, que inclui o recebimento de todas as partes.
Se a camada que consome a mensagem estiver consumindo o corpo da mensagem a uma taxa menor do que a taxa de mensagens de bloco de entrada, os
ChunkingReader
buffers desses blocos de entrada até o limite especificado porChunkingBindingElement.MaxBufferedChunks
. Uma vez atingido esse limite, não são retirados mais pedaços da camada inferior até que um pedaço em buffer seja consumido ou o tempo limite de recebimento seja atingido.
Substituições de CommunicationObject
OnOpen
OnOpen
chamadas innerChannel.Open
para abrir o canal interno.
OnClose
OnClose
primeiro define stopReceive
para true
sinalizar o pendente ReceiveChunkLoop
para parar. Em seguida, aguarda o receiveStopped
ManualResetEvent, que é definido quando ReceiveChunkLoop
para. Supondo as ReceiveChunkLoop
paradas dentro do tempo limite especificado, OnClose
as chamadas innerChannel.Close
com o tempo limite restante.
OnAbort
OnAbort
chamadas innerChannel.Abort
para abortar o canal interno. Se houver uma pendente ReceiveChunkLoop
, ela receberá uma exceção da chamada pendente innerChannel.Receive
.
OnFaulted
O ChunkingChannel
não requer comportamento especial quando o canal está com defeito, portanto, OnFaulted
não é substituído.
Implementando o Channel Factory
O ChunkingChannelFactory
é responsável pela criação de instâncias e por transições de ChunkingDuplexSessionChannel
estado em cascata para a fábrica de canais internos.
OnCreateChannel
Usa a fábrica de canais internos para criar um IDuplexSessionChannel
canal interno. Em seguida, ele cria um novo ChunkingDuplexSessionChannel
passando-o este canal interno, juntamente com a lista de ações de mensagem a serem fragmentadas e o número máximo de blocos para buffer após receber. A lista de ações de mensagem a serem fragmentadas e o número máximo de partes a serem armazenadas em buffer são dois parâmetros passados para ChunkingChannelFactory
em seu construtor. A seção em ChunkingBindingElement
descreve de onde vêm esses valores.
O OnOpen
, OnClose
OnAbort
e seus equivalentes assíncronos chamam o método de transição de estado correspondente na fábrica de canal interno.
Implementando o Channel Listener
O ChunkingChannelListener
é um invólucro em torno de um ouvinte de canal interno. Sua principal função, além de delegar chamadas para esse ouvinte do canal interno, é envolver novos ChunkingDuplexSessionChannels
canais aceitos do ouvinte do canal interno. Isto é feito em OnAcceptChannel
e OnEndAcceptChannel
. O recém-criado ChunkingDuplexSessionChannel
é passado o canal interno juntamente com os outros parâmetros descritos anteriormente.
Implementando o elemento de vinculação e a vinculação
ChunkingBindingElement
é responsável pela criação do ChunkingChannelFactory
e ChunkingChannelListener
. O ChunkingBindingElement
verifica se T em<CanBuildChannelFactory
T> e CanBuildChannelListener
<T> é do tipo IDuplexSessionChannel
(o único canal suportado pelo canal de fragmentação) e se os outros elementos de ligação na associação suportam esse tipo de canal.
BuildChannelFactory
<T> primeiro verifica se o tipo de canal solicitado pode ser criado e, em seguida, obtém uma lista de ações de mensagem a serem divididas. Para obter mais informações, consulte a seção a seguir. Em seguida, ele cria uma nova ChunkingChannelFactory
passagem para a fábrica do canal interno (conforme retornado de context.BuildInnerChannelFactory<IDuplexSessionChannel>
), a lista de ações de mensagem e o número máximo de blocos para buffer. O número máximo de partes vem de uma propriedade chamada MaxBufferedChunks
exposed by the ChunkingBindingElement
.
BuildChannelListener<T>
tem uma implementação semelhante para criar ChunkingChannelListener
e passar o ouvinte do canal interno.
Há um exemplo de vinculação incluído neste exemplo chamado TcpChunkingBinding
. Esta ligação consiste em dois elementos de ligação: TcpTransportBindingElement
e ChunkingBindingElement
. Além de expor a MaxBufferedChunks
propriedade, a associação também define algumas das TcpTransportBindingElement
propriedades, como MaxReceivedMessageSize
(define como ChunkingUtils.ChunkSize
+ 100KB bytes para cabeçalhos).
TcpChunkingBinding
também implementa IBindingRuntimePreferences
e retorna true do ReceiveSynchronously
método indicando que apenas as chamadas de recebimento síncronas são implementadas.
Determinando quais mensagens devem ser fragmentadas
O canal de fragmentação fragmenta apenas as mensagens identificadas através do ChunkingBehavior
atributo. A ChunkingBehavior
classe implementa IOperationBehavior
e é implementada chamando o AddBindingParameter
método. Neste método, o ChunkingBehavior
examina o valor de sua AppliesTo
propriedade (InMessage
, OutMessage
ou ambos) para determinar quais mensagens devem ser divididas. Em seguida, ele obtém a ação de cada uma dessas mensagens (da coleção Messages em OperationDescription
) e a adiciona a uma coleção de cadeia de caracteres contida em uma instância de ChunkingBindingParameter
. Em seguida, acrescenta isso ChunkingBindingParameter
ao fornecido BindingParameterCollection
.
Isso BindingParameterCollection
é passado dentro do BindingContext
para cada elemento de ligação na associação quando esse elemento de ligação cria a fábrica de canais ou o ouvinte de canal. A ChunkingBindingElement
implementação do BuildChannelFactory<T>
's e BuildChannelListener<T>
retirar isso ChunkingBindingParameter
do BindingContext'
s BindingParameterCollection
. A coleção de ações contidas no ChunkingBindingParameter
é então passada para o ChunkingChannelFactory
ou ChunkingChannelListener
, que por sua vez passa para o ChunkingDuplexSessionChannel
.
Executando o exemplo
Para configurar, compilar e executar o exemplo
Instale o ASP.NET 4.0 usando o seguinte comando.
%windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
Certifique-se de ter executado o procedimento de instalação única para os exemplos do Windows Communication Foundation.
Para criar a solução, siga as instruções em Criando os exemplos do Windows Communication Foundation.
Para executar o exemplo em uma configuração de máquina única ou cruzada, siga as instruções em Executando os exemplos do Windows Communication Foundation.
Execute Service.exe primeiro, depois execute Client.exe e observe as duas janelas do console para a saída.
Ao executar o exemplo, a saída a seguir é esperada.
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