Datos en linea con WebSockets y C#


Actualmente me encuentro desarrollando un Dashboard y un sistema que trabaja con flujos de trabajo en tiempo real, lo cual me parecio un problema dado que tenia que hacer llamadas todo el tiempo al BackEnd para poder obtener los datos, generalmente usas un setInterval con llamada a Ajax asincrono, pero me daba vueltas la cabeza de la “talacha” que tendría que hacer.

Por lo tanto, me he visto obligado a hacer una pequeña investigación que maneja WebSockets, al utlizar MVC5 mi primera idea fue usar SignalR, pero luego recordé que todo lo que hago tiene que ser portable de alguna manera a otros lenguajes y se me paso, así que adiós SignalR, por lo tanto regresemos al origen de todo los Sockets, estos los puedo programar como me convenga en cualquier lenguaje, ya sea C#, Pyton, Java, C, lo cual me permite portar el servidor a donde lo necesite

Al comenzar esta tarea decidí hacer una consola, ya que después podré levantar un Servicio con el resultado, para esto tengo que crear un Socket – Asyncrono, el cuál me permitirá obtener los datos.

Para el caso del cliente, como su base es HTML en si mismo es portable a cualquier lenguaje web ya sea PHP, C# con MVC o JAVA con Groovy

Que debe hacer el programa Servidor:

  • Crear un Programa de Consola
  • Crear un SocketServer
  • Habilitar la Autenticación del SocketCliente
  • Obtener Datos de una Base de Datos
  • Enviar los datos cada determinado tiempo

Vale la pena mencionar la siguiente pagina https://developer.mozilla.org/es/docs/WebSockets-840092-dup/Escribiendo_servidores_con_WebSocket que es muy importante para comprender el concepto e implementación de los sockets server

Despues de haber definido lo que hará el programa servidor, haremos manos a la obra.

Abriremos nuestro Visual Studio y pondremos el nombre de la solución como WebSocketServer como vemos en la siguiente imagen:

Luego vamos a ir definiendo el codigo del socket, al ser un socket asincrono, es necesario utilizar solamente los metodos BeginAccept y BeginReceive, lo cual nos permitirá hacerlo completamente asincrono.

Vamos a definir las siguientes Variables:


static private string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B13";
static List<Socket> lstSocket = new List<Socket>();
static Socket serverSocket;
static byte[] b = new byte[1024];

En el codigo de Main Crearemos el Listen de nuestro Socket:


static void Main(string[] args)
{
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 9090));
serverSocket.Listen(128);
serverSocket.BeginAccept(null, 0, OnAccept, null);
Console.ReadKey();
}

Con el codigo anterior, estamos poniendo a escuchar el socket debemos mirar la seccion OnAccept que es el metodo al que hará referencia y que crearemos para continuar con la programación del Socket como veremos en el siguiente codigo:


private static void OnAccept(IAsyncResult result)
{
try
{
Socket client = null;
if (serverSocket != null && serverSocket.IsBound)
{
client = serverSocket.EndAccept(result);
}
if (client != null)
{
/* Handshaking and managing ClientSocket */
if (b == null) { b = new byte[1024]; }
client.BeginReceive(b, 0, b.Length, SocketFlags.None, OnReceive, client);
serverSocket.BeginAccept(null, 0, OnAccept, null);
}
}
catch (SocketException exception)
{
serverSocket.BeginAccept(null, 0, OnAccept, null);
}
}

En el codigo anterior hacemos tambien una llamada OnReceives en el cual tenemos el codigo principal de la información, como veremos a continuación:

