VB .NET - Programando Sockets com Multithreads


Há muito tempo escrevi um artigo sobre Sockets na plataforma .NET. Foi apenas uma introdução por isso estou voltando ao assunto.

Nota: Uma thread significa uma única linha de execução; múltiplas threads indicam diversas linhas de execução.

Os programadores Visual Basic (versão 6.0, 5.0 e 4.0) não podiam contar com o uso de threads,  era preciso criar lançar mão dos famosos ActiveX Exe, o que na verdade não faziam o papel de uma thread e sim criavam um novo processo através de um aplicativo VB.

Neste artigo vou criar uma aplicação usando programação multithreads com sockets no Visual Basic. Vou fazer um chat multithread.

Thread, ou linha de execução , é uma forma de um processo dividir a si mesmo em duas ou mais tarefas que podem ser executadas simultaneamente. O suporte à thread é fornecido pelo próprio SO, no caso da Kernel-Level Thread (KLT), ou implementada através de uma biblioteca de uma determinada linguagem, no caso de uma User-Level Thread (ULT).

Uma linha de execução permite que o usuário de programa, por exemplo, utilize uma funcionalidade do ambiente enquanto outras linhas de execução realizam outros cálculos e operações.

Em hardwares equipados com uma única CPU, cada linha de execução(Thread) é processada de forma aparentemente simultânea, pois a mudança entre uma linha e outra é feita de forma tão rápida que para o usuário isso está acontecendo paralelamente. Em hardwares com múltiplas CPUs ou multi-cores as linhas de execução(Threads) podem ser realizadas realmente de forma simultânea;
(wikipédia)

Se você ainda tem dúvidas sobre sockets e threads sugiro que leia os meus artigos sobre o assunto. (veja as referências)

Um soquete pode ser usado em ligações de redes de computadores para um fim de um elo bidirecional de comunicação entre dois programas. A interface padronizada de soquetes surgiu originalmente no sistema operacional Unix BSD (Berkeley Software Distribution); portanto, eles são muitas vezes chamados de Berkeley Sockets.

É também uma abstração computacional que mapeia diretamente a uma porta de transporte (TCP ou UDP) e mais um endereço de rede. Com esse conceito é possível identificar unicamente um aplicativo ou servidor na rede de comunicação IP.(wikipédia)

Usar a programação multithreads com sockets significa que um servidor que seja multithread tem a capacidade de se comunicar com mais de um cliente ao mesmo tempo.

Dessa forma sempre que o servidor recebe uma requisição de um cliente ele cria uma linha de execução separada e independente; dessa forma cada cliente esta sendo atendido em uma linha de execução separada. Abaixo temos um esquema representando esta situação:

Logo, se desejamos criar uma aplicação chat multithread teremos que criar um servidor multithread e um cliente multithread. Como diria Jack, vamos por partes...

Criando um servidor de chat multithread

Para criar o servidor vou economizar na interface pois o tempo anda escasso, por isso irei criar  uma aplicação do tipo console para o nosso servidor que poderá atender diversos clientes ao mesmo tempo.  O 'bichinho' é simples mas valente...

O nosso servidor de chat multithread vai permitir que um cliente se comunique com qualquer número de outros clientes que estiverem conectados no servidor conforme mostra o esquema abaixo:

Cada cliente envia uma mensagem para o servidor e o servidor faz um broadcast da mensagem para todos os clientes conectados ao servidor.

 

Broadcast (do inglês, "transmitir") ou radiodifusão é o processo pelo qual se transmite ou difunde determinada informação, tendo como principal característica que a mesma informação está sendo enviada para muitos receptores ao mesmo tempo. Este termo é utilizado em radio, telecomunicações e em informática.(wikipédia)

 

O servidor CHAT será uma aplicação console escutando na porta 8888 na máquina local (127.0.0.1) (Você pode escolher qualquer outra porta disponível)

Quando o servidor recebe uma requisição de comunicação de um cliente, ele inclui o nome do cliente em uma lista (listaClientes) implementada como uma HashTable e cria uma nova thread para a comunicação com o servidor.

A .NET Framework fornece uma poderosa classe HashTable.

Uma HasTable é uma coleção onde os elementos possuem chave/valor , e onde cada elemento é indexado usando uma chave alfanumérica.

Ao trabalhar com a classe Hastable tenha em mente que a ordenação dos elementos na coleção é independente da ordem na qual ele entrou na tabela . A classe emprega seu próprio algoritmo hash para ordenar de forma eficiente os pares chave/valor da coleção.

Quando o servidor obtém uma mensagem de qualquer cliente , ele seleciona todos os clientes a partir da HashTable listaClientes e envia a mensagem para todos os clientes (um broadcast) da lista. De forma que cada cliente pode ver as mensagens uns dos outros e então se comunicar através do servidor de chat.

A lista de clientes é uma HashTable que armazena o nome do cliente e uma instância do socket do Cliente.

Quando um cliente se conecta no servidor , ele cria uma nova thread para comunicação através da classe tratarCliente para tratar o cliente em uma thread separada.

A classe tratarCliente possui uma rotina chamada doChat() que esta tratando a comunicação entre o servidor e o socket Cliente do lado do servidor e um socket Cliente que vem chegando.

Quando o servidor recebe uma mensagem de qualquer cliente conectado, ele faz um broadcast da mensagem para todos os clientes. Isso foi implementado na rotina broadcast().

Após esta explicação vamos implementar o código.

Abra o Visual Basic 2008 Express Edition e crie um novo projeto do tipo console com o nome SuperServidorNet e clique em OK;

Agora vamos incluir o seguinte código no módulo Module1 :

Imports System.Net
Imports System.Text
Module Module1

    Dim listaClientes As New Hashtable
    Sub Main()
        Dim enderecoLocal As IPAddress = IPAddress.Parse("127.0.0.1")
        Dim serverSocket As New TcpListener(enderecoLocal, 8888)
        Dim clientSocket As TcpClient = Nothing
        Dim contador As Integer
        serverSocket.Start()
        Mensagem("Servidor Chat Iniciado ....")
        contador = 0
        While (True)
            contador += 1
            clientSocket = serverSocket.AcceptTcpClient()
            Dim bytesFrom(10024) As Byte
            Dim dadosDoCliente As String
            Dim networkStream As NetworkStream = clientSocket.GetStream()
            networkStream.Read(bytesFrom, 0, CInt(clientSocket.ReceiveBufferSize))
            dadosDoCliente = Encoding.ASCII.GetString(bytesFrom)
            dadosDoCliente = dadosDoCliente.Substring(0, dadosDoCliente.IndexOf("$"))
            listaClientes(dadosDoCliente) = clientSocket
            broadcast(dadosDoCliente + " Entrou ", dadosDoCliente, False)
            Mensagem(dadosDoCliente + " Entrou na Sala ")
            Dim cliente As New tratarCliente
            cliente.iniciaCliente(clientSocket, dadosDoCliente, listaClientes)
        End While
        clientSocket.Close()
        serverSocket.Stop()
        Mensagem("sair")
        Console.ReadLine()
    End Sub
    Sub Mensagem(ByVal texto As String)
        texto.Trim()
        Console.WriteLine(" >> " + texto)
    End Sub
    Private Sub broadcast(ByVal Mensagem As String, ByVal nomeUsuario As String, ByVal flag As Boolean)
        Dim Item As DictionaryEntry
        For Each Item In listaClientes
            Dim broadcastSocket As TcpClient
            broadcastSocket = CType(Item.Value, TcpClient)
            Try
                Dim broadcastStream As NetworkStream = broadcastSocket.GetStream()
                Dim broadcastBytes As [Byte]()
                If flag = True Then
                    broadcastBytes = Encoding.ASCII.GetBytes(nomeUsuario + " diz : " + Mensagem)
                Else
                    broadcastBytes = Encoding.ASCII.GetBytes(Mensagem)
                End If
                broadcastStream.Write(broadcastBytes, 0, broadcastBytes.Length)
                broadcastStream.Flush()
            Catch ex As Exception
                MsgBox(ex.Message)
            End Try
        Next
    End Sub
    Public Class tratarCliente
        Dim clientSocket As TcpClient
        Dim clNo As String
        Dim listaClientes As Hashtable
        Public Sub iniciaCliente(ByVal inClientSocket As TcpClient, ByVal clineNo As String, ByVal cList As Hashtable)
            Me.clientSocket = inClientSocket
            Me.clNo = clineNo
            Me.listaClientes = cList
            Dim ctThread As Threading.Thread = New Threading.Thread(AddressOf doChat)
            ctThread.Start()
        End Sub
        Private Sub doChat()
            Dim contadorRequisicao As Integer
            Dim bytesFrom(10024) As Byte
            Dim dadosDoCliente As String
            Dim rContador As String
            contadorRequisicao = 0
            While (True)
                Try
                    contadorRequisicao = contadorRequisicao + 1
                    Dim networkStream As NetworkStream = clientSocket.GetStream()
                    networkStream.Read(bytesFrom, 0, CInt(clientSocket.ReceiveBufferSize))
                    dadosDoCliente = System.Text.Encoding.ASCII.GetString(bytesFrom)
                    dadosDoCliente = dadosDoCliente.Substring(0, dadosDoCliente.IndexOf("$"))
                    Mensagem("Cliente - " + clNo + " : " + dadosDoCliente)
                    rContador = Convert.ToString(contadorRequisicao)
                    broadcast(dadosDoCliente, clNo, True)
                Catch ex As Exception
                    MsgBox(ex.ToString)
                End Try
            End While
        End Sub
    End Class
