Silverlight y los Sockets

 Hace tiempo que vengo intentando cubrir algunos huecos en mi formación sobre .NET que por falta de tiempo se me había hecho imposible llevar adelante. Se trata de Silverlight y WPF. Ahora y por cuestiones de trabajo, me ha tocado hacer frente a estos “baches”.

Como soy programador y no diseñador, paso un poco de toda esta tecnología en la que las posibilidades de crear User Interface (UI) ricas en animaciones o efectos visuales cobra mayor importancia. Así, enfrentar este nuevo reto me lleva por encontrar desde el punto de vista de análisis y desarrollo, las posibilidades que estas nuevas tecnologías traen asociadas.

Uno de los principales problemas cuando desarrollamos en Web, es la posibilidad de una comunicación asincrónica entre cliente y servidor. Con esto me refiero a la posibilidad de que la comunicación fluya en ambos sentidos y no tenga que ser el cliente (Aplicación WEB) quien realice constantemente las peticiones al servidor.

Silverlight es un Framework que corre del lado del cliente y que contiene una infraestructura que nos permite aumentar de modo significativo el alcance de nuestras aplicaciones Web. Partiendo de la problemática anterior y conociendo las ventajas de esta nueva forma de desarrollo, me planteo para mi propio estudio la utilización de sockets dentro de SilverLight para realizar una conexión en la que el servidor sea quien se mantenga brindando continua información al cliente.

Esquema de desarrollo:

El esquema lo que muestra es una aplicación que desde un servidor interactúa con una pizarra telefónica (PBX) mediante TAPI y envía hacia un cliente WEB desarrollado con Silverlight el estado actual de cada una de las extensiones conectadas a la PBX. Para el artículo, solo nos concentraremos en la comunicación por socket entre el cliente Web y el servidor. He dibujado el diagrama completo para que vean las líneas que representan el sentido en que fluye la información en toda la aplicación. (Desde el servidor hacia el cliente)

Empezamos desarrollando una aplicación de consola con un socket server como normalmente lo hemos visto siempre en cualquier aplicación Windows. Este socket server estará a la escucha mediante un determinado puerto de cualquier petición de conexión realizada desde un cliente Web.

El TcpListener: Voy a pasar por alto la implementación detallada del servidor ya que no es el objetivo del artículo.

//create our TCPListener object
_sockServer = new System.Net.Sockets.TcpListener(IPAddress.Any, 4530);

Console.WriteLine(“Started server…”); 

try
{
  
//start the chat server
  
_sockServer.Start();

   while (true)
  
{
     
_tcpClientConnected.Reset();

      Console.WriteLine(“Waiting for client connection…”);

       _sockServer.BeginAcceptTcpClient(new AsyncCallback(OnBeginAccept), null);
     
_tcpClientConnected.WaitOne(); //Block until client connects
  
}
}

catch
(Exception ex)
{
  
Console.Write(ex.ToString());
}

… OnBeginAccept

var _client = _sockServer.EndAcceptTcpClient(result);
InitializeExtensionList(_extensions); 

_tcpClientConnected.Set(); //Allow waiting thread to proceed

Con este código inicializamos nuestro servidor y lo mantenemos a la escucha sobre el puerto 4530 de cualquier petición de conexión. En el momento en que una conexión es solicitada, se inicializa el listado de extensiones que no es más que enviar al cliente la información de todas las extensiones conectadas a la PBX.

La primera observación es el puerto seleccionado. Esto no es un antojo, nuestro cliente será una aplicación Silverlight y el mismo solo permite realizar conexiones por sockets a los puertos comprendidos en el rango de 4502 hasta el 4534. Cualquier intento de conexión desde una aplicación desarrollada en Silverlight a un puerto fuera de este rango, será denegado automáticamente.

Segunda observación es que usamos TCP como protocolo de comunicación, esto es lo normal cuando trabajamos con sockets aunque no está de más indicar que es el único protocolo permitido por Silverlight.

Lanzamos nuestro server para asegurarnos que todo marcha bien…

Todo bien… así que seguimos con lo que realmente importa, la aplicación cliente.

MainPage.xaml.cs : No voy a entrar en detalles de diseño o implementación del socket para no hacer extenso el artículo.

public MainPage()
{
  
InitializeComponent();
  
this.Loaded += Page_Loaded;    
}

En el constructor de mi UserControl me subscribo al evento Loaded en el cual conectaré al servidor y esperaré como respuesta el listado de extensiones.

void Page_Loaded(object sender, RoutedEventArgs e)
{
   
var endPoint = new DnsEndPoint(Application.Current.Host.Source.DnsSafeHost, 4530);
   
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
   
args.UserToken = socket;
   
args.RemoteEndPoint = endPoint;
   
args.Completed += new EventHandler<SocketAsyncEventArgs>(OnSocketConnectCompleted);

    socket.ConnectAsync(args);
}