</pre>
private static void OnReceive(IAsyncResult AR)
{
byte[] ResponseBytes = null;
Console.WriteLine("Recieving");
Socket client = (Socket)AR.AsyncState;
int dataLen = client.EndReceive(AR);
byte[] dataBuff = new byte[dataLen];
Array.Copy(b, dataBuff, dataLen);

string szReceived = Encoding.ASCII.GetString(dataBuff, 0, dataBuff.Length);

if (!lstSocket.Contains(client))
{
Console.WriteLine(szReceived);
lstSocket.Add(client);
#region ParseHeaders
var headers = new Dictionary<string, string="">();</string,>
string[] lines = szReceived.Split('\n');
foreach (string line in lines)
{
var tokens = line.Split(new char[] { ':' }, 2);
if (!string.IsNullOrWhiteSpace(line) && tokens.Length > 1)
{
headers[tokens[0]] = tokens[1].Trim();
}
}
string Key = headers["Sec-WebSocket-Key"];
#endregion
#region CreateResponseHeaders
string ResponseKey = AcceptKey(ref Key);

var response =
"HTTP/1.1 101 WebSocket Protocol Handshake" + Environment.NewLine +
"Upgrade: WebSocket" + Environment.NewLine +
"Connection: Upgrade" + Environment.NewLine +
"Sec-WebSocket-Origin: " + headers["Origin"] + Environment.NewLine +
"Sec-WebSocket-Accept: " + ResponseKey + Environment.NewLine +
"Sec-WebSocket-Location: ws://localhost:8080/websession" + Environment.NewLine +
Environment.NewLine;
#endregion
ResponseBytes = Encoding.ASCII.GetBytes(response);
client.Send(ResponseBytes);
}
else
{
Console.Write(szReceived + " = ");
#region DecodeMessage
string Path = DecodeMessage(dataBuff);
#endregion
#region CreateTimerToSendInfo
Timer t = new Timer();
t.Enabled = true;
t.Interval = 5000;
t.Start();
t.Elapsed += (sender, args) => ExecuteQuery(sender, client, Path);
#endregion
Console.WriteLine(Path);
string Data = DataInfo(Path);
ResponseBytes = EncodeMessageToSend(Data);
client.Send(ResponseBytes);
}
if (b != null) { b = new byte[1024]; }
try
{
client.BeginReceive(b, 0, b.Length, SocketFlags.None, OnReceive, client);
}
catch (Exception ex)
{
Console.WriteLine("Error");
}
}
<pre>

Lo más relevante que podemos encontrar el en metodo OnRecieve  es que revisamos que el socket se encuentre en una lista de Sockets, esto nos permite identificar cada conexión, esto nos da dos opciones cuando nos envian Headers o Datos, los Headers siempre se mandarán en la primera conexión, todo lo demás para nosotros siempre serán Datos o Parametros o Consultas… o Algo es un Socket, siempre va a ser texto.

Headers

Lo primero es identificar las etiquetas de #region y #endregion que son las marcas que están puestas en el codigo para poder identificarlo y analizarlo parte por parte lo primero es la sección de ParseHeaders al ser un WebServer obviamente es necesario obtener los tokens y ponerlos en una lista que podamos utilizar ya que los encabezados son necesarios para obtener una única información que nos manda el socket cliente, y es Sec-WebSocket-Key para retornar la información al socket cliente como podemos ver en las etiquetas CreateResponseHeaders finalmente retornamos los encabezados de respuesta al Socket con el metodo client.Send. A todo este proceso se le llama Handshake.

Información

En el contenido del else como podemos ver se encuentran las regiones DecodeMessage que en la cual esta el método que decodificara el mensaje que nos envié el WebSocketCliente y la región CreateTimerToSendInfo la cual crea un pequeño timer que nos permite enviar la información cada cierto tiempo.

La información estará definida por el método DataInfo, que prácticamente contiene un Swith con una palabra mágica para saber que datos se van a enviar, después la información retornada en string será enviada al codificador del mensaje, para finalmente enviar la respuesta de la petición por primera vez.

Podemos ver que dentro del sistema se utilizan los siguientes Metodos:

  • AcceptKey
  • DecodeMessage
  • ExecuteQuery
  • EncodeMessageToSend
  • DataInfo

Los cuales se mostrarán a continuación:

AcceptKey es utilizado para generar la nueva llave apartir de la que el clientre nos envio:


private static string AcceptKey(ref string key)
{
string longKey = key + guid;
SHA1 sha1 = SHA1CryptoServiceProvider.Create();
byte[] hashBytes = sha1.ComputeHash(System.Text.Encoding.ASCII.GetBytes(longKey));
return Convert.ToBase64String(hashBytes);
}

ExcuteQuery es el Metodo que será utilizado como delegado para la propiedad Elapsed:


public static void ExecuteQuery(object sender, Socket client, string Path)
{
string Data = DataInfo(Path);
byte[] ResponseBytes = EncodeMessageToSend(Data);
if (client.Connected)
{
client.Send(ResponseBytes);
}
else
{
client.Disconnect(false);
lstSocket.Remove(client);
}
}

DataInfo es el metodo que nos permitirá obtener informaciónes segun haya llamadas a la base de datos, como puedes ver en el metodo, no hay conexión a la base pero esa implementación la pueden hacer ustedes ya a su gusto:


public static string DataInfo(string Path)
{
string json = string.Empty;
Random rnd = new Random();
switch (Path)
{
case "DATOS1":
json = "[{\"Key\":"+ rnd.Next(0, 100) +",\"Value\":\"" + Guid.NewGuid() + "\"},{\"Key\":"+rnd.Next(0, 100)+",\"Value\":\"" + Guid.NewGuid() + "\"}]";
break;
case "DATOS2":
json = "[{\"KeyData\":"+ rnd.Next(0, 100) +",\"ValueData\":\"" + Guid.NewGuid() + "\"},{\"KeyData\":"+rnd.Next(0, 100)+",\"ValueData\":\"" + Guid.NewGuid() + "\"}]";
break;
}
return json;
}

DecodeMessage es un metodo un tanto complicado, dado que se debe seguir la implmentación mencionada en la pagina dada al inicio de este post Especificamente Aqui.


private static String DecodeMessage(Byte[] bytes)
{
String incomingData = String.Empty;
Byte secondByte = bytes[1];
Int32 dataLength = secondByte & 127;
Int32 indexFirstMask = 2;
if (dataLength == 126)
indexFirstMask = 4;
else if (dataLength == 127)
indexFirstMask = 10;

IEnumerable<Byte> keys = bytes.Skip(indexFirstMask).Take(4);
Int32 indexFirstDataByte = indexFirstMask + 4;

Byte[] decoded = new Byte[bytes.Length - indexFirstDataByte];
for (Int32 i = indexFirstDataByte, j = 0; i < bytes.Length; i++, j++)
{
decoded[j] = (Byte)(bytes[i] ^ keys.ElementAt(j % 4));
}

return incomingData = Encoding.UTF8.GetString(decoded, 0, decoded.Length);
}

EncodeMessageToSend hace practicamente lo mismo pero de manera inversa:


private static Byte[] EncodeMessageToSend(String message)
{
Byte[] response;
Byte[] bytesRaw = Encoding.UTF8.GetBytes(message);
Byte[] frame = new Byte[10];

Int32 indexStartRawData = -1;
Int32 length = bytesRaw.Length;

frame[0] = (Byte)129;
if (length <= 125)
{
frame[1] = (Byte)length;
indexStartRawData = 2;
}
else if (length >= 126 && length <= 65535)
{
frame[1] = (Byte)126;
frame[2] = (Byte)((length >> 8) & 255);
frame[3] = (Byte)(length & 255);
indexStartRawData = 4;
}
else
{
frame[1] = (Byte)127;
frame[2] = (Byte)((length >> 56) & 255);
frame[3] = (Byte)((length >> 48) & 255);
frame[4] = (Byte)((length >> 40) & 255);
frame[5] = (Byte)((length >> 32) & 255);
frame[6] = (Byte)((length >> 24) & 255);
frame[7] = (Byte)((length >> 16) & 255);
frame[8] = (Byte)((length >> 8) & 255);
frame[9] = (Byte)(length & 255);

indexStartRawData = 10;
}

response = new Byte[indexStartRawData + length];

Int32 i, reponseIdx = 0;

//Add the frame bytes to the reponse
for (i = 0; i < indexStartRawData; i++)
{
response[reponseIdx] = frame[i];
reponseIdx++;
}

//Add the data bytes to the response
for (i = 0; i < length; i++)
{
response[reponseIdx] = bytesRaw[i];
reponseIdx++;
}

return response;
}

Con estos metodos terminamos nuestro socket server ahora vamos a compilar nuestro servidor, vamos a ejecutarlo y nos mostrará una pantalla negra, y dejemoslo esperando ahí. y crearemos un cliente creando nuestro html con el siguiente codigo:

</pre>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js">
<style>
#Data1 {
float:left;
}

#Data2 {
float:left;
background:#fc0;
}
</style>
<body>
<div id="Data1"></div>
<div id="Data2"></div>
</body>

<script>
$(document).ready(function () {

var uri = "ws://localhost:9090/api/test";

//Initialize socket
websocket = new WebSocket(uri);
//Open socket and send message
websocket.onopen = function () {
$('#messages').prepend('
<div>Connected to server.</div>
');
websocket.send("DATOS1");
websocket.send("DATOS2");
};

//Socket error handler
websocket.onerror = function (event) {
alert("Error de Conexión");
};

//Socket message handler
websocket.onmessage = function (event) {
if (event.data != "[]") {
var parse = JSON.parse(event.data)
$.each(parse, function (i, data) {
if (data.KeyData == undefined) {
$('#Data1').prepend('
<div>Key:' + data.Key + ' Value: ' + data.Value + '</div>
');
}
else {
$('#Data2').prepend('
<div>Key:' + data.KeyData + ' Value: ' + data.ValueData + '</div>
');
}
});
}
};
});

</script>
<pre>

Una vez creado lo abriremos con el navegador de chrome y veremos el siguiente resultado:

Un Resultado interesante ¿No?, con esto ya no tenemos que hacer llamadas recursivas es el server el que se encarga de enviarnos la información y la pagina solo se dedica a parsear, imaginen las cosas que podrían hacerse.
Descarga(Pendiente)

Anuncios

Acerca de Francisco Castán

Creador, Diseñador, Investigador y Programador de Software Lenguajes Preferidos: C/C++, C#, Java, PHP, Python, PERL, Shell, JavaScript

Publicado el 06/09/2017 en .Net, MS SQLServer y etiquetado en , , , . Guarda el enlace permanente. 12 comentarios.

  1. buenas amigo, me parecio interesante lo que planteas, yo he creado una aplicacion con angular para mi uso, lo cual necesito imprimir desde cualquier sitio, y ando buscando desarrollar algo casi igual, pero lo mio es por ejemplo que yo envie un json al socket y el solo imprima la factura, eso se puede implementar?

  2. Interesante. A nivel presentación, cómo quedó el Dashboard ? algún compomente para Dashboards específicico?

  3. pablo vranken

    Hola podrias pasar el codigo de ejemplo? por email o ponerlo para descargar.. muchas gracias

    • No es necesario, solo necesitas copiar en una solución los codigos como están en el post

      • Gracias!

        Qué es mejor (rendimiento, fácil de usar) websockets o signalr?
        Descartamos AJAX asíncrono con jquery. Sí el stack es totalmente net, merece la pena seguir con signalr?

        Webssockets no soporta todos los navegadores?

        La idea es un ejemplo real world con buenas prácticas para minimizar curva de aprendizaje.
        Signalr siempre se ve ejemplo chat, no real world como un Dash board.

        Saludos.

      • 1.- Si tu plataforma es full .NET SignalR es mejor, pero si quieres compatibilidad tu mejor opción son los WebSockets.
        2.- Para descartar AJAX Asincrono, primero debes checar la viavilidad de tu compatibilidad, si haces modelos asincronos con AJAX, tendrás que usar SetInterval, lo cual no es malo si solo trabajarás con web, por lo tanto vuelve a la respuesta 1 todo depende de la calidad de compatibilidad que quieras.
        3.- Hasta donde he visto y trabajado solo Explorer 10 (Edge) y Chrome lo soportan
        4.- ¿Quieres ver un ejemplo ya hecho o quieres hacer uno?

      • Sí, ver un ejemplo con buenas prácticas, sea en codeproject, c# corner,.. y a partir de ahí iniciar mi ejemplo con el expertise adecuado.

      • Ya veo, por el momento no puedo poner uno ya que necesitas una base de datos cambiante, esto es que tus transacciones o registros cambien, en intervalos regulares, necesitas un proyecto completo y alguien que suministre datos lo cual es un poco complicado si me lo preguntas, pero, para obtener esos datos se me ocurre un monitoreo de servidores, esto es que en tu maquina ejecutes un pequeño programa con web sockets que este tomando la información del sistema sin necesidad de entrar en el… no suena nada mal… veremos si proximamente armo un programa para extraer los datos principales del sistema y hacemos un dashboard de actividad de servidores.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

w

Conectando a %s

A %d blogueros les gusta esto: