Uma Thread pode ser considerada um fluxo de controle sequencial dentro de um programa, onde damos algum job e ela o realiza, provendo maior performance. Em programação é muito importante saber aplicar formas de processamento assíncrono, pois atualmente temos processadores altamente velozes e não sabemos explorá-los devidamente. No nosso chat que criaremos nesse artigo, a Thread será usada para controlar o fluxo de mensagens enviadas e recebidas por um cliente, pois imagina se todos elas fossem armazenadas numa fila e processadas unicamente por uma thread: o serviço seria precário e provavelmente ninguém usaria.

O que é Socket

Socket é um meio de comunicação usado para viabilizar a conexão cliente/servidor, onde um cliente informa o endereço de IP e a respectiva porta do servidor. Se este aceitar a conexão, ele irá criar um meio de comunicação com esse cliente. Logo, a combinação de Threads e Socket é perfeita para implementação de um chat.

OOP

Frente a quantidade de linguagens orientadas a objetos (OO) existentes, como C#, C++, Java, entre outras, fica evidente que para dominá-las é necessário entender bem os pilares OO, como Herança (por interface e por classe), Encapsulamento, Polimorfismo e Abstração. Se dominar bem esses assuntos, com certeza terá mais facilidade em construir códigos simples e com qualidade, tendo baixo acoplamento e alta coesão. Neste artigo nos depararemos com Herança por interface e classe base, abstração e encapsulamento, mas não será o foco do mesmo abordar seus conceitos.

A seguir são descritas as responsabilidades e comportamentos das classes Server.java e Cliente.java usadas para a construção do Chat:

  • Responsabilidade e comportamentos do Server.java: o servidor servirá como unidade centralizadora de todas as conexões recebidas via socket e terá como responsabilidade o envio de uma mensagem (recebida de um cliente) para todos os demais conectados no servidor. Quando um cliente se conecta a ele o mesmo cria uma Thread para aquele cliente, ou seja, cada conexão terá sua respectiva Thread e o servidor fará a gestão disso;
  • Responsabilidade e comportamentos do Client.java: Cada usuário criará uma instância do cliente e fará uma conexão com o servidor socket. O cliente deverá informar o endereço do server socket e a respectiva porta, por isso é necessário executar o Server.java antes.

Lembre-se: escolha uma porta que não esteja sendo usada para a execução do server socket e certifique-se que o firewall ou algum antivírus não esteja bloqueando a porta escolhida. Para esse artigo definimos como 12345.

Na Listagem 1 temos a declaração dos pacotes usados na classe servidor.java. Veja que usamos “streams”, “collections” e classes para a construção de formulários.

Listagem 1. Declaração dos imports

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

A seguir temos a declaração da classe servidor.java. Veja que ela extends Thread, logo é um tipo de Thread, adotando todos os comportamentos e propriedades desta classe

public class Servidor extends Thread {

Na Listagem 2 temos a declaração dos atributos estáticos e de instâncias da classe servidor.java. O atributo “clientes” é usado para armazenar o BufferedWriter de cada cliente conectado e o server socket é usado para a criação do servidor, que teoricamente deve ser feita apenas uma vez.

Listagem 2. Atributos estáticos

private static ArrayList<BufferedWriter>clientes;
private static ServerSocket server;
private String nome;
private Socket con;
private InputStream in;
private InputStreamReader inr;
private BufferedReader bfr;

Na Listagem 3 temos a declaração do método construtor, que recebe um objeto socket como parâmetro e cria um objeto do tipo BufferedReader, que aponta para o stream do cliente socket.

Listagem 3. Declaração do método construtor

/**
  * Método construtor
  * @param com do tipo Socket
  */
public Servidor(Socket con){
   this.con = con;
   try {
         in  = con.getInputStream();
         inr = new InputStreamReader(in);
          bfr = new BufferedReader(inr);
   } catch (IOException e) {
          e.printStackTrace();
   }
}

A Listagem 4 mostra a declaração do método “run”: toda vez que um cliente novo chega ao servidor, esse método é acionado e alocado numa Thread e também fica verificando se existe alguma mensagem nova. Caso exista, esta será lida e o evento “sentToAll” será acionado para enviar a mensagem para os demais usuários conectados no chat.

Listagem 4. Declaração do método run

/**
  * Método run
  */
public void run(){

  try{

    String msg;
    OutputStream ou =  this.con.getOutputStream();
    Writer ouw = new OutputStreamWriter(ou);
    BufferedWriter bfw = new BufferedWriter(ouw);
    clientes.add(bfw);
    nome = msg = bfr.readLine();

    while(!"Sair".equalsIgnoreCase(msg) && msg != null)
      {
       msg = bfr.readLine();
       sendToAll(bfw, msg);
       System.out.println(msg);
       }

   }catch (Exception e) {
     e.printStackTrace();

   }
}

Na Listagem 5 temos a declaração do método “sendToAll”. Quando um cliente envia uma mensagem, o servidor recebe e manda esta para todos os outros clientes conectados. Veja que para isso é necessário percorrer a lista de clientes e mandar uma cópia da mensagem para cada um.

Listagem 5. Declaração do método sendToAll

/***
 * Método usado para enviar mensagem para todos os clients
 * @param bwSaida do tipo BufferedWriter
 * @param msg do tipo String
 * @throws IOException
 */
public void sendToAll(BufferedWriter bwSaida, String msg) throws  IOException
{
  BufferedWriter bwS;

  for(BufferedWriter bw : clientes){
   bwS = (BufferedWriter)bw;
   if(!(bwSaida == bwS)){
     bw.write(nome + " -> " + msg+"\r\n");
     bw.flush();
   }
  }
}

A Listagem 6 mostra a declaração do método main, que ao iniciar o servidor, fará a configuração do servidor socket e sua respectiva porta. Veja que ele começa criando uma janela para informar a porta e depois entra no “while(true)”. Na linha “server.accept()” o sistema fica bloqueado até que um cliente socket se conecte: se ele fizer isso é criada uma nova Thread do tipo servidor.

Lembre-se que a classe servidor é um tipo de Thread e é iniciada na instrução “t.start()”. Então o controle do fluxo retorna para a linha “server.accept()” e aguarda outro cliente se conectar.

Listagem 6. Declaração do método main

/***
   * Método main
   * @param args
   */
public static void main(String []args) {

  try{
    //Cria os objetos necessário para instânciar o servidor
    JLabel lblMessage = new JLabel("Porta do Servidor:");
    JTextField txtPorta = new JTextField("12345");
    Object[] texts = {lblMessage, txtPorta };
    JOptionPane.showMessageDialog(null, texts);
    server = new ServerSocket(Integer.parseInt(txtPorta.getText()));
    clientes = new ArrayList<BufferedWriter>();
    JOptionPane.showMessageDialog(null,"Servidor ativo na porta: "+
    txtPorta.getText());

     while(true){
       System.out.println("Aguardando conexão...");
       Socket con = server.accept();
       System.out.println("Cliente conectado...");
       Thread t = new Servidor(con);
        t.start();
    }

  }catch (Exception e) {

    e.printStackTrace();
  }
 }// Fim do método main
} //Fim da classe

A Listagem 7 mostra a declaração dos pacotes usados na classe cliente.java.

Listagem 7. Declaração dos Imports

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.Socket;
import javax.swing.*;

A seguir temos a declaração da classe Cliente.java:

public class Cliente extends JFrame implements ActionListener, KeyListener {

Para a construção do formulário foram usados objetos do pacote javax.swing. A Listagem 8 mostra a declaração dos atributos estáticos e de instâncias da classe Cliente.java.

Listagem 8. Declaração dos atributos

private static final long serialVersionUID = 1L;
private JTextArea texto;
private JTextField txtMsg;
private JButton btnSend;
private JButton btnSair;
private JLabel lblHistorico;
private JLabel lblMsg;
private JPanel pnlContent;
private Socket socket;
private OutputStream ou ;
private Writer ouw;
private BufferedWriter bfw;
private JTextField txtIP;
private JTextField txtPorta;
private JTextField txtNome;

Ao executar a classe cliente aparecerá uma tela para o usuário informar alguns parâmetros como o IP do servidor, a porta e o nome que será visto para os demais usuários no chat. No código está definido como padrão o IP 127.0.0.1, porta 12345 e nome cliente.

Observe também que a classe herda de JFrame, possibilitando a criação de formulários e implementação das interfaces ActionListener e KeyListener para prover ações nos botões e ações das teclas, respectivamente.

A Listagem 9 mostra a declaração do método construtor, que verifica os objetos sendo instanciados para a construção da tela do chat. Lembre-se que cada cliente deverá ser uma instância independente.

Listagem 9. Declaração do método construtor

public Cliente() throws IOException{
    JLabel lblMessage = new JLabel("Verificar!");
    txtIP = new JTextField("127.0.0.1");
    txtPorta = new JTextField("12345");
    txtNome = new JTextField("Cliente");
    Object[] texts = {lblMessage, txtIP, txtPorta, txtNome };
    JOptionPane.showMessageDialog(null, texts);
     pnlContent = new JPanel();
     texto              = new JTextArea(10,20);
     texto.setEditable(false);
     texto.setBackground(new Color(240,240,240));
     txtMsg                       = new JTextField(20);
     lblHistorico     = new JLabel("Histórico");
     lblMsg        = new JLabel("Mensagem");
     btnSend                     = new JButton("Enviar");
     btnSend.setToolTipText("Enviar Mensagem");
     btnSair           = new JButton("Sair");
     btnSair.setToolTipText("Sair do Chat");
     btnSend.addActionListener(this);
     btnSair.addActionListener(this);
     btnSend.addKeyListener(this);
     txtMsg.addKeyListener(this);
     JScrollPane scroll = new JScrollPane(texto);
     texto.setLineWrap(true);
     pnlContent.add(lblHistorico);
     pnlContent.add(scroll);
     pnlContent.add(lblMsg);
     pnlContent.add(txtMsg);
     pnlContent.add(btnSair);
     pnlContent.add(btnSend);
     pnlContent.setBackground(Color.LIGHT_GRAY);
     texto.setBorder(BorderFactory.createEtchedBorder(Color.BLUE,Colo  r.BLUE));
     txtMsg.setBorder(BorderFactory.createEtchedBorder(Color.BLUE, Color.BLUE));
     setTitle(txtNome.getText());
     setContentPane(pnlContent);
     setLocationRelativeTo(null);
     setResizable(false);
     setSize(250,300);
     setVisible(true);
     setDefaultCloseOperation(EXIT_ON_CLOSE);
}

O método da Listagem 10 é usado para conectar o cliente com o servidor socket. Nesse método é possível visualizar a criação do socket cliente e dos streams de comunicação.

Listagem 10. Declaração do método conectar

/***
  * Método usado para conectar no server socket, retorna IO Exception caso dê algum erro.
  * @throws IOException
  */
public void conectar() throws IOException{

  socket = new Socket(txtIP.getText(),Integer.parseInt(txtPorta.getText()));
  ou = socket.getOutputStream();
  ouw = new OutputStreamWriter(ou);
  bfw = new BufferedWriter(ouw);
  bfw.write(txtNome.getText()+"\r\n");
  bfw.flush();
}

A Listagem 11 tem o método usado para enviar mensagens do cliente para o servidor socket. Assim, toda vez que ele escrever uma mensagem e apertar o botão “Enter”, esta será enviada para o servidor.

Listagem 11. Declaração do método enviar mensagem

/***
  * Método usado para enviar mensagem para o server socket
  * @param msg do tipo String
  * @throws IOException retorna IO Exception caso dê algum erro.
  */
  public void enviarMensagem(String msg) throws IOException{

    if(msg.equals("Sair")){
      bfw.write("Desconectado \r\n");
      texto.append("Desconectado \r\n");
    }else{
      bfw.write(msg+"\r\n");
      texto.append( txtNome.getText() + " diz -> " +         txtMsg.getText()+"\r\n");
    }
     bfw.flush();
     txtMsg.setText("");
}

Na Listagem 12 temos o método usado para escutar (receber) mensagens do servidor. Toda vez que alguém enviar uma, o método será processado pelo servidor e envia para todos os clientes conectados, por isso a necessidade do código.

Listagem 12. Declaração do método escutar

/**
 * Método usado para receber mensagem do servidor
 * @throws IOException retorna IO Exception caso dê algum erro.
 */
public void escutar() throws IOException{

   InputStream in = socket.getInputStream();
   InputStreamReader inr = new InputStreamReader(in);
   BufferedReader bfr = new BufferedReader(inr);
   String msg = "";

    while(!"Sair".equalsIgnoreCase(msg))

       if(bfr.ready()){
         msg = bfr.readLine();
       if(msg.equals("Sair"))
         texto.append("Servidor caiu! \r\n");
        else
         texto.append(msg+"\r\n");
        }
}

O método da Listagem 13 é usado para desconectar do server socket. Nele o sistema apenas fecha os streams de comunicação.

Listagem 13. Declaração do método sair

/***
  * Método usado quando o usuário clica em sair
  * @throws IOException retorna IO Exception caso dê algum erro.
  */
  public void sair() throws IOException{

   enviarMensagem("Sair");
   bfw.close();
   ouw.close();
   ou.close();
   socket.close();
}

O método usado para receber as ações dos botões dos usuários é visto na Listagem 14. Nele foi feito um chaveamento: se o usuário pressionar o botão “send” então será enviada uma mensagem, senão será encerrado o chat.

Listagem 14. Declaração do método actionPerformed

@Override
public void actionPerformed(ActionEvent e) {

  try {
     if(e.getActionCommand().equals(btnSend.getActionCommand()))
        enviarMensagem(txtMsg.getText());
     else
        if(e.getActionCommand().equals(btnSair.getActionCommand()))
        sair();
     } catch (IOException e1) {
          // TODO Auto-generated catch block
          e1.printStackTrace();
     }
}

O método da Listagem 15 é acionado quando o usuário pressiona “Enter”, verificando se o key code é o Enter. Caso seja, a mensagem é enviada para o servidor.

Listagem 15. Declaração do método keyPressed

@Override
public void keyPressed(KeyEvent e) {

    if(e.getKeyCode() == KeyEvent.VK_ENTER){
       try {
          enviarMensagem(txtMsg.getText());
       } catch (IOException e1) {
           // TODO Auto-generated catch block
           e1.printStackTrace();
       }
   }
}

@Override
public void keyReleased(KeyEvent arg0) {
  // TODO Auto-generated method stub
}

@Override
public void keyTyped(KeyEvent arg0) {
  // TODO Auto-generated method stub
}

A Listagem 16 mostra o método main, onde é criado apenas um cliente e são configurados os métodos conectar e escutar.

Listagem 16. Declaração do método main

public static void main(String []args) throws IOException{

   Cliente app = new Cliente();
   app.conectar();
   app.escutar();
}

Depois do código implementado na IDE Eclipse ou Netbeans, temos duas classes: a Servidor.java e a Cliente.java. Execute uma vez a classe Servidor.java e a classe Cliente.java quantas vezes achar necessário, porém, se executar apenas uma vez, você não verá sua mensagem sendo enviada para ninguém.

Quando executar o servidor pela primeira vez aparecerá uma caixa para informar os parâmetros necessários. Informe o número da porta, conforme mostra a Figura 1.

Input
    para informar o número da porta onde o servidor socket receberá as conexões

Figura 1. Input para informar o número da porta onde o servidor socket receberá as conexões.

Ao executar um cliente também será necessário informar alguns parâmetros como a porta e o endereço de IP do servidor socket, como vemos na Figuras 2.

Inputs do IP do servidor socket, a porta e o nome que será visto por você e pelos outros
    usuários

Figura 2. Inputs do IP do servidor socket, a porta e o nome que será visto por você e pelos outros usuários.

Chat
    em execução

Figura 3. Chat em execução.

Após executar o servidor e dois ou mais clientes será possível conversar com todos eles. Repare na primeira tela da Figura 3 , em que o Paul começa a conversa.

Vimos como é simples a construção básica de um chat, desde de que a ideia de Thread e Socket seja bem compreendida.

Espero que tenham gostado!