Como fazer um servidor Java para múltiplos clientes Flash
por nunof, 19:57 20-03-2008 (actualizado em 22:14 03-11-2008)
Neste tutorial vamos fazer um servidor em Java capaz de lidar com vários clientes. O propósito do tutorial será construir uma aplicação de chat e usando o Flash para os clientes.
Introdução - Cliente Flash e servidor Java
Temos visto um constante aumento de aplicações multimédia online e o Flash é um candidato ideal para este tipo de aplicações. Tem as funcionalidades necessárias para fornecer uma interface rica, capaz de correr num browser.
Desde as suas versões mais recentes que o Flash ganhou a possibilidade de trabalhar com sockets (e não apenas XMLSockets). Sockets são uma extremidade numa comunicação e são usados para estabelecer uma conversa entre diferentes entidades. Isto abriu muitas possibilidades para as aplicações Flash.
O Java constitui uma boa escolha para um servidor multi-cliente. Tanto o Flash como o Java são soluções multi-plataforma, fazendo com que possam ser facilmente distribuídas.
Fazer uma aplicação de chat com o Flash e o Java
Neste tutorial vamos explorar estes conceitos e vamos fazer uma aplicação usando Flash como cliente e Java como servidor multi-cliente. Um exemplo duma aplicação simples que se encaixa neste domínio é uma aplicação de chat e faremos isso mesmo.
A Adobe introduziu umas protecções de segurança e para uma aplicação Flash poder ligar-se a um servidor, primeiro precisa de saber a política desse servidor, para garantir que tem acesso a este. Para resolver isto, faremos um servidor de política em Java (policy server), que também pode ser usado separadamente por outros servidores que necessitem que clientes Flash se liguem a eles.
Basicamente, a nossa aplicação de chat precisa de 3 intervenientes: cliente de chat em Flash, servidor de chat em Java e servidor de política em Java. Esta sinergia é descrita na imagem seguinte:

Iremos usar ActionScript 3 neste tutorial. Vou usar o Adobe Flash CS3 Professional para desenvolver o cliente em Flash.
Fazer o cliente do chat em Flash
Vamos começar por fazer o cliente Flash. Este será composto por 2 ficheiros diferentes:
- ChatClient.as: A classe ChatClient é responsável por comunicar com o servidor
- Chat.fla: O ficheiro principal de Flash com uma interface para o chat e o código para usar o cliente
Fazer com que a classe ChatClient fale com o servidor
Vamos começar por criar a nossa classe ChatClient. Esta classe terá a capacidade de enviar e receber simples mensagens do servidor. Para tornar isto possível, precisará de um socket e de saber o IP do servidor e a sua porta. Também vamos ter de passar um TextField para a classe ChatClient para que esta possa escrever as mensagens que recebeu.
A definição da classe e o seu construtor deverão estar assim descritos:
package { import flash.display.*; import flash.events.*; import flash.net.*; import flash.text.*; public class ChatClient extends Sprite { private var host:String; private var port:int; private var socket:Socket; private var chatArea:TextField; public function ChatClient(h:String, p:int, ca:TextField) { this.host = h; this.port = p; this.chatArea = ca; } }
Agora precisamos de fazer um método para nos ligarmos ao servidor e também para tratar dos eventos gerados pelo socket. Para trabalhar com sockets em Flash precisamos de adicionar escutas aos eventos dos sockets, que são chamados quando algo de significativo acontece. Podemos fazer isto usando o seguinte código (coloque-o na classe ChatClient):
public function connect():Boolean {
this.socket = new Socket(this.host, this.port);
this.socket.addEventListener(Event.CONNECT, socketConnect);
this.socket.addEventListener(Event.CLOSE, socketClose);
this.socket.addEventListener(IOErrorEvent.IO_ERROR, socketError);
this.socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityError);
this.socket.addEventListener(ProgressEvent.SOCKET_DATA, socketData);
try {
this.socket.connect(this.host, this.port);
}
catch (e:Error) {
trace("Error on connect: " + e);
return false;
}
return true;
}
private function socketConnect(event:Event):void {
trace("Connected: " + event);
}
private function socketData(event:ProgressEvent):void {
trace("Receiving data: " + event);
receiveData(this.socket.readUTFBytes(this.socket.bytesAvailable));
}
private function socketClose(event:Event):void {
trace("Connection closed: " + event);
this.chatArea.appendText("Connection lost." + "n");
}
private function socketError(event:IOErrorEvent):void {
trace("Socket error: " + event);
}
private function securityError(event:SecurityErrorEvent):void {
trace("Security error: " + event);
}
O método connect apenas cria um novo socket com o servidor e porta fornecidos no construtor. Adiciona escutas aos eventos mais significativos e tenta ligar-se ao servidor.
A maioria dos eventos são tratados apenas para fornecer informações de estado (críticas para depuração de uma aplicação cliente-servidor). As excepções são os métodos socketData e socketClose. No método socketClose simplesmente escrevemos informação de estado para o cliente. O método socketData lê todos os bytes disponíveis no buffer do socket e chama o método receiveData que se encontra descrito de seguida.
As únicas coisas que faltam são o método para tratar dos dados recebidos e o método para enviar mensagens para o servidor. Vamos criar o método sendMessage para escrever uma mensagem no socket e o método receiveData para tratar dos dados recebidos através do método socketData. O código para estes métodos pode ser encontrado de seguida (este código pertence à classe ChatClient):
public function sendMessage(msg:String):void {
msg += "n";
try {
this.socket.writeUTFBytes(msg);
this.socket.flush();
trace("Message sent: " + msg);
}
catch(e:Error) {
trace("Error sending data: " + e);
}
}
private function receiveData(msg:String):void {
trace("Message received: " + msg);
this.chatArea.appendText(msg + "n");
}
A mensagem recebida é simplesmente escrita para o chat do cliente. O método sendMessage adiciona uma quebra de linha à mensagem antes de a escrever no socket. Não vamos escrever directamente a nossa própria mensagem no lado do cliente - vamos esperar que o servidor envie a mensagem para todos os clientes para apenas escrevermos nessa altura.
A nossa classe ChatClient está assim terminada.
Fazer a interface para o chat e o cliente Flash
Agora precisamos de fazer o principal ficheiro Flash com a interface. Será composto por uma caixa de texto, um botão para enviar mensagens e uma área de texto para mostrar as mensagens de conversação.
Comecem por adicionar um novo campo de texto. Chamem-lhe inputLine_txt e mudem o seu tipo para Input Text. Façam-no comprido o suficiente para conseguir levar uma linha de conversação.
Agora construam um botão e chamem-lhe send_btn. Será usado para enviar mensagens (alternativamente também usaremos o Enter).
A única coisa que falta adicionar é a área de texto onde mostraremos a conversação. Adicionem um novo campo de texto, chamem-lhe chatArea_txt e mudem o seu tipo para Dynamic Text. Certifiquem-se que Multiline está seleccionado.
Vejam a seguinte imagem para perceber este processo.

