Utilizando Sockets en VB .NET

[Enviar y recibir mensajes]

Fecha: 12/Jul/04 (09/07/04)
Autor: Tilli, Pablo D. tillipablo@hotmail.com 


Introducción

 

Hoy quiero aportar un granito de arena escribiendo mi primer tutorial, para que el intercambio de información entre los desarrolladores siga creciendo día a día. Creo que es necesaria esta divulgación de conocimientos, ya que como es mi caso (y supongo el de muchas personas mas) fue uno de mis principales medios de aprendizaje. Si bien concurro a cursos y a la universidad, este camino tiene otras características que hacen mucho más llevadera la etapa de aprender, permitiendo manejar mejor los tiempos de cada uno.

 

Bueno, ahora si es hora de explicar un poco de que vamos a hablar en este tutorial. La idea es poder implementar dos clases: Cliente y Servidor. Probablemente el nombre de las clases digan todo, pero igual voy explicar un poco mas de que se trata. Un objeto de la clase Cliente, podrá conectarse a un objeto de la clase Servidor, permitiendo el envió de mensajes en los dos sentidos (Cliente a Servidor y Servidor a Cliente).

 

Esto puede ser útil en una gran cantidad de aplicaciones, donde se necesita una comunicación entre dos o más computadoras conectadas en red.

 

Un poco de teoría

 

Se que esto de la teoría muchas veces resulta aburrido, pero la verdad es importante tener una base teórica antes de ponerse a trabajar en el código: “Uno no puede codificar lo que no sabe”.

 

Como dije al principio, el objetivo es lograr una conexión entre varias computadoras. Para esto necesitamos algún medio por el cual se pueda mantener una “conversación” entre las mismas. Aquí entran en juego los famosos “Sockets”, pero ¿Qué es un socket?

 

¿Qué es un socket?

 

Seguramente habrás escuchado nombrar la palabra Socket muchas veces, y posiblemente te preguntaste que era, así que intentare sacar tu duda de la manera mas simple: un Socket es una relación entre un puerto de un equipo y el puerto de otro equipo. Ahora nombre la palabra puerto, ¿pero de que se trata esto de los puertos?

 

¿Qué es un puerto?

 

Los puertos son básicamente, una entrada/salida de información. Estos se encuentran identificados por un número entero y muchos se encuentran reservados para determinadas tareas, por ejemplo: el puerto 80 es para un servidor web.

 

Conclusión

 

Ahora bien, para que nos sirve saber que es un Socket y un Puerto (Al menos tener una idea de que son). Si miramos con atención nos daremos cuenta que es justo lo que necesitamos para nuestro propósito.

 

Tendremos un objeto de la clase Servidor, que creara un Socket que se quedara “escuchando” en un puerto que elijamos nosotros. El servidor seria un extremo del Socket, pero para que un Socket este realmente completo necesitamos otro extremo, que obviamente serán objetos de la clase Cliente, que se conectaran al puerto del equipo donde este el objeto servidor.

 

Si todavía no queda del todo claro, sólo seguí leyendo y todo se ira aclarando de a poco, cuando veamos algo de código.

 

Manos a la obra

 

Antes de mostrar el código, les voy a describir las propiedades, métodos y eventos de las clases a implementar.

 

El Servidor

 

*      Propiedades

 

PuertoDeEscucha() As String

 

Establece/devuelve el puerto donde se quiere que el servidor quede “escuchando”.

 

*      Métodos

 

Escuchar()

 

Inicia el proceso de escuchar peticiones de conexión de parte de los clientes, en el puerto establecido en la propiedad PuertoDeEscucha.

 

ObtenerDatos(ByVal IDCliente As Net.IPEndPoint) As String

 

Obtiene los últimos datos enviados por el cliente especificado.

 

Cerrar(ByVal IDCliente As Net.IPEndPoint)

 

Cierra la conexión con el cliente especificado.

 

Cerrar()

 

Cierra todas las conexiones abiertas con los clientes.

 

EnviarDatos(ByVal IDCliente As Net.IPEndPoint, ByVal Datos As String)

 

Envía un mensaje al cliente especificado.

 

EnviarDatos(ByVal Datos As String)

 

Envía un mensaje a todas los clientes.

 

*      Eventos

 

NuevaConexion(ByVal IDTerminal As Net.IPEndPoint)

 

