Tuesday, July 28, 2009Trabalhando com Flex e XML Sockets com Apache Mina
Buenas Galera, vamos abordar aqui um framework simples para se trabalhar com Xml Sockets entre Java e Flex.
O Framework abordado se chama Apache Mina Framework que implementa a nova API Java NIO , onde foi otimizado muito o processamento de IO em Java.
O que é Apache Mina?
Apache Mina é indicado para quem precisa desenvolver um servidor utilizando um protocolo expecífico, ou em um protocolo novo
onde você mesmo poderá implementa-lo, e que necessite de escalabilidade e boa performace. Onde você possue tempo apertado e o cronograma não permite desenvover um servidor do zero.
Apache MINA ( A Multi-purpose Infrastructure for Network Applications)
MINA e um framework para aplicações em rede, desenvolvido em java, com um conjunto de API para capturar eventos assincronamente, auxilia facilmente a desenvolver aplicações que requer conectividade, com uma alta performace e alta escalabilidade.
Pode ser desenvolvido com o MINA um servidor com um protocolo especifico sem ficar escovando bits, e com uma performace e desempenho considerável, permitindo separar a logica de conectividade da logica do protocolo. Diversas camadas de transporte(*Acceptor) já esta implementado no framework como TCP/IP , UDP/ID e Porta Serial, mas você pode desenvolver a sua e plugar.
Podemos resumir o MINA como um conjunto de classes `templates`, bastando o desenvolvedor se preocupar, com a logica do seu protocolo.
Oque é Xml Sockets?
Há dois tipos diferentes de conexões de Socket possíveis no ActionScript 3.0: Conexões por XML Sockets e conexões por Binary Socket. Um XML Socket permite que você se conecte com um servidor remoto e crie uma conexão de servidor que permaneça aberta até que seja fechada explicitamente. Isso permite a troca de dados XML entre um servidor e um cliente sem necessidade de abrir continuamente novas conexões de servidor. Outro benefício do uso de um XML Socket é que o usuário não precisa solicitar dados explicitamente. É possível enviar dados do servidor sem solicitações e enviar dados a cada cliente conectado com o XML Socket.
As conexões de XML Socket requerem a presença de uma política de Socket no servidor de destino.
Uma conexão de Binary Socket é semelhante a um XML Socket, exceto que o cliente e o servidor não precisam trocar pacotes XML especificamente. Em vez disso, a conexão pode transferir dados como informações binárias. Isso permite conectar-se a uma ampla faixa de serviços, incluindo servidores de email (POP3, SMTP e IMAP) e novos servidores (NNTP).
Bom vamos ao que intereça.
Baixe os fontes do Mina aqui
Crie um projeto Java e adicione os Jars do Mina no seu classpath.
O Mina possue um procolo já implementado chamado TextLineCodecFactory que se baseia em procolo de texto puro,
onde o final de cada requisição é um \n ou seja um ENTER.
Mas este procolo não nos ajuda muito, então iremos implementar nosso próprio protocolo chamado XmlProtocolCodecFactory e em seguida vamos implementar nosso servidor e o Handler que receberá e tratará as requisições.
Aqui a criação do XmlDecoder, responsável por decodificar uma requisição e transformar a mesma em um Document.
package br.com.ronaldorigoni.mina.codec;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoderAdapter;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Decoder de XML, responsável por interceptar a requisiçao do usuário
* e transformar a mesma em um Objeto DOM, e passar o mesmo para o Handler
* manipular.
* @author Ronaldo Rigoni
*/
public class XmlDecoder extends ProtocolDecoderAdapter {
/**
* Decodifica a requisiçao e passa a mesma para o handler.
*/
public void decode(IoSession session, IoBuffer buffer, ProtocolDecoderOutput output) throws Exception {
output.write(parserXML(buffer));
}
/**
* Efetua o parser dos bits recebidos e transforma em um Document
* @param xmlBuffer O Buffer de entrada.
* @return O documento xml
* @throws ParserConfigurationException Caso nao consiga efetuar o parser.
* @throws SAXException Caso houver erro da biblioteca SAX
* @throws IOException Caso ouver erro de IO
*/
public Object parserXML(IoBuffer xmlBuffer) throws ParserConfigurationException, SAXException, IOException {
// captura os bits da requisicao e aloca
byte[] data = new byte[xmlBuffer.limit()];
xmlBuffer.get(data);
// transforma em string removendo espacoes em branco
String xml = new String(data).trim();
// cria o documento e retorna.
Document document = DocumentBuilderFactory.newInstance().
newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes()));
return (document);
}
}
Aqui o XMLEncoder responsável por escrever os bits para o cliente.
package br.com.ronaldorigoni.mina.codec;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
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;
/**
* Encoder de XML, responsavel por transformar a saida ao usuário e escrever a mesma.
* @author Ronaldo Rigoni
*/
public class XmlEncoder extends ProtocolEncoderAdapter {
/**
* ENCODER final
*/
private final AttributeKey ENCODER = new AttributeKey(getClass(), "encoder");
/**
* Charset da Requisição
*/
private final Charset charset;
/**
* Delimitador da requisição.
*/
private final LineDelimiter delimiter;
/**
* Tamanho máximo da requisição.
*/
private int maxLineLength = Integer.MAX_VALUE;
/**
* Construtor do Encoder
* Define o Charset e o Delimitador
*/
public XmlEncoder() {
this.charset = this.charset = Charset.forName("UTF-8");
this.delimiter = new LineDelimiter("\0");
}
/**
* Retorna o tamanho maximo da requisição.
* @return O Tamanho maximo da requisição.
*/
public int getMaxLineLength() {
return maxLineLength;
}
/**
* Codifica a saida ao Usuário.
*/
public void encode(IoSession session, Object message,ProtocolEncoderOutput out) throws Exception {
// capturando o charset da sessao.
CharsetEncoder encoder = (CharsetEncoder) session.getAttribute(ENCODER);
// caso for nulo é criado e setado na sessão do usuário.
if (encoder == null) {
encoder = charset.newEncoder();
session.setAttribute(ENCODER, encoder);
}
// mensagem a ser escrita.
// Por se tratar de XML e nao ser nada mais que uma string
// pegamos o toString() dela.
String value = message.toString();
//Cria o buffer de saida.
IoBuffer buf = IoBuffer.allocate(value.length()).setAutoExpand(true);
// adiciona ao buffer de saida a mensagem e o charset
buf.putString(value, encoder);
if (buf.position() > maxLineLength) {
throw new IllegalArgumentException("Tamanho da linha muito grande: " + buf.position());
}
// seta o delimitador \0
buf.putString(delimiter.getValue(), encoder);
// fecha o buffer
buf.flip();
// escreve na saida.
out.write(buf);
}
}
Agora criaremos nossa Factory que fabricará os objetos para as requisições.
package br.com.ronaldorigoni.mina.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;
/**
* Classe Factory para Encoder e Decoder de XML utilizando Apache MINA
* @author Ronaldo Rigoni
*/
public class XmlProtocolCodecFactory implements ProtocolCodecFactory {
/**
* Cria um novo Encoder de XML
*/
public ProtocolEncoder getEncoder(IoSession arg0) throws Exception {
return new XmlEncoder();
}
/**
* Cria um novo Decoder de XML
*/
public ProtocolDecoder getDecoder(IoSession arg0) throws Exception {
return new XmlDecoder();
}
}
Agora vamos criar A classe de servidor, que será responsável por abrir a porta no servidor e inicializar o Handler que manipulará as requisições do usuário.
package br.com.ronaldorigoni.mina.core;
import java.net.InetSocketAddress;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.filter.logging.MdcInjectionFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;
import br.com.ronaldorigoni.bingo.codec.XmlProtocolCodecFactory;
import br.com.ronaldorigoni.bingo.config.Config;
/**
* Inicializa o servidor.
*
* @author Ronaldo Rigoni
*/
public class Server {
static Logger logger = Logger.getLogger(Server.class);
private static void start(){
try {// objeto que receberá novas conecoes
NioSocketAcceptor acceptor = new NioSocketAcceptor();
// filtro de requisicoes
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
// logger
LoggingFilter loggingFilter = new LoggingFilter();
chain.addLast("logging", loggingFilter);
// injecao do filtro
MdcInjectionFilter mdcInjectionFilter = new MdcInjectionFilter();
chain.addLast("mdc", mdcInjectionFilter);
// atribuicao do protocolo de parser de XML que criamos.
chain.addLast("codec", new ProtocolCodecFilter(new XmlProtocolCodecFactory()));
// adicionando logger.
addLogger(chain);
// adicionando Handler, que tratará as requisiçoes
acceptor.setHandler(new ChatServerHandler());
// setando porta para ouvir.
acceptor.bind(new InetSocketAddress(8090));
logger.debug("Servidor de chat ouvindo na porta:" + 8090);
} catch (Exception e) {
logger.error("Erro ao inicializar servidor de chat.",e);
}
}
/**
* Adiciona um logger para o filtro de requisicoes.
**/
private static void addLogger(DefaultIoFilterChainBuilder chain) throws Exception {
chain.addLast("logger", new LoggingFilter());
}
/**
* Inicializa o servidor de chat.
**/
public static void main(String[] args) {
try {
start();
} catch (Exception e) {
e.printStackTrace();
System.exit(0);
}
}
}
Agora vamos criar um objeto para encapsular as requisiçoes chamado Request, ele irá armazenar o tipo de requisiçao e os parametros contidos nela.
package br.com.ronaldorigoni.mina.codec;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Classe para encapsular uma requisição.
* @author Ronaldo Rigoni
*/
public class Request {
private static final Logger logger = Logger.getLogger(Request.class);
//Possíveis campos de um input
public static final String FIELD_USER_NAME = "userName";
public static final String FIELD_USER_LOGIN = "login";
public static final String FIELD_USER_PASSWORD = "password";
public static final String FIELD_TO = "to";
public static final String FIELD_FROM = "from";
public static final String FIELD_MESSAGE = "message";
public static final String OPERATION_CHAT_USER_IN ="add-user-in-chat-room";
public static final String OPERATION_CHAT_USER_IN ="remove-user-in-chat-room";
public static final String OPERATION_CHAT_SEND_MESSAGE ="chat-send-message";
public static final String OPERATION_XML_POLICY ="<policy-file-request/>";
private String operation;
private HashMap<String, String> parameters = new HashMap<String, String>();
private String inputText;
public Request(Document document) {
parseDocument(document);
}
/**
* @return the operation
*/
public String getOperation() {
return operation;
}
/**
* @param operation the operation to set
*/
public void setOperation(String operation) {
this.operation = operation;
}
/**
* @return the parameters
*/
public HashMap<String, String> getParameters() {
return parameters;
}
/**
* @param parameters the parameters to set
*/
public void setParameters(HashMap<String, String> parameters) {
this.parameters = parameters;
}
/**
* @return the inputText
*/
public String getInputText() {
return inputText;
}
/**
* @param inputText the inputText to set
*/
public void setInputText(String inputText) {
this.inputText = inputText;
}
private void parseDocument(Document document){
if(document == null)
throw new NullPointerException("Documento nulo.");
else{
NodeList nodes = document.getDocumentElement().getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
if(nodes.item(i).getFirstChild() != null){
logger.debug(nodes.item(i).getNodeName()+","+ nodes.item(i).getFirstChild().getNodeValue());
parameters.put(nodes.item(i).getNodeName(), nodes.item(i).getFirstChild().getNodeValue());
}
}
if(document.getDocumentElement() != null){
this.setOperation("<"+document.getDocumentElement().getTagName()+"/>");
logger.debug("Operation:"+document.getDocumentElement().getTagName());
}
}
}
@Override
public String toString() {
return "Operation:"+getOperation()+",parameters{"+getParameters()+"}";
}
}
Agora vamos criar o Manipulador de Requisiçoes que será responsavel por tratar todas as requisições dos usuários tanto de entrada como de saida, login no chat, envio de mensagem e saida do usuáro.
package br.com.ronaldorigoni.mina.core;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.log4j.Logger;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.w3c.dom.Document;
import br.com.ronaldorigoni.mina.codec.Request;
/**
* Classe manipuladora de requisiçoes, é onde toda a lógica do chat acontece.
* É nela que toda requisição após passar pelo Codec XML é encaminhado para cá/
* @author Ronaldo Rigoni rrigoni@gmail.com
*
*/
public class ChatHandler extends IoHandlerAdapter {
/** Logger estatico do Handler **/
private static final Logger logger = Logger.getLogger(ChatHandler.class);
/**
* Constante para armazenamento do usuario na sessao.
*/
private static final String USER_NAME = "username";
/**
* XML policy caso a requisicao venha de outro host diferente ao do servidor.
*/
private static final String XML_POLICY = "<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"8090\"/></cross-domain-policy>";
/**
* Lista para armazenar todas as sessoes de chat.
* Esta lista necessita estar sincronizada pois em um ambiente multi-thread precisamos
* proteger acesso concorrente a mesma.
*/
private List<IoSession> sessions = Collections.synchronizedList(new ArrayList<IoSession>());
/**
* Metodo invocado quando ocorrer alguma exception na escrita de mensagens ou
* no recebimento.
* @param session Sessao do usuario.
* @param cause Exception lancada
*/
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
super.exceptionCaught(session, cause);
}
/**
* Metodo que valida um usuario baseado nos seguintes criterios para poder ingressar no chat.
* 1 - Nao pode haver a sessao do usuario na lista de chat.
* 2 - Nao pode haver uma sessao com o mesmo atributo <code>username</code> que a do usuario
* que esta tentando logar.
* @param session A sessao do usuario que requisitou login.
* @return <code>true</code> caso esta apto a ingressar no chat,
* <code>false</code> caso nao esta apto.
*/
private boolean loginUser(IoSession session){
boolean isValid = true;
String userLogin = (String) session.getAttribute(USER_NAME);
if(userLogin != null && userLogin.length() > 5){
if(!sessions.contains(session)){
synchronized (session) {
for(IoSession ses : sessions){
// capturando login da sessao
String username = (String)session.getAttribute(USER_NAME);
if(username.equalsIgnoreCase(userLogin)){
isValid = false;
}
}
}
}
}else{
isValid = false;
}
return isValid;
}
/**
* Invocado a cada mensagem recebida.
* Pelo fato de construirmos um Codec XML o objeto message sempre ser'a uma instancia
* de org.w3c.dom.Document, caso nao for a mensagem ser'a ignorada.
* @param session Sessao do usuario.
* @param message Mensagem recebida.
*/
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
if(message instanceof org.w3c.dom.Document){
// efetuando a conversao do documento recebido.
Document document = (Document) message;
Request request = new Request(document);
if (request.getOperation().equalsIgnoreCase(Request.OPERATION_XML_POLICY)) {
session.write(XML_POLICY);
} else if (request.getOperation().trim().equalsIgnoreCase(Request.OPERATION_CHAT_USER_IN)) {
// adiciona o username na sessao e envia para login.
session.setAttribute(USER_NAME,request.getParameters().get(USER_NAME));
boolean logged = loginUser(session);
if(!logged){
session.write("<forbiden/>");
}else{
// adiciona usuario nas sessoes do chat.
sessions.add(session);
//processa entrada do usuario.
processUserEnterInChat(session);
}
}else if(request.getOperation().trim().equalsIgnoreCase(Request.OPERATION_CHAT_USER_EXIT)){
// remove sessao do usuario da lista de sessoes do chat.
sessions.remove(session);
// processa saida do usuario
processUserExitInChat(session);
// fecha sessao do usuario.
session.close(true);
}else if(request.getOperation().trim().equalsIgnoreCase(Request.OPERATION_CHAT_SEND_MESSAGE)){
String chatMessage = request.getParameters().get(Request.FIELD_MESSAGE);
// processa mensagem de chat.
processChatMessage(chatMessage, session);
}
}else{
logger.error("Formato da requisicao invalido.");
}
}
/**
* Processa o envio de uma mensagem de chat.
* @param message Mensagem
* @param session Sessao do usuario que est'a enviando.
*/
private void processChatMessage(String message, IoSession session){
logger.debug("Usuario:"+session.getAttribute(USER_NAME)+" enviou mensagem:"+message);
writeBroadCastChatMessage(message);
}
/**
* Processa a entrada de um usu'ario no chat.
* @param session Sessao do usuario.
*/
private void processUserEnterInChat(IoSession session){
StringBuffer buff = new StringBuffer("<response>");
buff.append("<type>user-enter-in-chat</type>");
buff.append("<username>"+session.getAttribute(USER_NAME)+"</username>");
buff.append("</response>");
writeBroadCastChatMessage(buff.toString());
}
/**
* Processa a sa'ida de um usu'ario no chat.
* @param session Sessao do usuario.
*/
private void processUserExitInChat(IoSession session){
StringBuffer buff = new StringBuffer("<response>");
buff.append("<type>user-exit-in-chat</type>");
buff.append("<username>"+session.getAttribute(USER_NAME)+"</username>");
buff.append("</response>");
writeBroadCastChatMessage(buff.toString());
}
/**
* Envia uma mensagem de chat para todos os usuarios.
* @param message A mensagem.
*/
private void writeBroadCastChatMessage(String message){
synchronized (sessions) {
for(IoSession session : sessions){
session.write(message);
}
}
}
/**
* Invocao apos uma mensagem ser enviada com sucesso.
* Aqui poderemos estar efetuando um log das mensagens que foram enviadas, ou descartarmos.
* @param session Sessao de destino da mensagem
* @param message Mensagem enviada.
*/
@Override
public void messageSent(IoSession session, Object message) throws Exception {
logger.debug(session+" envia >>> "+message);
}
/**
* Metodo invocado quando a sessao do usuario for fechada, quando o socket do cliente for fechado ou
* pelo servidor de chat ou pelo cliente.
* Aqui poderemos remover atributos da sessao do usuario, ou quaisquer operacoes que precisarmos quando
* o mesmo se desconectar.
* @param session Sessao do Usuario.
*/
@Override
public void sessionClosed(IoSession session) throws Exception {
// quando conexao fechada 'e processado a saida do usuario.
logger.debug("Sessao fechada:"+session);
processUserExitInChat(session);
}
/**
* Metodo invocado quando um usuario solicitar uma conexao, (quando ela for criada).
* @param session Sessao do usuario.
*/
@Override
public void sessionCreated(IoSession session) throws Exception {
logger.debug("Sessao criada:"+session);
super.sessionCreated(session);
}
/**
* Invocado quando uma sessao entrar em timeout.
* @param session Sessao do usuario
* @param status Status da sessao.
*/
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
logger.debug("Sessao em timeout:"+session);
super.sessionIdle(session, status);
}
/**
* Metodo invocado quando uma sessao for aberta.
* @param session Sessao do usuario.
*/
@Override
public void sessionOpened(IoSession session) throws Exception {
logger.debug("Sessao aberta:"+session);
super.sessionOpened(session);
}
}
<span>
Toda a parte de servidor está concluída, em um próximo post vamos criar o cliente em Adobe Flex, e abordaremos toda a integração e funcionamento detalhado do mesmo.
Um abraço, Ronaldo.
Dúvidas? ronaldo@ronaldorigoni.com.br
[/sourcecode]
[/sourcecode]

Português
Italiano
English