Desenvolvimento - Visual Basic .NET

WCF - Transferência e Codificação de Dados

O WCF - Windows Communication Foundation - fornece algumas técnicas e alternativas que podemos adotar durante a configuração ou criação de um determinado serviço. A finalidade deste artigo é mostrar como implementar tal configuração/criação e analisar os impactos (inclusive a nível de contrato), benefícios e restrições de cada uma destas técnicas.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Baixe o exemplo: WCFTransfCodDados.zip

Um dos maiores benefícios que serviços da Web tem em relação à tecnologias de comunicação distribuídas é o uso de XML como base da codificação, permitindo assim, a interoperabilidade entre as mais diversas plataformas. Entretanto, a escolha do padrão de codificação e a forma de transferência destas informações a serem adotados pelo serviço influenciará diretamente na performance e interoperabilidade do mesmo e também daqueles que o consomem.

O WCF - Windows Communication Foundation - fornece algumas técnicas e alternativas que podemos adotar durante a configuração ou criação de um determinado serviço. A finalidade deste artigo é mostrar como implementar tal configuração/criação e analisar os impactos (inclusive a nível de contrato), benefícios e restrições de cada uma destas técnicas.

Quando desenhamos um contrato para um serviço, definimos alguns métodos para satisfazer as regras de negócio que ele se propõe a fazer. Por padrão, para cada método criado, o WCF cria duas versões do método (mais popularmente conhecido como mensagens), uma para requisição (Request) e outro para resposta (Response). Cada mensagem é representada por um envelope SOAP que, por sua vez, contém o cabeçalho (header) e corpo (body). Enquanto o cabeçalho traz informações a respeito da segurança, correlação, contexto, etc., o corpo da mensagem é responsável por conter os dados que estão sendo enviados para um serviço ou quando a resposta de um serviço está sendo retornada para o cliente.

As informações contidas no cabeçalho podem ser utilizadas pelo destinatário final bem como por algum intermediário, caso exista. Já o conteúdo do corpo da mensagem, por conversão, deve ser utilizado pelo último recebedor, ou melhor, o destinatário final, independente de quantos firewalls ou roteadores existam.

Quando temos um contrato que expõe métodos que recebem ou devolvem tipos de dados simples, como strings, números ou até mesmo objetos complexos, mas que disponibiliza propriedades também simples, não vemos muitos problemas em termos de performance. O problema começa a aparecer quando o serviço que estamos desenhando irá aceitar um conteúdo mais "pesado", como é o caso de arquivos, imagens, multimídia, etc. Neste caso, se não nos atentarmos a alguns detalhes, podemos sofrer grandes penalidades na performance do serviço.

O padrão Text (texto estruturado) consiste em codificar os dados em um formato legível e, sendo assim, é possível ler o seu conteúdo em qualquer editor de texto, e qualquer sistema será capaz de processar tal texto. Com o padrão Text, além de cada item estar envolvido por um elemento ou atributo, dados "não-texto" serão convertidos para texto antes de serem enviados, contribuindo para aumentar o tamanho das mensagens que trafegam pela rede. Para um serviço mais simples, o overhead é mínimo. Mas e quando precisamos transferir grandes quantidades de dados, como por exemplo: imagens, multimídia, etc. (representados por array de bytes)?

O comportamento atual do WCF faz com que ele aplique a codificação Base64 a um conteúdo binário se desejarmos continuar utilizando o formato Text, resultando assim em um texto mais compactado mas, mesmo assim, ainda será um conteúdo muito maior em relação ao seu tamanho original. Além disso, o parser do XML não trabalha muito bem com conteúdos extensos, o que poderá acarretar em problemas durante o runtime.

Em situações onde o conteúdo binário é uma exigência e não há necessidade de interoperabilidade (onde cliente e servidor fazem uso do WCF), podemos recorrer à utilização da codificação binária, que é utilizada em todos os bindings para uso em ambiente homogêneo. Falaremos mais sobre os tipos de codificação e os bindings na seção Encodings, um pouco mais abaixo.

