Continuando coa serie de artigos en curso dedicados ás implementacións personalizadas das utilidades da consola de Windows, non podemos deixar de tocar o TFTP (Trivial File Transfer Protocol), un protocolo de transferencia de ficheiros sinxelo.
Como a última vez, repasemos brevemente a teoría, vexamos o código que implementa unha funcionalidade similar á requirida e analízao. Máis detalles - baixo o corte
Non vou copiar e pegar información de referencia, as ligazóns ás que tradicionalmente se poden atopar ao final do artigo, só direi que, no seu núcleo, TFTP é unha variación simplificada do protocolo FTP, no que a configuración de control de acceso ten foi eliminado e, de feito, non hai nada aquí excepto comandos para recibir e transferir un ficheiro . Non obstante, para facer a nosa implementación un pouco máis elegante e adaptada aos principios actuais de escritura de código, a sintaxe cambiouse lixeiramente; isto non cambia os principios de funcionamento, pero a interface, en mi humilde opinión, faise un pouco máis lóxica e combina os aspectos positivos de FTP e TFTP.
En particular, cando se inicia, o cliente solicita o enderezo IP do servidor e o porto no que está aberto o TFTP personalizado (debido á incompatibilidade co protocolo estándar, considerei adecuado deixar ao usuario a posibilidade de seleccionar un porto), despois de que un conexión ocorre, como resultado do cal o cliente pode enviar un dos comandos - get ou put, para recibir ou enviar un ficheiro ao servidor. Todos os ficheiros son enviados en modo binario para simplificar a lóxica.
Para implementar o protocolo, tradicionalmente usei 4 clases:
- Cliente TFTPC
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Debido ao feito de que as clases de proba só existen para depurar as principais, non as analizarei, pero o código estará no repositorio; pódese atopar unha ligazón ao final do artigo. Agora vou mirar as clases principais.
Cliente TFTPC
A tarefa desta clase é conectarse a un servidor remoto polo seu ip e número de porto, ler un comando do fluxo de entrada (neste caso, o teclado), analizalo, transferilo ao servidor e, dependendo de se precisa enviar ou recibir un ficheiro, transferilo ou obtelo.
O código para iniciar o cliente para conectarse ao servidor e esperar un comando do fluxo de entrada é así. Unha serie de variables globais que se usan aquí descríbense fóra do artigo, no texto completo do programa. Pola súa trivialidade non os cito para non sobrecargar o artigo.
public void run(String ip, int port)
{
this.ip = ip;
this.port = port;
try {
inicialization();
Scanner keyboard = new Scanner(System.in);
while (isRunning) {
getAndParseInput(keyboard);
sendCommand();
selector();
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
Imos repasar os métodos chamados neste bloque de código:
Aquí envíase o ficheiro: mediante un escáner, presentamos o contido do ficheiro como unha matriz de bytes, que escribimos un por un no socket, despois de que o pechamos e o abrimos de novo (non é a solución máis obvia, pero garante a liberación de recursos), despois de que amosamos unha mensaxe sobre a transferencia exitosa.
private void put(String sourcePath, String destPath)
{
File src = new File(sourcePath);
try {
InputStream scanner = new FileInputStream(src);
byte[] bytes = scanner.readAllBytes();
for (byte b : bytes)
sout.write(b);
sout.close();
inicialization();
System.out.println("nDonen");
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}
Este fragmento de código describe a recuperación de datos do servidor. Todo volve ser trivial, só interesa o primeiro bloque de código. Para comprender exactamente cantos bytes hai que ler desde o socket, cómpre saber canto pesa o ficheiro transferido. O tamaño do ficheiro no servidor represéntase como un número enteiro longo, polo que aquí se aceptan 4 bytes, que posteriormente se converten nun só número. Este non é un enfoque moi Java, é bastante similar para SI, pero resolve o seu problema.
Entón todo é trivial: recibimos un número coñecido de bytes do socket e escribimos nun ficheiro, despois de que amosamos unha mensaxe de éxito.
private void get(String sourcePath, String destPath){
long sizeOfFile = 0;
try {
byte[] sizeBytes = new byte[Long.SIZE];
for (int i =0; i< Long.SIZE/Byte.SIZE; i++)
{
sizeBytes[i] = (byte)sin.read();
sizeOfFile*=256;
sizeOfFile+=sizeBytes[i];
}
FileOutputStream writer = new FileOutputStream(new File(destPath));
for (int i =0; i < sizeOfFile; i++)
{
writer.write(sin.read());
}
writer.close();
System.out.println("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
Se se introduciu un comando distinto de get ou put na xanela do cliente, chamarase á función showErrorMessage, indicando que a entrada foi incorrecta. Por trivialidade, non o citarei. Algo máis interesante é a función de recibir e dividir a cadea de entrada. Pasámoslle o escáner, do que esperamos recibir unha liña separada por dous espazos e que contén o comando, o enderezo de orixe e o de destino.
private void getAndParseInput(Scanner scanner)
{
try {
input = scanner.nextLine().split(" ");
typeOfCommand = input[0];
sourcePath = input[1];
destPath = input[2];
}
catch (Exception e) {
System.out.println("Bad input");
}
}
Enviar un comando: transmite o comando introducido desde o escáner ao socket e obriga a enviarlo
private void sendCommand()
{
try {
for (String str : input) {
for (char ch : str.toCharArray()) {
sout.write(ch);
}
sout.write(' ');
}
sout.write('n');
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
Un selector é unha función que determina as accións do programa dependendo da cadea introducida. Todo aquí non é moi bonito e o truco empregado non é o mellor con saída forzada fóra do bloque de código, pero a principal razón para iso é a ausencia en Java dalgunhas cousas, como delegados en C#, punteiros de función de C++ ou en Java. polo menos o terrible e terrible goto, que che permite implementar isto ben. Se sabes como facer o código un pouco máis elegante, agradezo as críticas nos comentarios. Paréceme que aquí se necesita un dicionario String-delegate, pero non hai ningún delegado...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}
TFTPServer
A funcionalidade do servidor difire da funcionalidade do cliente, en xeral, só en que os comandos chegan a el non desde o teclado, senón desde o socket. Algúns dos métodos son en xeral os mesmos, polo que non os citarei, só tocarei as diferenzas.
Para comezar, utilízase o método de execución, que recibe un porto como entrada e procesa os datos de entrada do socket nun bucle eterno.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}
O método put, que envolve o método writeToFileFromSocket que abre un fluxo de escritura nun ficheiro e escribe todos os bytes de entrada do socket, mostra unha mensaxe que indica a finalización exitosa da transferencia cando se completa a escritura.
private void put(String source, String dest){
writeToFileFromSocket();
System.out.print("nDonen");
};
private void writeToFileFromSocket()
{
try {
FileOutputStream writer = new FileOutputStream(new File(destPath));
byte[] bytes = sin.readAllBytes();
for (byte b : bytes) {
writer.write(b);
}
writer.close();
}
catch (Exception e){
System.out.println(e.getMessage());
}
}
O método get recupera o ficheiro do servidor. Como xa se mencionou na sección do lado do cliente do programa, para transferir con éxito un ficheiro, cómpre coñecer o seu tamaño, almacenado nun número enteiro longo, polo que o dividín nunha matriz de 4 bytes, transfiraos byte a byte ao socket, e despois, recibilos e reunilos no cliente nun número de volta, transfiro todos os bytes que compoñen o ficheiro, lidos desde o fluxo de entrada do ficheiro.
private void get(String source, String dest){
File sending = new File(source);
try {
FileInputStream readFromFile = new FileInputStream(sending);
byte[] arr = readFromFile.readAllBytes();
byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
sout.write(bytes[i]);
sout.flush();
for (byte b : arr)
sout.write(b);
}
catch (Exception e){
System.out.println(e.getMessage());
}
};
O método getAndParseInput é o mesmo que no cliente, a única diferenza é que le os datos desde o socket e non desde o teclado. O código está no repositorio, igual que o selector.
Neste caso, a inicialización colócase nun bloque de código separado, porque dentro desta implementación, despois de que se complete a transferencia, os recursos son liberados e reocupados de novo, de novo para proporcionar protección contra fugas de memoria.
private void incialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}
Para resumir:
Acabamos de escribir a nosa propia variación nun protocolo de transferencia de datos sinxelo e descubrimos como debería funcionar. En principio, aquí non descubrín América e non escribín moitas cousas novas, pero non había artigos similares sobre Habré, e como parte de escribir unha serie de artigos sobre as utilidades cmd era imposible non tocar nel.
Referencias:
Fonte: www.habr.com