Como criar um Chat Multithread com Socket em Java

Veja nesse artigo como criar um chat Multithread com Java. Para isso será necessário usar e aplicar conceitos de Thread e Socket, além da programação Orientado a Objetos.

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:

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.

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.

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

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!

Artigos relacionados