terça-feira, 16 de junho de 2015

Vamos fazer um chat ?

Olá amigos do Preciso Estudar Sempre, meu nome é João Paulo e minha paixão é estudar. O tema que estudaremos hoje é de nível avançado e para que possamos desenvolvê-lo, precisaremos antes entender o que são Sockets e Threads.

Se você já possui conhecimento sobre esses dois assuntos, pode baixar o projeto clicando aqui e seja feliz. :D

Maaaaaaaaaaaas, se você não sabe e quer saber, continue lendo este post, entenda o que explicarei, aí sim, baixe o projeto. Não recomendo baixar antes porque ter algo em mãos que não conseguimos entender não é tão legal.

O que é um Socket? O que um Socket faz ? Você já ouviu falar sobre isso ? Se você possui essa ou mais perguntas, relaxe, pegue um café, acenda um cigarro e confia no pai que, o inimigo cai.

Um socket é nada mais nada menos que, a ponta de uma comunicação de duas vias entre programas em uma rede de computadores. Ele é vinculado a uma porta de rede para que, a camada TCP possa identificar a aplicação que, destina dados para alguma outra aplicação.

Fácil, não !?

Você lendo isso e pirando: "Ainnn João, ainda não entendi, tem como dar um exemplo?"

Bem, vamos lá ! Vamos imaginar a seguinte: Você está digitando seu texto em seu editor de texto que você mais ama no mundo e precisar imprimir ele. O que acontece quando você clica no ícone de imprimir ?

O seu editor de texto cria um socket para a impressora, onde ele passa o ip e a porta para o socket e se conecta. Depois de conectado, ele envia os dados para a impressão. A impressora, por sua parte, já espera conexões. Ela realiza essa espera ouvindo uma porta de rede. Qualquer requisição feita para essa porta, será ouvida por ela onde, ela aceitará ou não essa conexão.

Então, se você entendeu o que eu escrevi aí em cima, você já tá "por dentro" do que é um socket. Vamos entender as threads ?

As threads são mais fácies ainda. Uma thread é um fluxo de dados executado de forma paralela junto ao processo que originou-o, por exemplo: Você já notou naquela barrinha de carregar quando você abre um programa? Aquela barra representa o progresso da inicialização do programa. Nessa inicialização, uma thread é iniciada para processar dados necessários que o programa precisará após ter sido carregado. Desta forma, há uma melhora na performance do programa. Tente imaginar o quanto o programa ficaria mais lento se ele não pudesse executar mais de uma tarefa por vez ?

Poxa que legal !!! Já entendemos os dois conceitos básicos para construir um chat. Agora vamos entender como um chat funciona e depois disso listar o que precisamos.

Para que um chat possa funcionar precisamos de clientes, um servidor, sockets, threads e canais de comunicação.
Figura 1 - Arquitetura do chat
Os cliente são as pessoas se falando onde, cada um possuirá sua própria tela de chat a qual, construiremos adiante. Cada cliente não tem noção de quantas pessoas estão no chat e para qualquer mensagem enviada, todos são notificados. O servidor é um programa que armazena todos os cliente conectados à ele e recebe todas as mensagens e redireciona as mesmas para todos, até para quem enviou. Isto é feito para que, o remetente de uma mensagem possa ver o que enviou.

Para que um cliente possa se conectar a um servidor, ele abre um socket e executa todo o processo explicado acima. Após a conexão ter sido feita, tanto o cliente quanto o servidor podem possuir controle sobre os fluxos de dados visando ler ou escrever dados.

Para sair da teoria e ir para a prática, vamos precisar de:
  • JDK instalado
  • Netbeans (estou usando a versão 7.4)
IMPORTANTE: Nosso chat será desenvolvido utilizando a tecnologia Java Swing. Se você não está familiarizado com ela, recomendo fortemente que você estude primeiro e depois volte pra cá porque, não explicarei Swing aqui.

