Ir para o conteúdo

Chat, Cliente e Servidor

Neste exemplo iremos pegar no que foi falado na secção de threads e de sockets e vamos tentar implementar uma aplicação que permite o envio de mensagens instantâneas para um conjunto de utilizadores.

Desenvolver uma aplicação de chat não é um processo complicado, a ligação entre um servidor e um cliente é implementada em meia dúzia de linhas, e só se torna mais difícil quando temos de implementar ou seguir um determinado protocolo e de fazer toda uma interface de utilização para que o utilizador consiga enviar as mensagens. Por isso iremos concentrar este exemplo apenas na comunicação entre um cliente e um servidor, sem implementar um protocolo específico, todas as mensagens são replicadas para todos os utilizadores, e sem uma interface complexa.

É preciso ter em conta que esta é uma implementação possível e não será certamente a única ou mesmo a mais adequada.

Portanto, a nossa aplicação vai ser composta por um servidor que será executado em linha de comandos e aceitará um parâmetro que indique o porto onde vamos estar a escutar por ligações, e por um cliente gráfico, que terá apenas uma secção para vermos todas as mensagens enviadas e uma caixa de texto para escrevermos mensagens a serem transmitidas.

Servidor: ChatServer

Esta classe implementa o nosso servidor. A única acção relevante que o servidor faz é iniciar um ServerSocket e por cada cliente que se ligar a esse socket criar uma nova thread que irá processar o pedido. O servidor contém também alguns métodos que podem ser úteis e regista numa lista, todos os clientes ligados.

O servidor é o ponto central de toda a comunicação, e os clientes não terão conhecimentos de outros clientes ligados. Será da responsabilidade do servidor, ao receber uma mensagem, propagar essa mensagem por todos os clientes registados. Como opção de implementação, optou-se por enviar a mensagem para todos os clientes, mesmo de volta para o cliente que a criou.

package org.pap.wiki.chatserver;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class ChatServer {

    private ServerSocket servidor;
    //guardar os clientes
    private final ArrayList<ServerConnHandler> clientes;

    public ChatServer(int port) throws IOException {
        clientes = new ArrayList<ServerConnHandler>();
        listen(port);
    }

    private void listen(int port) throws IOException {

        //criar o socket servidor
        servidor = new ServerSocket(port);
        System.out.println("À escuta em " + servidor);

        //aceitar ligações para sempre
        while (true) {

            Socket cliente = servidor.accept();
            System.out.println("Ligação aceite de " + cliente);

            //criar um novo handler para este cliente
            clientes.add(new ServerConnHandler(this, cliente));
        }
    }

    public void replicarMensagem(String mensagem) {
        //vamos sincronizar o nosso acesso à lista para evitar problemas se alguma
        //outra thread estiver a tentar adicionar ou remover elementos
        synchronized (clientes) {
            for (ServerConnHandler cl : clientes) {
                //enviar mensagem para cliente
                cl.enviarMensagem(mensagem);
            }
        }
    }

    public void removerCliente(ServerConnHandler cliente) {
        synchronized (clientes) {
            System.out.println("A remover a ligação de " + cliente);
            clientes.remove(cliente);
            System.out.println("Ligações restantes: " + clientes.size());
            try {
                cliente.fechar();
            } catch (IOException ex) {
                System.out.println("Erro ao desligar o contacto com " + cliente);
                System.out.println(ex.getMessage());
            }
        }
    }

    public void removeConnection(Socket cliente) {
    }

    public static void main(String args[]) {
        try {
            if (args.length == 0) {
                new ChatServer(5000);
            } else {
                new ChatServer(Integer.parseInt(args[0]));
            }
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        }
    }
}

Processador de Clientes: ServerConnHandler

Processar um cliente significa tão só ficar eternamente à escuta de novas mensagens e possibilitar o envio de mensagens para esse cliente. O nosso processador de clientes é um thread porque de outro modo só conseguiríamos aceitar uma ligação de cada vez e processar uma mensagem de cada vez.

Com a utilização de threads, cada thread escuta um socket específico e recebe ou envia dados para esse socket libertando assim o servidor e permitindo que novos clientes se liguem.

package org.pap.wiki.chatserver;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.net.Socket;

public class ServerConnHandler extends Thread {

    private ChatServer servidor;
    private Socket socket;
    private DataOutputStream dout;

    public ServerConnHandler(ChatServer servidor, Socket socket) throws IOException {
        this.servidor = servidor;
        this.socket = socket;
        dout = new DataOutputStream(socket.getOutputStream());
        //somos uma thread, vamos começar...
        start();
    }

