Redes do Windows 8

Windows 8 e o protocolo WebSocket

Kenny Kerr

 

A meta do protocolo WebSocket é fornecer comunicação bidirecional em um mundo saturado pela Web e dominado por clientes exclusivamente responsáveis por estabelecer conexões e iniciar pares de solicitação/resposta. Por fim, esse protocolo permite que aplicativos aproveitem muito mais benefícios do TCP, mas de uma maneira amigável à Web. Considerando que o protocolo WebSocket foi padronizado pela Internet Engineering Task Force apenas em dezembro de 2011 (e enquanto escrevo este artigo, ele ainda está passando pelo crivo da World Wide Web Consortium), talvez seja uma surpresa ver a forma abrangente com que o Windows 8 adotou essa nova tecnologia de Internet.

Neste artigo, primeiramente, vou mostrar como o protocolo WebSocket funciona e explicar sua relação com o pacote maior de TCP/IP. Em seguida, vou explorar as várias maneiras pelas quais o Windows 8 permite aos programadores adotar, com facilidade, essa nova tecnologia de dentro dos respectivos aplicativos.

Por que o WebSocket?

A principal meta desse protocolo é fornecer uma maneira padrão e eficiente para que seus aplicativos baseados em navegador se comuniquem com servidores livremente fora dos pares de solicitação/resposta. Alguns anos atrás, os desenvolvedores da Web estavam radiantes falando do AJAX (Asynchronous JavaScript and XML) e em como ele permite cenários dinâmicos e interativos - e, certamente, permitiu, mas o objeto XMLHttpRequest que o inspirou permitia apenas que o navegador fizesse solicitações HTTP. E se o servidor quisesse enviar uma mensagem ao cliente fora da banda? É aí que entra o protocolo WebSocket. Além de permitir que o servidor envie mensagens ao cliente, ele faz isso sem a sobrecarga do HTTP, fornecendo comunicação bidirecional que se aproxima da velocidade de uma conexão TCP raw. Sem o protocolo WebSocket, os desenvolvedores da Web tinham que abusar do HTTP, pesquisando o servidor em busca de atualizações, usando técnicas de programação estilo Comet e empregando muitas conexões HTTP com uma grande sobrecarga de protocolos apenas para manter os aplicativos atualizados. Os servidores eram sobrecarregados, a largura de banda era desperdiçada e os aplicativos Web eram excessivamente complicados. O protocolo WebSocket resolve esses problemas de uma maneira surpreendentemente simples e eficiente, mas antes de descrever como ele funciona, preciso fornecer algum contexto histórico e elementar.

O pacote TCP/IP

O TCP/IP é um pacote de protocolo, ou conjunto de protocolos inter-relacionados, que implementa a arquitetura de Internet. Ele evoluiu para sua forma atual ao longo de muitos anos. O mundo mudou radicalmente desde a década de 60, quando foi desenvolvido o primeiro conceito de redes com comutação de pacotes. Os computadores tornaram-se muito mais rápidos, o software cresceu mais do que a demanda e a Internet explodiu em uma Web completa de informações, comunicação e interação, que sustenta muitos softwares de uso popular de hoje.

O pacote TCP/IP consiste em várias camadas imprecisamente modeladas depois do modelo em camadas OSI (Interconexão de Sistemas Abertos). Embora os protocolos nas diferentes camadas não fossem particularmente bem delineadas, o TCP/IP provou claramente sua eficácia, e os problemas de camada foram superados por uma engenhosa combinação de designs de hardware e software. Separar o TCP/IP em camadas, por mais vagas que elas pudessem ser, ajudou-o a evoluir ao longo do tempo, à medida que o hardware e a tecnologia mudavam, e permitiu que os programadores com diferentes habilidades trabalhassem em diferentes níveis de abstração, seja ajudando a criar a pilha de protocolo em si ou a criar aplicativos fazendo uso de seus vários recursos.

Nas camadas mais baixas estão os protocolos físicos, incluindo as preferências do controle de acesso à mídia com fio e Wi-Fi, fornecendo conectividade física, bem como endereçamento local e detecção de erro. A maioria dos programadores não pensa muito sobre esses protocolos.

Movendo a pilha para cima, o IP em si reside na camada de rede e permite que o TCP/IP torne-se interoperável pelas diferentes camadas físicas. Ele cuida do mapeamento dos endereços do computador para endereços físicos e pacotes de roteamento, de computador para computador.

Assim, há protocolos auxiliares, e poderíamos debater em qual camada eles residem, mas eles realmente fornecem uma função de suporte necessária para tarefas como configuração automática, resolução de nome, detecção, otimizações de roteamento e diagnóstico.