Se produce cuando un Cliente se conecta al Servidor, y nos devuelve un ID, para que podamos identificarlo mas tarde.

 

DatosRecibidos(ByVal IDTerminal As Net.IPEndPoint)

 

Se produce cuando un Cliente nos envía un mensaje. Para obtener los datos recibidos, usaremos el método ObtenerDatos pasándole como parámetro el ID del cliente.

 

ConexionTerminada(ByVal IDTerminal As Net.IPEndPoint)

 

Nos avisa que se ha cerrado la conexión con el Cliente recibido como parámetro.

 

 

El Cliente

 

*      Propiedades

 

IPDelHost() As String

 

Establece/devuelve la dirección IP (o el nombre DNS) del equipo donde se encuentra el objeto de la clase Servidor.

 

PuertoDelHost() As String

 

Establece/devuelve el numero de puerto en el que estará escuchando el objeto de la clase Servidor al cual nos queremos conectar.

 

*      Métodos

 

Conectar()

 

Permite conectarse al objeto Servidor que se encuentra escuchando en la dirección especificada por la propiedad IPDelHost en el puerto establecido en la propiedad PuertoDelHost.

 

EnviarDatos(ByVal Datos As String)

 

Envía un mensaje al objeto de la clase Servidor a la que se este conectado.

 

*      Eventos

 

ConexionTerminada()

 

Se produce cuando se termina la conexión con el objeto Servidor.

 

DatosRecibidos(ByVal Datos As String)

 

Nos avisa que el servidor nos ha enviado un mensaje, y el mismo se encuentra en el parámetro “Datos”.

 

 

Al fin, ahora si llego la hora de ver código… empecemos.

 

Código de la clase Servidor

 

Imports System

Imports System.Threading

Imports System.Net.Sockets

Imports System.IO

Imports System.Text

 

Public Class WinSockServer

 

#Region "ESTRUCTURAS"

    Private Structure InfoDeUnCliente

        'Esta estructura permite guardar la información sobre un cliente

 

        Public Socket As Socket 'Socket utilizado para mantener la conexion con el cliente

        Public Thread As Thread 'Thread utilizado para escuchar al cliente

        Public UltimosDatosRecibidos As String 'Ultimos datos enviados por el cliente

    End Structure

#End Region

 

#Region "VARIABLES"

    Private tcpLsn As TcpListener

    Private Clientes As New Hashtable() 'Aqui se guarda la informacion de todos los clientes conectados

    Private tcpThd As Thread

    Private IDClienteActual As Net.IPEndPoint 'Ultimo cliente conectado

    Private m_PuertoDeEscucha As String

#End Region

 

#Region "EVENTOS"

    Public Event NuevaConexion(ByVal IDTerminal As Net.IPEndPoint)

    Public Event DatosRecibidos(ByVal IDTerminal As Net.IPEndPoint)

    Public Event ConexionTerminada(ByVal IDTerminal As Net.IPEndPoint)

#End Region

 

#Region "PROPIEDADES"

    Property PuertoDeEscucha() As String

        Get

            PuertoDeEscucha = m_PuertoDeEscucha

        End Get

 

        Set(ByVal Value As String)

            m_PuertoDeEscucha = Value

        End Set

    End Property

#End Region

 