Por último, temos ainda uma terceira alternativa que é o MTOM (Message Transmission Optimization Mechanism), que é o mesmo padrão utilizado quando enviamos a um e-mail um conteúdo binário. Este padrão aberto, regido pelo W3C, ao invés de codificar o conteúdo binário como Text, ele é enviado de forma intacta como sendo um "anexo" da mensagem, seguindo a especificação do padrão MIME (Multipurpose Internet Mail Extension). Assim como antes, qualquer texto da mensagem original será codificado como um XML Infoset, mas o conteúdo binário será representado como uma referência, apontando para o anexo MIME. Quando a mensagem chega ao seu destino, o runtime do WCF é capaz de reconstituir a mensagem original a partir do envelope SOAP e seus respectivos "anexos".

Quando fazemos o uso do MTOM, o WCF é capaz de determinar o tamanho da mensagem e, caso ela seja pequena demais, ao invés de fazer o uso do MTOM, ele opta por codificar a mensagem utilizando a codificação Base64. Já quando o conteúdo é grande e o WCF vê que terá benefícios com o uso do WCF, ele criará uma nova seção MIME e colocará o conteúdo binário lá e, no envelope SOAP, teremos uma referência para tal conteúdo. A imagem abaixo ilustra o uso do MTOM:

Figura 1 - O MTOM define externamente o conteúdo binário.

Encodings

Um encoding dentro do WCF define as regras de como será realizada a codificação dos dados, ou melhor, da mensagem. As mensagens (Request e Response) são representadas dentro da infraestrutura do WCF através de uma classe chamada Message. Temos um outro elemento importante dentro do runtime do WCF, chamado encoder, que nada mais é que uma implementação de um determinado encoding e que fará tarefas distintas dependendo do momento, a saber: do lado do remetente, o encoder é responsável por recuperar a instância da classe Message, transformá-la em um stream de bytes e enviá-la para o destino através da rede; já do lado do destinatário, o encoder captura a seqüência de bytes da rede e a transforma em uma instância da classe Message novamente, para que a mesma possa ser processada.

Todos os padrões de codificação (encoding) que falamos acima estão fortemente relacionados ao binding e, sendo assim, isso se trata de uma configuração que pode ser realizada sem interferir na criação ou implementação do contrato. Cada um dos bindings pré-definidos dentro do .NET Framework já possui um encoding específico. A tabela abaixo ilustra as quatro opções (classes) que temos e que falamos acima:

Encoding Descrição
TextMessageEncodingBindingElement

Este encoding é o padrão para todos os bindings baseados em HTTP e também é o apropriado para todos os customizados que você venha a criar e que exigem interoperabilidade. Lembrando que este encoding lê e escreve o conteúdo baseando-se no formato SOAP 1.1/1.2, em formato Text, sem nenhum tratamento específico para conteúdo binário.

MtomMessageEncodingBindingElement

Este encoding fornece uma customização importante para o conteúdo binário e não é utilizado por padrão em nenhum binding.

BinaryMessageEncodingBindingElement

Este encoding é o padrão para todos os bindings Net* e é a escolha ideal para quando a comunicação estiver envolvendo o WCF em ambas as partes (cliente e servidor). Este encoding utiliza o formato .NET Binary XML, que é uma representação binária da Microsoft para XML Infosets.

WebMessageEncodingBindingElement *

Este encoding habilita a codificação em formato plain-text XML e JSON (JavaScript Object Notation). Este tipo de encoding é comumente utilizado quando necessitamos expor o serviço para consumo via POX, REST, RSS ou AJAX.

Você deve avaliar cuidadosamente qual destes encodings escolher. É importante lembrar que se você precisar um combinado destes detalhes, como por exemplo, suportar interoperabilidade e boa performance, você pode expor diferentes endpoints, onde cada um deles é específico para cada uma dessas características.

Todas essas classes estão contidas dentro do namespace System.ServiceModel.Channels, herdando de uma classe base chamada MessageEncodingBindingElement. A hierarquia dos encodings está sendo exibida através da imagem abaixo:

Figura 2 - Hierarquia dos encodings.

A tabela abaixo ilustra o encoding correspondente (padrão) para cada um dos bindings existentes dentro do .NET Framework:

