Canal de agrupamento
O exemplo de ChunkingChannel mostra como um protocolo personalizado ou canal em camadas pode ser usado para fazer a separação em partes e agrupamento das partes de mensagens aleatoriamente grandes.
Ao enviar mensagens grandes usando o WCF (Windows Communication Foundation), costuma ser 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 (supondo que a maior parte dos dados estejam no corpo). No entanto, alguns protocolos exigem o buffer de toda a mensagem. Mensagens confiáveis e segurança são dois exemplos desse tipo. Outra solução possível é dividir a mensagem grande em mensagens menores chamadas de partes, enviar essas partes individualmente e reconstituir a mensagem grande no lado receptor. O próprio aplicativo pode fazer essa separação e agrupamento em partes ou pode usar um canal personalizado para fazer isso.
A separação em partes sempre deve ser empregada somente depois que toda a mensagem a ser enviada tiver sido construída. Um canal de separação em partes sempre deve estar em camadas abaixo de um canal de segurança e de um canal de sessão confiável.
Observação
Os procedimentos de instalação e as instruções de build desse exemplo estão localizadas no final deste tópico.
Suposições e limitações do canal de separação em partes
Estrutura da mensagem
O canal de separação em partes pressupõe a seguinte estrutura para separação das mensagens em partes:
<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 um parâmetro de entrada estão em conformidade com essa forma de mensagem para a mensagem de entrada. Da mesma forma, as operações de contrato que têm um parâmetro de saída ou valor retornado estão em conformidade com essa forma de mensagem para a mensagem de saída. Veja a seguir exemplos dessas 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 separação em partes exige que as mensagens sejam entregues exatamente uma vez, em entrega ordenada de mensagens (partes). Isso significa que a pilha de canais subjacente deve ter uma sessão. As sessões podem ser fornecidas pelo transporte (por exemplo, transporte TCP) ou por um canal de protocolo com sessão (por exemplo, canal ReliableSession).
Envio e recebimento assíncronos
Os métodos de envio e recebimento assíncronos não são implementados nesta versão do exemplo de canal de separação de partes.
Protocolo de separação de partes
O canal de separação de partes define um protocolo que indica o início e o fim de uma série de partes, bem como o número de sequência de cada parte. As três mensagens de exemplo a seguir demonstram as mensagens de início, de parte e de término com comentários que descrevem os principais aspectos 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 de parte
<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 de término
<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 de separação em partes
O canal de separação em partes é um IDuplexSessionChannel
que, em um nível alto, segue a arquitetura típica do canal. Há um ChunkingBindingElement
que pode criar um ChunkingChannelFactory
e um ChunkingChannelListener
. O ChunkingChannelFactory
cria instâncias de ChunkingChannel
quando recebe a solicitação. O ChunkingChannelListener
cria instâncias de ChunkingChannel
quando um novo canal interno é aceito. O ChunkingChannel
em si é responsável por enviar e receber mensagens.
No próximo nível inferior, ChunkingChannel
depende de vários componentes para implementar o protocolo de separação em partes. No lado do envio, o canal usa um XmlDictionaryWriter personalizado chamado ChunkingWriter
que faz a separação em partes. ChunkingWriter
usa o canal interno diretamente para enviar partes. Usar um XmlDictionaryWriter
personalizado 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 do receptor, ChunkingChannel
efetua pull de mensagens do canal interno e as entrega a um XmlDictionaryReader personalizado chamado ChunkingReader
, que reconstitui a mensagem original das partes de entrada. ChunkingChannel
encapsula isso ChunkingReader
em uma implementação Message
personalizada chamada ChunkingMessage
e retorna essa mensagem para a camada acima. Essa combinação de ChunkingReader
e ChunkingMessage
nos permite agrupar as partes do corpo da mensagem original, pois ele está sendo lido pela camada acima, em vez de ter que armazenar em buffer todo o corpo da mensagem original. ChunkingReader
tem uma fila em que armazena em buffer as partes de entrada até um número máximo configurável de partes armazenadas em buffer. Quando esse limite máximo é atingido, o leitor aguarda que as mensagens sejam drenadas da fila pela camada acima (ou seja, apenas lendo do corpo da mensagem original) ou até que o tempo limite máximo de recebimento seja atingido.
Modelo de programação da separação em partes
Os desenvolvedores de serviço podem especificar quais mensagens devem ser separadas em partes aplicando o atributo ChunkingBehavior
às operações dentro do contrato. O atributo expõe uma propriedade AppliesTo
que permite que o desenvolvedor especifique se a separação em partes se aplica à mensagem de entrada, à mensagem de saída ou às duas. O exemplo a seguir mostra o uso do 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);
}
Nesse modelo de programação, ChunkingBindingElement
compila uma lista de URIs de ação que identificam mensagens a serem agrupadas. A ação de cada mensagem de saída é comparada com essa lista para determinar se a mensagem deve ser separada em partes ou enviada diretamente.
Implementação da operação Send
Em um alto nível, a operação Send primeiro verifica se a mensagem de saída deve ser separada em partes e, em caso negativo, envia a mensagem diretamente usando o canal interno.
Se a mensagem precisar ser agrupada, Send criará um novo ChunkingWriter
e chamará WriteBodyContents
na mensagem de saída passando a ela este ChunkingWriter
. Em seguida, o ChunkingWriter
faz a separação em partes da mensagem (incluindo a cópia de cabeçalhos da mensagem original na mensagem de parte inicial) e envia as partes usando o canal interno.
Vale notar alguns detalhes:
Primeiro, Send chama
ThrowIfDisposedOrNotOpened
para garantir queCommunicationState
esteja aberto.O envio é sincronizado para que apenas uma mensagem possa ser enviada por vez para cada sessão. Há um
ManualResetEvent
nomeadosendingDone
que é redefinido quando uma mensagem em partes está sendo enviada. Após o envio da mensagem de parte final, esse evento é definido. O método Send aguarda a definição desse evento antes de tentar enviar a mensagem de saída.Send bloqueia
CommunicationObject.ThisLock
para evitar alterações de estado sincronizadas durante o envio. Confira a documentação CommunicationObject para saber mais sobre estados CommunicationObject e máquina de estado.O tempo limite passado para Send é usado como o tempo limite para toda a operação de envio, que inclui o envio de todas as partes.
O design personalizado XmlDictionaryWriter foi escolhido para evitar o buffer de todo o corpo da mensagem original. Se tivéssemos um XmlDictionaryReader no corpo usando
message.GetReaderAtBodyContents
, todo o corpo seria armazenado em buffer. Em vez disso, temos um XmlDictionaryWriter personalizado que é passado paramessage.WriteBodyContents
. Como a mensagem chama WriteBase64 no gravador, o gravador empacota partes em mensagens e as envia usando o canal interno. WriteBase64 gera um bloqueio até que a parte seja enviada.
Implementação da operação Receive
Em um alto nível, a operação Receive primeiro verifica se a mensagem de entrada não é null
e se a ação dela é ChunkingAction
. Se não atender aos dois critérios, a mensagem retornará inalterada de Receive. Caso contrário, Receive criará um novo ChunkingReader
e um novo ChunkingMessage
encapsulado em torno dele (chamando GetNewChunkingMessage
). Antes de retornar esse novo ChunkingMessage
, Receive usa um thread de threadpool para executar ReceiveChunkLoop
, que chama innerChannel.Receive
em um loop e entrega partes para o ChunkingReader
até que a mensagem da parte final seja recebida ou o tempo limite de recebimento seja atingido.
Vale notar alguns detalhes:
Assim como Send, Receive chama primeiro
ThrowIfDisposedOrNotOpened
para garantir queCommunicationState
esteja Aberto.Receive também é sincronizado para que apenas uma mensagem possa ser recebida por vez da sessão. Isso é especialmente importante porque, depois que uma mensagem de parte inicial é recebida, espera-se que todas as mensagens recebidas na sequência sejam partes dentro dessa nova sequência de partes, até que uma mensagem de parte final seja recebida. O recebimento não poderá efetuar pull de mensagens do canal interno até que todas as partes que pertencem à mensagem que está sendo agrupada sejam recebidas. Para fazer isso, Receive usa um
ManualResetEvent
chamadocurrentMessageCompleted
, que é definido quando a mensagem de parte final é recebida, e redefinida quando uma nova mensagem de parte inicial é recebida.Ao contrário de Send, Receive não impede transições de estado sincronizadas durante o recebimento. Por exemplo, é possível chamar Close durante o recebimento e aguardar 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 Receive é 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 partes de entrada, o
ChunkingReader
armazenará em buffer essas partes de entrada até o limite especificado porChunkingBindingElement.MaxBufferedChunks
. Depois que esse limite for atingido, nenhuma outra parte será extraída da camada inferior até que uma parte armazenada em buffer seja consumida ou o tempo limite de recebimento seja atingido.
Substituições de CommunicationObject
OnOpen
OnOpen
chama innerChannel.Open
para abrir o canal interno.
OnClose
OnClose
primeiro define stopReceive
como true
para sinalizar o ReceiveChunkLoop
pendente para parar. Em seguida, ele aguardará o receiveStopped
ManualResetEvent, que será definido quando ReceiveChunkLoop
parar. Supondo que ReceiveChunkLoop
pare dentro do tempo limite especificado, OnClose
chama innerChannel.Close
com o tempo limite restante.
OnAbort
OnAbort
chama innerChannel.Abort
para anular o canal interno. Se houver um ReceiveChunkLoop
pendente, ele obterá uma exceção da chamada innerChannel.Receive
pendente.
OnFaulted
ChunkingChannel
não exige um comportamento especial quando o canal tem falha, portanto OnFaulted
não é substituído.
Implementação da fábrica de canais
ChunkingChannelFactory
é responsável por criar instâncias de ChunkingDuplexSessionChannel
e para transições de estado em cascata para a fábrica de canais interno.
OnCreateChannel
usa a fábrica de canais interno para criar um canal interno IDuplexSessionChannel
. Em seguida, cria um novo ChunkingDuplexSessionChannel
passando esse canal interno junto com a lista de ações de mensagem a serem separadas em partes e o número máximo de partes a serem armazenadas em buffer no recebimento. A lista de ações de mensagem a serem separadas em partes 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 sobre ChunkingBindingElement
descreve de onde esses valores vêm.
Os OnOpen
, OnClose
OnAbort
e seus equivalentes assíncronos chamam o método de transição de estado correspondente na fábrica de canais interno.
Implementação do ouvinte de canais
ChunkingChannelListener
é um wrapper em torno de um ouvinte de canal interno. Sua função principal, além de delegar chamadas para esse ouvinte de canal interno, é encapsular novos ChunkingDuplexSessionChannels
em torno de canais aceitos do ouvinte de canal interno. Isso é feito em OnAcceptChannel
e OnEndAcceptChannel
. O recém-criado ChunkingDuplexSessionChannel
é passado pelo canal interno junto com os outros parâmetros descritos anteriormente.
Implementação do elemento de associação e da associação
Um ChunkingBindingElement
é responsável por criar o ChunkingChannelFactory
e o ChunkingChannelListener
. O ChunkingBindingElement
verifica se T em CanBuildChannelFactory
<T> e CanBuildChannelListener
<T> é do tipo IDuplexSessionChannel
(o único canal com suporte pelo canal de separação em partes) e se os outros elementos de associação na associação dão suporte a esse tipo de canal.
Primeiro, BuildChannelFactory
<T> verifica se a forma de canal solicitada pode ser criada e, em seguida, obtém uma lista de ações de mensagem a serem separadas em partes. Para saber mais, veja a seção a seguir. Em seguida, ele cria um novo ChunkingChannelFactory
passando-lhe a fábrica de canais internos (conforme retornado de context.BuildInnerChannelFactory<IDuplexSessionChannel>
), a lista de ações de mensagem e o número máximo de partes para buffer. O número máximo de partes vem de uma propriedade chamada MaxBufferedChunks
exposta por ChunkingBindingElement
.
BuildChannelListener<T>
tem uma implementação semelhante para criar ChunkingChannelListener
e passar o ouvinte de canal interno.
Há um exemplo de associação incluído neste exemplo chamado TcpChunkingBinding
. Essa associação é formada por dois elementos de associação: TcpTransportBindingElement
e ChunkingBindingElement
. Além de expor a propriedade MaxBufferedChunks
, a associação também define algumas das propriedades, TcpTransportBindingElement
, tais como MaxReceivedMessageSize
(define como ChunkingUtils.ChunkSize
+ 100 KB para cabeçalhos).
TcpChunkingBinding
também implementa IBindingRuntimePreferences
e retorna true do método ReceiveSynchronously
que indica que apenas as chamadas síncronas de Receive são implementadas.
Determinar quais mensagens separar em partes
O canal de separação em partes separa apenas as mensagens identificadas por meio do atributo ChunkingBehavior
. A classe ChunkingBehavior
implementa IOperationBehavior
e é implementada chamando o método AddBindingParameter
. Nesse método, ChunkingBehavior
examina o valor de sua propriedade AppliesTo
(InMessage
OutMessage
ou ambos) para determinar quais mensagens devem ser separadas em partes. Em seguida, obtém a ação de cada uma dessas mensagens (da coleção Messages em OperationDescription
) e a adiciona a uma coleção de cadeias de caracteres contida em uma instância do ChunkingBindingParameter
. Em seguida, ele adiciona isso ChunkingBindingParameter
ao BindingParameterCollection
fornecido.
Isso BindingParameterCollection
é passado dentro do BindingContext
para cada elemento de associação na associação quando esse elemento de associação cria a fábrica de canais ou o ouvinte do canal. A implementação de ChunkingBindingElement
de BuildChannelFactory<T>
e BuildChannelListener<T>
efetua pull desse ChunkingBindingParameter
dos BindingContext'
s BindingParameterCollection
. Em seguida, a coleção de ações contidas no ChunkingBindingParameter
é 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
Verifique se você executou o Procedimento de instalação única para os exemplos do Windows Communication Foundation.
Para compilar a solução, siga as instruções contidas em Como compilar as amostras do Windows Communication Foundation.
Para executar a amostra em uma configuração de computador único ou entre computadores, siga as instruções contidas em Como executar as amostras do Windows Communication Foundation.
Execute o Service.exe primeiro, execute Client.exe em seguida e observe as duas janelas do console para obter 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