O primeiro passo numa aplicação sockets, antes mesmo da programação, é definir como vai ser o protocolo de comunicação entre seu cliente e seu servidor.
O protocolo descreve como as mensagens serão codificadas, que mensagens existirão, etc. Por exemplo, você pode dizer que toda mensagem tem o seguinte formato:
1 int - Tamanho da mensagem
1 byte - Código da mensagem (de 0 até 127)
outros bytes - DADOS DA MENSAGEM
O campo “tamanho da mensagem” é muito importante. É ele que diz ao servidor quantos bytes devem ser lidos, para que aquela mensagem tenha sido completamente recebida. Esteja atento ao detalhe que, em sockets, o número de writes no lado do de quem escreve pode não ter nada a ver com o número de reads necessários no lado de quem lê a mensagem. Por exemplo, é possível que o cliente escreva um grande arquivo com um só write, mas que o servidor precise de vários pequenos reads para ler a mensagem completa, pois os bytes trafegam pela rede devagar e chegam aos poucos. O contrário também pode ocorrer, o cliente pode fazer vários writes pequenos, e os dados chegarem juntos ao servidor, e serem lidos num só read.
Sem a informação do tamanho, é impossível saber quantos bytes esperar, ou até onde ler. E como saber por onde começar a leitura? Nosso protocolo SEMPRE começa com o int de tamanho, ou seja, nosso servidor precisa começar lendo um int, que indica o tamanho da primeira mensagem.
Vamos adiante. A mensagem de código 01, poderia ser a de “listar diretório”. Ela, poderia exigir que os outros bytes estivesse preenchidos com uma String, contendo que diretório no servidor seria listado. Vejamos a estrutura da mensagem 01:
String - Caminho a ser listado.
Ela poderia ser respondida pela mensagem 01 do servidor. Essa mensagem, poderia ter o seguinte formato:
Para cada arquivo
1 int - Tamanho da string com o nome do diretório
String - Com o nome do diretório.
Note que como a resposta do servidor serão várias string, precisamos de um byte antes de cada delas para saber, na massa de dados, onde termina uma string e onde outra começa… Outra alternativa seria incluir um caracter separador, como faz o C++. Poderíamos, opcionalmente, fazer assim:
Para cada arquivo (forma opcional)
String - Com o nome do diretório.
um byte contendo o valor 0.
Eu prefiro a primeira forma, pois para os sockets é conveniente saber quantos bytes ler.
Ok, agora que você já sabe como será a conversa do servidor com o cliente, é possível codificar a mensagem que vai para o servidor:
public ByteArrayOutputStream codificarListarDiretorio(String diretorio)
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(1); //Mensagem 1, listar diretório
dos.writeChars(diretorio); //Diretório a ser listado
return bos;
}
Agora vamos fazer o método de enviar mensagem. Note que no método anterior, eu não incluí o campo do tamanho da mensagem. Isso porque, como esse campo é igual para todas as mensagens, preferi que o método de envio cuidasse desse detalhe. Isso é uma divisão em camadas do protocolo. O método de envio cuida da parte “bruta” que é dizer o tamanho total da massa de dados, e os outros métodos (como o de cima), se preocupam em codificar as mensagens em si.
Alguns protocolos incluem checksums, criptografia, e outras coisas, que poderiam ser colocadas aqui no método de envio. Coisas como essa validam a mensagem, ou a codificam, sem alterar sua estrutura. Como podem ser calculadas externamente, a lógica fica melhor dividida dessa forma.
Como o nosso protocolo é muito simples, e não tem nada disso, vamos ver como o método de envio fica:
public void enviarMensagem(ByteArrayOutputStream mensagem)
{
byte[] msg = mensagem.getByteArray();
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
out.writeInt(msg.length); //Agora sim, o tamanho da mensagem
out.write(msg.toByteArray()); //Os dados
out.flush();
}
Note que para comandar uma listagem de diretório, agora basta fazer:
enviarMensagem(codificarListarDiretorio("C:\"));
E como seria a decodificação do servidor? Bom, agora você sabe o que esperar. O primeiro passo é ler quatro bytes (um int), que sabemos (pelo nosso protocolo) que será o tamanho da mensagem.
Depois, lemos a quantidade de bytes indicada nesse parâmetro. O primeiro byte lido, será o código da mensagem. Ao recebermos o código 01, saberemos (pelo protocolo) que, necessariamente, refere-se a uma mensagem de listar diretório.
Os bytes seguintes podem, então, ser seguramente copiados para uma string. E essa string terá o nome do diretório.
Espero que você tenha entendido o conceito. Como o procotolo é seu, sinta-se à vontade de fazer qualquer modificação, inclusive, usar somente strings. As informações estariam contidas nessa string, que deverá ser parseada entre o cliente e o servidor. O seu protocolo definiria então como o parser trabalharia nessa string. Muitos protocolos hoje são feitos dessa forma. É o caso dos protocolos SIP e dos web-services (os últimos usam xml).
Você também pode ler alguma RFC de um protocolo simples, como o TFTP, para ver como os projetistas organizaram o protocolo.
Eu não recomendo que você use somente strings, no entanto. É um bom exercício trabalhar com bytes, e ver como você pode otimizar o protocolo para consumir a menor banda possível. Na verdade, muitos protocolos são orientados a bytes, e seria uma boa experiência você aprender a trabalhar desse jeito também.
Dê uma olhada nesse programa de chat, que já deixei aqui no fórum. Ele aplica esses conceitos:
http://www.guj.com.br/posts/list/15/74218.java#506163