Binding Encoding
BasicHttpBinding

TextMessageEncodingBindingElement

WebHttpBinding *

WebMessageEncodingBindingElement *

WSDualHttpBinding

TextMessageEncodingBindingElement

WSFederationHttpBinding

TextMessageEncodingBindingElement

WSHttpBinding

TextMessageEncodingBindingElement

NetTcpBinding

BinaryMessageEncodingBindingElement

NetPeerTcpBinding

BinaryMessageEncodingBindingElement

NetNamedPipeBinding

BinaryMessageEncodingBindingElement

NetMsmqBinding

BinaryMessageEncodingBindingElement


* Tipos contidos dentro do Assembly System.ServiceModel.Web.dll que está disponível a partir do .NET Framework 3.5.

Agora que já sabemos quais são os encodings definidos como padrão para cada um dos bindings existentes, precisamos entender como devemos proceder para customizar e/ou alterar os encodings dos bindings existentes. Todo binding herda direta ou indiretamente de uma classe base denominada Binding. Essa classe fornece um método, também abstrato, chamado CreateBindingElements, que fornece como retorno uma coleção do tipo BindingElementCollection. A finalidade deste método, quando sobrescrito nas classes derivadas (BasicHttpBinding, NetPeerTcpBinding, etc.), é retornar uma coleção contendo todos os elementos (segurança, transação, codificação, etc.) que compõem o binding, devidamente configurados e na ordem correta.

Para alternarmos entre padrão de codificação Text ou MTOM, podemos recorrer à propriedade MessageEncoding, que aceita como parâmetro um dos itens especificados pelo enumerador WSMessageEncoding. O trecho de código abaixo ilustra como efetuar a configuração de um determinado binding para suportar o formato MTOM.

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

BasicHttpBinding b = new BasicHttpBinding();
b.MessageEncoding = WSMessageEncoding.Mtom;
Imports System
Imports System.ServiceModel
Imports System.ServiceModel.Channels

Dim b As New BasicHttpBinding()
b.MessageEncoding = WSMessageEncoding.Mtom
C# VB.NET

Observação Importante: A propriedade MessageEncoding está disponível apenas para bindings que suportam interoperabilidade, como é o caso dos bindings HTTP. Como dissemos acima, os outros bindings são úteis e tem uma melhor performance em ambiente totalmente homogêneo.

A customização na codificação que fizemos através do código acima também é suportada à nível de configuração. Para cada um dos encodings há um elemento correspondente que podemos utilizar durante a configuração do binding no arquivo de configuração da aplicação. Os elementos são: <textMessageEncoding />, <mtomMessageEncoding />, <binaryMessageEncoding /> e <webMessageEncodingElement />. É importante lembrar que o mesmo tipo de codificação terá que ser utilizado em ambos os lados, caso contrário, uma exceção do tipo ProtocolException será atirada.

Para finalizar, você pode recorrer à técnica de logging (já abordada neste artigo) para que você consiga visualizar/interceptar a geração ou leitura do conteúdo da mensagem e analisar a sua codificação. A imagem abaixo ilustra o mesmo conteúdo sendo trafegado em um formato normal (Text) e, em seguida, fazendo o uso do MTOM.

Figura 3 - Diferenciando o padrão de codificação utilizado.

Formas de Transferência

Uma vez que determinamos qual será o binding e o encoding a serem utilizados pelo serviço, precisamos agora entender as possíveis formas de transferência de dados e como incorporá-las ao serviço. É importante dizer que as funcionalidades aqui abordadas não são válidas para todos os bindings e que, no decorrer desta seção, veremos como e onde podemos aplicar essas técnicas.

O WCF suporta quatro modos de transferência de mensagens (opções do enumerador TransferMode):

  • Buffered: Com esta opção, a mensagem é carregada totalmente na memória antes de ser enviada ao destinatário. Esta opção é a configuração padrão para todos os bindings existentes dentro do .NET Framework.

  • Streamed: Já com a opção Streamed, somente o header da mensagem será carregado em memória (não devendo ultrapassar a quantidade máxima especificada na propriedade MaxBufferSize), enquanto o body será definido como um stream, permitindo que pequenas partes de um conteúdo sejam lidas por vez. Essa configuração afetará tanto a requisição quanto a resposta.

  • StreamedRequest: Apenas aplicará o stream em uma requisição, enquanto a resposta será colocada em buffer. É uma boa opção apenas quando aceitamos Stream como parâmetro, em operações one-way.

  • StreamedResponse: Apenas aplicará o stream em uma resposta, enquanto a requisição será colocada em buffer. Esta opção é útil apenas quando o retorno do método devolve um Stream.

