Escribiendo un servidor WebSocket en C#
Introducción
Si deseas utilizar la API WebSocket, es conveniente si tienes un servidor. En este artículo te mostraré como puedes escribir uno en C#. Tú puedes hacer esto en cualquier lenguaje del lado del servidor, pero para mantener las cosas simples y más comprensibles, elegí el lenguaje de Microsoft.
Este servidor se ajusta a RFC 6455 por lo que solo manejará las conexiones de Chrome version 16, Firefox 11, IE 10 and superiores.
Primeros pasos
WebSocket se comunica a través de conexiones TCP (Transmission Control Protocol), afortunadamente C# tiene una clase TcpListener la cual hace lo que su nombre sugiere. Esta se encuentra en el namespace System.Net.Sockets.
Nota:
Es una buena idea usar la instrucción using
para escribir menos. Eso significa que no tendrás que re escribir el namespace de nuevo en cada ocasión.
TcpListener
Constructor:
TcpListener(System.Net.IPAddress localaddr, int port)
localaddr
especifica la IP a escuchar y port
especifica el puerto.
Nota:
Para crear un objeto IPAddress
desde un string
, usa el método estático Parse
de IPAddres.
Métodos:
Start()
System.Net.Sockets.TcpClient AcceptTcpClient()
Espera por una conexión TCP, la acepta y la devuelve como un objeto TcpClient.
Aquí está como utilizar lo que hemos aprendido:
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("El server se ha iniciado en 127.0.0.1:80.{0}Esperando una conexión...", Environment.NewLine);
TcpClient client = server.AcceptTcpClient();
Console.WriteLine("Un cliente conectado.");
}
}
TcpClient
Métodos:
System.Net.Sockets.NetworkStream GetStream()
Obtiene el stream del canal de comunicación. Ambos lados del canal tienen capacidad de lectura y escritura.
Propiedades:
int Available
Este es el número de bytes de datos que han sido enviados. El valor es cero hasta queNetworkStream.DataAvailable
estrue
.
NetworkStream
Métodos:
Write(Byte[] buffer, int offset, int size)
Escribe bytes desde el buffer; el offset y el size determinan la longitud del mensaje.
Read(Byte[] buffer, int offset, int size)
Lee bytes al buffer; el offset y el size determinan la longitud del mensaje.
Ampliemos nuestro ejemplo anterior.
TcpClient client = server.AcceptTcpClient();
Console.WriteLine("Un cliente conectado.");
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
Cuando un cliente se conecta al servidor, envía una solicitud GET para actualizar la conexión al WebSocket desde una simple petición HTTP. Esto es conocido como handshaking.
Este código de ejemplo detecta el GET desde el cliente. Nota que esto bloqueará hasta los 3 primeros bytes del mensaje disponible. Soluciones alternativas deben ser investigadas para ambientes de producción.
using System.Text; using System.Text.RegularExpressions; while(client.Available < 3) { // wait for enough bytes to be available } 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 (Regex.IsMatch(data, "^GET")) { } else { }
Esta respuesta es fácil de construir, pero puede ser un poco díficil de entender. La explicación completa del handshake al servidor puede encontrarse en RFC 6455, section 4.2.2. Para nuestros propósitos, solo construiremos una respuesta simple.
Debes:
- Obtener el valor de "Sec-WebSocket-Key" sin espacios iniciales ni finales de el encabezado de la solicitud
- Concatenarlo con "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
- Calcular el código SHA-1 y Base64
- Escribe el valor Sec-WebSocket-Accept en el encabezado como parte de la respuesta 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);
}
Decoding messages
Luego de un handshake exitoso el cliente puede enviar mensajes al servidor, pero estos serán codificados.
Si nosotros enviamos "MDN", obtendremos estos bytes:
129 | 131 | 61 | 84 | 35 | 6 | 112 | 16 | 109 |
---|
- 129:
FIN (¿Es el mensaje completo?) | RSV1 | RSV2 | RSV3 | Opcode |
---|---|---|---|---|
1 | 0 | 0 | 0 | 0x1=0001 |
FIN: Puedes enviar tu mensaje en marcos, pero ahora debe mantener las cosas simples. Opcode 0x1 significa que es un texto. Lista completa de Opcodes
- 131:
Si el segundo byte menos 128 se encuentra entre 0 y 125, esta es la longitud del mensaje. Si es 126, los siguientes 2 bytes (entero sin signo de 16 bits), si es 127, los siguientes 8 bytes (entero sin signo de 64 bits) son la longitud.
Nota: Puedo tomar 128, porque el primer bit siempre es 1.
- 61, 84, 35 y 6 son los bytes de la clave a decodificar. Cambian en cada oportunidad.
- Los bytes codificados restantes son el mensaje.
Algoritmo de decodificación
byte decodificado = byte codificado XOR (posición del byte codificado Mod 4) byte de la clave
Ejemplo en 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]);
}