Aquí vamos… Explicación rapidita: Este código identifica en primer lugar el servidor de la aplicación e inicializa un EndPoint mediante el puerto 4530 (Recuerden que Silverlight sólo puede conectarse a los servidores mediante los puerto 4502 al 4532). Posteriormente se crea un objeto Socket capaz de comunicarse mediante el protocolo TCP. Una vez que el objeto Socket es creado, creamos una instancia del objeto SocketAsyncEventArgs y asignamos el Socket a la propiedad UserToken de modo que podamos utilizarlo a través de otros métodos. El punto final de destino en el servidor se establece mediante la propiedad RemoteEndPoint y el evento Completed nos va a indicar el resultado de la conexión. Una vez que estos objetos son creados y están listos para su uso, El método ConnectAsync del objeto Socket puede ser llamado indicando como argumento el SocketAsyncEventArgs.

private void OnSocketConnectCompleted(object sender, SocketAsyncEventArgs e)
{
  
if (e.SocketError == SocketError.Success)
  
{
     
… Success Connection
  
}
  
else
  
{
     
Dispatcher.BeginInvoke(() =>
        
System.Windows.Browser.HtmlPage.Window.Alert(e.SocketError.ToString()));

  
}
  

}

Con este código ya listo, lanzo mi aplicación cliente:

¡¡¡BOOM!!! ¿Acceso denegado? Y yo aprendiendo Silverlight, menudo problema. Mis primeros intentos de solución son los tan “mal” acostumbrados “prueba y error”. Sin tener muy claro qué estaba pasando empecé a intentar adivinar.

– Ya sé… el Firewall de Windows. Fuera Firewall pero el error seguía – Umm.. ya sé… el servidor no está funcionando bien. Hice una pequeña aplicación de consola que conectara al servidor y… ¡¡¡funcionó bien!!!

Cansado de tanto estira y encoje hice lo que debía hacer desde el principio. ¡Leer más!

Resulta que nuestro nuevo amiguito Silverlight establece unas políticas de seguridad, muy acertadas a mi criterio, en las que no permite conexión alguna si antes el servidor no valida que realmente se tenga acceso a este recurso.

El MSDN dice:

“Silverlight 2 y las versiones posteriores son compatibles con la conectividad entre dominios, lo que permite a una aplicación obtener acceso a recursos situados en ubicaciones que no son el sitio de origen. Se trata de una característica importante para que las aplicaciones de Silverlight puedan utilizar los servicios existentes en la Web.

El sistema de directivas de seguridad del motor en tiempo de ejecución de Silverlight requiere que se descargue un archivo de directivas desde un dominio de destino antes de permitir que una conexión de red tenga acceso a un recurso de red que pertenezca a ese dominio de destino. Este sistema de directivas de seguridad afecta al acceso de red entre dominios para las clases WebClient y HTTP en el espacio de nombres System.Net.”

¿Qué archivo es este y qué contiene? ¿Dónde pongo este archivo?

El archivo que expone las directivas de seguridad para que Silverlight pueda validar los recursos de red a los cuales deseamos acceder es un XML con el siguiente formato:

<?xml version=”1.0″ encoding=”utf-8″?>
<access-policy>
 
<cross-domain-access>
   
<policy>
     
<allow-from>
       
<domain uri=”*” />
     
</allow-from>
     
<grant-to>
       
<socket-resource port=”4530″ protocol=”tcp” />
     
</grant-to>
   
</policy>
 
</cross-domain-access>
</access-policy>

Relajitos con mi servidor ¡NO! 😉

Mediante este archivo nosotros le indicamos a cualquier cliente que intente conectar a nuestro recurso de red, que solo tendrá permiso si la petición se realiza desde un dominio que esté validado por nosotros (En este caso, vía libre a todo el mundo) y el recurso al que esté intentando conectar sea sí y solo sí, el puerto 4530. Muy bien aquí por Microsoft 😉

Ahora bien… ¿dónde pongo este archivo? De nuevo MSDN:

“En Silverlight versión 3, en el caso de una solicitud de conexión mediante System.Net.Sockets al sitio (entre dominios o sitio de origen), el runtime de Silverlight intenta abrir una conexión utilizando TCP a un puerto conocido (el puerto 943) en el sitio de destino. Si se puede establecer una conexión TCP, el runtime de Silverlight envía la cadena especial al servidor para solicitar un archivo de directivas de sockets de Silverlight.

El motor en tiempo de ejecución de Silverlight, a continuación, espera recibir una respuesta desde el sitio de destino que contenga un archivo de directivas de Silverlight. Si se devuelve este archivo de directivas de sockets de Silverlight (aun cuando haya un error de análisis del archivo), se utiliza como archivo de directivas para esa solicitud de sockets y para todas las solicitudes subsiguientes a ese sitio de destino durante la sesión completa de la aplicación de Silverlight.”