Para efetuar tal configuração, devemos recorrer ao uso da propriedade TransferMode que, por sua vez, aceita uma das quatro opções contidas no enumerador TransferMode. Essa propriedade está apenas exposta em bindings que suportam a técnica de streaming, e são eles: BasicHttpBinding, NetTcpBinding e NetNamedPipeBinding. O motivo de apenas estes bindings suportarem o streaming é que há algumas regras funcionais que devem ser explicitamente seguidas, como por exemplo, assinaturas digitais são definidas e computadas em cima do conteúdo da mensagem e, com a opção de stream habilitada, o conteúdo não estará totalmente à disposição para executar a tarefa de verificação da mesma. Como a opção Buffered é a padrão e não necessita nenhuma mudança, não há o que falar sobre ela a não ser como o WCF a trata internamente, como já vimos acima.

Além de restrições funcionais impostas pelo modo Streamed, ainda há algumas regras que precisamos seguir para conseguir expor um serviço como stream, e essas regras envolvem a mudança em como desenhamos o contrato de serviço. A idéia é que os métodos que irão compor o contrato devem receber ou retornar uma instância da classe Stream, contida dentro do namespace System.IO; isso irá depender se o serviço irá retornar algum conteúdo binário para o cliente ou se o cliente enviará algum conteúdo binário para o serviço.

Nestes casos (uso da classe Stream), o parâmetro ou o retorno de um método (operação) corresponderá ao corpo da mensagem. Para exemplificar o uso desta técnica em nossos serviços, podemos criar um contrato (Interface) que possui dois métodos: Upload e Download, onde recebe e retorna uma instância da classe Stream, respectivamente:

using System;
using System.IO;
using System.ServiceModel;

[ServiceContract]
public interface IImagens
{
    [OperationContract]
    Stream Download(string imagemId);

    [OperationContract]
    string Upload(Stream imagem);
}
Imports System
Imports System.IO
Imports System.ServiceModel

<ServiceContract()> _
Public Interface IImagens
    <OperationContract()> _
    Function Download(ByVal imagemId As String) As Stream

    <OperationContract()> _
    Function Upload(ByVal imagem As Stream) As String
End Interface
C# VB.NET

Como podemos notar, a forma como desenhamos o contrato muda drasticamente. A sua implementação terá que saber como lidar com o Stream; no caso do método Download a idéia é, dado uma string que representa o nome do arquivo, devemos capturá-lo e retornar o mesmo através do método; já no método Upload, recebem o Stream que corresponde à imagem e salvamos a mesma no disco e retornamos o nome que é randomicamente gerado.

Observação: Por questões de espaço, a implementação do contrato não será exibida aqui, mas está contida dentro do projeto de exemplo, relacionado ao artigo.

Como mostrado e comentado acima, você terá o Stream como sendo o body da mensagem. Mas e se, em algum momento, quisermos passar alguma informação extra? Como o body é exclusivamente reservado para o conteúdo do Stream, podemos recorrer aos headers da mensagem para acomodar tais informações. Mais uma vez, isso mudará a forma como criamos o contrato, necessitando especificar, declarativamente, o contrato da mensagem, informando os headers adicionais e o corpo da mensagem, que continuará sendo o Stream. O trecho de código ilustra como devemos proceder para a criação deste contrato:

using System;
using System.IO;
using System.ServiceModel;

[MessageContract]
public class StreamData
{
    [MessageHeader]
    public string NomeDoArquivo;

    [MessageBodyMember]
    public Stream Conteudo;
}
Imports System
Imports System.IO
Imports System.ServiceModel

