Chat Socket Baseado em Eventos

Tempos atrás encontrei um exercício sobre chat com sockets que me chamou a atenção. Achei interessante porque parece aqueles sistemas antigos, antes das aplicações com interface gráfica, que eram baseados apenas em comandos no modo console e quando consegui um tempinho livre resolvi tentar implementá-lo. Acabei não pensando muito qual seria a melhor maneira de fazer, apenas fui programando pouco a pouco, então com certeza depois que você conferir o código, terá novas idéias, tanto em relação a qualidade de código quanto a novas funções ou mesmo melhorando as já implementadas.

1. Exercício proposto

Faça uma aplicação Socket cliente-servidor baseada em eventos. Desta forma, clientes podem se subscrever a eventos de outros clientes, bem como publicar eventos que são divulgados a outros clientes que se subscreveram a este. Desta forma, a aplicação deverá atender aos seguintes requisitos:

  • O cliente, ao iniciar sua aplicação, deverá se conectar no servidor. Para isso ele deverá digitar, por exemplo, /connect 192.168.1.1 , sendo este o IP do servidor.
  • Caso o cliente ainda não tenha se registrado, ele deverá se registrar digitando o comando /register. O servidor deverá verificar se já não existe outro cliente com o mesmo login, e caso não, deverá armazenar de alguma forma as informações do cliente (com arquivos, por exemplo) e retornar uma mensagem indicando sucesso no registro.
  • Caso o cliente já possua registro, ele poderá se logar com /login. Por sua vez, o servidor deverá confirmar se o login ocorreu corretamente ou não. Se o login ocorreu de maneira correta, os eventos abaixo poderão ser utilizados, e caso contrário, não poderão.
  • Após logar, o cliente poderá publicar eventos, que no caso desta aplicação, tais eventos serão somente mensagens de texto. Para publicar um evento, o cliente deverá digitar /public. Após publicar tal evento, outros clientes que haviam se subscrito nos eventos deste cliente deverão receber uma notificação indicando o acontecimento de tal evento.
  • Além disso, o cliente também poderá se inscrever em eventos de outros clientes. Para isso, ele poderá utilizar o comando /list para listar todos os clientes cadastrados e também o comando /subscreve, em que é o login do cliente no qual este deseja se subscrever para receber notificações de seus eventos.
  • Deverá ser implementado um comando /logout para deslogar.
  • Os comandos citados acima deverão existir obrigatoriamente no sistema, mas nada impede que sejam adaptados para um melhor funcionamento.

2. Classes de comandos

Como o sistema é baseado em comandos de eventos, resolvi criar duas classes que estarão presentes tanto na aplicação cliente quanto na aplicação servidor. A classe CommandRequestListagem 1 – contem os comandos que serão enviados do cliente ao servidor, e a classe CommandResponseListagem 2 – contem os comandos de resposta do servidor para o cliente. As classes possuem variáveis estáticas que representam os comandos que serão digitados pelo usuário, como também comandos de resposta do servidor. Além disso, a classe CommandRequest possui três variáveis de instancia que serão úteis para o seguinte propósito. Quando a aplicação cliente enviar uma mensagem ao servidor, essa mensagem será enviada através de um objeto CommandRequest, onde a variável cmd1 conterá um comando, a cmd2 conterá ou o login do usuário ou o titulo da mensagem publicada e a cmd3 conterá ou a senha do usuário ou o corpo da mensagem de uma mensagem publicada.

Listagem 1. Classe CommandRequest
package com.wp.mb.evt.command;

import java.io.Serializable;

/**
 * https://www.mballem.com/
 */
public class CommandRequest implements Serializable {
    //variaveis estaticas que armazenam os possiveis comandos do cliente.
    public static final String CONNECT = "/connect";
    public static final String LOGIN = "/login";
    public static final String REG = "/register";
    public static final String PUB = "/public";
    public static final String SUBS = "/subscreve";
    public static final String LIST = "/list";
    public static final String LOGOUT = "/logout";
    public static final String HELP = "/help";