End Module

Iniciamos importando os namespaces :

Imports System.Net.Sockets
Imports System.Text

que nos dão acesso as classes da plataforma .NET para realizar comunicações com Socket.

Neste código criei um Server Socket a partir da classe TcpListener que esta escutando na porta 8888.(poderia ser outra porta)

A classe TcpListner é usada para escuta de conexões de clientes de rede TCP.

A classe TcpListener fornece métodos simples que escutam e aceitam solicitações de conexão de entrada no mode de bloqueio síncrono. Você pode usar um TcpClient ou um Socket para se conectar com um TcpListener.

Use o método Start para começar escutar solicitações de conexão de entrada. Ele irá enfileirar as requisições até você chamar o método Stop ou até atingir o máximo de conexões.(MaxConnections).

Você pode usar  tanto AcceptSocket como AcceptTcpClient para receber uma  requisição de conexão de entrada e colocá-la na fila.

Se você quiser evitar o bloqueio, você pode usar o método Pending primeiro, para determinar se as solicitações de conexão estão disponíveis na fila.

Quando o servidor recebe uma requisição do cliente ele passa a instância desta requisição para uma classe chamada tratarCliente.

A classe tratarCliente é uma thread que faz o tratamento da comunicação entre a instância do cliente no servidor e o cliente.

Para cada requisição no servidor existe uma nova thread que é criada  para efetuar a comunicação, de forma que o podemos efetuar a comunicação com mais de um cliente ao mesmo tempo no servidor e efetuar uma comunicação independente.

Executando este programa teremos o servidor inicializado e pronto para atender requisições: Que venham os clientes...

Criando um cliente multithread

Vamos agora criar um cliente Multithread usando sockets como uma aplicação Windows Forms. O cliente irá se conectar com a porta 8888 do servidor. Como o cliente e o servidor estão rodando na minha máquina local possuem o mesmo endereço IP (127.0.0.1). Note que o cliente tem que saber em qual porta o servidor esta escutando.

clientSocket.Connect("127.0.0.1", 8888)

Quando o cliente se conectar com o servidor , o servidor irá criar uma thread para a comunicação com o cliente, em seguida podemos efetuar a conexão com outro cliente para mostrar que a comunicação é simultânea.

A classe TcpClient fornece conexões de cliente de serviços de rede TCP.