IMPORTANTE: O layout das telas foi desenvolvido no Netbeans. Caso você queira importar o projeto daqui para o Eclipse, não recomendo logo, poderei ajudar.

Mãos na massa !

1 - Crie uma pasta em algum local de preferência sua.

2 - Abra o Netbeans e crie os projetos Cliente e Servidor na pasta recém criada. 

3 - Crie a classe Servidor no projeto Servidor.
package servidor;
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 *
 */
public class Servidor {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        List<PrintStream> clientes = new ArrayList<>();
        
        try {
            ServerSocket server = new ServerSocket(3306);
            Socket socket;

            while (true) {
                socket = server.accept();
                /*registro dos clientes*/
                clientes.add(new PrintStream(socket.getOutputStream()));
                new EnviadorMensagem(socket, clientes).iniciarMensagens();
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
    
}

Neste momento vamos focar somente bloco try. Lembra que eu expliquei lá em cima que o servidor (impressora) fica esperando conexões em uma determinada porta ? Então, o nosso não será diferente. Nosso servidor esperará conexões na porta 3306. Se você já trabalhou com MySQL, sabe que esta porta é utilizada por ele. Utilizei ela para que não precisasse abrir uma nova porta no meu roteador e é esse o jump of the cat (pulo do gato). Se você quiser fazer isso também é fácil, é só parar o serviço do MySQL.

Construímos esse loop infinito para que ele aguarde por infinitas conexões. Geralmente, loops infinitos consomem muita CPU mas, esse não será o nosso caso pois, o Servidor fica parado na primeira linha do loop onde, ele espera a conexão. Logo após da conexão ter sido estabelecida, o loop continua.

4 - Crie a classe Cliente no projeto Cliente.
package cliente;
import javax.swing.JOptionPane;

/**
 *
 */
public class Cliente {
    public static void main(String[] args) {
        while (true) {            
            String nomeCliente = JOptionPane.showInputDialog(null, "Informe seu nome", "Configurações iniciais", JOptionPane.INFORMATION_MESSAGE);
            if(nomeCliente == null || nomeCliente != null && "".equalsIgnoreCase(nomeCliente.trim())){
                JOptionPane.showMessageDialog(null, "O campo de nome do cliente é obrigatório.", "Dados obrigatórios", JOptionPane.ERROR_MESSAGE);
            } else {
                Chat chat = new Chat(nomeCliente);
                chat.setVisible(true);
                break;
            }
        }
    }
}

Essa classe é de fácil entendimento e eu dispenso explicações.

5 - Crie a classe Chat, no projeto Cliente e desenvolva a seguinte tela.
Figura 2 - Protótipo da Tela
6 - Crie a conexão com o servidor no construtor da classe Chat.
    public Chat(String nomeCliente) {
        initComponents();
        this.nomeCliente = nomeCliente;
        try {
            socket = new Socket("127.0.0.1", 3306);
        } catch (IOException ex) {
            JOptionPane.showMessageDialog(null, "Não foi possível conectar ao servidor.", "Erro", JOptionPane.ERROR_MESSAGE);
            System.exit(0);
        }
        jLabel1.setText(MessageFormat.format(jLabel1.getText(), nomeCliente));
        iniciarThread();
        this.escolherCor();
        this.configurarFonte();
        jTextArea1.requestFocus();
    }

No bloco try fazemos o que foi explicado no início desse post. O cliente está solicitando uma requisição ao servidor o qual, está no ip 127.0.0.1 (localhost) e na porta 3306. Vamos entender agora o método iniciarThread().

private void iniciarThread(){
        this.thread = new Thread(new Runnable() {
            String mensagemServidor;
            @Override
            public void run() {
                try {
                    inputStreamReader = new InputStreamReader(socket.getInputStream());
                    bufferedReader = new BufferedReader(inputStreamReader);
                    entrarSala();
                    while((mensagemServidor=bufferedReader.readLine())!=null){
                        try {
                            int indiceColchetes2 = mensagemServidor.indexOf("],[");
                            String message = mensagemServidor.substring(mensagemServidor.indexOf('[')+1,indiceColchetes2);
                            mensagemServidor = mensagemServidor.substring(indiceColchetes2+2);
                            Integer color = Integer.valueOf(mensagemServidor.substring(1,mensagemServidor.indexOf(']')));
                            StyleConstants.setForeground(styleNomeCliente, new Color(color));
                            styledDocument.insertString(styledDocument.getLength(), message + "\n", styleNomeCliente);
                        } catch (BadLocationException ex) {
                            ex.printStackTrace();
                        }
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
        thread.start();      
    }

Como o próprio nome já diz, esse método abre uma thread mas, para que precisamos dela ? Lembra o que falei sobre a execução paralela de tarefas ? Então, precisamos nesse momento executar de forma paralela, junto com a inicialização da tela, a abertura da stream de leitura do cliente para o servidor visando, ouvir as mensagens que o servidor envia para o cliente. De aonde conseguimos o input stream dessa comunicação cliente/servidor ? Do socket, obviamente. Senão fizéssemos tudo isso de forma paralela, a tela nunca abriria pois, o stream seria aberto e o programa iria ficar aguardando mensagens do servidor para o cliente.

7 - Crie o método que manda as mensagens para o chat e associe-o ao botão Enviar.
    private void enviarMensagem(){
        String mensagem = "["+nomeCliente + " diz: ";
        try {
            PrintStream printStream = new PrintStream(socket.getOutputStream());
            mensagem += jTextArea1.getText() + "],[" + this.corDaFonte.getRGB() + "]";
            printStream.println(mensagem);
            printStream.flush();
            jTextArea1.setText("");
        } catch (IOException ex) {
            JOptionPane.showMessageDialog(null, "Não foi possível enviar a mensagem.", "Erro", JOptionPane.ERROR_MESSAGE);
        }
    }

Neste método abrimos o stream de escrita do cliente para o servidor, adicionamos a mensagem nele e enviamos. Mais uma vez, aonde conseguimos o output stream para a escrita dos dados do cliente para servidor ? Do socket, mais uma vez.

8 - Encerre a conexão e saia do chat.
     private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
        try {
            PrintStream printStream = new PrintStream(socket.getOutputStream());
            printStream.println("[[" + new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new Date()) + "] > " + nomeCliente + " saiu.],[" + this.corDaFonte.getRGB() + "]");
            printStream.flush();
            Thread.sleep(15);
            thread.stop();
            socket.close();
            System.exit(0);
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

Para sair do chat é necessário encerrarmos o socket e dar exit no programa. Eu encerrei a thread por uma questão de luxo mas, não é necessário. O sleep de 15 milisegundos é realizado porque antes do programa ser encerrado, o cliente envia uma mensagem para todos informando que saiu da sala. Caso o sleep não fosse feito, uma exception de escrita em socket seria propagada porque a velocidade do flush da stream é menor que a velocidade da execução do programa, ou seja, o flush não é imediato, ele demora um pouco.

9 - Cria a classe EnviadorMensagem no projeto Servidor.
package servidor;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.List;

/**
 *
 */
public class EnviadorMensagem {
    private Socket socket;
    private List<PrintStream> clientes;

    public EnviadorMensagem(Socket socket, List<PrintStream> clientes) {
        this.socket = socket;
        this.clientes = clientes;
    }
 
    public void iniciarMensagens(){
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                String mensagem = null;
                try {
                    InputStreamReader inputStreamReader = new InputStreamReader(socket.getInputStream());
                    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                    while ((mensagem=bufferedReader.readLine()) != null) {              
                        enviarMensagem(mensagem);
                    }
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        });
        thread.start();
    }
 
    private void enviarMensagem(String mensagem){
        for(PrintStream cliente : clientes){
            cliente.println(mensagem);
            cliente.flush();
        }
    }
}

Nesta classe ouvimos as mensagens que os clientes nos mandam e escrevemos as mensagens para os clientes. Precisamos iniciar uma thread aqui pelo mesmo motivo que precisamos iniciar uma thread na classe Chat, execução paralela de tarefas.

É somente possível enviar as mensagens para todos os clientes porque na classe Servidor mantemos uma lista de quem está conectado no servidor. Então, para um novo cliente que entra no chat, é adicionado à lista o stream de escrita daquele cliente. Como a lista é passada por referência para o construtor da nossa classe, ela sempre estará atualizada.

Antes de terminar, fazer um checklist de tudo o que construímos ?
  1. Criação de projetos no Netbeans OK
  2. Criação da recepção de conexões no lado servidor. OK
  3. Criação da conexão do lado cliente. OK
  4. Criação da thread do lado cliente. OK
  5. Abertura dos streams de leitura e escrita do lado cliente. OK
  6. Encerramento do chat. OK
  7. Abertura dos streams de leitura e escrita do lado servidor. OK
Pronto, construímos os alicerces base para o nosso chat. Os pontos adicionais que você já deve ter notado como, formatação da fonte, entre outros, não abordarei aqui porque senão o post ficará mais extenso do que já está.

Para baixar o projeto, clique aqui. Caso você não tenha entendido algum ponto que eu não tenha deixado claro, deixe sua dúvida aí nos comentários.

Dúvidas !? Sugestões ?! Críticas ou elogios ?!

Deixe aí nos comentários ou na nossa página do facebook.

Facebook: https://www.facebook.com/precisoestudarsempre/

Referências:
What Is a Socket? - https://docs.oracle.com/javase/tutorial/networking/sockets/definition.html
Criando um chat com java (Parte 1) - https://www.youtube.com/watch?v=9__5MRYPVxc
Criando um chat com java (Parte 2) - https://www.youtube.com/watch?v=lqSEpj517Qc
Criando um chat com java (Parte 3) - https://www.youtube.com/watch?v=SzUZAvFFLI0

8 comentários:

Anônimo disse...

Oi vc tem o executável do programa?

Preciso estudar sempre disse...

Olá, tudo bem !?

Obrigado pelo comentário. Espero que tenha gostado do post.

É possível adquirir o executável dos dois projetos através do link na postagem (tem uma parte que eu escrevo: clique aqui). Estão dentro do pacote .rar, na pasta dist.

Abraços !

Anônimo disse...

Tem como colocar quando manda a mensagem exemplo Você: ao invés do nome?

Preciso estudar sempre disse...

Sim, tranquilamente.

Qualquer dúvida, só chamar.

Anônimo disse...

E onde coloca no código o você ao invés do nome?

Preciso estudar sempre disse...

A mudança é simples, basta você no método enviarMensagem()
enviar um identificador (uma numeração ou uma string, você escolhe) que o cliente, ou o servidor possam gerar.

No servidor, quando ele receber uma mensagem ele vai ter que comparar
o identificador enviado com os identificadores que ele guarda internamente.
Quando ele achar uma correspondência, no método enviarMensagem(String mensagem), ele troca o nome do usuário pela string "você".

Se você ao invés de enviar o nome do usuário, enviar a string "você" todos os clientes conectados verão sua mensagem com o nome de usuário "você", e você não quer isso.

Deu pra entender ? Se não deu, manda um e-mail para precisoestudarsempre@gmail.com que a gente continua por lá.

Grandes abraços e volte sempre

Unknown disse...

Ola como faço para rodar em dois computadores diferentes?

Preciso Estudar Sempre disse...

Ambos precisam estar na mesma rede, pois ambos se conectaram ao mesmo servidor. Desde que ambos consigam se enxergar em rede basta em cada um configurar o IP do servidor e liberar a porta sendo usada e pronto. Caso você esteja enfrentando dificuldades mande um e-mail para precisoestudarsempre@gmail.com que a gente continua por lá.

Grandes abraços e volte sempre