#Region "METODOS"

 

    Public Sub Escuchar()

        tcpLsn = New TcpListener(PuertoDeEscucha)

        'Inicio la escucha

        tcpLsn.Start()

 

        'Creo un thread para que se quede escuchando la llegada de un cliente

        tcpThd = New Thread(AddressOf EsperarCliente)

        tcpThd.Start()

    End Sub

 

    Public Function ObtenerDatos(ByVal IDCliente As Net.IPEndPoint) As String

        Dim InfoClienteSolicitado As InfoDeUnCliente

 

        'Obtengo la informacion del cliente solicitado

        InfoClienteSolicitado = Clientes(IDCliente)

 

        ObtenerDatos = InfoClienteSolicitado.UltimosDatosRecibidos

    End Function

 

    Public Sub Cerrar(ByVal IDCliente As Net.IPEndPoint)

        Dim InfoClienteActual As InfoDeUnCliente

 

        'Obtengo la informacion del cliente solicitado

        InfoClienteActual = Clientes(IDCliente)

 

        'Cierro la conexion con el cliente

        InfoClienteActual.Socket.Close()

    End Sub

 

    Public Sub Cerrar()

        Dim InfoClienteActual As InfoDeUnCliente

 

        'Recorro todos los clientes y voy cerrando las conexiones

        For Each InfoClienteActual In Clientes.Values

            Call Cerrar(InfoClienteActual.Socket.RemoteEndPoint)

        Next

    End Sub

 

    Public Sub EnviarDatos(ByVal IDCliente As Net.IPEndPoint, ByVal Datos As String)

        Dim Cliente As InfoDeUnCliente

 

        'Obtengo la informacion del cliente al que se le quiere enviar el mensaje

        Cliente = Clientes(IDCliente)

 

        'Le envio el mensaje

        Cliente.Socket.Send(Encoding.ASCII.GetBytes(Datos))

    End Sub

 

    Public Sub EnviarDatos(ByVal Datos As String)

        Dim Cliente As InfoDeUnCliente

 

        'Recorro todos los clientes conectados, y les envio el mensaje recibido

        'en el parametro Datos

        For Each Cliente In Clientes.Values

            EnviarDatos(Cliente.Socket.RemoteEndPoint, Datos)

        Next

    End Sub

 

#End Region

 

#Region "FUNCIONES PRIVADAS"

    Private Sub EsperarCliente()

        Dim InfoClienteActual As InfoDeUnCliente

 

        With InfoClienteActual

 

            While True

                'Cuando se recibe la conexion, guardo la informacion del cliente

 

                'Guardo el Socket que utilizo para mantener la conexion con el cliente

                .Socket = tcpLsn.AcceptSocket() 'Se queda esperando la conexion de un cliente

 

                'Guardo el el RemoteEndPoint, que utilizo para identificar al cliente

                IDClienteActual = .Socket.RemoteEndPoint

 

                'Creo un Thread para que se encargue de escuchar los mensaje del cliente

                .Thread = New Thread(AddressOf LeerSocket)

 

                'Agrego la informacion del cliente al HashArray Clientes, donde esta la

                'informacion de todos estos

                SyncLock Me

                    Clientes.Add(IDClienteActual, InfoClienteActual)

                End SyncLock

 

                'Genero el evento Nueva conexion

                RaiseEvent NuevaConexion(IDClienteActual)

 

                'Inicio el thread encargado de escuchar los mensajes del cliente

                .Thread.Start()

            End While

 

        End With

 

    End Sub

 

    Private Sub LeerSocket()

        Dim IDReal As Net.IPEndPoint 'ID del cliente que se va a escuchar

        Dim Recibir() As Byte 'Array utilizado para recibir los datos que llegan

        Dim InfoClienteActual As InfoDeUnCliente 'Informacion del cliente que se va escuchar

        Dim Ret As Integer = 0

 

        IDReal = IDClienteActual

        InfoClienteActual = Clientes(IDReal)

 

        With InfoClienteActual

 

            While True

                If .Socket.Connected Then

                    Recibir = New Byte(100) {}

 

                    Try

                        'Me quedo esperando a que llegue un mensaje desde el cliente

                        Ret = .Socket.Receive(Recibir, Recibir.Length, SocketFlags.None)

 

                        If Ret > 0 Then

                            'Guardo el mensaje recibido

                            .UltimosDatosRecibidos = Encoding.ASCII.GetString(Recibir)

                            Clientes(IDReal) = InfoClienteActual

 

                            'Genero el evento de la recepcion del mensaje

                            RaiseEvent DatosRecibidos(IDReal)

                        Else

                            'Genero el evento de la finalizacion de la conexion

                            RaiseEvent ConexionTerminada(IDReal)

                            Exit While

                        End If

 

                    Catch e As Exception

                        If Not .Socket.Connected Then

                            'Genero el evento de la finalizacion de la conexion

                            RaiseEvent ConexionTerminada(IDReal)

                            Exit While

                        End If

                    End Try

                End If

            End While

 

            Call CerrarThread(IDReal)

        End With

    End Sub

 

    Private Sub CerrarThread(ByVal IDCliente As Net.IPEndPoint)

        Dim InfoClienteActual As InfoDeUnCliente

 

        'Cierro el thread que se encargaba de escuchar al cliente especificado

        InfoClienteActual = Clientes(IDCliente)

 

        Try

            InfoClienteActual.Thread.Abort()

 

        Catch e As Exception

            SyncLock Me

                'Elimino el cliente del HashArray que guarda la informacion de los clientes

                Clientes.Remove(IDCliente)

            End SyncLock

        End Try

 

    End Sub

 