Conforme subimos pela pilha de camadas, os protocolos de aplicativo e transporte vão se apresentando. Os protocolos de transporte cuidam da multiplexação e da demultiplexação dos pacotes das camadas inferiores, de modo que, mesmo que possa haver somente uma única camada de rede física, muitos aplicativos diferentes podem compartilhar o canal de comunicação. Geralmente, a camada de transporte fornece mais detecção de erro, entrega confiável e, até mesmo, recursos relacionados aos desempenho, como controle de fluxo e congestionamento. A camada de aplicativo, tradicionalmente, tem sido a casa dos protocolos, como HTTP (implementado pelos servidores e navegadores da Web) e SMTP (implementado por servidores e clientes de email). À medida que o mundo começou a depender cada vez mais de protocolos como o HTTP, a implementação deles foi colocada para dentro da profundidade do sistema operacional, tanto para aumentar o desempenho, como para compartilhar a implementação entre diferentes aplicativos.

TCP e HTTP

Dentre os protocolos no pacote TCP/IP, o TCP e o UDP encontrados na camada de transporte são, talvez, os mais conhecidos pela média de programadores. Ambos definem uma abstração de "porta" que usam em combinação com endereços IP para multiplexar e demultiplexar pacotes à medida que eles chegam e quando são enviados.

Embora o UDP seja usado intensivamente para outros protocolos TCP/IP, como o DHCP e DNS, e tenha sido amplamente adotado para aplicativos de rede privada, sua adoção na Internet, no geral, não tem sido tão extensa quanto seu parceiro. O TCP, por outro lado, tem sido adotado extensamente por todos, graças, em grande parte, ao HTTP. Embora o TCP seja muito mais complexo que o UDP, muito dessa complexidade fica oculta da camada de aplicativo, onde o aplicativo aproveita os benefícios do TCP sem estar sujeito à sua complexidade.

O TCP fornece um fluxo de dados confiável entre computadores, cuja implementação é imensamente complexa. Ele se preocupa com a ordenação do pacote e reconstrução de dados, detecção de erros e recuperação, controle de congestionamento e desempenho, tempos limite, retransmissões e muito mais. No entanto, o aplicativo vê apenas uma conexão bidirecional entre portas e supõe que os dados enviados e recebidos serão transferidos corretamente e na ordem.

O HTTP moderno pressupõe um protocolo orientado por conexão confiável, e o TCP é claramente a opção óbvia e generalizada. Nesse modelo, o HTTP funciona como um protocolo de cliente/servidor. O cliente abre uma conexão TCP com um servidor. Em seguida, ele envia uma solicitação, a qual o servidor avalia e responde. Esse ciclo é repetido incontavelmente a cada segundo, todos os dias no mundo todo.

Obviamente, essa é uma simplificação ou restrição da funcionalidade fornecida pelo TCP. O TCP permite que ambas as partes enviem dados simultaneamente. Um não precisa esperar que o outro envie uma solicitação para que possa respondê-la. No entanto, essa simplificação permitiu o armazenamento em cache de respostas do lado do servidor, o que teve um enorme impacto na capacidade de dimensionamento da Web. Porém, a popularidade do HTTP foi indubitavelmente auxiliada pela sua simplicidade inicial. Enquanto o TCP fornece um canal bidirecional para dados binários (um par de transmissões, se preferir), o HTTP fornece uma mensagem de solicitação que antecede uma mensagem de resposta, ambas consistindo em caracteres ASCII, embora os corpos das mensagens, se houver, possam ser codificados de alguma outra forma. Uma solicitação simples pode ter esta aparência:

GET /resource HTTP/1.1\r\n
host: example.com\r\n
\r\n

Cada linha é concluída com um caractere de retorno de carro (\r) e alimentação de linha (\n). A primeira linha, chamada de linha de solicitação, especifica o método pelo qual um recurso deve ser acessado (nesse caso GET), o caminho do recurso e, por fim, a versão do HTTP a ser usado. Da mesma forma que os protocolos de camada inferior, o HTTP fornece multiplexação e demultiplexação por esse caminho de recurso. Seguindo essa linha de solicitação, há uma ou mais linhas de cabeçalho. Os cabeçalhos consistem em um nome e um valor, conforme ilustrado no exemplo anterior. Alguns cabeçalhos são necessários, como host, enquanto a maioria não e, simplesmente, auxiliam os navegadores e servidores na comunicação mais eficiente ou negociam recursos e funcionalidades.

Uma resposta pode ter esta aparência:

HTTP/1.1 200 OK\r\n
content-type: text/html\r\n
content-length: 1307\r\n
\r\n
<!DOCTYPE HTML><html> ... </html>

