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.
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()); } } }
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(); } }
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); } }); } }
Este exemplo foi dividido em dois projectos.