Escrevendo um servidor WebSocket em C #

Introdução

Se você quiser usar uma API WebSocket, você precisara ter um servidor. Neste artigo vou mostrar como escrever um WebSocket em C#. Você pode fazer isso em qualquer linguagem server-side, mas para manter as coisas simples e mais compreensíveis eu escolhi uma linguagem Microsoft.

Este servidor está em conformidade com a RFC 6455, por isso irá tratar apenas as conexões com os navegadores Chrome versão 16, Firefox 11, IE 10 ou superior.

Primeiros passos

Os WebSocket´s se comunicam através de uma conexão TCP (Transmission Control Protocol), felizmente o C# possui a classe TcpListener que, como o nome sugere, tem a função de escutar (Listener) as comunicações via TCP. A classe TcpListener está no namespace System.Net.Sockets.

Nota: É uma boa idéia usar a palavra chave using para escrever menos. Isso significa que não é preciso você reescrever o namespace toda vez que usar uma classe dele.

TcpListener

Construtor:

cpp
TcpListener(System.Net.IPAddress localaddr, int port)

Aqui você define onde o servidor será acessível.

Nota: Para setar facilmente o tipo esperado no primeiro parâmetro, use o método estático Parse da classe IPAddress.

Métodos:

  • Start()
  • System.Net.Sockets.TcpClient AcceptTcpClient() Espera por uma conexão TCP, aceita a conexão e retorna um objeto TcpClient.

Veja como usar o que aprendemos:

cs
using System.Net.Sockets;
using System.Net;
using System;

class Server {
    public static void Main() {
        TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 80);

        server.Start();
        Console.WriteLine("Server has started on 127.0.0.1:80.{0}Waiting for a connection...", Environment.NewLine);

        TcpClient client = server.AcceptTcpClient();

        Console.WriteLine("A client connected.");
    }
}

TcpClient

Métodos:

  • System.Net.Sockets.NetworkStream GetStream() Obtém o fluxo (stream) do canal de comunicação. Ambos os lados do canal de comunicação possuem a capacidade de ler e escrever.

Propriedades:

  • int Available Este é o numero de bytes de dados que foram enviados. o valor é zero enquanto NetworkStream.DataAvailable for falso.

NetworkStream

Métodos:

cpp
Write(Byte[] buffer, int offset, int size)

Grava bytes do buffer, offset e size determinam o tamanho da mensagem.

Read(Byte[] buffer, int offset, int size)

Lê bytes para o buffer, offset e size determinam o tamanho da mensagem.

Vamos estender nosso exemplo.

cpp
TcpClient client = server.AcceptTcpClient();

Console.WriteLine("A client connected.");

NetworkStream stream = client.GetStream();

//enter to an infinite cycle to be able to handle every change in stream
while (true) {
    while (!stream.DataAvailable);

    Byte[] bytes = new Byte[client.Available];

    stream.Read(bytes, 0, bytes.Length);
}

Handshaking

Quando um cliente se conecta a um servidor, ele envia uma solicitação GET para atualizar a conexão com o WebSocket a partir de uma simples requisição HTTP. Isto é conhecido como handshaking (aperto de mão).

Aviso: Este código tem um defeito. Digamos que a propriedade client.Available retorna o valor 2 porque somente a requisição GET está disponível até agora. a expressão regular iria falhar mesmo que os dados recebidos sejam perfeitamente válidos.

cpp
using System.Text;
using System.Text.RegularExpressions;

Byte[] bytes = new Byte[client.Available];

stream.Read(bytes, 0, bytes.Length);

//translate bytes of request to string
String data = Encoding.UTF8.GetString(bytes);

if (new Regex("^GET").IsMatch(data)) {

} else {

}

Criar a resposta é mais fácil do que entender porque você deve fazê-lo desta forma.

Você deve,

  1. Obter o valor do cabeçalho da requisição Sec-WebSocket-Key sem qualquer espaço à direita e à esquerda;
  2. Concatenar com "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
  3. Calcular o código SHA-1 e Base64 dele;
  4. Reescreva no cabeçalho de resposta o valor de Sec-WebSocket-Accept como parte de uma resposta HTTP.
cpp
if (new Regex("^GET").IsMatch(data)) {
    Byte[] response = Encoding.UTF8.GetBytes("HTTP/1.1 101 Switching Protocols" + Environment.NewLine
        + "Connection: Upgrade" + Environment.NewLine
        + "Upgrade: websocket" + Environment.NewLine
        + "Sec-WebSocket-Accept: " + Convert.ToBase64String (
            SHA1.Create().ComputeHash (
                Encoding.UTF8.GetBytes (
                    new Regex("Sec-WebSocket-Key: (.*)").Match(data).Groups[1].Value.Trim() + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
                )
            )
        ) + Environment.NewLine
        + Environment.NewLine);

    stream.Write(response, 0, response.Length);
}

Decodificação de mensagens

Após um handshake de sucesso o cliente ponde enviar mensagens ao servidor, mas agora estas mensagens são codificadas.

Se nós enviarmos "MDN", nós obtemos estes bytes:

129 131 61 84 35 6 112 16 109

- 129:

FIN (Esta é toda a mensagem?) RSV1 RSV2 RSV3 Opcode
1 0 0 0 0x1=0001

FIN: Você pode enviar sua mensagem em quadros (frames), mas agora as coisas ficaram mais simples. Opcode 0x1 significa que este é um texto. Veja aqui a lista completa de Opcodes.

- 131:

Se o segundo byte menos 128 estiver entre 0 e 125, este é o tamanho da mensagem. Se for 126, os 2 bytes seguintes (16-bit inteiro sem sinal) e se 127, os 8 bytes seguintes (64-bit inteiro sem sinal) são o comprimento.

Nota: Eu posso escolher 128, porque o primeiro bit sempre será 1.

- 61, 84, 35 e 6 são os bytes de chave para decodificar. Sempre mudam.

- O restante dos bytes codificados são a mensagem.

Algoritmo de decodificação

byte decodificado = [byte codificado XOR (posição do byte codificado MOD 4º byte da chave)]

Exemplo em C#:

cpp
Byte[] decoded = new Byte[3];
Byte[] encoded = new Byte[3] {112, 16, 109};
Byte[] key = Byte[4] {61, 84, 35, 6};

for (int i = 0; i < encoded.Length; i++) {
    decoded[i] = (Byte)(encoded[i] ^ key[i % 4]);
}