    private String cmd1;

    private String cmd2;

    private String cmd3;

    // gere os métodos get/set das variaveis cmd1, cmd2, cmd3

    public static void help() {
        System.out.println("/connect         - Efetua a conexao.");
        System.out.println("/register   - Registro de novo cliente.");
        System.out.println("/login      - Executa o login no servidor.");
        System.out.println("/public    - Publica mensagens para seus " +
                           "subscritos.");
        System.out.println("/subscreve            - Realiza a subscricao em um " +
                           "determinado cliente.");
        System.out.println("/list                        - Lista todos os clientes ativos no " +
                           "servidor.");
        System.out.println("/logout                      - Encerra a conexao com o servidor.");
    }
}
Listagem 2. Classe CommandResponse
package com.wp.mb.evt.command;

import java.io.Serializable;

/**
 * https://www.mballem.com/
 */
public class CommandResponse implements Serializable {
    //variaveis estaticas que armazenam as possiveis respostas do servidor.
    public static final String SUBS_YES = "SUBS_YES";
    public static final String LOGIN_YES = "LOGIN_YES";
    public static final String LOGIN_NO = "LOGIN_NO";
    public static final String LOGIN_LOGGED = "LOGIN_LOGGED";
    public static final String EVT_YES = "EVT_YES";
    public static final String REG_YES = "REG_YES";
    public static final String REG_NO = "REG_NO";
    public static final String REG_LOGGED = "REG_LOGGED";
    public static final String ON_LINE = "ON_LINE";
}

3. App Server

Na aplicação servidor foram implementas algumas classes a mais do que será no cliente, isso porque, temos o elemento arquivo que deverá ser criado pelo servidor para gravar quais usuários estão subscritos nos tópicos de outros usuários. Para isso, primeiro implementei uma classe User, conforme Listagem 3. Esta classe foi baseada em mapeamento JABX2 para manipulação de XML. O XML será o tipo de arquivo utilizado pelo servidor para gravar as informações dos usuários. Se você não tem conhecimento sobre JAXB2, confira mais na seção Referências.

Listagem 3. Classe User
package com.wp.mb.evt.entity;

import javax.xml.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

/**
 * https://www.mballem.com/
 */
//indica o elemento raiz do arquivo XML
@XmlRootElement(name = "User")
//indica que vamos trabalhar com os metodos get/set
@XmlAccessorType(XmlAccessType.FIELD)
public class User { //Classe User, também contem as instrucões para montar os arquivos XML
    //instrui a criacao da tag login no arquivo xml
    @XmlElement(name = "login", required = true)
    private String login;
    //instrui a criacao da tag password no arquivo xml
    @XmlElement(name = "password", required = true)
    private String password;
    //instrui a criacao da tag subscritos no arquivo xml (sera uma lista)
    @XmlElementWrapper(name = "subscritos")
    //instrui a criacao das tags filhas de subscritos no arquivo xml (elementos da lista)
    @XmlElement(name = "subscrito")
    private List subscritos = new ArrayList();

    // gere os métodos get/set

    @Override
    public String toString() {
        return "User{" +
                "login='" + login + ''' +
                ", password='" + password + ''' +
                ", subscritos=" + subscritos +
                '}';
    }
}

Para manipular os arquivos, vamos usar o padrão DAO. Para isso, foram criadas a interface IUserDaoListagem 4 – e a classe UserDaoListagem 5 – que implementa os métodos da interface. O método marshalToFile() converte o objeto User em uma estrutura XML e o método unmarshalFromFile() executa a tarefa contrária.

Listagem 4. Classe IUserDao
package com.wp.mb.evt.dao;

/**
 * https://www.mballem.com/
 */
public interface IUserDao {

    void marshalToFile(User user, String fileName);

    User unmarshalFromFile(String fileXml);
}
Listagem 5. Classe UserDao
package com.wp.mb.evt.dao;

