[Resolvido] Transferência de Arquivo via Socket

Olá Amigos,

estou desenvolvendo duas aplicações, um Servidor em Java e um cliente em Android. O problema é que estou tentando transferir uma imagem (que está em arquivo e por isto nem está na memória) do servidor para o cliente, só que aparentemente o cliente não recebe todos os dados ou não é informado do fim do envio do arquivo.

Eu imprimi um log do envio das duas aplicações, e veja como estão diferentes o envio e o recebimento:
Servidor:
Enviando= 4096
Enviando= 4096
Enviando= 4096
Enviando= 4096
Enviando= 4096
Enviando= 3479
Saiu -1

Cliente:
leitura=‘1448’
leitura=‘1448’
leitura=‘4096’
leitura=‘4096’
leitura=‘4096’
leitura=‘4096’
leitura=‘4096’
leitura=‘584’

Alguém poderia me ajudar por favor?

Servidor

public boolean sincronizaCroquis(DataInputStream in, DataOutputStream out, OutputStream ous) throws IOException {

        //Aguarda um OK
        msg = in.readUTF();
        int qtd = 0;

        //Verifica a mensagem recebida
        if (msg.equals(Constantes.OK)) {
            controle.addMessage("Sincronizando croquis.");
            
            //Aguarda um pedido de sincronização
            msg = in.readUTF();

            //Enquanto o cliente estiver sincronizando croquis
            while (msg.equals(Constantes.SINC_CROQUI)) {

                //Caso a mensagem seja para sincronizar croqui
                if (msg.equals(Constantes.SINC_CROQUI)) {

                    //Recebe o idLocalidade
                    int idLocalidade = in.readInt();

                    //Abre o arquivo para envio
                    File file = new File("croquis/" + idLocalidade + ".png");
                    FileInputStream fin = new FileInputStream(file);
                                        
                    //Cria um buffer de 4KB
                    int tamanho = 1024;
                    byte[] buffer = new byte[tamanho];

                    //Envia dados
                    Integer leitura = fin.read(buffer, 0, tamanho);
                    while (leitura != -1) {
                        System.out.println("Enviando= " + leitura);
                        System.out.flush();
                        ous.write(buffer, 0, leitura);
                        leitura = fin.read(buffer, 0, tamanho);
                        
                    }
                    out.write(leitura);
                    System.out.println("Saiu " + leitura);
                    out.flush();
                    qtd++;
                }
                
                msg = in.readUTF();
            }
            controle.addMessage("Total de croquis sincronizados: " + qtd);
        }

        return true;
    }

Cliente:

/**
	 * Recebe um croqui via Socket e anexa a sua localidade
	 * @throws IOException 
	 */
	public boolean recebeCroqui(Integer idLocalidade, ConnectionSocket connection) throws IOException{

		//Abre os devidos canais de comuicação
		DataOutputStream out = connection.getDataOutput();
		InputStream ins = connection.getIns();
		//InputStreamReader insReader = new InputStreamReader(ins);
		//BufferedReader reader = new BufferedReader(insReader);
		//String name = reader.readLine();

		//Envia um SINC_CROQUI - avisando ao servidor para sincronizar um croqui
		Log.d("ufop.smid", "Recebendo um croqui.");
		out.writeUTF(Constantes.SINC_CROQUI);

		//Envia idLocalidade
		Log.d("ufop.smid", "Enviando Localidade.");
		out.writeInt(idLocalidade);

		//FileOutputStream para salvar o arquivo
		File path = new File(Environment.getExternalStorageDirectory() + Constantes.DIRETORIO_CROQUIS);
		Log.d("ufop.smid", "Criando diretório caso não exista.");
		path.mkdirs();

		FileOutputStream fOut = new FileOutputStream(new File(path, idLocalidade + ".png"));

		//Recebe os bytes do croqui
		Log.d("ufop.smid", "Recebendo bytes.");
		int tamanho = 1024; //tamanho 4KB
		byte[] buffer = new byte[tamanho];
		Integer leitura = ins.read(buffer, 0 , tamanho);
		while(leitura != -1){
			Log.d("ufop.smid", "leitura='" + leitura +"'");
			fOut.write(buffer, 0, leitura);
			
			leitura = ins.read(buffer, 0 , tamanho);
		}

		//Fecha o arquivo
		fOut.flush();
		fOut.close();
		Log.d("ufop.smid", "Croqui recebido com sucesso!");

		return true;
	}

Resolvi de maneira simples:

No servidor, antes de realizar o envio do arquivo eu envio o tamanho do mesmo através de um DataOuputStream:

//Envia o tamanho do arquivo
                    out.writeLong(file.length());
                    out.flush();

E no cliente, antes de realizar o recebimento dos bytes do arquivo eu recebe o tamanho do mesmo enviado pelo servidor:

//Recebe o tamanho do arquivo
		long tamanho = connection.getDataInput().readLong();

Código mais detalhado:

Servidor:

public boolean sincronizaCroquis(DataInputStream in, DataOutputStream out, OutputStream ous) throws IOException {

        //Aguarda um OK
        msg = in.readUTF();
        int qtd = 0;

        //Verifica a mensagem recebida
        if (msg.equals(Constantes.OK)) {
            controle.addMessage("Sincronizando croquis (Aguarde, essa etapa pode demorar)...");

            //Aguarda um pedido de sincronização
            msg = in.readUTF();

            //Enquanto o cliente estiver sincronizando croquis
            while (msg.equals(Constantes.SINC_CROQUI)) {

                //Caso a mensagem seja para sincronizar croqui
                if (msg.equals(Constantes.SINC_CROQUI)) {

                    //Recebe o idLocalidade
                    int idLocalidade = in.readInt();

                    //Abre o arquivo para envio
                    File file = new File("croquis/" + idLocalidade + ".png");
                    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
                    
                    //Envia o tamanho do arquivo
                    out.writeLong(file.length());
                    out.flush();

                    //Cria um buffer
                    byte[] buffer = new byte[Constantes.BUFFER_SIZE];

                    //Envia dados
                    Integer leitura = null;
                    do {
                        leitura = bis.read(buffer, 0, buffer.length);
                        System.out.flush();
                        if (leitura != -1) {
                            ous.write(buffer, 0, leitura);
                        } else {
                            ous.write(leitura);
                        }
                        ous.flush();

                    } while (leitura != -1);

                    bis.close();
                    qtd++;
                }

                msg = in.readUTF();
            }
            controle.addMessage("Total de croquis sincronizados: " + qtd);
        }

        return true;
    }
Cliente:

public boolean recebeCroqui(Integer idLocalidade, ConnectionSocket connection) throws IOException{

		//Abre os devidos canais de comuicação
		DataOutputStream out = connection.getDataOutput();
		InputStream ins = connection.getIns();

		//Envia um SINC_CROQUI - avisando ao servidor para sincronizar um croqui
		Log.d("ufop.smid", "Recebendo um croqui.");
		out.writeUTF(Constantes.SINC_CROQUI);

		//Envia idLocalidade
		Log.d("ufop.smid", "Enviando Localidade.");
		out.writeInt(idLocalidade);
		
		//Recebe o tamanho do arquivo
		long tamanho = connection.getDataInput().readLong();

		//FileOutputStream para salvar o arquivo
		File path = new File(Environment.getExternalStorageDirectory() + Constantes.DIRETORIO_CROQUIS);
		Log.d("ufop.smid", "Criando diretório caso não exista.");
		path.mkdirs();
		BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(path, idLocalidade + ".png")));

		//Recebe os bytes do croqui
		Log.d("ufop.smid", "Recebendo bytes.");
		byte[] buffer = new byte[Constantes.BUFFER_SIZE];
		
		//Envia os bytes
		Integer leitura = null;
		Integer count = 0;
		do{
			leitura = ins.read(buffer, 0 , buffer.length);
			count += leitura;
			bos.write(buffer, 0, leitura);
			bos.flush();
			
		} while(leitura != -1 && count < tamanho);

		//Fecha o arquivo
		bos.close();
		Log.d("ufop.smid", "Croqui recebido com sucesso!");

		return true;
	}

Um detalhe importante a respeito de sockets: O número de writes dificilmente corresponde ao número de reads. Não se sabe onde um dado começa e outro termina, por isso, você é obrigado a incluir essa informação no seu protocolo.

Gosto muito dessa solução que você usou. É, inclusive, o que sugiro aqui:

Outra opção seria usar um terminador. Mas não gosto de terminadores, pois você nunca sabe quando ele vai aparecer. Aí vc é obrigado a ficar analisando e fatiando dados nos buffer, o que além de ineficiente, é bem mais trabalhoso.

Olá ViniGodoy,

a minha solução foi justamente enviar o tamanho do arquivo antes de realizar a transferência, além de funcionar, também é mais seguro. Li o tópico que você postou e gostei muito de saber também sobre a ineficiência do ObjectInputStream, eu iria utilizá-lo em meu projeto, porém apenas pelo detalhe de alguns objetos terem alguns tipos de atributos diferentes eu acabei fazendo com DataInputStream.

Aliás, no caso de enviar arquivos, mais precisamente imagens, o aconselhável é usar InputStream/OutputStream ? Ou seria melhor utilizar outra Classe?

Obrigado pela dica.

O problema do ObjectOutputStream é que ele delega para a classe o papel de saber como se serializar.

Aí vc fica a mercê de serializações que não estão sob seu controle. O que significa que um update no cliente pode, sem você ficar sabendo, alterar o que é enviado na rede e quebrar seu servidor. Muitas serializações também vão enviar absolutamente todo estado do objeto, o que pode ser muito mais do que você precisa em muitas comunicações.

O ideal mesmo é você ter controle total do seu protocolo. Assim você pode inserir nele uma mensagem de handshake de versão, por exemplo, e saber exatamente o que será enviado/recebido naquela versão. Sem surpresas caso entre uma versão de java e outro alguém resolva mudar a forma que o objeto é serializado, só porque um campo novo surgiu.

Muito Obrigado ViniGodoy, você foi bem esclarecedor!