O formato, basicamente, é o mesmo, mas em vez de uma linha de solicitação, a linha de resposta informa a versão do HTTP a ser usado, um código de status (200) e uma descrição do código de status. O código de status 200 indica ao cliente que a solicitação foi processada com êxito e que qualquer resultado será incluído imediatamente após as linhas de cabeçalho. O servidor pode, por exemplo, indicar que o recurso solicitado não existe, retornando um código de status 404. Os cabeçalhos têm a mesma forma que têm na solicitação. Nesse caso, o cabeçalho do tipo de conteúdo informa ao navegador que o recurso solicitado no corpo da mensagem deve ser interpretado como HTML e o cabeçalho do comprimento do conteúdo informa ao navegador quantos bytes o corpo da mensagem contém. Isso é importante porque, à medida que você faz nova chamada, as mensagens HTTP fluem pelo TCP, que não fornece limites de mensagem. Sem um comprimento de conteúdo, os aplicativos HTTP precisam usar várias heurísticas para determinar o comprimento do corpo de qualquer mensagem.

Isso tudo é bastante simples, um testamento para o design direto do HTTP. Mas o HTTP deixou de ser simples. Os servidores e navegadores da Web de hoje são programas tecnologicamente avançados, com milhares de recursos inter-relacionados, e o HTTP é o "burro de carga" que precisa corresponder a tudo isso. Muito da complexidade surgiu da necessidade de velocidade. Hoje, há cabeçalhos para negociar a compactação do corpo da mensagem, cabeçalhos de expiração e cache para evitar transmissão do corpo de uma mensagem, e muito mais. As técnicas foram desenvolvidas para reduzir o número de solicitações HTTP combinando diferentes recursos. As CDNs (redes de fornecimento de conteúdo) têm sido distribuídas pelo mundo em uma tentativa de hospedar recursos frequentemente acessados mais próximos dos navegadores da Web que os acessam.

Apesar de todos esses avanços, muitos aplicativos Web poderiam atingir maior escalabilidade e, até mesmo, simplicidade se houvesse alguma maneira de interromper ocasionalmente o HTTP e retornar para o modelo de streaming do TCP. Isso é exatamente o que o protocolo WebSocket fornece.

O handshake do WebSocket

O protocolo WebSocket se encaixa até que harmoniosamente no pacote TCP/IP, acima do TCP e ao lado do HTTP. Um dos desafios da introdução de um novo protocolo na Internet é, de alguma forma, fazer com que os incontáveis roteadores, proxies e firewalls pensem que nada mudou. O protocolo WebSocket atinge sua meta ao se disfarçar de HTTP antes de alternar para a própria transferência de dados na mesma conexão TCP subjacente. Dessa maneira, muitos intermediários insuspeitos não precisam ser atualizados para permitir que a comunicação WebSocket percorra suas conexões de rede. Na prática, isso nem sempre funciona tão perfeitamente, pois alguns roteadores excessivamente zelosos remexem nas solicitações e respostas HTTP, tentando reescrevê-las para adequá-las às próprias finalidades, como cache de proxy, ou conversão de recurso ou endereço. Uma solução efetiva de curto prazo é usar o protocolo WebSocket por um canal seguro, TLS, pois isso tende a manter a violação em nível mínimo.

O protocolo WebSocket pega emprestadas as ideias de diversas fontes, incluindo, IP, UDP, TCP e HTTP, e disponibiliza esses conceitos aos navegadores da Web e a outros aplicativos de uma forma mais simples. Ele inicia tudo com um handshake que foi desenvolvido para se parecer e operar como um par de solicitação/resposta HTTP. Isso não foi feito para que os clientes ou servidores possam de alguma forma enganar uns aos outros quanto ao uso de WebSockets, mas para enganar os vários intermediários quanto a pensar que é apenas outra conexão TCP atendendo ao HTTP. Na verdade, o protocolo WebSocket foi especificamente desenvolvido para impedir que alguma das partes seja enganosamente levada a aceitar uma conexão acidentalmente. Ele começa com um cliente enviando um handshake, isto é, para todas as intenções e finalidades, uma solicitação HTTP, que pode se parecer com esta:

GET /resource HTTP/1.1\r\n
host: example.com\r\n
upgrade: websocket\r\n
connection: upgrade\r\n
sec-websocket-version: 13\r\n
sec-websocket-key: E4WSEcseoWr4csPLS2QJHA==\r\n
\r\n

Como você pode ver, nada o impede de ser uma solicitação HTTP perfeitamente válida. Um intermediário insuspeito deve simplesmente passar essa solicitação ao longo do servidor, que pode até mesmo ser um servidor HTTP sendo duplicado como um servidor WebSocket. A linha de solicitação neste exemplo especifica uma solicitação GET padrão. Isso também significa que um servidor WebSocket pode permitir que vários pontos de extremidade sejam atendidos por um único servidor da mesma maneira que faz a maioria dos servidores HTTP. O cabeçalho de host é exigido pelo HTTP 1.1 e serve para a mesma finalidade - garantir que as partes concordem em hospedar o domínio em cenários de hospedagem compartilhada. Os cabeçalhos de atualização e conexão também são cabeçalhos HTTP padrão usados pelos clientes para solicitar uma atualização do protocolo usado na conexão. Às vezes, essa técnica é usada pelos clientes HTTP para fazer a transição para uma conexão TLS protegida, embora isso seja raro. No entanto, esses cabeçalhos são exigidos pelo protocolo WebSocket. Especificamente, o cabeçalho de atualização indica que a conexão deve ser atualizada para o protocolo WebSocket e o cabeçalho de conexão especifica que esse cabeçalho de atualização é específico da conexão, o que significa que ele não deve ser comunicado pelos proxies por outras conexões.