<MessageContract()> _
Public Class StreamData

    <MessageHeader()> _
    Public NomeDoArquivo As String

    <MessageBodyMember()> _
    Public Conteudo As Stream
End Class
C# VB.NET

No exemplo acima, o atributo MessageContractAttribute define que uma classe irá corresponder à mensagem SOAP que será trafegada; já o atributo MessageHeaderAttribute especifica um membro que fará parte do header da mensagem e, finalmente, o atributo MessageBodyMemberAttribute determinará o membro que será o corpo da mensagem. Com isso, a Interface que representa o contrato do serviço também sofrerá mudanças: ao invés dela lidar diretamente com Streams, ela receberá e retornará instâncias da classe StreamData. O código abaixo ilustra uma Interface bem semelhante a que utilizamos acima (IImagens), mas agora com essa nova técnica:

using System;
using System.IO;
using System.ServiceModel;

[ServiceContract]
public interface IArquivos
{
    [OperationContract]
    StreamData Download();

    [OperationContract]
    void Upload(StreamData dados);
}
Imports System
Imports System.IO
Imports System.ServiceModel

<ServiceContract()> _
Public Interface IArquivos
    <OperationContract()> _
    Function Download() As StreamData

    <OperationContract()> _
    Sub Upload(ByVal dados As StreamData)
End Interface
C# VB.NET

Observação: Mais uma vez, por questões de espaço, a implementação do contrato não será exibida aqui, mas está contida dentro do projeto de exemplo, relacionado ao artigo.

Independente da técnica que você utilize durante a criação do contrato, um detalhe muito importante e que precisa ser comentado é em relação ao fechamento do Stream que trafega de um lado para outro. Quem será responsável por fechá-lo? Geralmente o Stream será enviado tanto do cliente para o servidor, quanto do servidor para o cliente. Quem envia o Stream não deve fechá-lo; o runtime do WCF fará isso. Já do lado de quem receberá o Stream, deverá fechá-lo depois que utilizá-lo.

Um último detalhe: quando estamos operando com dados que excedem a configuração padrão para envio e recebimento de informações, precisamos nos atentar para especificar um valor que consiga acomodar as informações que serão trocadas. Os bindings relacionados com esta técnica possuem duas propriedades que afetam diretamente o Streamed, sendo elas: MaxBufferSize e MaxReceivedMessageSize. Como essas configurações não estão contempladas dentro do contrato (WSDL) do serviço, então fica a cargo do desenvolvedor especificar um valor, diferente do padrão, caso queira aceitar um conteúdo maior do que o especificado.

O formato Streamed causa dependência de plataforma e, conseqüentemente, não é interoperável como o MTOM. Além disso, tem restrições à nível de contrato, está restrito a ser utilizado por apenas três bindings e a segurança poderá ser somente garantida pelo transporte. Já como ponto positivo, temos um consumo de memória menor e também uma melhor performance em virtude da natureza do envio. Pensando nestas características, é provável que você já consiga saber se deve ou não optar pelo uso dele.

Serialização

É importante dizer que o processo de serialização é independente da transferência (encoding). A serialização é um processo que define como os objetos serão mapeados para XML Infosets, enquanto o encoding determinará como estes XML Infosets serão escritos (em bytes) e enviados para algum dos encodings que vimos acima e que, por sua vez, ocorre em nível de transporte.

Um outro ponto importante é em relação ao contrato. Independentemente do encoding escolhido, ele não acarretará em nenhuma mudança no contrato do serviço, ao contrário do processo de serialização que, por sua vez, poderá influenciar na criação do mesmo. Como a serialização é um processo importante e um pouco extenso, ela não será abordada neste artigo.

Conclusão: Como pudemos notar no decorrer do artigo, o WCF desacopla o máximo possível a implementação da execução, permitindo que o contrato não tenha conhecimento à respeito de como o serviço está sendo exposto e consumido. Mas, em alguns casos, podemos mudar o comportamento padrão, não se resume a apenas mudar uma configuração mas sim a mudança da Interface que determina o contrato do serviço. É importante analisar cuidadosamente cada um dos encodings e os modos de transferência para saber qual deles se encaixa melhor em sua solução.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.