Colabora
 

Manejo de hilos (Threads) en VB.NET

Escaneador de puertos TCP abiertos

 

Fecha: 23/Dic/2007 (22-12-07)
Autor: Federico Daniel Colombo Gennarelli – [email protected]

 


Introducción

Leyendo una revista de informática vi que Java trae una interfaz llamada Executor que permite el manejo de hilos concurrentes de alto nivel. En la revista se ejemplificaba su uso con un sencillísimo ejemplo de un comprobador de puertos TCP abiertos. Luego también vi por ahí (http://www.ibm.com/...) que existe otra clase llamada PooledExecutor que permite la implementación de estructuras más complejas de hilos encolados (threadpools).  Luego en internet encontré alguna información de Hilos aunque la mayoría en lenguaje C#.

Fue entonces cuando se me ocurrió hacer una versión en VB.NET de un programa que escanee (compruebe) un rango de puertos para determinar cuales están abiertos y utilizarlo con las diferentes anternativas que da VB.NET 2005 en lo que respecta al manejo de hilos (threads, hebras, subprocesos, ...) concurrentes.

Nota:
El concepto de tareas concurrentes en idioma inglés es deniminado Threading y a cada una de las tareas se las denomina simplemente Thread. En español las traducciones que he visto son hilo, hebray subproceso.

Conceptos generales

Para este programa (el escaneador de puertos), se necesita una clase que se encargue de determinar si un puerto de un host específico está abierto o no (clase TCPPort). Esta clase expondrá un método que, a partir de un Host (o IP) y un Puerto, intentará crear una conexión TCP a éste mediante un Socket y generará un evento indicando el resultado de dicho intento.

Por otro lado, es necesaria otra clase (clase PortScanner) que se encargue de validar el estado de un rango de puertos (para un host específico), esta clase ejecutará un hilo por cada puerto a escanear y permitirá la consulta del estatus en tiempo real, es decir, permitirá conocer cuántos hilos se están corriendo en cualquier momento de la ejecución del programa. 

Y por último explotar a la clase PortScanner (una interfaz), informando el estatus del proceso en todo momento y el tiempo total de ejecución. También es deseable que se permita cancelar el proceso en cualquier momento.

Antes de empezar con el programa, expondré dos ejemplos básicos necesarios (tal vez) para comprender el resto del artículo: Cómo determinar si un puerto está abierto y un ejemplo de hilos.

Cómo determinar si un puerto está abierto

Para detectar si un puerto está abierto, basta con intentar conectar un Socket a dicho puerto. En VB.NET la manera más sencilla (al menos para mí) se muestra en el siguiente código:

Imports System.Net.Sockets
 
Public Function IsPortOpen(ByVal Host As String, ByVal Port As Integer) As Boolean
    Dim m_sck As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
    Try
        m_sck.Connect(Host, Port)
        Return True
    Catch ex As SocketException
        'Código para manejar error del socket (cerrado, conexión rechazada)
    Catch ex As Exception
        'Código para manejar otra excepción
    End Try
    Return False
End Function

Como se puede apreciar, solamente se crea el objeto de tipo Socket y se intenta conectarlo mediante la función sincrónica Connect. Esta función bloqueará la ejecución hasta obtener una respuesta (favorable o no).

La llamada a la función Connect está dentro de un bloque Try-Catch puesto que esta función generará excepciones dependiendo del motivo por el cual no pudo abrir el socket hacia el puerto indicado. En este caso sólo nos interesa saber si se pudo o no.

Hilos en VB.NET

Ahora la parte interesante, los hilos. En este artículo el Guille explica los conceptos básicos de Threads en VB.NET. También recomiendo este otro artículo de Joseph Albahari que, aunque esté en inglés y sea para C#, explica de manera detallada cada uno de los conceptos de ejecución concurrente de hilos.

La forma más fácil de visualizar la implementación de hilos es con un ejemplo. A continuación se muestra un ejemplo básico de la utilización de dos hilos (Thread) para ejecutar un método (sub) con parámetros:

Imports System.Threading

Module PruebaHilo

    'Sub que ejecutarán los hilos
    Public Sub MiSub(ByVal Parametro As Object)
        Try
            Randomize()
            Do
                Dim iDormir As Integer = CInt(3000 * Rnd()) 'Valor random entre 0 y 3000
                Console.WriteLine("{0} sleep({1})", Parametro, iDormir)
                Thread.Sleep(iDormir) 'Me bloqueo entre 0 y 3 segundos
            Loop
        Catch ex As ThreadAbortException
            Console.WriteLine("{0} Abortado", Parametro)
        End Try
    End Sub

    'Sub principal
    Sub Main()
        Dim hilo1 As New Thread(AddressOf MiSub) 'Crear el hilo 1
        Dim hilo2 As New Thread(AddressOf MiSub) 'Crear el hilo 2

        Console.WriteLine("Ejecutando hilos a abortar en 6 segudos...")
        hilo1.Start("hilo 1 ")                   'Comenzar ejecución de hilo 1
        Thread.Sleep(500)                        'Me Bloqueo 500 ms
        hilo2.Start(" hilo 2")                   'Comenzar ejecución de hilo 2
        Thread.Sleep(6000)                       'Me bloqueo 6 segundos

        hilo1.Abort()                            'Abortar al hilo 1
        hilo2.Abort()                            'Abortar al hilo 2

        'Esperar a que realmente mueran los hilos
        While hilo1.IsAlive Or hilo2.IsAlive
        End While

        Console.WriteLine("Abortados")
        Console.ReadKey()
    End Sub
End Module

En este caso el método MiSub es el proceso que será ejecutado por los hilos. Este método contien un bucle (do-loop) infinito que escribe una línea y luego se bloquea (Thread.Sleep) durante un tiempo aleatorio comprendido entre 0 y 3 segundos (iDormir). Notar que todo el código del sub está dentro de un bloque try-catch esto no es obligatorio, pero sirve para atrapar la excepción (ThreadAbortException) producida al abortar explícitamente al hilo con el método Abort.

En el sub Main puede apreciarse la creación de dos objetos del tipo Thread (hilo1, hilo2). Luego se comienza la ejecución del primer hilo con la instrucción hilo1.Start. Inmediatamente continúa la ejecución de la siguiente línea Thread.Sleep(500) que bloquea (al sub main) durante 500 milisegundos, esto fue necesario para que el generador de números aleatorios no tomara la misma semilla en ambos hilos.

A continuación se ejecuta al segundo hilo hilo2.Start y se esperan 6 segundos para luego dar la orden de abort a ambos hilos.

Por último, el while sirve para esperar a que los hilos hayan finalizado realmente su ejecución mediante la propiedad IsAlive de la clase Thread. Un hilo finaliza (muere) al finalizar el método que ejecuta (en este caso MiSub) o hasta haber recibido y procesado la excepción de abort. 

A continuación se muestra el resultado de dos ejecuciones diferentes del mismo programa:

Nota:
El método Start de la clase Thread puede ser llamado sin parámetros o con un parámetro del tipo Object. Al pasarse un parámetro del tipo Object éste termina siendo el valor pasado como parámetro a la función que ejecutará el hilo.

La Clase TCPPort

La función IsPortOpen tal y como se describió más arriba no puede ser utilizada para ser ejecutada por un hilo, básicamente por dos razones:

Primero porque IsPortOpen es una función y, como tal, devuelve un valor, pero como la ejecución será asincrónica, no podemos esperar que se devuelva un valor.

Segundo, la función IsPortOpen recibe dos parámetros (Host y Puerto) y sólo está permitido un parámetro como máximo para ser pasado al método (sub) que ejecute un hilo

Para la primera situación, la solución más lógica es declarar un evento estático (compartido por todos los elementos de la clase) el cual será disparado por el sub, por ejemplo, al finalizar el intento de conexión. Así, ya no es necesario un valor de retorno; el mismo evento pasará como parámetros los valores necesarios para indicar tanto el puerto del que se trata como su estatus.

Para la segunda situación, para poder enviar más de un parámetro al método que ejecute el hilo, se declara una estructura (Structure) con los tipos de datos necesarios y luego se pasa una instancia de dicha estructura (casteada como Object) a la función Start de la clase Thread. (en el código de ejemplo que sigue a continuación se ve esto más claramente en el Structure HostPortData)

Por lo tanto es necesario generar una clase (que se llamará TCPPort) que exponga un evento estático y que posea un método (sub) que sólo reciba un parámetro del tipo Object y se encargue de intentar abrir un puerto y disparar al evento con el resultado. Aquí está:

Imports System.Net.Sockets
Public Class TCPPort
    'Variable pública estática (shared) para mantener una cuenta de hilos en ejecución 
    '(hilos que están corriendo el sub PortOpen)
    Public Shared m_CountThreads As Long = 0
 
    'Evento estático (shared) que se disparará con cada puerto que finalice de procesarse
    Public Shared Event PuertoProcesado(ByVal Host As String, ByVal Port As Integer, _
    ByVal bPuertoAbierto As Boolean, ByVal sMensaje As String)
 
    'Structure que contendrá la información pasada como parámetro al sub PortOpen
    'Esto porque no podemos pasar más de un parámetro al sub que ejecute el hilo
    Friend Structure HostPortData
        Dim Host As String
        Dim Port As Integer
    End Structure
 
    'Función que checa si un puerto está abierto y dispara el evento PuertoProcesado
    'Este sub será ejecutado por los hilos
    Public Sub PortOpen(ByVal Data As Object)
        'Incremento la cantidad de hilos en ejecución
        System.Threading.Interlocked.Increment(m_CountThreads)
        'Creo un nuevo socket
        Dim sck As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
        'Casteo directamente el parámetro Data del tipo object al tipo HostPortData
        Dim Datos As HostPortData = DirectCast(Data, HostPortData)
 
        'Inicializo las variables que se pasarán al evento
        Dim bPuertoAbierto As Boolean = False, sError As String = ""
        Try
            'Inicio la conexión sincrónicamente 
            sck.Connect(Datos.Host, Datos.Port)
            'Si llega hasta aquí el porque pudo conectar => el puerto está abierto
            bPuertoAbierto = True
        Catch ex As Exception
            'Si finaliza con error => el puerto está cerrado y guardo la descripción del error
            sError = ex.Message
            bPuertoAbierto = False
        Finally
            'Disparo el evento de puerto procesado
            RaiseEvent PuertoProcesado(Datos.Host, Datos.Port, bPuertoAbierto, sError)
            'Decremento la cantidad de hilos en ejecución
            System.Threading.Interlocked.Decrement(m_CountThreads)
        End Try
    End Sub
End Class

La clase TCPPort expone un método (PortOpen) que puede ser llamado por un hilo para verificar el estado de un puerto y disparar un evento al finalizar dicha verificación.

Notar que tanto la variable m_CountThreads como el evento PuertoProcesado fueron declarados como estáticos (Shared), ésto hace que sean compartidos por todas las instancias de la clase.

La variable m_CountThreads es incrementada al inicio del método PortOpen y decrementada al final. Notar que este incremento/decremento se efectua mediante la clase System.Threading.Interlocked, que provee la forma de modificar variables compartidas de forma atómica (Descripción de la clase Interlocked)

Notar también que toda excepción generada dentro del Try-Catch es considerada como puerto cerrado.

Nota:
Una solución tal vez más óptima sería la de compartir el nombre del Host entre todos las instancias de la clase TCPPort (los hilos), ya que lo que varía es el número de puertos y no el host.

Otra mejora podría ser atrapar en el bloque try-catch las excepciones del tipo ThreadAbortException para manejar de forma diferentes los hilos que fueron abortados explícitamente por el usuario.

El escaneador (Clase PortScanner)

Entonces ahora sí. ¿Cómo ejecutar los hilos?. O mejor dicho, ¿Cómo dar la orden al sistema operativo para que los administre?.

Esto lo hará la clase PortScanner que se lista a continuación:

Imports system.Threading
 
Public Class PortScanner
    'Variable para manejar la condición de parada
    Private m_StopSignal As Boolean = False
 
    'Variable para interceptar al evento estático TCPPort.PuertoProcesado (es necesario el New)
    Private WithEvents oTCPPort As New TCPPort
 
    Public Event ScaneoIniciado()   'Evento que se dispara al iniciar el proceso de escaneo
 
    Public Event PuertoScaneado(ByVal Port As Integer, ByVal bAbierto As Boolean, _
                ByVal sMensaje As String) 'Evento que se dispara con cada puerto procesado
 
    Public Event ScaneoFinalizado(ByVal bStopped As Boolean) 'Evento disparado al finalizar el proceso
 
    'Hostname y rango de puertos
    Private m_Host As String
    Private m_PortIni As Integer
    Private m_PortFin As Integer
 
    Private m_Hilos() As Thread     'Arreglo de hilos (uno por puerto)
 
    'Constructor de la clase. 
    Public Sub New(ByVal Host As String, ByVal PortIni As Integer, ByVal PortFin As Integer)
        m_Host = Host
        m_PortIni = PortIni
        m_PortFin = PortFin
    End Sub
 
    'Sub llamado para cancelar el proceso 
    Public Sub [Stop]()
        m_StopSignal = True
    End Sub
 
    'Método que comienza el proceso de scanneo de puertos
    Public Sub Start()
        'Objeto del tipo TCPPort que contiene el método PortOpen que ejecutarán los hilos
        Dim Puerto As New TCPPort, i As Integer
 
        'Redimensiono el arreglo de hilos según cantidad de puertos a escanear
        ReDim m_Hilos(m_PortFin - m_PortIni + 1)
 
        Dim Datos As TCPPort.HostPortData       'Parámetros para el método TCPPort.PortOpen
 
        RaiseEvent ScaneoIniciado()             'Disparo el evento de comienzo  
        Datos.Host = m_Host                     'Especifico el Host
 
        'Ejecuto los hilos (uno por puerto)
        For i = 0 To m_PortFin - m_PortIni
            Datos.Port = i + m_PortIni                      'Especifico el Puerto
            m_Hilos(i) = New Thread(AddressOf Puerto.PortOpen) 'Direcciono el hilo
            m_Hilos(i).Start(Datos)                            'Inicio el hilo
            If m_StopSignal Then
                Exit For 'Salir del for si la señal de stop está activa
            End If
        Next i
 
        'Esto es para saber en qué momento se terminaron de encolar de hilos
        Console.WriteLine("--- Terminó el for de ejecución")
 
        'Espero asincrónicamente a que finalice el proceso completo
        Dim WaiterThread As New Thread(AddressOf Waiter)
        WaiterThread.Start()
    End Sub
 
    'Espera que finalicen todos los hilos del arreglo y luego dispara el evento de fin
    'También se encarga de abortar los hilos en caso de que se reciba una señal de parada
    Private Sub Waiter()
        Dim i, j As Integer
        Do
            j = 0
            For i = 0 To m_PortFin - m_PortIni
                If Not IsNothing(m_Hilos(i)) AndAlso m_Hilos(i).IsAlive() Then
                    j = 1
                    If m_StopSignal Then
                        m_Hilos(i).Abort()
                    End If
                End If
            Next
        Loop While j = 1
 
        RaiseEvent ScaneoFinalizado(m_StopSignal) 'Disparo el evento de escaneo finalizado
    End Sub
 
    'Intercepto el evento PuertoProcesado de la clase TCPPort y disparo el evento de ésta clase
    Private Sub oTCPPort_PuertoProcesado(ByVal Host As String, ByVal Port As Integer, ByVal bAbierto _
                                As Boolean, ByVal sMensaje As String) Handles oTCPPort.PuertoProcesado
        RaiseEvent PuertoScaneado(Port, bAbierto, sMensaje)
    End Sub
End Class

La clase PortScanner provee el mecanismo necesario para evaluar un rango de puertos mediante la ejecución de un hilo por cada puerto y además permite la interrupción del proceso en cualquier momento.

La variable booleana m_StopSignal se utiliza dentro de la clase para permitir la interrupción del proceso. Al llamar al método Stop esta señal es activada provocando la cancelación de todos los hilos.

Notar que en la línea Private WithEvents oTCPPort As New TCPPort se declara una instancia de la clase TCPPort. Esto es necesario para interceptar al evento estático que se dispara en los diferentes hilos al finalizar cada evaluación. Esta intercepción se da en el sub oTCPPort_PuertoProcesado que maneja al evento PuertoProcesado de la clase TCPPort.

La clase PortScanner expone 3 eventos: ScaneoIniciado, PuertoScaneado, ScaneoFinalizado. El primero se dispara al iniciar el escaneo, el segundo se dispara con cada evaluación de puerto finalizada y el tercero se dispara al finalizar totalmente el escaneo.

Se declara también un arreglo de hilos llamado m_Hilos() que contendrá un elemento por cada puerto a escanear.

En el método Start se crea el arreglo de hilos, se dispara el evento ScaneoIniciado y en el for, se van creando y dando las órdenes de ejecución para cada uno de los hilos. Asimismo, dentro de este for, se evalúa la condición de parada m_StopSignal para evitar seguir encolando hilos si el usuario canceló la operación. Al momento de la finalización de este for, el sistema operativo ya tendrá encolados los hilos que intentarán acceder a los puertos y seguramente algunos de éstos estén trabajando y otros ya hayan terminado.

A continuación se crea y ejecuta un hilo (WaiterThread) que ejecuta al método Waiter, encargado de esperar que todos los hilos del arreglo finalicen y luego disparar el evento de finalización ScaneoFinalizado. Esto se hace evaluando la propiedad IsAlive de cada uno de los hilos del arreglo. Aquí también se evalúa la condición de parada m_StopSignal para abortar los hilos en caso de ser necesario.

Por último en el manejador del evento TCPPort.PuertoProcesado se intercepta y dispara un evento propio de la clase (PuertoScaneado).

La interfaz (Módulo Main)

A continuación se muestra el código necesario para hacer funcionar (y ver cómo funciona) la clase PortScanner en una aplicación de consola:

Imports System.Threading
Imports System.Diagnostics

Module Main_PortScan
    Private WithEvents m_Scanner As PortScanner 'Instancia de la clase PortScanner
    Private m_stp As New Stopwatch      'Stopwatch para el cálculo de tiempo
    Private supervisor As Thread        'Hilo encargado de informar el estatus del proceso

    Sub main()
        Dim sHost As String, iPortIni As Integer, iPortFin As Integer

        Console.Write("Escriba el nombre del Host (enter para localhost): ")
        sHost = Console.ReadLine()
        If Len(Trim(sHost)) = 0 Then sHost = "localhost"

        Console.Write("Escriba el puerto inicial (enter para 1): ")
        iPortIni = CInt(Val(Console.ReadLine()))
        If iPortIni = 0 Then iPortIni = 1

        Console.Write("Escriba el puerto final (enter para 100): ")
        iPortFin = CInt(Val(Console.ReadLine()))
        If iPortFin = 0 Then iPortFin = 100

        'Nuevo hilo que se encargá de informar la cantidad de hilos en ejecución
        supervisor = New Thread(AddressOf subSupervisor)
        supervisor.Start()

        Console.WriteLine(vbCrLf & "Scanner iniciado para {0} puertos {1} al {2}", _
                          sHost, iPortIni, iPortFin)

        'Escanear puertos 
        m_Scanner = New PortScanner(sHost, iPortIni, iPortFin)
        m_Scanner.Start()
    End Sub

    'Sub para informar la cantidad de hilos en ejecución y determinar si se desea cancelar el proceso 
    '(Thread supervisor) 
    Private Sub subSupervisor()
        While True
            'Informo la cantidad de hilos en ejecución
            Console.Title = "Hilos en ejecución: " & TCPPort.m_CountThreads

            If Console.KeyAvailable Then                          'Si se presionó una tecla
                Dim cki As ConsoleKeyInfo = Console.ReadKey(True) 'obtengo cual fue
                If LCase(cki.KeyChar) = "s" Then                  'y si fue la tecla "s"...
                    Console.WriteLine("Inicia cancelación...")
                    m_Scanner.Stop()                              'Envío la señal de cancelación
                End If
            End If
        End While
    End Sub

    'Manejador del evento de escaneo iniciado
    Private Sub m_Scanner_ScaneoIniciado() Handles m_Scanner.ScaneoIniciado
        m_stp.Start() 'Inicio el cronómetro contador 
        Console.WriteLine("--- INICIA PROCESO (presione ""s"" para cancelar) ---")
    End Sub

    'Manejador del evento de puerto escaneado
    Private Sub m_Scanner_PuertoScaneado(ByVal Port As Integer, ByVal bAbierto As Boolean, _
                                ByVal sMensaje As String) Handles m_Scanner.PuertoScaneado
        Console.WriteLine("Puerto {0}: {1} ({2})", _
                          Port, IIf(bAbierto, "Abierto", "Cerrado"), Right(sMensaje, 44))
    End Sub

    'Manejador del evento de escaneo finalizado
    Private Sub m_Scanner_ScaneoFinalizado(ByVal bStopped As Boolean) Handles m_Scanner.ScaneoFinalizado
        Console.WriteLine("--- FIN PROCESO. Tiempo total: {0} segs. ---", m_stp.Elapsed.TotalSeconds)
        If bStopped Then Console.WriteLine("(El proceso fue abortado)")
        supervisor.Abort()      'Aborto el hilo que informa la cantidad de hilos
        Console.ReadKey()
    End Sub
End Module

El imports de System.Diagnostics es necesario para utilizar el StopWatch que sirve para manejar un cronómetro de forma fácil. La variable m_stp del tipo StopWatch se utiliza con el propósito de indicar el tiempo total del proceso.

Se declara la variable m_Scanner como instancia de la clase PortScanner con sus eventos (WithEvents).

El sub main es el método que se ejecutará al iniciar el programa. Aquí se piden los datos necesarios (host y rango de puertos) y a continuación se crea un Thread (al que llamé supervisor) que es el que se encargará de informar la cantidad de hilos en ejecución en todo momento de la ejecución del programa (valor de la variable estática m_CountThreads de la clase TCPPort). Además este hilo también se encargará de determinar si el usuario presionó la tecla s para iniciar la cancelación del proceso. No confundir este hilo “supervisor” con los hilos del escaneador.

Nota:
Para acceder a una variable estática (shared) de una clase, se pone NOMBRE_CLASE.Nombre_Variable. No puede accederse a una variable estática desde un miembro de la clase, debe anteponerse explícitamente el nombre de la clase.

A continuación se crea una nueva instancia de la clase PortScanner indicando el Host y el rango de puertos a escanear y se inicia el proceso de escaneo al llamar al método Start de la clase PortScanner. Este método encolará los hilos que escanean los puertos y finalizará inmediatamente luego de encolarlos.

El sub m_Scanner_ScaneoIniciado maneja al evento ScaneoIniciado iniciando el cronómetro e imprimiendo la señal de comienzo del proceso.

El sub m_Scanner_PuertoScaneado maneja al evento PuertoScaneado imprimiendo el puerto que acaba de ser verificado junto con su estatus (abierto o cerrado) y mensaje de error.

Por último, El sub m_Scanner_ScaneoFinalizado maneja al evento ScaneoFinalizado, imprime la señal de fin de proceso, informa el tiempo total transcurrido y aborta al hilo supervisor que ya no es necesario (supervisor.Abort). Este evento recibe el parámetro bStopped que indica si el proceso fue abortado por el usario.

A continuación se muestran dos ejecuciones diferentes del programa escaneador. En la primera se abortó el el proceso de escaneo (presionando la tecla s) y en la segunda se completó el proceso.

 

Proceso abortado

 

Proceso completado

 

Hilos y Windows Forms

Si intentaste utilizar la clase PortScanner dentro de un form (forma o formulario) de Windows Forms, seguramente te encontraste con la excepción InvalidOperationException al intentar modificar el valor de un textbox o cualquier otro control dentro del evento disparado por los hilos.

Esto es porque no se puede tener acceso a un control de windows forms desde un proceso distinto al que lo creó. Al utilizar una aplicación de consola no se tiene este inconveniente, pero si es necesario utilizar Windows Forms, la forma de solucionar este problema es mediante un delegado y el método Invoke y se describe en el siguiente código:

Public Class Form1
    Private WithEvents PS As PortScanner 'Instancia de la clase PortScanner
    
    'Delegado para poder acceder a los miembros del form desde el sub ImprimirEstatus
    Delegate Sub ImprimirEstatusCB(ByVal Status As String)  

    'Comenzar el proceso al hacer click en un botón
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
                                Handles Button1.Click
        PS = New PortScanner("localhost", 1, 100)
        PS.Start()
    End Sub
 
    'Manejar al evento generado por los hilos
    Private Sub PS_PuertoScaneado(ByVal Port As Integer, ByVal bAbierto As Boolean, _
                                  ByVal sMensaje As String) Handles PS.PuertoScaneado
        ImprimirEstatus(Port & " " & bAbierto.ToString)
    End Sub
 
    'Imprimir el estatus del proceso en el caption del form y el ultimo estatus en un TextBox
    Private Sub ImprimirEstatus(ByVal Status As String)         
        If Me.InvokeRequired Then
            'Si es necesario utilizar Invoke, llamo al delegado
            Me.Invoke(New ImprimirEstatusCB(AddressOf ImprimirEstatus), New Object() {Status})
        Else
            'Aquí puedo modificar los controles de esta forma
            TextBox1.Text = Status
            Me.Text = TCPPort.m_CountThreads
        End If
    End Sub
End Class

Lo interesante está en el sub ImprimirEstatus que al principio evalúa la propiedad InvokeRequired que indicará si  es necesario utilizar el método Invoke (then) o si es posible modificar los controles de Windows Forms directamente (else). En el caso de ser necesario el Invoke, se pasará a éste (al invoke) un nuevo delegado del sub ImprimirEstatusCB y un arreglo de Objects conteniendo los parámetros que el sub requiera.

El delegado debe tener la misma firma que el sub al que invocará (y será invocado). En este caso el delegado es llamado ImprimirEstatusCB y el sub es llamado ImprimirEstatus.

Otras opciones (BackgroundWorker y ThreadPool)

La clase Thread (System.Threading) brinda la mayor flexibilidad en manejo de hilos, pero, cuando se trata de arreglos o colas, la codificación se torna complicada y confusa. Existen algunas clases que pueden ser útiles en alguno casos simplificando un poco el manejo de los hilos, como por ejemplo la clase BackgroundWorker (System.ComponentModel) y la clase ThreadPool (System.Threading).

La clase BackgroundWorker ofrece una práctica solución para los casos de operaciones que se demoren mucho tiempo y se necesite una interfaz rápida. Permite ejecutar en segundo plano una operación, y permite la generación sencilla de eventos para crear informes del progreso y para que señalen su finalización. La clase está pensada para la ejecución de una operacion larga (más que para muchas operaciones pequeñas).

La clase ThreadPool permite crear un grupo de subprocesos (ó una cola de hilos ó una lista elementos de trabajo) para su ejecución en forma concurrente. Para encolar un subproceso se llama al método QueueUserWorkItem que toma como parámetro una referencia al método (o delegado) al que llamará el subproceso.

El inconveniente del ThreadPool es que no permite cancelar un subproceso después de haber sido encolado. Stephen Toub de Microsoft (Ver Artículo) escribió la clase AbortableThreadPool que brinda las mismas carácterísticas de un ThreadPool pero además permite la interrupción de subprocesos. Pero está codificado en C#.

Para los que no sean amigos de C# también adjunto la versión en VB.NET de esta clase (la clase AbortableThreadPool_VB) que un día, aburrido, la traduje y comenté.

A continuación se muestra una forma de explotar la clase TCPPort con un Threadpool normal (no abortable), en este caso no tendremos control sobre los hilos y no será necesaria la clase PortScanner.

Simplemente encolamos las tareas y éstas se ejecutan: 

Imports System.Threading
 
Module Main_ThreadPool
    Private WithEvents TCP As New TCPPort
 
    Sub main()
        Dim datos As TCPPort.HostPortData, i As Integer
        datos.Host = "localhost"
        For i = 1 To 100
            datos.Port = i
            'Encolar la tarea
            ThreadPool.QueueUserWorkItem(AddressOf TCP.PortOpen, datos)
        Next
        Console.ReadKey()
    End Sub
 
    'Interceptar los eventos generados e informar el estatus
    Private Sub TCP_PuertoProcesado(ByVal Host As String, ByVal Port As Integer, ByVal bAbierto As Boolean, _
                                      ByVal sMensaje As String) Handles TCP.PuertoProcesado
        Console.WriteLine(Port & " : " & bAbierto.ToString)
    End Sub
End Module

Conclusión

A lo largo de este pequeño artículo se describieron algunas de las formas de manejar subprocesos concurrentes, por supuesto esto es mucho menos que lo básico, pero espero que sirva de guía o apoyo para aquellos que comienzan con .NET. A continuación puedes bajar el código de ejemplo.

 

Notas sobre el código de ejemplo:

El código está todo dentro de una solución llamada “Hilos” y dentro de la misma hay 3 proyectos:

El proyecto PortScan contiene a las clases TCPPort y PortScanner y al módulo Main_PortScan que es el que contiene el sub main de entrada por defecto a la aplicación. Este proyecto es del tipo Consola.

El proyecto ScanThreadpool (versión B) es otra versión del escaneador y también es del tipo Consola. Contiene el código para correr el escaneo con un ThreadPool (de System.Threading). En este proyecto no se necesita la clase PortScanner, sólo la TCPPort. Para ejecutar esta versión, debe cambiarse el proyecto de inicio (StartUp Project) en la solución. (por ejemplo con click derecho en el proyecto).

El proyecto ScanWinForms (versión C) es otra versión del escaneador pero en este caso se utiliza una forma de Windows Forms y se utiliza la clase PortScanner para llevar a cabo el proceso. Para ejecutar esta versión, debe cambiarse el proyecto de inicio.

Por defecto el programa comienza en el sub Main del módulo Main_PortScan que está en el proyecto PortScan.

 

Saludos,

Federico Daniel Colombo Gennarelli


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

System.Net.Sockets
System.Threading
System.Diagnostics

 


Código de ejemplo (comprimido):

 

Fichero con el código de ejemplo: thepirat_HilosYpuertos_Codigo.zip - 43.4 KB

(MD5 checksum: 96703AB55E7FAC592F6D20227FB00364)

 


Ir al índice principal de el Guille