O cabeçalho sec-websocket-version deve ser incluído e seu valor deve ser 13. Se o servidor for um servidor WebSocket, mas não oferecer suporte a essa versão, ele anulará o handshake, retornando um código de status HTTP adequado. Como você verá daqui a pouco, mesmo se o servidor não souber nada sobre o protocolo WebSocket e, felizmente, retornar uma resposta bem-sucedida, o cliente será designado a anular a conexão.

O cabeçalho sec-websocket-key realmente é essencial para o handshake do WebSocket. Os designers do protocolo WebSocket quiseram garantir que um servidor não pudesse possivelmente aceitar uma conexão de um cliente que não fosse de fato um cliente do WebSocket. Eles não queriam que um script mal-intencionado construísse um envio de formulário nem usasse o objeto XMLHttpRequest para forjar uma conexão WebSocket adicionando os cabeçalhos sec-*. Para provar a ambas as partes que uma conexão legítima está sendo estabelecida, o cabeçalho sec-websocket-key também deve estar presente no handshake do cliente. O valor deve ser um número de 16 bytes selecionado aleatoriamente - o ideal é que fosse criptograficamente aleatório - conhecido como um valor de uso único no linguajar de segurança, que é então codificado pela base64 para esse valor de cabeçalho.

Assim que o handshake do cliente for enviado, o cliente aguardará por uma resposta para validar que o servidor está, de fato, disposto e apto a estabelecer uma conexão WebSocket. Supondo que o servidor não se oponha, ele pode enviar um handshake de servidor como uma resposta HTTP, como se segue:

HTTP/1.1 101 OK
upgrade: websocket\r\n
connection: upgrade\r\n
sec-websocket-accept: 7eQChgCtQMnVILefJAO6dK5JwPc=\r\n
\r\n

Novamente, essa é uma resposta HTTP perfeitamente válida. A linha de resposta inclui a versão HTTP seguida pelo código de status, mas em vez do código regular 200 indicando êxito, o servidor deverá responder com o código padrão 101 indicando que o servidor entende a solicitação de atualização e está disposto a alternar protocolos. A descrição do código de status em inglês não faz absolutamente nenhuma diferença. Ela pode ser um "OK", ou "Switching to WebSocket" ou, até mesmo, uma citação aleatória de Mark Twain. O importante é o código de status e o cliente deve garantir que ele seja 101. O servidor pode, por exemplo, rejeitar a solicitação e pedir ao cliente para autenticar usando um código de status 401 antes de aceitar um handshake de cliente WebSocket. No entanto, uma resposta bem-sucedida deve incluir os cabeçalhos de atualização e conexão para confirmar que o código de status 101 refere-se especificamente a uma alternação para o protocolo WebSocket, novamente para evitar que alguém esteja sendo enganado.

Por fim, para validar o handshake, o cliente garante que o cabeçalho sec-websocket-accept está presente na resposta e que seu valor está correto. O servidor não precisa decodificar o valor codificado pela base64 enviado pelo cliente. Ele simplesmente pega essa cadeia de caracteres, concatena a representação da cadeia de um GUID conhecido e mistura a combinação com o algoritmo SHA-1 para gerar um valor de 20 bytes que é então codificado pela base64 e usado como o valor para o cabeçalho sec-websocket-accept. O cliente pode então validar com facilidade que o servidor, de fato, fez conforme o necessário e não há dúvida de que ambas as partes estão consentindo em uma conexão WebSocket.

Se tudo correr bem, nesse ponto uma conexão WebSocket válida é estabelecida e ambas as partes podem se comunicar de forma livre e simultânea em ambas as direções usando quadros de dados WebSocket. Estudando o protocolo WebSocket, fica claro que ele foi desenvolvido após o apocalipse de insegurança na Web. Diferentemente da maioria de seus antecessores, o protocolo WebSocket foi desenvolvido com a segurança em mente. O protocolo também exige que o cliente inclua o cabeçalho de origem se o cliente for, de fato, um navegador da Web. Isso permite que os navegadores forneçam proteção contra ataques entre origens. Obviamente, isso só faz sentido no contexto de um ambiente de hospedagem confiável, como a de um navegador.

Transferência de dados WebSocket

