Colabora
 

Emulación Multicast en TCP

Cómo crear un servidor de envíos multicast

 

Fecha: 03/Mar/2008 (03-03-08)
Autor: Jose Francisco Villanueva Moreno - [email protected]

 


Introducción

En este articulo se propone una pequeña solución al problema del envío simultaneo a muchos clientes por tcp, es por ello que implementaremos unas funciones para solventar de alguna forma dicho problema.

El articulo se dividirá en tres apartados:
1- Exposición de la solución justificando porque se eligió.
2- Explicación de las funciones más importantes para su implementación.
3- Pequeña soluciones para errores comunes en proyectos de sistemas distribuidos y Cliente-Servidor.

La idea con la clase que cree es la de un servidor que pudiera enviar datos multicast para una aplicación que tenía requisito de Tiempo Real.

Como anexo también pondré la funciones necesaria para recibir un fichero del servidor, esta función la tendrá que tener implementada el cliente si quiere poder recibir fichero del servidor.

El cliente no esta puesto porque se implementa como cualquier cliente normal exceptuando la función de recibir fichero, dicha función es la típica que se asocia a el proceso de revivir_datos.

Contenido 1

1-Explicación de la solución propuesta:

Todos no hemos encontrado con el problema del envío multicast en tcp, y por tanto hemos estado obligados a utilizar el protocolo udp para nuestra tareas, pero algunas veces utilizar el protocolo udp no es solución debido a que puede ser que nuestro sistema tenga que respetar cierto requisitos , como tiempo real o seguridad en la transferencia de datos.

Es por ello que se me ocurrió utilizar procesos para los envíos multicast, en un principio esta solución conlleva dos preguntas:¿cuanto de grandes son los datos a enviar?¿y cuanto se debe de respetar el Tiempo Real?

Hay dos soluciones: Por un lado podemos crear un proceso de envío para cada cliente que tengamos conectado y que dicho proceso se encargue de enviar los datos, pero claro esta que si los datos son muy largos y se va a tardar mucho tiempo en enviar (como queremos respetar el Tiempo Real, mucho tiempo es un par de segundos) deberemos de crear nuestro planificador que divida el tiempo de uso de CPU o el tiempo de ejecución entre todos los procesos, para que de esta forma un proceso no utilice mucho tiempo de ejecución de forma continua.

Para ello podemos implementar planificadores que ya existen como Round-Robbin o EDF. Así que, como esta forma sería mas complicada de implementar sugiero que dividamos los datos en paquetes mas pequeños para de esta forma no tener que implementar un Planificador.

Por ello yo implemente la segunda opción, esta opción trata de enviar siempre mensajes de tamaño pequeño, con ello ya no tengo que crear un planificador, sino que solo tengo que crear una tarea encargada de enviar el dato a todos los clientes, esta tarea se llamara ENVIO_MULTICAST.

Dicho hilo o tarea se creará cada vez que queramos enviar un datos multicast y se destruirá cada vez que finalice de enviar dichos datos a todos los clientes, es por ello que lo mas importante de el proceso es que bloquee el acceso a la variable que hace referencia al socket para que no se produzcan problemas de concurrencia.

Un problema de concurrencia podría producirse si por ejemplo estamos enviando datos en multicast y el cliente decide también transferir cierta información a un cliente de forma independiente, en este caso podría ocurrir que el proceso principal del programa y el proceso de envio_multicast quisieran acceder al mismo tiempo al socket, es por ello que se bloqueará el acceso al socket cada vez que se haga uso de el, esto lo haremos haciendo uso de semáforos o de synclock.

Tras explicar el problema de la concurrencia solo queda solucionar el problema de las prioridades de los procesos que tenemos, por ahora solo tenemos el Procesos Principal, Proceso Escucha de conexiones Entrantes en el caso de ser un servidor, Proceso de recepción de datos para cada conexión establecida, y el Proceso de Envio Multicast. Puesto que nuestro requisito es que no perdamos Tiempo Real, le daremos prioridad Normal o Por encima de lo Normal al Proceso de Enviomulticast, y los demás tendrán por debajo de lo normal.

De todas formas el tema de las prioridades es independiente para los requisitos de cada sistema, por ejemplo en el caso de un chat en plan IRC lo mejor es que el proceso multicast tuviera por encima de lo normal y que luego los procesos de recepción de datos de cada uno de los canales tuviera prioridad normal, pero claro, esto es olo una idea.

Como pequeña aclaración diré que cuando creamos un hilo podemos asignarle la prioridad con el propiedad "priority", de todas formas en otros artículos viene mucho mejor explicado la utilización de hilos o Thread.

También hay que tener en cuenta que si nuestra aplicación es un servidor el cual solo realiza tareas de Intercomunicar mensajes entre los clientes, las prioridades del proceso de multicast puede ser por encima de lo normal, pero tened cuidad con el proceso de escucha de datos de los clientes, si le ponéis por encima de lo normal y dicho proceso recibe datos continuamente el acceso a la interfaz grafica de programa será complicada, puesto que un formulario principal tiene prioridad normal, y por tanto si un proceso con prioridad mayor que ella se quiere ejecutar, el formulario principal no tendrá tiempo de ejecución y por tanto no podremos interactuar mucho con el.

Tened en cuenta que un formulario se ejecuta con eventos que van activándose, por lo que si un proceso de prioridad superior a el esta ejecutándose siempre, será muy complicado de el formulario atienda los eventos que se produzcan.

Una vez comentando los aspectos más importantes a la hora de implementar un servidor multicast solo me queda comentar las estructuras de datos utilizadas para mi clase.

Cree una estructura de datos que contiene la información de cada cliente:Ip y puerto, Socket por el que envío y recibo datos, Un socket auxiliar para conexiones independientes, y los procesos encargados de controlar la recepción de datos.

Lo mas importante a explicar en referencia a lo anteriormente comentado es la cuestión de tener otro socket en escucha para cada cliente, la razón es poder realizar envíos punto-a -punto de archivos o datos mas grandes sin que interrumpa en el envío de datos en Tiempo Real. En mi caso en particular como el envío de fichero o de datos mas grandes por parte del servidor no se realizaba muy a menudo pues no me supuso un problema.

Sin embargo la idea es muy bueno para un gran problema que surge en algunos sistemas. ¿que ocurre si un cliente se conecta a un sistema que continuamente envía datos en Tiempo Real, pero además al ser nuestra primera conexión necesitamos los datos de una hora anterior, si utilizamos el mismo canal, el requisito de tiempo real se perdería, por lo que la solución a priori más efectiva es la de crear un canal independiente. Esta claro que el único problema que se os puede plantear viene del otro lado de la comunicación, el cliente.

¿que ocurriría si por el canal de Tiempo Real recibieran datos que solo pueden procesarse con la información que se tiene de un minuto anterior y sin embargo como me acabo de conectar y estoy actualizándome , no tengo dichos datos? Para este caso lo mejor es tener un buffer que almacene los datos que se reciben por el canal de Tiempo Real durante la actualización, y una vez acabada procese el buffer.

Nota:
A continuación solo muestro las funciones mas importantes de la clase, la clase entera se adjunta en el zip.

El código:

Las dos primeras funciones son las utilizadas para el envío multicast, como veréis no son nada del otro mundo. Por supuesto tener en cuenta que el código de la clase es mas grande.

 Public Sub EnviarDatos_multicast(ByVal Datos As String)
        Try
           
            If Not (Me.proceso_envio Is Nothing) Then
                ''esperamos por si todabia se estan enviando datos a todas las conexiones
                Me.proceso_envio.Join()
                Me.proceso_envio = New Thread(New ParameterizedThreadStart(AddressOf envio_multicast))
                Me.proceso_envio.Priority = ThreadPriority.Normal
                Me.proceso_envio.Start(Datos)
            Else
                Me.proceso_envio = New Thread(New ParameterizedThreadStart(AddressOf envio_multicast))
                Me.proceso_envio.Priority = ThreadPriority.Normal
                Me.proceso_envio.Start(Datos)
            End If
        Catch ex As Exception
            Throw New Exception("Error en EnvioDatos . CE:" + ex.Message)
        End Try
    End Sub

Private Sub envio_multicast(ByVal d As Object)
        Try
            Dim datos As String = d
            SyncLock Me.info_clientes.SyncRoot
                For Each c As InfoDeUnCliente In Me.info_clientes.Values
                    If c.Socket_Datos.Connected Then
                        Try
                            c.Socket_Datos.Send(Encoding.Default.GetBytes(datos))
                        Catch e As SocketException
                            RaiseEvent Mensages_error(c.Identificador.ToString, _
                                    "Error en el envio multicast.CE:" & e.ErrorCode)
                        End Try
                    Else

                    End If
                Next
            End SyncLock
        Catch ex As Exception
            Throw New Exception("Error en Envio_multicast. CE:" + ex.Message)
        End Try
    End Sub

 

Función para que el cliente reciba las transferencias de ficheros.

 

''La variable socket_cliente es el socket del cliente que se conecta con el servir
''Leer actualizacion es la funcion asociada a un proceso para espera datos del servidor 
Private Sub leerActualizacion()
        Dim fichero As FileStream
        Dim Recibir() As Byte
        Dim Ret As Integer = 0
        Dim tamaño As Integer
        Dim k As Integer
        Dim UltimosDatosRecibido As String = ""
        
            While True
                Try
                    If socket_cliente.Connected Then
                        Recibir = New Byte(62) {}
                        socket_cliente.ReceiveBufferSize = 62


                        'Me quedo esperando a que llegue un mensaje desde el cliente 
                        If Me.modo_lectura_normal = True Then
                            Ret = socket_cliente.Receive(Recibir)
                            If Ret > 0 Then

                                UltimosDatosRecibido = Trim(Encoding.ASCII.GetString(Recibir))

                                If InStr(UltimosDatosRecibido, "IF") Then
                                    tamaño = 0
                                    Dim p As paquete_tcp = empaqueta(UltimosDatosRecibido)
                                    k = 0
                                    tamaño = Int(p.op)

                                    fichero = New FileStream("FicheroTemp.xml", _
                                                FileMode.Create, FileAccess.Write, FileShare.ReadWrite)
                                    Me.modo_lectura_normal = False
                                 End If
                            Else
                                Throw New Exception()
                            End If
                        Else
                            Dim b(tamaño - 1) As Byte
                            socket_cliente.ReceiveBufferSize = tamaño
                            Ret = socket_cliente.Receive(b)
                            If Ret > 0 Then
                                If InStr(Encoding.ASCII.GetChars(b), "FA") > 0 Then
                                    Dim s As String = Encoding.ASCII.GetChars(b)
                                    s = Mid(s, 1, Ret - 2)
                                    If s <> "" Then
                                        fichero.Write(Encoding.ASCII.GetBytes(s), 0, s.Length)
                                    End If
                                    fichero.Close()
                                    Me.modo_lectura_normal = True
                                    .Close()
                                    Exit While
                                Else
                                    Dim s As String = Encoding.ASCII.GetChars(b)
                                    fichero.Write(b, 0, Ret)
                                    k = k + Ret
                                End If

                            Else
                                If Not (fichero Is Nothing) Then
                                    fichero.Close()
                                End If
                                RaiseEvent Mensages("Error en la tranferencia del fiechor")
                                Exit While

                            End If


                        End If
                    Else

                    End If
               Catch ex As Exception
                    ''Error en la transferencia
                End Try

            End While
        
    End Sub


 ''' <summary>
    ''' Empaqueta
    ''' </summary>
    ''' <param name="sms">Datos en formato String</param>
    ''' <returns>Devuelve el empaquetamiento de los datos a paquete_tcp</returns>
    ''' <remarks>Traduce los datos que llegan y los encapsula en el formato paquete_tcp</remarks>
    Public Function empaqueta(ByVal sms As String) As paquete_tcp
        sms = Trim(sms)
        Dim pa As paquete_tcp
        pa.codOp = Mid(sms, 1, 2)
        pa.tm = Int(Mid(sms, 3, 3))
        pa.op = Mid(sms, 6, pa.tm)
        Return pa
    End Function

 

3-Soluciones a problemas varios.

En este apartado explico algunos errores comunes que surgen al crear una aplicación con uso de socket.

Un error típico o por lo menos que en mis comienzos tuve, es creer que si yo enviaba 2 byte la función recibe de socket los recibiría sin problemas, pero esto no fue así, hay que tener en cuenta que si nuestro buffer pone que recibiremos 30 bytes pues la función recibe nos devolverá los datos cuando el buffer se haya llenado, por lo tanto si yo envío Hola y después envío Adios y mi buffer es de 7 byte, el socket me devolverá primero "holaAdi" y no "Hola" y luego "Adios".

Es por eso, que lo que yo hice fue encapsular mis mensajes siempre con un tamaño fijo, por ejemplo 60 byte, de tal forma que si mi mensaje tiene Adios y luego otro mensaje con Hola pues encapsulo "Adios" en mi paquete de 60 byte rellenado los que falten con espacios en blanco, de esta forma me aseguro que unos mensajes no se pisen con otros.

En mi caso y como podréis comprobar por la funciones que hay en la clase que os dejo yo cree un paquete con 3 campos:Codigo Operacion,Operandos, tamaño de los operandos y mi cliente luego lo desempaqueta de tal forma que lo puede procesar de forma correcta.

Otro problema cuando se trabaja con muchos hilos es la concurrencia, tenemos que tener en cuenta que dos procesos no deben de modificar o acceder a recursos simultáneamente, por lo tanto tenemos que tener cuidad en los código de los procesos con utilizar semáforos o en mi caso synclock, para de esta forma salvaguardarnos de problemas de este tipo.

Para finalizar nada más decir que la aplicación funciona al 100% y que esta probada con uno sistema de 40 ordenadores, respetando bastante bien el tiempo real.

 


Espacios de nombres usados en el código de este artículo:

System.Windows.Forms
System.Threading
System.Net
System.IO
System.Text

 



Compromiso del autor del artículo con el sitio del Guille:

Lo comentado en este artículo está probado (y funciona) con la siguiente configuración:

El autor se compromete personalmente de que lo expuesto en este artículo es cierto y lo ha comprobado usando la configuración indicada anteriormente.

En cualquier caso, el Guille no se responsabiliza del contenido de este artículo.

Si encuentras alguna errata o fallo en algún link (enlace), por favor comunícalo usando este link:

Gracias.


Código de ejemplo (comprimido):

 

Fichero con el código de ejemplo: jose_villanueva_Emulacion_Multicast_en_TCP.zip - 5.38 KB

(MD5 checksum: 23BAD1F0C6122793A1243CBB805697FB)

 


Ir al índice principal de el Guille