#End Region

 

End Class

 

 

Código de la clase Cliente

 

Imports System

Imports System.Net

Imports System.Net.Sockets

Imports System.Threading

Imports System.Text

Imports System.IO

 

Public Class Cliente

 

#Region "VARIABLES"

    Private Stm As Stream 'Utilizado para enviar datos al Servidor y recibir datos del mismo

    Private m_IPDelHost As String 'Direccion del objeto de la clase Servidor

    Private m_PuertoDelHost As String 'Puerto donde escucha el objeto de la clase Servidor

#End Region

 

#Region "EVENTOS"

    Public Event ConexionTerminada()

    Public Event DatosRecibidos(ByVal datos As String)

#End Region

 

#Region "PROPIEDADES"

    Public Property IPDelHost() As String

        Get

            IPDelHost = m_IPDelHost

        End Get

 

        Set(ByVal Value As String)

            m_IPDelHost = Value

        End Set

    End Property

 

    Public Property PuertoDelHost() As String

        Get

            PuertoDelHost = m_PuertoDelHost

        End Get

        Set(ByVal Value As String)

            m_PuertoDelHost = Value

        End Set

    End Property

#End Region

 

#Region "METODOS"

    Public Sub Conectar()

        Dim tcpClnt As TcpClient

        Dim tcpThd As Thread 'Se encarga de escuchar mensajes enviados por el Servidor

 

        tcpClnt = New TcpClient()

        'Me conecto al objeto de la clase Servidor,

        '  determinado por las propiedades IPDelHost y PuertoDelHost

        tcpClnt.Connect(IPDelHost, PuertoDelHost)

        Stm = tcpClnt.GetStream()

 

        'Creo e inicio un thread para que escuche los mensajes enviados por el Servidor

        tcpThd = New Thread(AddressOf LeerSocket)

        tcpThd.Start()

    End Sub

 

    Public Sub EnviarDatos(ByVal Datos As String)

        Dim BufferDeEscritura() As Byte

 

        BufferDeEscritura = Encoding.ASCII.GetBytes(Datos)

 

        If Not (Stm Is Nothing) Then

            'Envio los datos al Servidor

            Stm.Write(BufferDeEscritura, 0, BufferDeEscritura.Length)

        End If

    End Sub

 

#End Region

 

#Region "FUNCIONES PRIVADAS"

    Private Sub LeerSocket()

        Dim BufferDeLectura() As Byte

 

        While True

            Try

                BufferDeLectura = New Byte(100) {}

                'Me quedo esperando a que llegue algun mensaje

                Stm.Read(BufferDeLectura, 0, BufferDeLectura.Length)

 

                'Genero el evento DatosRecibidos, ya que se han recibido datos desde el Servidor

                RaiseEvent DatosRecibidos(Encoding.ASCII.GetString(BufferDeLectura))

            Catch e As Exception

                Exit While

            End Try

        End While

 

        'Finalizo la conexion, por lo tanto genero el evento correspondiente

        RaiseEvent ConexionTerminada()

    End Sub

#End Region

 

End Class

 

¿Cómo utilizar las clases?

 

Bueno, hemos terminado con lo que se refiere al código de las clases Cliente y Servidor, pero antes de terminar con el tutorial vamos a ver un pequeño ejemplo de como utilizarlas. En mi caso tengo la clase Cliente por un lado (Cliente.dll) y el Servidor por el otro (Servidor.dll). Lo que vamos ha hacer en este ejemplo es crear dos pequeñas aplicaciones donde una se quede escuchando (El Servidor), y otra que se conecta a la que escucha (Cliente). Ambas podrán enviar y recibir mensajes.

 

Aplicación Servidor

 

 

Nota: Se debe establecer una referencia a Servidor.dll.

 