O protocolo WebSocket volta a Web para o desempenho relativamente alto, ao modelo de baixa sobrecarga da comunicação fornecida pelo IP e TCP, não adicionando outras camadas de complexidade e sobrecarga. Por esse motivo, assim que o handshake é concluído, a sobrecarga WebSocket é mantida no nível mínimo. Ele fornece um mecanismo de enquadramento de pacote em cima do que sobrou do TCP do empacotamento IP em que o próprio TCP foi criado e pelo qual o UDP é tão popular, mas sem as limitações de tamanho de pacote com as quais esses protocolos são sobrecarregados. Enquanto o TCP fornece uma abstração baseada em fluxo, o WebSocket fornece uma abstração baseada em mensagem ao aplicativo. E enquanto os fluxos do TCP são transmitidos por segmentos, as mensagens WebSocket são transportadas como uma sequência de quadros. Esses quadros são transmitidos pela mesma conexão TCP e, dessa forma, assumem naturalmente um fornecimento confiável e sequencial. O protocolo de enquadramento é um tanto quanto elaborado, mas foi especificamente desenvolvido para ser extremamente pequeno, exigindo, em muitos casos, somente alguns bytes adicionais de sobrecarga de enquadramento. Os quadros de dados podem ser transmitidos pelo cliente ou servidor a qualquer momento depois que o handshake de abertura tiver sido concluído.

Cada quadro inclui um opcode descrevendo o tipo de quadro, bem como o tamanho da carga. Essa carga representa os dados reais que o aplicativo pode querer comunicar, bem como os dados de extensão pré-organizados. Curiosamente, o protocolo permite que as mensagens sejam fragmentadas. Se você vem de uma experiência de rede recorrente, você pode se lembrar das implicações de desempenho da fragmentação no nível de IP e do tormento pelo qual o TCP passa para evitar a fragmentação. Mas o conceito de fragmentação do WebSocket é muito diferente. A ideia aqui é permitir que o protocolo WebSocket forneça a conveniência dos pacotes de rede, mas sem os limites de tamanho. Se o remetente não souber o comprimento exato de uma mensagem que está sendo enviada, ela poderá ser fragmentada, com cada quadro indicando o volume de dados fornecido e se é ou não o último fragmento. Além disso, o quadro indica simplesmente se ele contém dados binários ou texto codificado por UTF-8.

Os quadros de controle também são definidos e usados basicamente para fechar uma conexão, mas também são usados como pulsação para executar ping do outro ponto de extremidade, a fim de garantir que ele ainda esteja respondendo ou para auxiliar na manutenção da conexão TCP. Por fim, devo apontar que se acontecer de você bisbilhotar em um quadro WebSocket enviado por um cliente usando um analisador de protocolo de rede, como o Wireshark, você pode observar que os quadros de dados parecem conter dados codificados. O protocolo WebSocket exige que todos os quadros de dados enviados do cliente para o servidor sejam mascarados. O mascaramento envolve um algoritmo simples "XOR'ing" dos bytes de dados com uma chave de mascaramento. A chave de mascaramento está contida no quadro, de modo que isso não significa que seja algum tipo de recurso de segurança absurdo, embora ela não esteja relacionada à segurança. Como já foi mencionado, os designers do protocolo WebSocket fazem um tremendo esforço trabalhando em vários cenários relacionados à segurança para tentar prever as diversas maneiras pelas quais o protocolo pode ser atacado. Um desses vetores de ataque que foi analisado envolvia atacar o protocolo WebSocket indiretamente, comprometendo outras partes da infraestrutura da Internet, nesse caso, os servidores proxy. Os servidores proxy insuspeitos que podem não estar cientes da semelhança do handshake do WebSocket com uma solicitação GET podem ser levados enganosamente a armazenar em cache dados de uma solicitação GET falsa iniciada por um invasor, na verdade, envenenando o cache para alguns usuários. Mascarar cada quadro com uma nova chave reduz essa ameaça específica garantindo que os quadros não sejam previsíveis e que, portanto, não possam ser interpretados erroneamente na conexão por fio. Há muito mais para esse ataque e, indubitavelmente, os pesquisadores descobrirão outras possíveis explorações na hora. Além disso, é impressionante ver as consequências pelas quais os designers passaram para tentar prever as muitas formas de ataque.

Windows 8 e o protocolo WebSocket

Tão útil quanto ter um entendimento profundo do protocolo WebSocket, também ajuda muito trabalhar em uma plataforma com um suporte abrangente que, certamente, o Windows 8 fornece. Vamos observar algumas maneiras pelas quais é possível usar o protocolo WebSocket sem que você mesmo precise, de fato, implementar o protocolo.