Esto significa que para probar mis aplicaciones en Silverlight, de algún modo necesito tener un servidor escuchando peticiones por el puerto 943 y, en caso de que se produzca una conexión, validamos que se trate de un cliente solicitando un archivo de directiva de seguridad para retornarle el XML.

Esto sería un problema si pensamos hostear servicios en servidores que no sean dedicados, por eso, para el caso de Silverlight 4, la petición de las directivas de seguridad se realizan tanto por el puerto 80, como por el puerto 943. De esta forma podemos adicionar a nuestro proyecto web el archivo XML para que los clientes accedan directamente a él.

Para pruebas o cuando ejecutamos la aplicación dentro de Visual Studio, no podríamos publicar el XML por el puerto 80 ya que nuestra aplicación se ejecuta dentro del servidor web que trae el propio Visual Studio.

Para poder probar mis aplicaciones sin problemas me cree un pequeño Socket server que valida y retorna el archivo de directivas de seguridad solicitado por Silverlight.

class PolicySocketServer
{
  
private TcpListener _listener = null;
  
private TcpClient _client = null; 

   private static ManualResetEvent _tcpClientConnected = new ManualResetEvent(false);
  
private const string _policyRequestString = “<policy-file-request/>”;

   private int _receivedLength = 0;
  
private byte[] _policy = null;
  
private byte[] _receiveBuffer = null;

   private void ReadXmlPolicyData()
  
{
     
string policyFile = ConfigurationManager.AppSettings[“PolicyFilePath”];
     
using (FileStream fs = new FileStream(policyFile, FileMode.Open))
     
{
         
_policy = new byte[fs.Length];
        
fs.Read(_policy, 0, _policy.Length);
     
}

      _receiveBuffer = new byte[_policyRequestString.Length];
   
}

   public void StartSocketServer()
  
{
     
ReadXmlPolicyData(); 

      _listener = new TcpListener(IPAddress.Any, 943);
     
_listener.Start();

      Console.WriteLine(“Policy server listening…”);

      while (true)
     
{
        
_tcpClientConnected.Reset();
        
Console.WriteLine(“Waiting for client connection…”);

         _listener.BeginAcceptTcpClient(new AsyncCallback(OnBeginAccept), null);
        
_tcpClientConnected.WaitOne();
     
}
   
} 

    private void OnBeginAccept(IAsyncResult result)
   
{
      
_client = _listener.EndAcceptTcpClient(result);           

       _client.Client.BeginReceive(
           
_receiveBuffer, 0, _policyRequestString.Length, SocketFlags.None,
            
new AsyncCallback(OnReceiveComplete), null);
   
}

    private void OnReceiveComplete(IAsyncResult result)
   
{
      
try
      
{
         
_receivedLength += _client.Client.EndReceive(result); 

          if (_receivedLength < _policyRequestString.Length)
         
{
             
_client.Client.BeginReceive(
                       
_receiveBuffer,
                       
_receivedLength,
                       
_policyRequestString.Length – _receivedLength,
                       
SocketFlags.None,
                       
new AsyncCallback(OnReceiveComplete), null); 

               return;
         
} 

          //Check <policy-file-request/>
         
string request = System.Text.Encoding.UTF8.GetString(
               
_receiveBuffer,
               
0,
               
_receivedLength); 

          if (StringComparer.InvariantCultureIgnoreCase.Compare(request, _policyRequestString) != 0)
         
{
            
//Isn’t valid… bye bye
            
_client.Client.Close();

            
return;
         
}

          //Is Okay….send policy file
         
_client.Client.BeginSend(
                   
_policy,
                   
0,
                   
_policy.Length,
                   
SocketFlags.None,
                   
new AsyncCallback(OnSendComplete),
                   
null);
      
}
     
 catch (Exception ex)
      
{
         
_client.Client.Close();
         
Console.Write(ex.ToString());
      
}

       _receivedLength = 0;
      
_tcpClientConnected.Set(); //Allow waiting thread to proceed
 
}

  private void OnSendComplete(IAsyncResult result)
 
{
     
tr
     
{
        
_client.Client.EndSendFile(result);
     
}
     
catch (Exception ex)
     
{
        
Console.Write(ex.ToString());
     
}
     
finally           
     
{
        
_client.Client.Close();
     
}
  
}
}

 

2 comentarios en “Silverlight y los Sockets”

  1. Muy buen post, si señor. Sin embargo, me surge una duda… Se comenta que hay que crear un servidor que escuche las peticiones del puerto 943, ya que al desarrollar en VS, el servidor no da la opción de descargar el archivo de políticas desde el puerto 80.

    Pero si desarrollamos la aplicación, basándonos en el IIS, no debería funcionar ? O hay que hacer algo ? Asi nos evitariamos tener que desarrollar el servidor del puerto 943.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *