O artigo está dividido nas seguintes seções:

  • Introdução
  • Exemplo: Servidor TCP/IP
  • Exemplo: Cliente TCP/IP
  • Arquitetura Básica
  • IoService
  • IoSession
  • IoHandler
  • IoFilter
  • Filtro ProtocolCodecFilter
  • Criando seu próprio mecanismo de serialização usando MINA

Introdução

O MINA (Multi-Purpose Infrastructure for Network Applications) é um framework Java que facilita a criação de aplicações de rede de alto desempenho e escalabilidade, de forma rápida e produtiva. Ele fornece suporte a vários tipos de protocolos, como TCP, UDP, XML, FTP, HTTP, Serial, etc, além de permitir a criação de protocolos personalizados.

Uma camada de abstração de alto nível para operações de rede
Figura 2. Uma camada de abstração de alto nível para operações de rede

O framework foi construído a partir das seguintes premissas:

  • Baseado na API NIO (Non-Blocking I/O)
  • Orientada a Eventos (Assíncronos)
  • Suporte a Múltiplos Protocolos
  • Separação de Conceitos
  • Baseado em Inversão de Controle

A vantagem de usar a NIO sobre a IO bloqueante é que as operações envolvidas não precisam aguardar por um resultado imediato (por isso são consideradas assíncronas). Ao criar um servidor TCP/IP, por exemplo, usando IO bloqueante, cada conexão deve ser processada por uma thread separada, pois operações de leitura/escrita podem ficar bloqueadas à espera de resposta, enquanto no NIO uma única thread pode lidar, de forma assíncrona, com várias conexões. Isso faz com que o desempenho do servidor seja mais eficiente.

Exemplo: Servidor TCP/IP

Nota: Para rodar todos exemplos do artigo, as seguintes dependências devem ser obtidas:


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

public class TCPServer {
    
    private static final int PORT = 5050;

    public static void main( String[] args ) throws IOException {
        IoAcceptor acceptor = new NioSocketAcceptor();
        acceptor.getFilterChain().addLast( "logger1", new LoggingFilter() );
        acceptor.getFilterChain().addLast( "codec1", new ProtocolCodecFilter( 
        new TextLineCodecFactory( Charset.forName( "UTF-8" ))));    
        acceptor.setHandler(  new ServerHandler() );        
        acceptor.bind( new InetSocketAddress(PORT) );
    }
}
Listagem 1. Criando um TCPServer com MINA

import java.util.Date;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class ServerHandler extends IoHandlerAdapter {

    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        String str = message.toString();
        
        System.out.println("Message received: " + str);
        
        if (str.trim().equalsIgnoreCase("quit")) {
            session.close(true);
            return;
        }
        
        if (str.trim().equals("data")) {
            Date date = new Date();
            session.write(date.toString());
        } else {  
            session.write("echo " + str);
        }
        
        System.out.println("Message written...");
    }
}
Listagem 2. ServerHandler

A partir dessas duas classes, já temos um servidor TCP/IP (na porta 5050) capaz de processar comandos de texto. Para realizar testes, basta utilizar o aplicativo telnet:

telnet 127.0.0.1 5050

E uma vez conectado, podem ser enviado ao servidor os seguintes comandos:

  • data: será retornado a data/hora atual do sistema
  • quit: para finalizar a conexão
  • qualquer outro comando: volta como echo.

Analisando o Código


IoAcceptor acceptor = new NioSocketAcceptor();
Listagem 3. IoAcceptor

Todas as classes que implementam a interface SocketAcceptor (SocketAcceptor é subinterface de IoAcceptor) atuam sobre o protocolo TCP/IP. Na versão 2.0.7 do MINA, a única classe concreta que implementa essa interface é a NioSocketAcceptor.


acceptor.getFilterChain().addLast( "logger", new LoggingFilter() );
acceptor.getFilterChain().addLast( "codec", new ProtocolCodecFilter( 
new TextLineCodecFactory( Charset.forName( "UTF-8" ))));
Listagem 4. Filter Chain

Nesse código, 2 filtros são adicionados:

  • LoggingFilter: Loga todos os eventos associados a conexão
  • ProtocolCodecFilter: Converte dados da conexão em objeto e vice-versa

Podemos aplicar filtros ao IoAcceptor, sendo que cada filtro realizará uma atividade em particular. É similar ao conceito de filtros da API de Servlets (padrão Chain Of Responsibility).


acceptor.setHandler( new ServerHandler() );
acceptor.bind( new InetSocketAddress(PORT) );
Listagem 5. Handler

Na primeira linha estamos configurando um handler (ServerHandler) para o IoAcceptor. Este handler irá tratar todos os eventos gerados pelo MINA. Na classe ServerHandler da Listagem 2, por exemplo, está sendo sobrescrito o método messageReceived (que é disparado quando algum cliente envia dados), para implementar a lógica da aplicação.

Na segunda linha, realizamos o bind com a porta 5050 e iniciamos o processamento das conexões remotas.

Exemplo: Cliente TCP/IP


import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.Scanner;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

public class TCPClient {

    private static int TIMEOUT = 1000;
    private static String HOSTNAME = "127.0.0.1";
    private static int PORT = 5050;

    public static void main(String[] args) throws Throwable {

        NioSocketConnector connector = new NioSocketConnector();
        // Configurando
        configureConnector(connector);
        // Cria sessao
        IoSession session = connect(connector);
        if (session != null) {
            // Envia comandos do teclado
            sendCommands(session);
        }
        // Encerra conexoes
        close(connector, session);
    }

    /*
     *  Configura NioSocketConnector 
     */
    public static void configureConnector(final NioSocketConnector connector) {
        connector.setConnectTimeoutMillis(TIMEOUT);

        connector.getFilterChain().addLast("codec",
            new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
        connector.getFilterChain().addLast("logger", new LoggingFilter());

        connector.setHandler(new ClientHandler());
    }

    /*
     *  Conexao com o server
     */
    private static IoSession connect(final NioSocketConnector connector) 
    throws InterruptedException {
        IoSession session = null;
        try {
            ConnectFuture future = connector.connect(new 
            InetSocketAddress(HOSTNAME, PORT));
            future.awaitUninterruptibly();
            session = future.getSession();
        } catch (RuntimeIoException e) {
            System.err.println("Failed to connect.");
            e.printStackTrace();
        }
        return session;
    }

    /*
     *  Envia comando do teclado ao servidor
     */    
    private static void sendCommands(final IoSession session) {
        final Scanner scanner = new Scanner(System.in);
        String text;
        do {
            System.out.println("Entre com texto: ");
            text = scanner.nextLine();
            session.write(text);
        } while (!"quit".equalsIgnoreCase(text));
    }

    /*
     *  Encerra conexao
     */        
    private static void close(final NioSocketConnector connector, 
    final IoSession session) {
        if (session != null) {
            if (session.isConnected()) {
                session.close(false);
                session.getCloseFuture().awaitUninterruptibly();
            }
        }
        connector.dispose();
    }
}
Listagem 6. Criando cliente TCP com MINA

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

public class ClientHandler extends IoHandlerAdapter {
    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        String str = message.toString();
        System.out.println("Message received: " + str);
    }
}
Listagem 7. ClientHandler

A aplicação cliente envia comandos digitados no console para a aplicação servidora. Pode ser usado para testar a classe da Listagem 1 (TCPServer).

Analisando o Código


/*
 *  Configura NioSocketConnector 
 */
public static void configureConnector(final NioSocketConnector connector) {
    connector.setConnectTimeoutMillis(TIMEOUT);

    connector.getFilterChain().addLast("codec",
            new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
    connector.getFilterChain().addLast("logger", new LoggingFilter());

    connector.setHandler(new ClientHandler());
}
Listagem 8. método configureConnector

Se no lado servidor usamos SocketAcceptor, para clientes TCP/IP usamos a interface SocketConnector, que é implementada pela classe NioSocketConnector.

Assim como no exemplo do TCPServer, podemos configurar vários tipos de filtro, como o “codec” e o “logger” (esses nomes são usados apenas para identificar o filtro, ou seja, poderiam ser “codec1”, “logger1”, “xyz”, etc), além de definir um handler (ClientHandler), que será chamado toda a vez que um evento relevante acontecer (criação da sessão, erros, recebimento de dados, etc).


/*
 *  Conexao com o server
 */
private static IoSession connect(final NioSocketConnector connector)
throws InterruptedException {
    IoSession session = null;
    try {
        ConnectFuture future = connector
        .connect(new InetSocketAddress(HOSTNAME, PORT));
        future.awaitUninterruptibly();
        session = future.getSession();
    } catch (RuntimeIoException e) {
        System.err.println("Failed to connect.");
        e.printStackTrace();
    }
    return session;
}
Listagem 9. Método connect

Efetua a conexão com o servidor. Ao invocar o método connect do NioSocketConnector, obtemos uma instância de objeto que implementa a interface ConnectFuture. Como estamos usando NIO, não podemos usar o objeto session antes de ter certeza que a conexão foi estabelecida. Para isso, usamos o método future.awaitUninterruptibly(), que só retornará quando a conexão estiver ok.

Feito isso, obtemos a instância de IoSession (que representa a conexão/sessão do cliente com o servidor), que pode ser usada, entre outras coisas, para enviar dados do servidor.


/*
 *  Envia comando do teclado ao servidor
 */    
private static void sendCommands(final IoSession session) {
    final Scanner scanner = new Scanner(System.in);
    String text;
    do {
        System.out.println("Entre com texto: ");
        text = scanner.nextLine();
        session.write(text);
    } while (!"quit".equalsIgnoreCase(text));
}
Listagem 10. Método sendCommands

Aqui, obtemos dados do teclado (modo console) e o enviamos ao servidor através do IoSession.


/*
 *  Encerra conexao
 */        
private static void close(final NioSocketConnector connector, final IoSession session) {
    if (session != null) {
        if (session.isConnected()) {
            session.close(false);
            session.getCloseFuture().awaitUninterruptibly();
        }
    }
    connector.dispose();
}
Listagem 11. Método close

Por fim, ao digitar “quit”, o método close será chamado. Ele irá encerrar a sessão (session.close()), e liberar o connector (connector.dispose()).

Como estamos trabalhando com NIO, a chamada session.close(false) não interromperá imediatamente as operações pendentes, por isso o método session.getCloseFuture().awaitUninterruptibly() é invocado, para aguardar que todas as operações de leitura/escrita em andamento possam encerrar de forma segura.

Se quisermos interromper todos os trabalhos correntes imediatamente, devemos passar true no parâmetro do método close de IoSession.

Estes 2 exemplos (TCPServer / TCPClient) foram analisados de forma bem superficial, apenas para que o leitor se familiarize com a estrutura de um aplicativo MINA típico, pois nos tópicos a seguir iremos explorar mais a fundo sua arquitetura.

Arquitetura Básica

Estrutura de uma aplicação MINA
Figura 3. Estrutura de uma aplicação MINA

IoService

A interface IoService abstrai todos os serviços de I/O do MINA e é dividida em 2 subgrupos:

  • interface IoAcceptor: Utilizado no lado servidor
  • interface IoConnector: Utilizado no lado Cliente
Hierarquia de IoService
Figura 4. Hierarquia de IoService

As subinterfaces mais conhecidas de IoAcceptor/IoConnector:

  • DatagramAcceptor: Lida com Protocolo UDP (lado Servidor)
  • DatagramConnector: Lida com Protocolo UDP (lado Cliente)
  • SocketAcceptor: Lida com Protocolo TCP (lado Servidor)
  • SocketConnector: Lida com Protocolo TCP (lado Cliente)
Hierarquia de IoAcceptor/IoConnector
Figura 5. Hierarquia de IoAcceptor/IoConnector

Por isso, no exemplo do Servidor TCP/IP, utilizamos a classe NioSocketAcceptor (única classe concreta que implementa SocketAcceptor), e no exemplo do Cliente TCP/IP, a classe NioSocketConnector (que implementa SocketConnector).

É a partir de IoService, que configuramos os filtros (IoFilterChain) e o handler (IoHandler).

IoSession

Representa a conexão/sessão entre dois pontos. Por exemplo, num servidor TCP/IP, para cada conexão feita por um cliente, o MINA irá criar uma instância de IoSession representando-o. É a partir dele que podemos enviar/receber dados, configurar atributos para sessão, estatísticas, etc.

O IoSession também é passado como argumento para todos os eventos do handler. Por exemplo, no caso do Servidor, utilizamos o objeto session do método messageReceived para retornar o dado para o cliente, através do método write().


...
public class ServerHandler extends IoHandlerAdapter {

@Override
public void messageReceived(IoSession session, Object message) throws Exception {
    String str = message.toString();
    
    System.out.println("Message received: " + str);
    
    if (str.trim().equalsIgnoreCase("quit")) {
        session.close(true);
        return;
    }
    
    if (str.trim().equals("data")) {
        Date date = new Date();
        session.write(date.toString());  /* Retornando dado ao cliente */
    } else {  
        session.write("echo " + str);     /* Retornando dado ao cliente */
    }
Listagem 12. método messageReceived

Para cada sessão, todos os filtros configurados para o IoService serão aplicados. Portanto, se definirmos um filtro de log, o mesmo será aplicado para todas as sessions.

IoHandler

Processa todos os eventos gerados num IoSession. Os eventos suportados são:

  • sessionCreated: quando a uma nova conexão é criada
  • sessionOpened: quando a conexão é aberta
  • sessionClosed: quando a conexão é fechada
  • sessionIdle: quando a conexão fica ociosa (tempo configurável)
  • exceptionCaught: quando ocorre alguma exceção
  • messageReceived: quando uma mensagem é recebida
  • messageSent: quando uma mensagem é enviada

Registramos o handler através do método setHandler do IoService. Para quem programa em Swing, é similar a registrar um listener para monitorar eventos de determinado componente. E assim como no Swing, existe uma classe Adapter chamada IoHandlerAdapter, que fornece uma implementação vazia para todos os eventos. Caso o usuário queira escrever alguma lógica, basta selecionar os eventos que ele quer monitorar, criando uma classe que estenda IoHandlerAdapter.

Esse é o conhecido padrão Observer, utilizado para diminuir o acoplamento entre o objeto gerador do evento e os ouvintes. No MINA só podemos configurar um único handler no IoService, por isso ao invés de um addHandler, temos somente setHandler.

O handler deve ser configurado tanto no lado cliente (IoConnector), quando no lado servidor(IoAdapter).

IoFilter

Podemos adicionar vários filtros ao IoService, para diversos propósitos, como: logging, estatística, thread pooling, blacklist, profile, etc.

Nos exemplos do cliente/servidor TCP/IP, usamos 2 filtros:


connector.getFilterChain().addLast("codec",
   new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
connector.getFilterChain().addLast("logger", new LoggingFilter());
Listagem 13. Uso de Filtros

O filtro LoggingFilter faz o log dos eventos de mensagem, como messageReceived, messageSent, etc. Como usamos log4J nos exemplos, basta apenas configurar um log4j.properties para logar as operações no Console ou num arquivo de log.

Todos os filtros adicionados ao IoService serão executados, ou seja, o objeto IoSession irá passar por toda a cadeia de filtros (Chain of Responsibility).

Podemos criar nossos próprios filtros, para isso basta implementar a interface IoFilter, que possui os seguintes métodos:

  • init()
  • destroy()
  • onPreAdd()
  • onPostAdd()
  • onPreRemove()
  • sessionCreated()
  • sessionOpened()
  • sessionClosed()
  • sessionIdle()
  • exceptionCaught()
  • messageReceived()
  • messageSent()
  • filterClose()
  • filterWrite()

A API fornece uma classe adapter, IoFilterAdapter, que oferece uma implementação vazia de todos os métodos da interface IoFilter. Para criar um novo filtro, basta então estender a classe IoFilterAdapter e sobrescrever os eventos que desejar.

Iremos analisar o BlacklistFilter da própria API do MINA, que implementa a lógica de bloquear conexões para endereços não-seguros:


public class BlacklistFilter extends IoFilterAdapter {

   private final List<Subnet> blacklist = 
   new CopyOnWriteArrayList<Subnet>();

   public void setBlacklist(InetAddress[] addresses) {
     if (addresses == null) {
         throw new IllegalArgumentException("addresses");
     }
     blacklist.clear();
     for (int i = 0; i < addresses.length; i++) {
         InetAddress addr = addresses[i];
         block(addr);
     }
  }

    /**
     * Blocks the specified endpoint.
     */
    public void block(InetAddress address) {
        if (address == null) {
            throw new IllegalArgumentException("Adress to block can not be null");
        }

        block(new Subnet(address, 32));
    }

    /**
     * Blocks the specified subnet.
     */
    public void block(Subnet subnet) {
        if (subnet == null) {
            throw new IllegalArgumentException("Subnet can not be null");
        }

        blacklist.add(subnet);
    }
}
Listagem 14. Trecho de código de BlacklistFilter

Observe que BlacklistFilter estende IoFilterAdapter e internamente cria um List de endereços não-seguros.


@Override
public void sessionCreated(NextFilter nextFilter, IoSession session) {
    if (!isBlocked(session)) {
        // forward if not blocked
        nextFilter.sessionCreated(session);
    } else {
        blockSession(session);
    }
}
Listagem 15. Trecho de BlacklistFilter

Quando uma conexão é feita, o evento sessionCreated é disparado. Se o InetAddress da nova sessão estiver na lista, a sessão será fechada (blockSession(session)) e a conexão recusada.

Portanto, implementar filtros personalizados é uma tarefa relativamente simples em MINA.

Para ver a implementação completa de BlacklistFilter.

Filtro ProtocolCodecFilter


connector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"))));
Listagem 17. Filtro ProtocolCodec

O filtro ProtocolCodecFilter é um dos recursos mais interessantes do MINA, pois permite a configuração do tipo de protocolo que o cliente/servidor irão usar para se comunicar. Nos exemplos, usamos o TextLineCodecFatory, que trata mensagens de texto com delimitador no final (quebra de linha é o delimitador default).

Para ficar mais claro como funciona o ProtocolCodecFilter, iremos implementar um protocolo personalizado usando XStream.

Criaremos uma aplicação cliente que transforma um objeto Java num XML usando XStream, e no servidor iremos converter esse XML novamente num objeto Java (serialização de objetos).

Criando seu próprio mecanismo de serialização usando MINA


package br.com.devmedia.entity;

/**
 *
 * @author marcelo
 */
public class Pessoa {

    private String nome;
    private int numeroSorte;

    public String getNome() {
        return nome;
    }

    public void setNome(String nome) {
        this.nome = nome;
    }

    public int getNumeroSorte() {
        return numeroSorte;
    }

    public void setNumeroSorte(int numeroSorte) {
        this.numeroSorte = numeroSorte;
    }

    @Override
    public String toString() {
        return "nome = " + getNome() + ", numeroSorte = " + getNumeroSorte();
    }
}
Listagem 18. Classe Pessoa

Iremos trafegar essa classe do cliente para o servidor. Utilizaremos o XStream para transformar um objeto pessoa, no lado cliente, num XML:


<br.com.devmedia.entity.Pessoa>
  <nome>teste</nome>
  <numeroSorte>30</numeroSorte>
</br.com.devmedia.entity.Pessoa>
Listagem 19. Código XML representando uma instância de Objeto

E depois reconvertê-lo para objeto Pessoa no lado servidor. Para isso devemos fornecer um encoder e um decoder para o ProtocolCodecFilter.

O encoder é responsável por transformar um objeto numa mensagem (binária, texto, data-user, etc).

O decoder converte a mensagem num objeto.

A classe ProtocolCodecFilter recebe uma factory, que implementa a interface ProtocolCodecFactory. Forneceremos então nossa própria fábrica, que registrará nossos objetos encoder/decoder personalizados para o MINA.


package br.com.devmedia.codec;

import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;

/**
 *
 * @author marcelo
 */
public class XStreamCodecFactory implements ProtocolCodecFactory {
    
    private ProtocolEncoder encoder;
    private ProtocolDecoder decoder;
    
    public XStreamCodecFactory() {
        encoder = new XStreamEncoder();
        decoder = new XStreamDecoder();    
    }    

    @Override
    public ProtocolEncoder getEncoder(IoSession is) throws Exception {
        return encoder;
    }

    @Override
    public ProtocolDecoder getDecoder(IoSession is) throws Exception {
        return decoder;
    }    
}
Listagem 20. XStreamCodecFactory

A classe XStreamCodecFactory implementa a interface ProtocolCodecFactory, cujos métodos são:

  • getEncoder() - retorna o encoder que será utilizado pelo MINA
  • getDecoder() - retorna o decoder que será utilizado pelo MINA

Como estamos criando um protocolo personalizado, devemos fornecer nossa própria implementação de encoder/decoder. Para criar um encoder/decoder:

  • encoder: devemos estender a classe abstrata ProtocolEncoderAdapter, que implementa a interface ProtocolEncoder, e codificar o método encode()
  • decoder: devemos estender a classe abstrata ProtocolDecoderAdapter, que implementa a interface ProtocolDecoder, e codificar o método decode()

Para esse exemplo, foram criadas as classes XStreamEncoder e XStreamDecoder.


package br.com.devmedia.codec;

import br.com.devmedia.entity.Pessoa;
import com.thoughtworks.xstream.XStream;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineEncoder;

/**
 *
 * @author marcelo
 */
public class XStreamEncoder extends ProtocolEncoderAdapter {

    private final TextLineEncoder textLineEnconder = 
    new TextLineEncoder(new LineDelimiter("\0"));
    
    @Override
    public void encode(IoSession session, Object o, 
    ProtocolEncoderOutput out) throws Exception {
        Pessoa pessoa = (Pessoa) o;
        XStream xstream = new XStream();
        String xml = xstream.toXML(pessoa);
        textLineEnconder.encode(session, xml, out);        
    }
}
Listagem 21. XStreamEncoder

O método encode transforma o objeto Pessoa num XML usando XStream. Uma vez criado o XML, ele é apenas uma String comum, por isso, usando delegação, foi criado um objeto TextLineEncoder (que já sabe tratar o envio de uma String simples) que irá realizar a tarefa de transformação da mensagem propriamente dita.

Note que o delimitador "\0" foi usado para demarcar o fim do XML a ser enviado pelo TextLineEncoder (o XStream adiciona quebras de linha, por isso não é possível usar o delimitador default).


package br.com.devmedia.codec;

import br.com.devmedia.entity.Pessoa;
import com.thoughtworks.xstream.XStream;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineDecoder;

/**
 *
 * @author marcelo
 */
public class XStreamDecoder extends TextLineDecoder {
    
    public XStreamDecoder() {
        super(new LineDelimiter("\0"));
    }            

    @Override
    protected void writeText(IoSession session, String xml, ProtocolDecoderOutput out) {
        Pessoa pessoa;
        XStream xstream = new XStream();
        pessoa = (Pessoa)xstream.fromXML(xml);
        out.write(pessoa);
    }
}
Listagem 22. XStreamDecoder

Aqui não foi possível usar delegação, pois a classe TextLineDecoder não tem nenhum método público que permita mudar o tipo de objeto manipulado. Tenha em mente que um TextLineDecoder só trabalha com objetos Strings e no nosso protocolo, trafegamos objetos Pessoa.

A solução é usar herança e sobrescrever o método writeText. O método writeText é chamado depois que o Decoder processou todos os dados da rede, transformando-os num objeto String e gravando-o no objeto ProtocolDecoderOutput. Como o método é protected, podemos sobrescrevê-lo na subclasse. Dessa forma interceptamos o objeto String (xml), convertemos ele para um objeto Pessoa, e gravamos esse objeto no ProtocolDecoderOutput.

O objeto gravado no ProtocolDecoderOutput (out.write(object)) será passado como argumento para o método messageReceived do IoHandler:


acceptor.setHandler(  new IoHandlerAdapter() {
    @Override
    public void messageReceived(IoSession session, Object message) throws Exception {
        Pessoa pessoa = (Pessoa)message; /* Object se refere a Pessoa */
        System.out.println("pessoa = " + pessoa.toString());
    }
});
Listagem 23. IoHandler

As classes TextLineDecoder/TextLineEncoder já possuem toda a lógica de comunicação de mensagens de texto entre cliente e servidor. Para ver como essas classes manipulam os dados que vem da rede:

Por fim, os códigos da classe cliente e servidor, utilizados para serialização de objetos com XStream:


package br.com.devmedia.app;

import br.com.devmedia.codec.XStreamCodecFactory;
import br.com.devmedia.entity.Pessoa;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.Scanner;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

public class XStreamClient {

    private static int TIMEOUT = 1000;
    private static String HOSTNAME = "127.0.0.1";
    private static int PORT = 5050;

    public static void main(String[] args) throws Throwable {

        NioSocketConnector connector = new NioSocketConnector();
        configureConnector(connector);
        IoSession session = connect(connector);
        if (session != null) {
            // Envia comandos do teclado
            sendCommands(session);
        }
        // Encerra conexoes
        close(connector, session);
    }

    /*
     *  Configura NioSocketConnector 
     */
    public static void configureConnector(final NioSocketConnector connector) {
        connector.setConnectTimeoutMillis(TIMEOUT);

        connector.getFilterChain().addLast("codec",
                new ProtocolCodecFilter(new XStreamCodecFactory()));
        connector.getFilterChain().addLast("logger", new LoggingFilter());
        connector.setHandler(new IoHandlerAdapter());
    }

    /*
     *  Conexao com o server
     */
    private static IoSession connect(final NioSocketConnector 
    connector) throws InterruptedException {
        IoSession session = null;
        try {
            ConnectFuture future = connector.connect(new 
            InetSocketAddress(HOSTNAME, PORT));
            future.awaitUninterruptibly();
            session = future.getSession();
        } catch (RuntimeIoException e) {
            System.err.println("Failed to connect.");
            e.printStackTrace();
            Thread.sleep(5000);
        }
        return session;
    }

    /*
     *  Envia comando do teclado ao servidor
     */    
    private static void sendCommands(final IoSession session) {
        final Scanner scanner = new Scanner(System.in);
        String text;
        do {
            System.out.println("Entre com o nome da pessoa: ");
            text = scanner.nextLine();            
            Pessoa pessoa = new Pessoa();
            pessoa.setNome(text);
            pessoa.setNumeroSorte(new Random().nextInt(100));
            session.write(pessoa);            
        } while (!"quit".equalsIgnoreCase(text));
    }

    /*
     *  Encerra conexao
     */        
    private static void close(final NioSocketConnector connector, 
    final IoSession session) {
        if (session != null) {
            if (session.isConnected()) {
                session.close(false);
                session.getCloseFuture().awaitUninterruptibly();
            }
        }
        connector.dispose();
    }
}
Listagem 24. XStreamClient

package br.com.devmedia.app;

import br.com.devmedia.codec.XStreamCodecFactory;
import br.com.devmedia.entity.Pessoa;
import java.io.IOException;
import java.net.InetSocketAddress;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;


public class XStreamServer {
    
    private static final int PORT = 5050;

    public static void main( String[] args ) throws IOException {
        IoAcceptor acceptor = new NioSocketAcceptor();
        acceptor.getFilterChain().addLast( "logger1", new LoggingFilter() );
        acceptor.getFilterChain().addLast( "codec1", new ProtocolCodecFilter
        (new XStreamCodecFactory()));    
        
        acceptor.setHandler(  new IoHandlerAdapter() {
            @Override
            public void messageReceived(IoSession session, Object message) 
            throws Exception {
                Pessoa pessoa = (Pessoa)message;
                System.out.println("pessoa = " + pessoa.toString());
            }
        });     
        
        acceptor.bind( new InetSocketAddress(PORT) );
    }
}
Listagem 25. Servidor XStream

A aplicação cliente se conecta ao servidor, cria um objeto pessoa, lê o nome da pessoa via console (classe Scanner), configura um número da sorte aleatório e o envia para o servidor.

Na própria documentação/fonte do MINA há outros exemplos para aprendizado.

Conclusão

O MINA permite a criação de aplicações de rede robustas, abstraindo boa parte da complexidade do NIO. Sua arquitetura baseada em eventos assíncronos, padrões (Chain of Responsibility / Observer / Adapter / etc) e voltada ao uso de interfaces permite a construção de aplicações modulares e com baixo acoplamento (é muito fácil utilizar o MINA com um framework de injeção de dependências), além de facilitar a construção de testes unitários.

Referências