Agora precisamos de adicionar as acções. Criem um novo layer, escolham o primeiro frame e abram o painel de ActionScript.
Precisamos de fazer 3 coisas:
- Fazer o requerimento da política ao servidor
- Criar um ChatClient e ligar ao servidor
- Tratar do processo de enviar mensagens
Lembrem-se, e isto é importante, se o Flash não receber a política do servidor a confirmar que lhe é dado o acesso ao servidor, então a ligação ao servidor não pode ser estabelecida. O cliente Flash envia implicitamente um requerimento de política ao servidor (<policy-file-request/>) e espera que a política seja enviada. Podemos também definir explicitamente o requerimento da política a um local específico (esta será a nossa abordagem).
Depois de carregarmos o ficheiro com a política vamos criar um objecto da classe ChatClient e estabelecer uma ligação com o servidor. Precisamos de definir o IP do servidor e a porta. Neste exemplo, usarei um endereço privado, porque tenho vários computadores em LAN e posso testar a aplicação desta forma. Devem mudar o endereço IP para o do vosso servidor e também mudar a porta. Vou usar o mesmo IP para o pedido da política, mas noutra porta.
Em seguida, vamos adicionar escutas aos eventos que fazem disparar o envio da mensagem. Vamos definir uma função para enviar mensagens que verifica se o campo inputLine_txt não está vazio, depois usa o objecto da classe ChatClient para enviar a mensagem para o servidor e no final também limpa o texto do campo inputLine_txt.
O código para conseguir isto é o seguinte:
import ChatClient; import flash.display.*; var host:String = "192.168.1.12"; var chatPort:int = 5555; var policyPort:int = chatPort + 1; Security.loadPolicyFile("xmlsocket://" + host + ":" + policyPort); var client:ChatClient = new ChatClient(host, chatPort, chatArea_txt); client.connect(); send_btn.addEventListener(MouseEvent.CLICK, onSendClick); inputLine_txt.addEventListener(KeyboardEvent.KEY_UP, onInputLineKey); function sendMessage():void { if (inputLine_txt.text != "") { client.sendMessage(inputLine_txt.text); inputLine_txt.text = ""; } } function onSendClick(e:MouseEvent):void { sendMessage(); } function onInputLineKey(e:KeyboardEvent):void { if (e.keyCode == 13) { // ENTER was pressed sendMessage(); } }
E acabamos o cliente. Agora precisamos de fazer a parte do servidor.
Como fazer um servidor de política com Java
Como foi dito anteriormente, o Flash espera que o servidor trate dos seus requerimentos de política. A política é um XML que contém os endereços dos domínios que têm acesso ao servidor e também a que portas têm acesso. É uma medida de segurança que a Adobe introduziu no Flash e todos os servidores a que um cliente Flash se queira ligar com sockets têm de estar preparados para isto.
Um servidor de política pode ser feito facilmente com Java e podem usar esta solução para outros servidores aos quais precisem que os clientes Flash se liguem.
O servidor de política irá consistir em 2 classes diferentes:
- PolicyServer: espera por ligações de clientes e cria PolicyServerConnections para tratar delas
- PolicyServerConnection: lê requerimentos de política dos clientes e escreve a política para estes
Apenas vou explicar e colocar aqui o código dos métodos mais importantes. Os ficheiros deste tutorial incluem o código todo, que se encontra bem documentado.
Vamos começar por definir o PolicyServer. Esta classe contém a política em XML. Neste caso vou definir o servidor como aceitando ligações de qualquer domínio e para qualquer porta. Nas vossas aplicações deverão ser mais restritivos e apenas dar acesso limitado.
public class PolicyServer extends Thread { public static final String POLICY_REQUEST = "<policy-file-request/>"; public static final String POLICY_XML = "<?xml version="1.0"?>" + "<cross-domain-policy>" + "<allow-access-from domain="*" to-ports="*" />" + "</cross-domain-policy>"; protected int port; protected ServerSocket serverSocket; protected boolean listening; /**
* Creates a new instance of PolicyServer.
*
* @param serverPort the port to be used by the server
*/ public PolicyServer(int serverPort) { this.port = serverPort; this.listening = false; } /**
* Waits for clients' connections and handles them to a new PolicyServerConnection.
*/ public void run() { try { this.serverSocket = new ServerSocket(this.port); this.listening = true; debug("listening"); while (this.listening) { Socket socket = this.serverSocket.accept(); debug("client connection from " + socket.getRemoteSocketAddress()); PolicyServerConnection socketConnection = new PolicyServerConnection(socket); socketConnection.start(); }; } catch (Exception e) { debug("Exception (run): " + e.getMessage()); } } }
Isto é bastante simples e não necessita de grandes explicações. Colocamos algumas mensagens de debug que dão bastante jeito para percebermos o que se está a passar. Devem ver o código fonte para o resto dos métodos, mas não há muito mais do que isto.
Tratar dos pedidos de política do Flash em Java
Agora vamos definir a classe PolicyServerConnection. Esta classe vai ler pedidos de política dos clientes, testar se são válidos e nesse caso enviar-lhes a política em XML. Irá receber um socket do servidor e vai abrir um leitor e um escritor para o poder usar para conversar com o cliente.
Vamos fazer o método readPolicyRequest que irá ler uma mensagem do socket, certificar-se que é um pedido de política e chamar o método writePolicy. Este método irá escrever o ficheiro XML da política do servidor e fechar o socket.
A nossa classe PolicyServerConnection deverá estar assim:
public class PolicyServerConnection extends Thread { protected Socket socket; protected BufferedReader socketIn; protected PrintWriter socketOut; /**
* Creates a new instance of PolicyServerConnection.
*
* @param socket client's socket connection
*/ public PolicyServerConnection(Socket socket) { this.socket = socket; } /**
* Create a reader and writer for the socket and call readPolicyRequest.
*/ public void run() { try { this.socketIn = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); this.socketOut = new PrintWriter(this.socket.getOutputStream(), true); readPolicyRequest(); } catch (Exception e) { debug("Exception (run): " + e.getMessage()); } } /**
* Reads a string from the client and if it is a policy request we write the policy, then we close the connection.
*/ protected void readPolicyRequest() { try { String request = read(); debug("client says '" + request + "'"); if (request.equals(PolicyServer.POLICY_REQUEST)) { writePolicy(); } } catch (Exception e) { debug("Exception (readPolicyRequest): " + e.getMessage()); } finalize(); } /**
* Writes the policy of the server.
*/ protected void writePolicy() { try { this.socketOut.write(PolicyServer.POLICY_XML + "u0000"); this.socketOut.close(); debug("policy sent to client"); } catch (Exception e) { debug("Exception (writePolicy): " + e.getMessage()); } } }
Não se esqueçam que isso não está completo. Vejam o código fonte para o resto.
O nosso servidor de política está feito. Simples, não foi?
Fazer o servidor de chat em Java
A estrutura do servidor principal é parecida com a do servidor de política. Também vamos usar 2 classes para isto:
- ChatServer: espera por ligações dos clientes e cria ChatServerConnection para tratar delas, adiciona e retira clientes e envia mensagens para todos eles
- ChatServerConnection: lê e escreve mensagens de chat para os clientes
Vamos começar pela classe ChatServer. Reparem que é muito parecida com a classe PolicyServer. Vamos adicionar o método remove para quando os clientes quiserem sair do servidor e o métodos writeToAll para difundir mensagens para todos.
Aqui está parte da definição desta classe:
public class ChatServer extends Thread { protected ServerSocket socketServer; protected int port; protected boolean listening; protected Vector<ChatServerConnection> clientConnections; /**
* Creates a new instance of ChatServer.
*
* @param serverPort the port to be used by the server
*/ public ChatServer(int serverPort) { this.port = serverPort; this.clientConnections = new Vector<ChatServerConnection>(); this.listening = false; } /**
* Listens for client conections and handles them to ChatServerConnections.
*/ public void run() { try { this.socketServer = new ServerSocket(this.port); this.listening = true; debug("listening"); while (listening) { Socket socket = this.socketServer.accept(); debug("client connection from " + socket.getRemoteSocketAddress()); ChatServerConnection socketConnection = new ChatServerConnection(socket, this); socketConnection.start(); this.clientConnections.add(socketConnection); }; } catch (Exception e) { debug(e.getMessage()); } } /**
* Broadcasts a message to all the clients.
*
* @param msg the message to be sent
*/ public void writeToAll(String msg) { try { for (int i = 0; i < this.clientConnections.size(); i++) { ChatServerConnection client = this.clientConnections.get(i); client.write(msg); } debug("broadcast message '" + msg + "' was sent"); } catch (Exception e) { debug("Exception (writeToAll): " + e.getMessage()); } } /**
* Removes a client from the server (it's expected that the client closes its own connection).
*
* @param remoteAddress the remote address of the client's socket connection
* @return true if the client was successfully removed
*/ public boolean remove(SocketAddress remoteAddress) { try { for (int i = 0; i < this.clientConnections.size(); i++) { ChatServerConnection client = this.clientConnections.get(i); if (client.getRemoteAddress().equals(remoteAddress)) { this.clientConnections.remove(i); debug("client " + remoteAddress + " was removed"); writeToAll(remoteAddress + " has disconnected."); return true; } } } catch (Exception e) { debug("Exception (remove): " + e.getMessage()); } return false; } }
Muito simples. Quando queremos remover um cliente dizemos aos outros que cliente se desligou. É da responsabilidade da classe ChatServerConnection terminar a ligação com o cliente, depois deste ser removido. Não fazemos isto no método remove.
Na classe ChatServerConnection simplesmente esperamos por uma mensagem do cliente. Se corresponder a "quit" desligaremos esse cliente. Vamos também fazer um método write para enviar mensagens para o cliente.
Aqui fica o código para a classe ChatServerConnection:
public class ChatServerConnection extends Thread { protected Socket socket; protected BufferedReader socketIn; protected PrintWriter socketOut; protected ChatServer server; /*
* Creates a new instance of ChatServerConnection.
*
* @param socket the client's socket connection
* @param server the server to each the client is connected
**/ public ChatServerConnection(Socket socket, ChatServer server) { this.socket = socket; this.server = server; } /**
* Waits from messages from the client and then instructs the server to send the messages to all clients.
*/ public void run() { try { this.socketIn = new BufferedReader(new InputStreamReader(this.socket.getInputStream())); this.socketOut = new PrintWriter(this.socket.getOutputStream(), true); this.server.writeToAll(this.getRemoteAddress() + " has connected."); String line = this.socketIn.readLine(); while (line != null) { debug("client says '" + line + "'"); // If it's a quit command, we remove the client from the server and exit if (line.compareToIgnoreCase("quit") == 0) { if (this.server.remove(this.getRemoteAddress())) { this.finalize(); return; } } this.server.writeToAll(this.getRemoteAddress() + ": " + line); line = this.socketIn.readLine(); } } catch (Exception e) { debug("Exception (run): " + e.getMessage()); } } /**
* Sends a message to the connected party.
*
* @param msg the message to send
*/ public void write(String msg) { try { this.socketOut.write(msg + "u0000"); this.socketOut.flush(); } catch (Exception e) { debug("Exception (write): " + e.getMessage()); } } }
Nunca é demais dizer que estes são apenas os métodos principais. Para o código completo vejam o código fonte.
Acabamos a funcionalidade do servidor, mas vamos ainda fazer mais no capítulo seguinte.
Construir a interface e fazer uma aplicação de teste
O último passo a fazer no servidor é ligar tudo e construir uma interface para que possamos ver o que está a acontecer. Vamos precisar de 2 classes para isto:
- ChatServerGUI: fornece a interface para uma melhor experiência de teste
- Main: a classe principal da aplicação, responsável por construir ambos os servidores
A classe ChatServerGUI é um simples JFrame com uma JLabel para mostrar o número de clientes e uma JTextArea para mostrar mensagens de depuração.
Também usamos uma TimeTask para actualizar o número de clientes ligados. Isto é fácil de fazer, mas está fora do âmbito deste tutorial. Vejam o código fonte sobre como fazer isto.
A classe ChatServerGUI apenas tem um método: write. Este método é usado para escrever mensagens de depuração. É definida da seguinte forma:
/**
* Writes a message to the text area of the form.
*
* @param msg the message to be written.
*/
public void write(String msg) {
try {
this.debugTextArea.getDocument().insertString(0, msg + "n", null);
}
catch (Exception e) {
}
}
Não há muito mais nesta classe. Agora vamos tratar da classe Main. Esta classe vai criar tanto o servidor de política como o servidor de chat e ainda a interface.
Este é o código para o seu método main:
public static void main(String[] args) {
try {
int chatPort = 5555;
int policyPort = chatPort + 1;
for (int i = 0; i < args.length; i++) {
if (i == 0) {
chatPort = Integer.parseInt(args[i]);
}
if (i == 1) {
policyPort = Integer.parseInt(args[i]);
}
}
PolicyServer policyServer = new PolicyServer(policyPort);
policyServer.start();
ChatServer chatServer = new ChatServer(chatPort);
chatServer.start();
ChatServerGUI gui = new ChatServerGUI(chatServer);
gui.setTitle("ChatServer");
gui.setLocationRelativeTo(null);
gui.setVisible(true);
}
catch (Exception e) {
debug("Main", "Exception (main)" + e.getMessage());
}
}
É basicamente isto.
Testar a aplicação de chat
Finalmente chegamos à parte divertida!
Comecem por executar a aplicação servidora. Esta precisa de ser a primeira a ser executada. Só depois é que os clientes se podem ligar a esta.
Agora, para uma última correcção. Como no meu caso preciso de testar a aplicação localmente, tive de mudar as definições nas Global Security Settings do meu Flash Player. Isto não deverá ser preciso quando corremos o Flash remotamente. Precisamos de dar permissões ao nosso ficheiro Flash.
O controlo Flash Settings Manager encontra-se na web. Abram o vosso Flash Settings Manager. Deverá abrir na divisão Global Security Settings. Agora adicionem a localização do vosso ficheiro Flash.

Eu sei, é uma chatice! A mensagem que mostram também não faz muito sentido - estamos a usar as últimas capacidades do ActionScript e do Flash, não métodos antigos!
Finalmente, executem os vossos clientes e boa conversação!
Fica aqui uma imagem do servidor a correr.

Agora, uma imagem do cliente chat.

Recursos
Recursos patrocinados
À procura de Java Server Hosting? Experimente a sugestão do nosso parceiro.




17:19 30-09-2008jorgebucaran.com
Thanks for this tutorial. Very impressive.