O Windows 8 fornece o Microsoft .NET Framework, oferece suporte a clientes por meio do Tempo de Execução do Windows para código nativo e gerenciado, além de permitir a criação de clientes WebSocket usando a API do Windows HTTP Services (WinHTTP) no C++. Por fim, o IIS 8 fornece um módulo nativo WebSocket e, é claro, o Internet Explorer fornece suporte nativo ao protocolo WebSocket. Isso é uma mistura completa de ambientes diferentes, mas o que poderia ser ainda mais surpreendente é que o Windows 8 inclui apenas uma única implementação do WebSocket, que é compartilhada entre todos eles. A API do Componente de Protocolo WebSocket implementa todas as regras de protocolo para handshake e enquadramento sem jamais, de fato, criar uma conexão de rede de algum tipo. Os diferentes tempos de execução e plataformas podem então usar essa implementação comum e atá-la à pilha de rede de sua escolha.

Clientes e servidores .NET

O .NET Framework fornece extensões ao ASP.NET, bem como fornece HttpListener - que, em si, se baseia na API do Servidor HTTP nativa usada pelo IIS - para oferecer suporte de servidor ao protocolo WebSocket. No caso do ASP.NET, você pode simplesmente escrever um manipulador HTTP que chama o novo método HttpContext.AcceptWebSocketRequest para aceitar uma solicitação WebSocket em um ponto de extremidade específico. É possível validar que a solicitação é, na verdade, um handshake de cliente WebSocket usando a propriedade HttpContext.IsWebSocketRequest. Fora do ASP.NET, você pode hospedar um servidor WebsSocket simplesmente usando a classe HttpListener. A implementação também é, na maioria das vezes, compartilhada entre as duas. A Figura 1 fornece um exemplo simples de tal servidor.

Figura 1 Servidor WebSocket usando HttpListener

static async Task Run()
{
  HttpListener s = new HttpListener();
  s.Prefixes.Add("http://localhost:8000/ws/");
  s.Start();
  var hc = await s.GetContextAsync();
  if (!hc.Request.IsWebSocketRequest)
  {
    hc.Response.StatusCode = 400;
    hc.Response.Close();
    return;
  }
  var wsc = await hc.AcceptWebSocketAsync(null);
  var ws = wsc.WebSocket;
  for (int i = 0; i != 10; ++i)
  {
    await Task.Delay(2000);
    var time = DateTime.Now.ToLongTimeString();
    var buffer = Encoding.UTF8.GetBytes(time);
    var segment = new ArraySegment<byte>(buffer);
    await ws.SendAsync(segment, WebSocketMessageType.Text,
      true, CancellationToken.None);
  }
  await ws.CloseAsync(WebSocketCloseStatus.NormalClosure,
    "Done", CancellationToken.None);
}

Aqui, estou usando um método assíncrono C# para manter o código sequencial e coerente, mas, na verdade, tudo é assíncrono. Começo registrando o ponto de extremidade e aguardando uma solicitação de entrada. Em seguida, verifico se a solicitação realmente se qualifica como um handshake WebSocket e retorna um código de status 400 "solicitação incorreta" se ela não se qualificar. Em seguida, chamo AcceptWebSocketAsync para aceitar o handshake do cliente e aguardo que o handshake seja concluído. Nesse ponto, posso me comunicar livremente usando o objeto WebSocket. Nesse exemplo, o servidor envia 10 quadros UTF-8, cada um contendo a hora, após um pequeno atraso. Cada quadro é enviado de modo assíncrono usando o método SendAsync. Esse método é bastante potente e pode enviar quadros UTF-8 ou binários, seja como um todo ou em fragmentos. O terceiro parâmetro - nesse caso, verdadeiro - indica se essa chamada a SendAsync representa o fim da mensagem. Dessa forma, você pode usar esse método repetidamente para enviar mensagens longas que serão fragmentadas por você. Por fim, o método CloseAsync é usado para executar um fechamento limpo da conexão WebSocket, enviando um quadro de controle de fechamento e aguardando que o cliente reconheça seu próprio quadro de fechamento.

No lado do cliente, a nova classe ClientWebSocket usa um objeto HttpWebRequest internamente para fornecer a capacidade de conexão com um servidor WebSocket. A Figura 2 fornece um exemplo simples de um cliente que pode ser usado para se conectar ao servidor na Figura 1.

Figura 2 Cliente WebSocket usando ClientWebSocket

static async Task Client()
{
  ClientWebSocket ws = new ClientWebSocket();
  var uri = new Uri("ws://localhost:8000/ws/");
  await ws.ConnectAsync(uri, CancellationToken.None);
  var buffer = new byte[1024];
  while (true)
  {
    var segment = new ArraySegment<byte>(buffer);
    var result =
      await ws.ReceiveAsync(segment, CancellationToken.None);
    if (result.MessageType == WebSocketMessageType.Close)
    {
      await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "OK",
        CancellationToken.None);
      return;
    }
    if (result.MessageType == WebSocketMessageType.Binary)
    {
      await ws.CloseAsync(WebSocketCloseStatus.InvalidMessageType,
        "I don't do binary", CancellationToken.None);
      return;
    }
    int count = result.Count;
    while (!result.EndOfMessage)
    {
      if (count >= buffer.Length)
      {
        await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData,
          "That's too long", CancellationToken.None);
        return;
      }
      segment =
        new ArraySegment<byte>(buffer, count, buffer.Length - count);
      result = await ws.ReceiveAsync(segment, CancellationToken.None);
      count += result.Count;
    }
    var message = Encoding.UTF8.GetString(buffer, 0, count);
    Console.WriteLine("> " + message);
  }
}