A classe TcpClient fornece métodos simples para conectar-se, enviar e de receber um de fluxo de dados em uma rede no modo síncrono de bloqueio.

A fim de da classe TcpClient se conectar e trocar dados, um TcpListener ou Socket criado com o ProtocolType TCP  deve estar escutando para solicitações de conexão de entrada. Você pode conectar-se com este Listner em uma das seguintes formas:

  • Criar um TcpClient  e chamar um dos três métodos Connect disponíveis;
  • Criar Um TcpClient usando o nome de host e número da porta do host remoto. Esse construtor irá automaticamente tentar uma conexão.

Abra o Visual Basic 2008 Express Edition e crie um novo projeto do tipo Windows Forms Application com o nome superClienteNet e clique em OK;

No formulário padrão form1.vb inclua os seguintes componentes:

A seguir inclua o código abaixo no formulário form1.vb:

Imports System.Net.Sockets
Imports System.Text

Public Class Form1
    Dim clientSocket As New TcpClient()
    Dim serverStream As NetworkStream
    Dim lerDados As String
    Private Sub btnConectar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnConectar.Click
        lerDados = "Conetado como Servidor ..."
        Mensagem()
        Try
            clientSocket.Connect("127.0.0.1", 8888)
            serverStream = clientSocket.GetStream()
            Dim outStream As Byte() = Encoding.ASCII.GetBytes(txtNome.Text + "$")
            serverStream.Write(outStream, 0, outStream.Length)
            serverStream.Flush()
            'cria uma nova thread para enviar mensagens
            Dim ctThread As Threading.Thread = New Threading.Thread(AddressOf getMensagem)
            ctThread.Start()
        Catch ex As Exception
            MsgBox(ex.Message)
        End Try
    End Sub
    Private Sub Mensagem()
        If Me.InvokeRequired Then
            Me.Invoke(New MethodInvoker(AddressOf Mensagem))
        Else
            txtDados.Text = txtDados.Text + Environment.NewLine + " >> " + lerDados
        End If
    End Sub
    Private Sub btnEnviarMensagem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnEnviarMensagem.Click
        Try
            Dim outStream As Byte() = Encoding.ASCII.GetBytes(txtMensagem.Text + "$")
            serverStream.Write(outStream, 0, outStream.Length)
            serverStream.Flush()
        Catch ex As Exception
            MsgBox(ex.Message)
        End Try
    End Sub
    Private Sub getMensagem()
        'loop infinito
        While (True)
            Try
                serverStream = clientSocket.GetStream()
                Dim buffSize As Integer
                Dim inStream(10024) As Byte
                buffSize = clientSocket.ReceiveBufferSize
                serverStream.Read(inStream, 0, buffSize)
                Dim dadosRetornados As String = Encoding.ASCII.GetString(inStream)
                lerDados = "" + dadosRetornados
                Mensagem()
            Catch ex As Exception
                MsgBox(ex.Message)
                Exit Sub
            End Try
        End While
    End Sub
End Class

O cliente se conecta com a porta 8888 em localhost (o servidor e o cliente) estão na mesma máquina local.

Quando o programa Cliente inicia , temos que informar o nome (apelido) do usuário para que o mesmo seja identificado no servidor.

O programa Cliente se conecta com o servidor de Chat e inicia uma thread para receber as mensagens dos clientes.

Implementamos um loop infinito (While(true)) na função getMessage() e chamamos esta função na thread.

Executando o projeto Servidor e o Cliente (em duas instâncias) iremos obter:

E ai esta o nosso chat, simples mas funcional, e, você não precisou de nenhum recurso a não ser as classes da .NET Framework.

O projeto pode ser incrementado com um tratamento de erros mais robusto e outras funcionalidades, meu objetivo foi mostrar como usar as classes do namespace System.NET para criar uma aplicação multithread.

Pegue o projeto completo aqui: servidor :  SuperServidorNet.zip   e   cliente: SuperClienteNet.zip

Eu sei é apenas VB .NET , mas eu gosto...

referências:


José Carlos Macoratti