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:
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:
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:
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.
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.
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,
- Obter o valor do cabeçalho da requisição Sec-WebSocket-Key sem qualquer espaço à direita e à esquerda;
- Concatenar com "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
- Calcular o código SHA-1 e Base64 dele;
- Reescreva no cabeçalho de resposta o valor de Sec-WebSocket-Accept como parte de uma resposta HTTP.
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#:
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]);
}