Aqui estou usando o método ConnectAsync para estabelecer uma conexão e executar o handshake WebSocket. Observe que a URL usa o novo esquema de URI "ws" para identificar isso como um ponto de extremidade do WebSocket. Assim como no HTTP, a porta padrão para o ws é a porta 80. O esquema "wss" também é definido para representar uma conexão TLS segura e usa a porta correspondente 443. O cliente então chama ReceiveAsync em um loop para receber quantos quadros o servidor estiver disposto a enviar. Uma vez recebido, o quadro é verificado para ver se ele representa um quadro de controle de fechamento. Nesse caso, o cliente responde enviando seu próprio quadro de fechamento, permitindo que o servidor feche a conexão imediatamente. O cliente então verifica se o quadro contém dados binários, caso em que ele fecha a conexão com um erro que indica que esse tipo de quadro não tem suporte. Por fim, os dados do quadro podem ser lidos. Para acomodar mensagens fragmentadas, um loop while aguarda até que o fragmento final seja recebido. A nova estrutura ArraySegment é usada para gerenciar o deslocamento do buffer, de modo que os fragmentos são remontados adequadamente.

O cliente WinRT

O suporte do Tempo de Execução do Windows para o protocolo WebSocket é um pouco mais restritivo. Somente os clientes têm suporte e as mensagens UTF-8 fragmentadas devem ser completamente colocadas em buffer para que possam ser lidas. Somente mensagens binárias podem ser transmitidas com essa API. A Figura 3 fornece um exemplo simples de um cliente que também pode ser usado para se conectar ao servidor na Figura 1.

Figura 3 Cliente WebSocket usando o Tempo de Execução do Windows

static async Task Client()
{
  MessageWebSocket ws = new MessageWebSocket();
  ws.Control.MessageType = SocketMessageType.Utf8;
  ws.MessageReceived += (sender, args) =>
  {
    var reader = args.GetDataReader();
    var message = reader.ReadString(reader.UnconsumedBufferLength);
    Debug.WriteLine(message);
  };
  ws.Closed += (sender, args) =>
  {
    ws.Dispose();
  };
  var uri = new Uri("ws://localhost:8000/ws/");
  await ws.ConnectAsync(uri);
}

Esse exemplo, embora também escrito em C#, depende de manipuladores de eventos para a maioria das partes, e o método assíncrono C# é de pouca utilidade, simplesmente estando apto a permitir que o objeto MessageWebSocket se conecte de modo assíncrono. O código é bastante simples, no entanto, um pouco peculiar. O manipulador de eventos MessageReceived é chamado assim que a mensagem inteira (possivelmente fragmentada) for recebida e estiver pronta para ser lida. Mesmo que a mensagem inteira tenha sido recebida e possa apenas ser uma cadeia de caracteres UTF-8, ela é armazenada em um fluxo, e um objeto DataReader deve ser usado para ler o conteúdo e retornar uma cadeia de caracteres. Por fim, o manipulador de eventos Closed permite que você saiba que o servidor enviou um quadro de controle de fechamento, mas assim como na classe ClientWebSocket do .NET, você ainda é responsável pelo envio de um quadro de controle de fechamento de volta ao servidor. No entanto, a classe MessageWebSocket envia esse quadro apenas um pouco antes que o objeto seja destruído. Para que isso aconteça imediatamente no C#, preciso chamar o método Dispose.

O cliente JavaScript prototípico

Há uma pequena dúvida de que o JavaScript seja o ambiente em que o protocolo WebSocket terá maior impacto e a API é impressionantemente simples. Veja tudo o que é conectado ao servidor na Figura 1:

var ws = new WebSocket("ws://localhost:8000/ws/");
ws.onmessage = function (args)
{
  var time = args.data;
  ...
};

Diferentemente de outras APIs no Windows, o navegador fica encarregado de fechar a conexão WebSocket automaticamente quando recebe um quadro de controle de fechamento. Obviamente, você pode fechar explicitamente uma conexão ou manipular o evento de fechamento, mas nenhuma ação é exigida de sua parte para concluir o handshake de fechamento.

O cliente WinHTTP para C++