    @Override
    public void run() {
        try {
            //como noutras situações, obter as streams de leitura e escrita.
            DataInputStream din = new DataInputStream(socket.getInputStream());
            String mensagem;
            while (true) {
                mensagem = din.readUTF();
                System.err.println("SR LIDO: " + mensagem);
                //se não foi enviada a mensagem de saida então enviamos o
                //texto para todos
                servidor.replicarMensagem(mensagem);
            }
        } catch (EOFException ex) {
            //DO NOTHING
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        } finally {
            servidor.removeConnection(socket);
        }
    }

    public void enviarMensagem(String mensagem) {
        try {
            dout.writeUTF(mensagem);
        } catch (IOException ex) {
            System.out.println(ex);
        }
    }

    public void fechar() throws IOException {
        socket.close();
    }
}

Cliente: ClientChat

O código do cliente gráfico é muito similar ao código feito para o processamento de clientes na classe ServerConnHandler. Este cliente cria uma ligação ao servidor e inicia uma thread de modo a escutar continuamente por mensagens que cheguem de outros utilizadores, enviadas pelo servidor.

package org.pap.wiki.chats;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import javax.swing.JOptionPane;

public class ClientChat extends javax.swing.JFrame {

    //Socket usado para a ligação
    private Socket socket;
    //Streams de leitura e escrita. A de leitura é usada para receber os dados do
    //servidor, enviados pelos outros clientes. A de escrita para enviar os dados
    //para o servidor.
    private DataInputStream din;
    private DataOutputStream dout;
    //apenas para que o utilizador não altere o nick a meio da conversa
    private String nick;

    public ClientChat() {
        initComponents();
    }

    public void ligar() {
        try {
            nick = jtfNick.getText().trim();
            jtaMensagens.append("<-cliente->: A ligar...\n");
            String host = jtfEndereco.getText().trim();
            int port = Integer.parseInt(jffPorto.getText().trim());

            //criar o socket
            socket = new Socket(host, port);
            //como não ocorreu uma excepção temos um socket aberto
            jtaMensagens.append("<-cliente->: Ligação estabelecida...\n");

            //Vamos obter as streams de comunicação fornecidas pelo socket
            din = new DataInputStream(socket.getInputStream());
            dout = new DataOutputStream(socket.getOutputStream());

            //e iniciar a thread que vai estar constantemente à espera de novas
            //mensages. Se não usassemos uma thread, não conseguiamos receber
            //mensagens enquanto estivessemos a escrever e toda a parte gráfica
            //ficaria bloqueada.
            new Thread(new Runnable() {
                //estamos a usar uma classe anónima...

                public void run() {
                    try {
                        while (true) {
                            //sequencialmente, ler as mensagens uma a uma e acrescentar ao
                            //texto que já recebemos
                            //para o utilizador ver
                            jtaMensagens.append(din.readUTF() + "\n");
                        }
                    } catch (IOException ex) {
                        jtaMensagens.append("<-cliente->: " + ex.getMessage());
                    }
                }
            }).start();

        } catch (IOException ex) {
            jtaMensagens.append("<-cliente->: " + ex.getMessage());
        }
    }

    private void initComponents() {
    //codigo omitido...
    }                        

    private void jtfMensagemActionPerformed(java.awt.event.ActionEvent evt) {                                            
        try {
            //enviar a mensagem para o servidor.
            //anexamos o nickname deste utilizador apenas para identificação
            dout.writeUTF("<" + nick + ">: " + jtfMensagem.getText().trim());
            jtfMensagem.setText("");
        } catch (IOException ex) {
            jtaMensagens.append("<-cliente->: " + ex.getMessage());
        }
    }                                           

    private void jbtnLigarActionPerformed(java.awt.event.ActionEvent evt) {                                          
        if (jtfNick.getText().trim().isEmpty()) {
            JOptionPane.showMessageDialog(this, "Nick não pode ser vazio.", "Nick vazio...", JOptionPane.ERROR_MESSAGE);
            return;
        }

        if (jtfEndereco.getText().trim().isEmpty()) {
            jtfEndereco.setText("localhost");
        }
        if (jffPorto.getText().trim().isEmpty()) {
            jffPorto.setText("5000");
        }

        ligar();
    }                                         

    private void formWindowClosing(java.awt.event.WindowEvent evt) {                                   
        if (socket != null) {
            try {
                socket.close();
            } catch (IOException ex) {
                System.out.println(ex.getMessage());
            }
        }
    }                                  

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {

            public void run() {
                new ClientChat().setVisible(true);
            }
        });
    }
}

Download de Ficheiros Exemplo

Este exemplo foi dividido em dois projectos.