import com.wp.mb.evt.entity.User;

import javax.xml.bind.*;
import java.io.*;

/**
 * https://www.mballem.com/
 */
public class UserDao implements IUserDao {

    private Marshaller marshaller;

    private Unmarshaller unmarshaller;

    /**
     * Converte o objeto user em uma estrutura XML.
     * @param user objeto a ser convertido em XML.
     * @param fileName nome do arquivo XML a ser gerado.
     */
    public void marshalToFile(User user, String fileName) {
        JAXBContext context;
        Writer writer = null;
        try {
            context = JAXBContext.newInstance(user.getClass());
            marshaller = context.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            writer = new FileWriter(fileName);
            marshaller.marshal(user, writer);
        } catch (PropertyException e) {
            e.printStackTrace();
        } catch (JAXBException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close(); //fecha o arquivo
                }
            } catch (Exception e) {
                e.getMessage();
            }
        }
    }

    /**
     * Realiza a conversao (unmarshal) de um arquivo XML em um objeto do seu tipo.
     * @param fileXml nome do arquivo XML a ser convertido em objeto.
     * @return novo objeto.
     */
    public User unmarshalFromFile(String fileXml) {
        JAXBContext context;
        try {
            context = JAXBContext.newInstance(User.class);
            unmarshaller = context.createUnmarshaller();
            return (User) unmarshaller.unmarshal(
                    new FileInputStream(fileXml)
            );
        } catch (JAXBException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Vamos agora passar para a criação do Socket no servidor, que se encontra na classe SocketService da Listagem 6. Já na Listagem 7 temos a classe EventService com os eventos de resposta do servidor. As duas classes estão bem comentadas, portanto, leia os comentários para ter uma idéia geral da função de cada método. Assim, encerramos o lado servidor. Veja na figura 1 a estrutura do projeto servidor:

Figura 1 - Estrutura da App Servidor

Figura 1 – Estrutura da App Servidor

Listagem 6. Classe SocketService
package com.wp.mb.evt.service;

import com.wp.mb.evt.command.CommandRequest;
import com.wp.mb.evt.entity.User;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

import static com.wp.mb.evt.command.CommandRequest.*;
import static com.wp.mb.evt.command.CommandRequest.LIST;

/**
 * https://www.mballem.com/
 */
public class SocketService {

    private static String MSG_ERROR_PUB = 
        "You must be logged in to post!";
    private static String MSG_ERROR_SUBS = 
        "You must be logged in to realize subscription!";
    private static String MSG_ERROR_LIST = 
        "You must be logged in to visualize the list of connected users!";

    private Socket socket;

    private ServerSocket serverSocket;

    //cria uma lista que guarda os Clientes conectados.
    static Map MAP_USERS = new HashMap();

    //no construtor inicializamos o servidor
    public SocketService() {
        try {
            serverSocket = new ServerSocket(5555);
            System.out.println("Server On!");

            //socket ouvinte
            while (true) {
                //espera aqui ate que um cliente novo se conecte
                socket = serverSocket.accept();

                System.out.println("Cliente conectado!");

                //inicia a classe thread ListenerCliente passando o socket 
                // atual como parametro
                new Thread(new ListenerSocket(socket)).start();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Classe interna, thread ouvinte do servidor.
     */
    private class ListenerSocket implements Runnable {

        //objeto para receber os dados entrada
        private ObjectInputStream input;

        //objeto para enviar os dados de saida
        private DataOutputStream output;

        //objeto que vai guardar os dados do usuario atual da thread
        private User user;

        //objeto para executar os metodos de eventos
        private EventService eventService;

        //objeto de manipulacao dos arquivos xml
        public ListenerSocket(Socket socket) {
            //variavel input recebe os dados do socket atual
            try {
                input = new ObjectInputStream(socket.getInputStream());
                output = new DataOutputStream(socket.getOutputStream());
                eventService = new EventService();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public synchronized void run() {
            CommandRequest message;

            try {
                while ((message = (CommandRequest) input.readObject()) != null) {
                    //Teste que verifica o tipo de comando recebido.
                    //Conforme tipo de comando, sera chamado o
                    // metodo referente para executar o processo.
                    if (message.getCmd1().equals(REG)) {

                        eventService.registrar(message, user, output);

                    } else if (message.getCmd1().equals(LOGIN)) {
                        //quando o usuario tentar logar retorna um objeto user
                        user = eventService.logon(message, output);

                        //Se o objeto retornado for != de null entao e porque o 
                        // usuario existe, caso contrario sera null e o usuario 
                        // ainda na existe e entao nao sera armazenado na lista 
                        // de clientes.
                        if (user != null) {
                            //adiciona na lista de clientes os usuarios que efetuarem
                            // o login com sucesso.
                            //a lista e um Map, a chave sera o login e o valor sera o
                            // objeto de saida de dados do socket
                            //do usuario da thread atual
                            MAP_USERS.put(user.getLogin(), new DataOutputStream(output));
                        }

                    } else if (message.getCmd1().equals(PUB)) {
                        //apenas usuários logados, existentes na lista, 
                        // podem publicar eventos
                        if (user != null) {
                            eventService.publicar(message, user);
                        } else {
                            eventService.send(output, MSG_ERROR_PUB);
                        }
                    } else if (message.getCmd1().equals(SUBS)) {
                        //apenas usuários logados, existentes na lista,
                        // podem requere uma subscricao
                        if (user != null) {
                            eventService.subscrever(message, output, user.getLogin());
                        } else {
                            eventService.send(output, MSG_ERROR_SUBS);
                        }
                    } else if (message.getCmd1().equals(LIST)) {
                        //apenas usuários logados, existentes na lista,
                        // podem listar os demais clientes
                        if (user != null) {
                            eventService.listarClientesConectados(output, user);
                        } else {
                            eventService.send(output, MSG_ERROR_LIST);
                        }
                    }
                }
            } catch (IOException e) {
                //quando o cliente efetuar o logout retiramos ele da 
                // lista de clientes ativos
                MAP_USERS.remove(user.getLogin());
                System.out.println("User [" + user.getLogin() + "] disconnected!");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new SocketService();
    }
}
Listagem 7. Classe EventService
package com.wp.mb.evt.service;

import com.wp.mb.evt.command.CommandRequest;
import com.wp.mb.evt.dao.IUserDao;
import com.wp.mb.evt.dao.UserDao;
import com.wp.mb.evt.entity.User;

import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Map;

import static com.wp.mb.evt.command.CommandResponse.*;
import static com.wp.mb.evt.service.SocketService.*;

/**
 * https://www.mballem.com/
 */
public class EventService {

    private static String XML = ".xml";

    private IUserDao dao = new UserDao();

    /**
     * metodo resposavel por enviar os eventos de um cliente para os
     * demais clientes cadastrados a ele.
     * @param message string com a mensagem a ser enviada para o grupo de
     *        clientes cadastrados ao user atual
     * @param user o proprio usuario que criou a mensagem que sera publicada
     */
    private void sendAll(String message, User user) {
        //fazemos um for na lista de clientes ativos
        for (Map.Entry map : MAP_USERS.entrySet()) {
            try {
                //Testamos se o usuario atual da thread possui 
                // o cliente na posicao do loop em sua lista 
                // de subscritos, se possuir, envia o envento para ele.
                if (user.getSubscritos().contains(map.getKey())) {
                    map.getValue().writeUTF(String.valueOf(message));
                    map.getValue().flush();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * metodo responsavel por enviar mensagem apenas para o proprio usuario.
     * util para todas requisoes que nao sao de publicacao de eventos.
     *
     * @param outputStream cliente atual
     * @param message mensagem a ser enviada ao cliente
     */
    public void send(DataOutputStream outputStream, String message) {
        try {
            outputStream.writeUTF(message);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * metodo que lista todos os usuários ativos no servidor
     *
     * @param outputStream cliente atual da thread, 
     *        o que fez o pedido da lista
     * @param user o usuario atual da thread
     */
    public void listarClientesConectados(DataOutputStream outputStream, User user) {
        String lista = ON_LINE;

        //faz o loop na lista de clientes
        for (Map.Entry map : MAP_USERS.entrySet()) {

            //evita que o proprio usuario que solicitou a lista receba
            // seu proprio nome entre os listados.
            if (!user.getLogin().equals(map.getKey())) {
                lista = lista + " [" + map.getKey() + "] ";
            }
        }

        send(outputStream, lista);
    }

    /**
     * metodo para subscricao de usuários em outro usuario
     *
     * @param request objeto com os dados enviados pelo cliente
     * @param outputStream   objeto socket do cliente atual
     * @param subscrever objeto que contem o login de quem quer 
     *        se escrever em outro cliente
     */
    public void subscrever(CommandRequest request, DataOutputStream outputStream,
                           String subscrever) {
                           
        //pelo comando obtemos em qual cliente vamos registar 
        // a subscricao
        String login = request.getCmd2();

        //criamos a url com o arquivo do cliente que recebera 
        // a inscricao de outro cliente
        String url = login + XML;

        //recuperamos do arquivo encontrado os dados la contidos
        User user = (User) dao.unmarshalFromFile(url);

        //adicionamos ao objeto user o novo login que sera 
        // subscrito na lista de subscritos
        user.getSubscritos().add(subscrever);

        //agora salvamos as alteracoes no arquivo
        dao.marshalToFile(user, url);

        //envia a mensagem para o cliente que a solicitou
        send(outputStream, SUBS_YES);
    }

    /**
     * metodo responsavel por publicar novos eventos
     *
     * @param request objeto com os dados enviados pelo cliente
     * @param user     objeto com o usuario atual da thread
     */
    public void publicar(CommandRequest request, User user) {
        //recuperamos o titulo do evento
        String titulo = request.getCmd2();

        //recuperamos o conteudo da mensagem do evento
        String conteudo = request.getCmd3();

        //le o arquivo para recuperar a lista atualizada 
        // dos clientes subscritos
        user = (User) new UserDao().unmarshalFromFile(user.getLogin() + ".xml");

        //mensagem que sera publicada. Na classe cliente 
        // retiraremos a parte EVT YES
        String mensagem = EVT_YES + "(" + user.getLogin() + ") " + titulo + " : " + conteudo;

        //envia a mensagem pelo metoso sendAll
        sendAll(mensagem, user);
    }

    /**
     * metodo resposavel pelo login
     *
     * @param request objeto com os dados enviados pelo cliente
     * @param outputStream objeto de saida de dados da thread atual
     * @return o retorno sera um objeto com o login, 
     *         senha e lista de subscritos.
     * Ou null, se o usuario ainda nao tiver um cadastro, ou seja, 
     *  um arquivo.
     */
    public User logon(CommandRequest request, DataOutputStream outputStream) {
        //instancia um objeto usuario e o popula com os dados enviados pelo cliente
        User user = new User();
        user.setLogin(request.getCmd2());
        user.setPassword(request.getCmd3());

        //cria a url do arquivo xml
        String url = user.getLogin() + XML;

        //vamos testar se o arquivo com os dados do cliente já existe.
        File file = new File(url);

        if (MAP_USERS.containsKey(user.getLogin())) {
            //caso o cliente ja esteja logado nao podera logar novamente
            send(outputStream, LOGIN_LOGGED);
        } else if (file.exists()) {
            //se o arquivo existe capturamos suas informacoes e 
            // populamos o novo objeto user
            User xml = dao.unmarshalFromFile(url);

            //agora testamos se o login e senha enviadas pelo cliente,
            // correspondem com o login e senha do arquivo lido. Se SIM,
            // retornamos esse usuario, e ele logara e sera inserido 
            // na lista de clientes ativos.
            // Caso contrario retornamos null
            if (xml.getLogin().equals(user.getLogin()) && xml.getPassword().equals(user.getPassword())) {
                //enviamos a mensagem LOGIN_YES para a classe Cliente
                send(outputStream, LOGIN_YES);
                //Aqui retornamos o usuario lido no xml para o objeto user da thread
                return xml;
            } else {
                //se o login e senha nao forem os corretos,
                // retornamos LOGIN_NO para a classe Cliente.
                send(outputStream, LOGIN_NO);
            }
        } else {
            //se arquivo nao existe retornamos a mensagem 
            // LOGIN_NO para a classe Cliente
            send(outputStream, LOGIN_NO);
        }
        return null;
    }

    /**
     * metodo resposavel por registrar novos clientes
     *
     * @param request objeto com os dados enviados pelo cliente
     * @param logado   objeto usuario da thread atual
     * @param output
     */
    public void registrar(CommandRequest request, User logado, DataOutputStream output) {
        User user = new User();
        user.setLogin(request.getCmd2());
        user.setPassword(request.getCmd3());

        String url = user.getLogin() + XML;

        File file = new File(url);

        if (logado != null) {
            //se logado for diferente de null e porque um usuario
            // online esta tentando registrar um novo cliente,
            // o que nao sera permitido.
            send(output, REG_LOGGED);
        } else if (file.exists()) {
            //se arquivo ja existe, o registro sera cancelado, isto porque
            // ja se tem um cliente com este login do registro
            send(output, REG_NO);
        } else {
            if (user.getLogin() != null && user.getPassword() != null) {
                //se nao existe, fazemos o registro
                dao.marshalToFile(user, url);
                send(output, REG_YES);
            } else {
                send(output, REG_NO);
            }
        }
    }
}

4. App Cliente

Na aplicação cliente, teremos quatro classes, CommandRequest, CommandResponse, SocketClient e EventClient. Na classe SocketClient temos os métodos responsáveis pela conexão com o servidor, como também pelos demais métodos que correspondem com cada evento do usuário. Já a classe EventClient fica responsável por exibir ao usuário as respostas enviadas do servidor. Novamente, leia os comentários dos códigos para ter uma idéia geral do que cada método faz.

Listagem 8. Classe SocketClient
package com.wp.mb.evt.client;

import com.wp.mb.evt.command.CommandRequest;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Scanner;

import static com.wp.mb.evt.command.CommandRequest.*;
import static com.wp.mb.evt.command.CommandResponse.*;

/**
 * https://www.mballem.com/
 */
public class SocketClient {

    private Socket socket;

    private ObjectOutputStream output;

    private CommandRequest request;

    public SocketClient() {
        try {
            initSocket();
        } catch (NullPointerException e) {
            System.out.println("without connection");
        } catch (IOException e) {
            System.out.println("failed connection " + e.getMessage());
        }
    }

    private void initSocket() throws IOException {
        //leitor de dados do teclado
        Scanner keyboard = new Scanner(System.in);

        String input = "";

        while (!input.equals(LOGOUT)) {
            System.out.print(":> ");
            //recebe a linha digitada no teclado
            input = keyboard.nextLine();
            //seleciona o comando digitado
            if (input.contains(CONNECT)) {
                connect(input);
            } else if (input.contains(LOGIN)) {
                actions(input);
            } else if (input.contains(REG)) {
                actions(input);
            } else if (input.contains(PUB)) {
                actions(input);
            } else if (input.contains(SUBS)) {
                actions(input);
            } else if (input.equals(LIST)) {
                list();
            } else if (input.equals(LOGOUT)) {
                //quando o usuario digitar /logout fechamos o socket cliente
                socket.close();
            } else if (input.equals(HELP)) {
                CommandRequest.help();
            }
        }
    }

    /**
     * Envia o comando /list para o servidor
     *
     * @throws IOException
     */
    private void list() throws IOException {
        CommandRequest message = new CommandRequest();
        //passamos o comando /list para o servidor
        message.setCmd1(LIST);
        //enviamos a mensagem para o servidor com o comando 
        // /list no objeto comandos
        sendMessage(message);
    }

    /**
     * Como os comandos estao em uma unica linha, 
     *  vamos separa-los para adicionar
     * as partes especificas em um objeto CommandRequest.
     * @param input entrada do teclado
     * @throws IOException
     */
    private void actions(String input) throws IOException {
        // instancia da classe CommandRequest para criar o objeto
        // que sera enviado como mensagem para o servidor
        CommandRequest message = new CommandRequest();

        //quebra a linha digitada no teclado em posicoes em um array
        // separadas pelo caracter espaco " ".
        String inputLine[] = input.split(" ");

        String aux = "";
        for (int i = 0; i < inputLine.length; i++) {
            if (i == 0) {
                //aqui pegamos o comando, /login, /register, ...
                message.setCmd1(inputLine[0]);
            } else if (i == 1) {
                //aqui pegamos a segunda instrucao, 
                // como login ou titulo do evento publicado.
                message.setCmd2(inputLine[1]);
            } else {
                //aqui pegamos as demais instrucoes, 
                // como senha ou a mensagem do evento publicado.

                //concatenamos as palavras. 
                // Se a mensagem for: Ola Mundo Java,
                // sera quebrada em 3 posicoes, assim,
                // precisamos concatena-las novamente.
                aux = aux.concat(inputLine[i] + " ");
                //trim() elimina qualquer espaco em branco do inicio e 
                // do final da frase.
                message.setCmd3(aux.trim());
            }
        }
        //se a entrada do teclado nao for fazia, 
        // entao enviamos a mensagem.
        if (!input.equals("")) {
            sendMessage(message);
        }
    }

    /**
     * metodo que envia as mensagens para o servidor
     *
     * @param message sera um objeto da classe Comandos
     */
    private void sendMessage(CommandRequest message) throws IOException {
        output.writeObject(message);
        output.flush();
    }

    /**
     * realiza a conexao com o servidor.
     * @param input entrada do teclado
     * @throws IOException
     */
    private void connect(String input) throws IOException {
        //aqui separamos o /connect do ip, criando um array de 2 posicoes
        //[0] = /con/ect
        //[1] = ip (localhost...)
        String[] ip = input.split(" ");

        //queremos apenas o ip (posicao 1)
        socket = new Socket(ip[1], 5555);

        //inicializa o objeto que envia dados para o servidor
        output = new ObjectOutputStream(socket.getOutputStream());

        System.out.println("connection ok! n Please, enter your login or register.");

        //inicializa a thread ouvinte, que recebera os dados do servidor
        receiveMessage();
    }

    /**
     * Metodo que contem a thread ouvinte do socket
     *
     * @throws IOException
     */
    private void receiveMessage() throws IOException {

        //recebe os dados enviados pelo servidor
        final DataInputStream input = new DataInputStream(socket.getInputStream());

        final EventClient eventClient = new EventClient();

        //thread que fica escutando o servidor
        Thread thread = new Thread() {
            @Override
            public void run() {
                String message;
                try {
                    while ((message = input.readUTF()) != null) {
                        //Quando uma mensagem chegar, chamamos o método referente.
                        if (message.equals(REG_YES) ||
                            message.equals(REG_NO) || message.equals(REG_LOGGED)) {
                            eventClient.confirmarRegistro(message);
                        } else if (message.equals(LOGIN_YES) ||
                            message.equals(LOGIN_NO) || message.equals(LOGIN_LOGGED)) {
                            eventClient.confirmarLogin(message);
                        } else if (message.contains(EVT_YES)) {
                            eventClient.receberEventos(message);
                        } else if (message.contains(ON_LINE)) {
                            eventClient.receberLista(message);
                        } else if (message.equals(SUBS_YES)) {
                            eventClient.subscrever();
                        }
                    }
                } catch (IOException e) {
                    System.out.println("connection finished!");
                }
            }
        };
        thread.start();
    }

    public static void main(String[] args) {
        new SocketClient();
    }
}
Listagem 9. Classe EventClient
package com.wp.mb.evt.client;

import static com.wp.mb.evt.command.CommandResponse.*;

/**
 * Created by IntelliJ IDEA.
 * User: Marcio Ballem
 * Date: 24/12/12
 * Time: 10:05
 * https://www.mballem.com/
 */
public class EventClient {
    /**
     * metodo que recebe a resposta do servidor.
     */
    public void subscrever() {
        System.out.print("Congratulations, your subscription was successful!" + "  ");
    }

    /**
     * metodo que recebe a lista de clientes ativos no servidor.
     *
     * @param resposta contem a lista enviada pelo servidor
     *                   com os clientes ativos + a instrucao Online
     */
    public void receberLista(String resposta) {
        //O método contains() é parecido com o equals(), porem ele analisa se em uma frase,
        // contem tal palavra.
        System.out.print("List of users " + resposta + "  ");
    }

    /**
     * metodo que recebe o evento publicado por outro cliente.
     *
     * @param resposta a instrucao EVT YES + a mensagem publica,
     *                   com titulo e o login de quem a enviou.
     */
    public void receberEventos(String resposta) {
        String mensagem = resposta.replace(EVT_YES, "");
        System.out.print(mensagem + "  " );
    }

    /**
     * metodo que confirma se houve sucesso na requisição do login.
     *
     * @param resposta mensagem com a instrucao LOGIN YES se houve sucesso,
     *                 LOGIN NO se nao foi possivel fazer o login e
     *                 LOGIN LOGGED se tentar logar com um usuario ja logado
     */
    public void confirmarLogin(String resposta) {
        if (resposta.equals(LOGIN_YES)) {
            System.out.print("You're logged!" + "  ");
        } else if (resposta.equals(LOGIN_NO)) {
            System.out.print("Login not exist or wrong password!" + "  " );
        } else if (resposta.equals(LOGIN_LOGGED)) {
            System.out.print("You're already logged!" + "  " );
        }
    }

    /**
     * metodo que confirma se houve sucesso no registro de um novo cliente.
     *
     * @param resposta se o registro foi feito a mensagem tera REG YES,
     *                 caso contrario REG NO e se o usuario logado tentar
     *                 se registrar novamento, recebe um REG LOGGED
     */
    public void confirmarRegistro(String resposta) {
        if (resposta.equals(REG_YES)) {
            System.out.print("Login registered successfully!" + "  ");
        } else if (resposta.equals(REG_NO)) {
            System.out.print("Login denied! Trying a new login and password." + "  ");
        } else if (resposta.equals(REG_LOGGED)) {
            System.out.print("Client logged must not make a new record!" + "  ");
        }
    }
}

Veja na fígura 2 a estrutura do projeto Cliente:

Fígura 2 - Estrutura da App Cliente

Fígura 2 – Estrutura da App Cliente

Para você entender o funcionamento deste chat, assista o vídeo a seguir. Neste vídeo demonstro como utilizar os comandos, testo algumas regras de lógica implementadas no sistema como não poder logar se já estiver logado, ou não permitir um novo registro de quem já está logado ou não permitir o cadastro de 2 usuários ou mais com o mesmo login, entre outras coisas.

Referências

Download do Projeto

Ballem

Marcio Ballem é bacharel em Sistemas de Informação pelo Centro Universitário Franciscano em Santa Maria/RS. Tem experiência com desenvolvimento Delphi e Java em projetos para gestão pública e acadêmica. Possui certificação em Java, OCJP 6.

Você pode gostar...