Arquivo de outubro \05\UTC 2011

Tutorial: escrevendo seu próprio servidor de transferência arquivos (parte2)

Bom, na segunda parte do tutorial vou mostrar como aceitar conexões de um cliente através de um socket e enviar as respostas.
Para criar um servidor precisamos, antes de mais nada, abrir um ServerSocket. Dessa maneira vamos abrir uma porta TCP para aceitar conexões de outros hosts. Durante os testes, é importante certificar-se de que o firewall vai deixar as conexões entrarem.
O código abaixo é a estrutura básica do servidor. Ele contém um ServerSocket dentro de um laço para aceitar as conexões. Ao aceitar uma conexão, o servidor delega a conexão para uma nova thread. Dessa maneira, o ServerSocket pode aceitar novas conexões sem precisar esperar o processamento da conexão anterior:

public class Servidor {
	private ServerSocket	server;
	private boolean started = false;

	public void start() throws Exception{
		started = true;
		
		int count = 1;
                server = new ServerSocket(3000);
		while(started){
			final Socket socket = server.accept();
			
			Thread thread = new Thread(new Runnable(){
				@Override
				public void run() {
					service(socket);
				}
			}, "service-" + count++);
		}
	}
	

	public void stop() throws IOException{
		started = false;
		server.close();
	}
}

Agora, resta escrever a lógica do tratamento das requisições do cliente. Pode até não parecer, mas esta parte é a mais “chata” de se escrever. Isso porque a transmissão de dados através de um stream tem algumas peculiaridades que quando não são observadas, podem custar algumas boas horas de debug e xingamento. Primeiro, vou começar escrevendo como NÃO ler dados de um stream, sabendo que é a entrada de um socket:

private void service(Socket socket) throws IOException{
		BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
		
		byte [] buf = new byte[256];
		int count = 0;
		while((count = bis.read(buf)) > -1){ //só retorna -1 quando o socket do outro lado é fechado
			//...
		}
	}

No código acima, o método lê bytes do stream de entrada do socket até que o método read do stream retorne -1. Pois bem, o valor -1 somente é retornado quando o stream encontra a marcação EOF nos dados, e isso somente acontece quando a conexão é fechada. Assim, se você estiver escrevendo uma lógica do tipo request-response, com essa abordagem é possível que você leia toda a request e mesmo assim ficar com a thread bloqueada, pois você ainda não recebeu aquele tão esperado -1.
Um outro erro seria usar o método available():

private void service(Socket socket) throws IOException{
		BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
		
		byte [] buf = new byte[256];
		int count = 0;
		while(bis.available() > 0){
			//...
		}

A pegadinha nesse caso é a seguinte: o método available() retorna o número de bytes disponíveis para no buffer. Isso significa que o método pode retornar 0 antes de terminar a leitura de toda a entrada, fazendo com que a thread continue com dados incompletos.
Mas então, qual a solução para este dilema ? É simples, basta trocar mensagens com tamanho fixo ou com algum tipo de delimitador.
O jeito mais fácil que eu encontrei é trocar objetos serializáveis, pois assim a própria JVM se encarrega de inserir delimitadores e fixar os tamanhos dos campos:

class Message implements Serializable {
	private static final long	serialVersionUID	= 1L;

	public int command;
	public String[] args;
	public String[] result;
	public boolean disconnect;
	public boolean fileToSend;
	public String fileName;
	public byte[] fileData;
}

A classe acima é um exemplo de como pode ser um objeto para troca de dados entre o cliente e o servidor. Praticamente, todos os campos que podem ser utilizados na comunicação são colocados em uma única estrutura. Vejamos agora um exemplo de como ficaria a nossa lógica, no servidor:

private void service(Socket socket) throws IOException, ClassNotFoundException {
		ObjectInputStream entrada = new ObjectInputStream(socket.getInputStream());
		ObjectOutputStream saida = new ObjectOutputStream(socket.getOutputStream());
		
		while(true){
			Message request = (Message) entrada.readObject();
			
			if(request.disconnect){
				socket.close();
				break;
			}
			
			Message response = new Message();
			Command cmd = new Command();
			
			response.result = cmd.execute(request.command, request.args);
			
			saida.writeObject(response);
		}
	}

Bom, espero que nesses 2 posts eu tenha conseguido colocar os principais pontos que devem ser levados em consideração na hora de escrever esse tipo de aplicação.

Tutorial: escrevendo seu próprio servidor de transferência arquivos (parte1)

Uma das primeiras aplicações de rede que surgiram foram os servidores de transferência de arquivos.
Hoje o protocolo mais usado para esse fim é o FTP
(File Transfer Protocol), e pode ser uma boa fonte de referências para o assunto.
Vamos ao que interessa. Acho que o passo mais simples é começar implementando os comandos, sem pensar
na comunicação em rede por enquanto. Um passo de cada vez.
O código abaixo é um exemplo simples de como criar um diretório:

class Command{
	public void criarDiretorio(String nomeDiretorio){
		File dir = new File(nomeDiretorio);
		
		if(dir.exists()){
			if(dir.isDirectory()){
				System.out.println("O diretório " + nomeDiretorio + " já existe!");
			}else if(dir.isFile()){
				System.out.println("O caminho " + nomeDiretorio + " é um arquivo!");
			}
		}else{
			dir.mkdir();
		}
	}
}

Reparemos que a execução do comando já está separada no seu próprio método. Isolar as partes do seu programa é um pouco mais trabalhoso, mais acreditem, deixa o desenvolvimento muito mais consistente. Bom, com essa estratégia podemos escrever cada comando em seu próprio método, e ainda, um método controlador que executa o comando apropriado de acordo com um código ou nome recebido:

class Command {
	public static final int	CRIAR_DIR	= 1;
	public static final int	LISTAR_DIR	= 2;

	public String[] executar(int codigoComando, String[] args) {
		switch (codigoComando) {
			case CRIAR_DIR:
				return criarDiretorio(args[0]);

			case LISTAR_DIR:
				return listarConteudo(args[0]);

			default:
				return null;
		}
	}

	private String[] criarDiretorio(String nomeDiretorio) {
		File dir = new File(nomeDiretorio);

		String[] result = null;

		if (dir.exists()) {
			if (dir.isDirectory()) {
				result = new String[] { "O diretório " + nomeDiretorio + " já existe!" };
			} else if (dir.isFile()) {
				result = new String[] { "O caminho " + nomeDiretorio + " é um arquivo!" };
			}
		} else {
			dir.mkdir();
		}

		return result;
	}

	private String[] listarConteudo(String nomeDiretorio) {
		File dir = new File(nomeDiretorio);

		String[] result = null;

		if (!dir.exists()) {
			result = new String[] { "O diretório " + nomeDiretorio + " não existe!" };
		} else if (dir.isFile()) {
			result = new String[] { "O caminho " + nomeDiretorio + " é um arquivo!" };
		} else {
			result = dir.list();
		}

		return result;
	}
}

Com essa estrutura, já é possível escrever diversos comandos, cada um com seu próprio código. Reparem que comando que seleciona o comando a ser executado recebe um array de Strings, para receber um número variável de argumentos. As saídas dos comandos deixaram de ser enviadas para saída padrão para serem retornadas em arrays também. Assim, a execução dos comandos fica isolada da exibição dos resultados, de forma que os resultados podem ser exibidos no console, em uma janela ou transmitidos pela rede.

Testando sua interface em Swing

A prática de escrever testes automatizados vem sendo amplamente adotada por equipes de todo o mundo. Embora a maior parte do material encontrado na internet trata sobre testes unitários, também é possível escrever testes de integração automatizados, testes do banco de dados, etc. Enfim, é possível automatizar qualquer tipo de teste, uma vez que a variedade de ferramentas para este propósito é grande.
Nesse post, eu quero mostrar um exemplo simples de como é possível escrever um teste automatizado para uma interface em Swing, usando apenas o framework JUnit. O nosso exemplo será uma simples calculadora, que realiza apenas a soma, para resumir o código, vejamos:

public class TestCalculadora {
	@Test
	public void testSomar() {
		Calculadora calculadora = new Calculadora();
		
		Integer numero1 = 10;
		Integer numero2 = 5;
		
		calculadora.textFieldNumero1.setText(numero1.toString());
		calculadora.textFieldNumero2.setText(numero2.toString());
		
		calculadora.buttonSomar.doClick(); //simula um click programaticamente
		
		Integer resultado = numero1 + numero2;
		
		Assert.assertEquals(resultado.toString(), calculadora.textFieldResultado.getText());
	}
}

O teste acima especifica que nossa calculadora deve receber os números a serem somados em campos de texto, e após clicar no botão de soma, o resultado será exibido em um terceiro campo de texto. Reparem, que o método doClick() simula programaticamente o click no botão.
A implementação de Calculadora é o código suficiente para passar nesse primeiro teste:

public class Calculadora {
	JTextField	textFieldResultado;
	JTextField	textFieldNumero1;
	JTextField	textFieldNumero2;
	JButton		buttonSomar;

	public Calculadora() {
		this.textFieldResultado = new JTextField();
		this.textFieldNumero1 = new JTextField();
		this.textFieldNumero2 = new JTextField();
		this.buttonSomar = new JButton();
		
		this.buttonSomar.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				somar();
			}
		});
	}
	
	private void somar(){
		int numero1 = Integer.valueOf(textFieldNumero1.getText());
		int numero2 = Integer.valueOf(textFieldNumero2.getText());
		
		textFieldResultado.setText(String.valueOf((numero1 + numero2)));
	}
}

Como podem reparar, esse classe não é uma interface gráfica propriamente dita, pois não estende nenhum componente gráfico. Mas isso é resolvido facilmente:

public class Calculadora extends JFrame {
	JTextField	textFieldResultado;
	JTextField	textFieldNumero1;
	JTextField	textFieldNumero2;
	JButton		buttonSomar;

	public Calculadora() {
		this.setSize(150, 200);
		this.textFieldResultado = new JTextField(12);
		this.textFieldResultado.setEnabled(false);
		this.textFieldNumero1 = new JTextField(12);
		this.textFieldNumero2 = new JTextField(12);
		this.buttonSomar = new JButton("Somar");

		this.buttonSomar.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				somar();
			}
		});

		getContentPane().setLayout(new BorderLayout());

		JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER));

		panel.add(textFieldNumero1);
		panel.add(textFieldNumero2);
		panel.add(buttonSomar);
		panel.add(textFieldResultado);

		getContentPane().add(panel, BorderLayout.CENTER);
	}

	private void somar() {
		int numero1 = Integer.valueOf(textFieldNumero1.getText());
		int numero2 = Integer.valueOf(textFieldNumero2.getText());

		textFieldResultado.setText(String.valueOf((numero1 + numero2)));
	}

	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			@Override
			public void run() {
				Calculadora calculadora = new Calculadora();
				calculadora.setVisible(true);
			}
		});
	}
}

Assim, com o teste testSoma() passando, a funcionalidade da tela está pronta, restando adicionar o código relativo à aparência propriamente dito. Percebemos, que o teste automatizado não substitui um teste manual, afinal de contas, requisitos como layout, estética da tela, usabilidade e etc. dependem da percepção do usuário. Porém, auxilia bastante no desenvolvimento das funcionalidades da tela. A desvantagem nesse caso, é a necessidade de expor os componentes da tela para a classe de teste. Nesse caso, coloquei a interface e o teste no mesmo pacote e declarei os componentes da tela com acesso default. As alternativas para manter estes campos private seria criar getters para os componentes, criar métodos que delegam o set das propriedades, ou ainda, acessar os campos por reflection.


Categorias

Atualizações Twitter