Posts Tagged 'thread'

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.

Anúncios

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.


Categorias

Atualizações Twitter