Obviamente, a API do cliente WebSocket do WinRT também pode ser usada no C++ nativo, mas se você estiver procurando um pouco mais de controle, então o WinHTTP é ideal para você. A Figura 4 fornece um exemplo simples de usar o WinHTTP para se conectar ao servidor na Figura 1. Esse exemplo está usando a API do WinHTTP no modo síncrono para concisão, mas isso também funcionaria bem no modo assíncrono.

Figura 4 Cliente WebSocket usando WinHTTP

auto s = WinHttpOpen( ... );
auto c = WinHttpConnect(s, L"localhost", 8000, 0);
auto r = WinHttpOpenRequest(c, nullptr, L"/ws/", ... );
WinHttpSetOption(r, WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, nullptr, 0);
WinHttpSendRequest(r, ... );
VERIFY(WinHttpReceiveResponse(r, nullptr));
DWORD status;
DWORD size = sizeof(DWORD);
WinHttpQueryHeaders(r,
  WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
  WINHTTP_HEADER_NAME_BY_INDEX,
  &status,
  &size,
  WINHTTP_NO_HEADER_INDEX);
ASSERT(HTTP_STATUS_SWITCH_PROTOCOLS == status);
auto ws = WinHttpWebSocketCompleteUpgrade(r, 0);
char buffer[1024];
DWORD count;
WINHTTP_WEB_SOCKET_BUFFER_TYPE type;
while (NO_ERROR ==
  WinHttpWebSocketReceive(ws, buffer, sizeof(buffer), &count, &type))
{
  if (WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE == type)
  {
    WinHttpWebSocketClose(
      ws, WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0);
    break;
  }
  if (WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE == type ||
    WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE == type)
  {
    WinHttpWebSocketClose(
      ws, WINHTTP_WEB_SOCKET_INVALID_DATA_TYPE_CLOSE_STATUS, nullptr, 0);
    break;
  }
  std::string message(buffer, count);
  while (WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE == type)
  {
    WinHttpWebSocketReceive(ws, buffer, sizeof(buffer), &count, &type);
    message.append(buffer, count);
  }
  printf("> %s\n", message.c_str());
}

Assim como todos os clientes WinHTTP, você precisa criar uma sessão, conexão e objeto de solicitação do WinHTTP. Não há nada novo aqui, de modo que ignorei alguns detalhes. Antes de enviar a solicitação de fato, você precisa definir a nova opção WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET na solicitação para instruir WinHTTP a executar um handshake WebSocket. A solicitação está pronta para ser enviada com a função WinHttpSendRequest. A função WinHttpReceiveResponse é então usada para aguardar a resposta que, nesse caso, incluirá o resultado do handshake do WebSocket. Como sempre, para determinar o resultado de uma solicitação, a função WinHttpQueryHeaders é chamada especificamente para ler o código de status retornado do servidor. Nesse ponto, a conexão WebSocket foi estabelecida e você pode começar a usá-la diretamente. A API do WinHTTP manipula naturalmente o enquadramento para você e essa funcionalidade é exposta por meio de um novo objeto WebSocket do WinHTTP que é recuperado quando se chama a função WinHttpWebSocketCompleteUpgrade no objeto de solicitação.

O recebimento de mensagens do servidor é feito, pelo menos conceitualmente, de forma muito parecida com a do exemplo na Figura 2. A função WinHttpWebSocketReceive aguarda para receber o próximo quadro de dados. Ela também permite que você leia fragmentos de qualquer tipo de mensagem do WebSocket e o exemplo na Figura 4 ilustra como isso pode ser feito em um loop. Se o quadro de controle de fechamento for recebido, um quadro de fechamento correspondente será enviado ao servidor usando a função WinHttpWebSocketClose. Se um quadro de dados binários for recebido, a conexão será fechada de forma semelhante. Lembre-se de que ela só fecha a conexão WebSocket. Você ainda precisará chamar WinHttpCloseHandle para lançar o objeto WinHTTP WebSocket, pois terá que fazer para todos os objetos WinHTTP em sua posse. Uma classe handle wrapper, como a que descrevi na minha coluna de julho de 2011, "C++ e a API do Windows" (msdn.microsoft.com/magazine/hh288076), fará o truque.

O protocolo WebSocket é uma importante inovação no mundo dos aplicativos Web e, apesar da sua simplicidade relativa, é uma adição de boas-vindas ao conjunto maior de TCP/IP de protocolos. Não tenho dúvidas de que o protocolo WebSocket logo será quase tão onipresente como o HTTP em si, ajudando aplicativos e sistemas conectados de todos os tipos a se comunicarem com mais facilidade e eficiência. O Windows 8 fez sua parte para fornecer um conjunto abrangente de APIs para criação de clientes e servidores WebSocket.

Kenny Kerr é um profissional de fabricação de software apaixonado pelo desenvolvimento nativo para Windows. Entre em contato com Kenny em kennykerr.ca.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Piotr Kulaga e Henri-Charles Machalani