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.