Public Class Form1

    Inherits System.Windows.Forms.Form

 

    " Código generado por el Diseñador de Windows Forms "

 

    Dim WithEvents WinSockServer As New Servidor()

 

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        With WinSockServer

            'Establezco el puerto donde escuchar

            .PuertoDeEscucha = 8050

 

            'Comienzo la escucha

            .Escuchar()

        End With

    End Sub

 

    Private Sub WinSockServer_NuevaConexion(ByVal IDTerminal As System.Net.IPEndPoint) Handles WinSockServer.NuevaConexion

        'Muestro quien se conecto

        MsgBox("Se ha conectado un nuevo cliente desde la IP= " & IDTerminal.Address.ToString & _

                                                                ",Puerto = " & IDTerminal.Port)

    End Sub

 

 

    Private Sub WinSockServer_ConexionTerminada(ByVal IDTerminal As System.Net.IPEndPoint) Handles WinSockServer.ConexionTerminada

        'Muestro con quien se termino la conexion

        MsgBox("Se ha desconectado el cliente desde la IP= " & IDTerminal.Address.ToString & _

                                                             ",Puerto = " & IDTerminal.Port)

 

    End Sub

 

    Private Sub WinSockServer_DatosRecibidos(ByVal IDTerminal As System.Net.IPEndPoint) Handles WinSockServer.DatosRecibidos

        'Muestro quien envio el mensaje

        MsgBox("Nuevo mensaje desde el cliente de la IP= " & IDTerminal.Address.ToString & _

                                                   ",Puerto = " & IDTerminal.Port )

 

        'Muestro el mensaje recibido

        Call MsgBox(WinSockServer.ObtenerDatos(IDTerminal))

 

    End Sub

 

    Private Sub btnEnviarMensaje_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEnviarMensaje.Click

        'Envio el texto escrito en el textbox txtMensaje a todos los clientes

        WinSockServer.EnviarDatos(txtMensaje.Text)

    End Sub

End Class

 

 

Aplicación Cliente

 

 

 

Nota: Se debe establecer una referencia a Cliente.dll.

 

Public Class Form1

    Inherits System.Windows.Forms.Form

 

    " Código generado por el Diseñador de Windows Forms "

 

    Dim WithEvents WinSockCliente As New Cliente

 

    Private Sub btnConectar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConectar.Click

        With WinSockCliente

            'Determino a donde se quiere conectar el usuario

            .IPDelHost = txtIP.Text

            .PuertoDelHost = txtPuerto.Text

 

            'Me conecto

            .Conectar()

 

            'Deshabilito la posibilidad de conexion

            txtIP.Enabled = False

            txtPuerto.Enabled = False

            btnConectar.Enabled = False

 

 

            'Habilito la posibilidad de enviar mensajes

            btnEnviarMensaje.Enabled = True

            txtMensaje.Enabled = True

        End With

    End Sub

 

    Private Sub WinSockCliente_DatosRecibidos(ByVal datos As String) Handles WinSockCliente.DatosRecibidos

        MsgBox("El servidor envio el siguiente mensaje: " & datos)

    End Sub

 

    Private Sub WinSockCliente_ConexionTerminada() Handles WinSockCliente.ConexionTerminada

        MsgBox("Finalizo la conexion")

 

        'Habilito la posibilidad de una reconexion

        txtIP.Enabled = True

        txtPuerto.Enabled = True

        btnConectar.Enabled = True

 

        'Deshabilito la posibilidad de enviar mensajes

        btnEnviarMensaje.Enabled = False

        txtMensaje.Enabled = False

    End Sub

 

    Private Sub btnEnviarMensaje_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEnviarMensaje.Click

        'Envio lo que esta escrito en la caja de texto del mensaje

        WinSockCliente.EnviarDatos(txtMensaje.Text)

    End Sub

 

    Private Sub Form1_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing

        End

    End Sub

 

End Class

 

 

Conclusión

 

Como conclusión, creo que si bien queda bastante por hacer (Validaciones, agregar mayor funcionalidad a las clases, etc.) este tutorial puede ser considerado como una introducción a la utilización de Sockets, que cubre lo básico para lograr una comunicación entre aplicaciones que se encuentran en distintas computadoras.

 

Quisiera que cualquier duda, sugerencia, crítica, mejora o cualquier otra cosa, me la hagan llegar mi correo electrónico (tillipablo@hotmail.com).

 

Espero, que este tutorial sea el primero de una larga lista, y espero también que a partir de las críticas que vaya recibiendo, pueda ir mejorando la manera de presentar el tutorial.

 

ir al ndice