Files
nanofiles/es/um/redes/nanoFiles/udp/client/DirectoryConnector.java
2026-04-29 00:34:45 +02:00

457 lines
16 KiB
Java

package es.um.redes.nanoFiles.udp.client;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketTimeoutException;
import java.net.SocketException;
import java.util.Map;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import es.um.redes.nanoFiles.tcp.client.NFConnector;
import es.um.redes.nanoFiles.application.NanoFiles;
import es.um.redes.nanoFiles.udp.message.DirMessage;
import es.um.redes.nanoFiles.udp.message.DirMessageOps;
import es.um.redes.nanoFiles.util.FileInfo;
/**
* Cliente con métodos de consulta y actualización específicos del directorio
*/
public class DirectoryConnector {
/**
* Puerto en el que atienden los servidores de directorio
*/
private static final int DIRECTORY_PORT = 6868;
/**
* Tiempo máximo en milisegundos que se esperará a recibir una respuesta por el
* socket antes de que se deba lanzar una excepción SocketTimeoutException para
* recuperar el control
*/
private static final int TIMEOUT = 1000;
/**
* Número de intentos máximos para obtener del directorio una respuesta a una
* solicitud enviada. Cada vez que expira el timeout sin recibir respuesta se
* cuenta como un intento.
*/
private static final int MAX_NUMBER_OF_ATTEMPTS = 5;
/**
* Socket UDP usado para la comunicación con el directorio
*/
private DatagramSocket socket;
/**
* Dirección de socket del directorio (IP:puertoUDP)
*/
private InetSocketAddress directoryAddress;
/**
* Nombre/IP del host donde se ejecuta el directorio
*/
private String directoryHostname;
public static class DownloadedFile {
public final String filename;
public final long filesize;
public final byte[] data;
public final String filehash;
public DownloadedFile(String filename, long fsize, byte[] data, String filehash) {
this.filename = filename;
this.filesize = fsize;
this.data = data;
this.filehash = filehash;
}
}
public DirectoryConnector(String hostname) throws IOException {
// Guardamos el string con el nombre/IP del host
directoryHostname = hostname;
/*
* DONE: (Boletín SocketsUDP) Convertir el string 'hostname' a InetAddress y
* guardar la dirección de socket (address:DIRECTORY_PORT) del directorio en el
* atributo directoryAddress, para poder enviar datagramas a dicho destino.
*/
/*
* DONE: (Boletín SocketsUDP) Crea el socket UDP en cualquier puerto para enviar
* datagramas al directorio
*/
directoryAddress = new InetSocketAddress(InetAddress.getByName(hostname), DIRECTORY_PORT);
socket = new DatagramSocket();
}
/**
* Método para enviar y recibir datagramas al/del directorio
*
* @param requestData los datos a enviar al directorio (mensaje de solicitud)
* @return los datos recibidos del directorio (mensaje de respuesta)
*/
private byte[] sendAndReceiveDatagrams(byte[] requestData) {
byte responseData[] = new byte[DirMessage.PACKET_MAX_SIZE];
byte response[] = null;
if (directoryAddress == null) {
System.err.println("DirectoryConnector.sendAndReceiveDatagrams: UDP server destination address is null!");
System.err.println(
"DirectoryConnector.sendAndReceiveDatagrams: make sure constructor initializes field \"directoryAddress\"");
System.exit(-1);
}
if (socket == null) {
System.err.println("DirectoryConnector.sendAndReceiveDatagrams: UDP socket is null!");
System.err.println(
"DirectoryConnector.sendAndReceiveDatagrams: make sure constructor initializes field \"socket\"");
System.exit(-1);
}
/*
* DONE¿?: (Boletín SocketsUDP) Enviar datos en un datagrama al directorio y
* recibir una respuesta. El array devuelto debe contener únicamente los datos
* recibidos, *NO* el búfer de recepción al completo.
*/
DatagramPacket pktToServer = new DatagramPacket(requestData, requestData.length, directoryAddress);
DatagramPacket pktFromServer = new DatagramPacket(responseData, responseData.length);
int intentos = 0;
boolean recibido = false;
//Hay que configurar el timeout antes del bucle
try {
socket.setSoTimeout(TIMEOUT);
} catch (SocketException e) {
System.err.println("* Error configurando el timeout del socket UDP: " + e.getMessage());
return null;
}
while (intentos < MAX_NUMBER_OF_ATTEMPTS && !recibido) {
try {
socket.send(pktToServer);
socket.receive(pktFromServer);
recibido = true;
//excepciones tratadas
} catch (SocketTimeoutException e){
intentos++;
System.err.println("* Timeout. Reintentando... (" + intentos + "/" + MAX_NUMBER_OF_ATTEMPTS + ")");
} catch (IOException e) {
System.err.println("* Error grave de I/O en UDP: " + e.getMessage());
System.exit(-1);
}
}
if (intentos == MAX_NUMBER_OF_ATTEMPTS) {
System.err.println("* El directorio no responde tras " + MAX_NUMBER_OF_ATTEMPTS + " intentos.");
System.exit(-1);
}
String servResp = new String(responseData, 0, pktFromServer.getLength());
response = servResp.getBytes();
System.out.println("Hemos recibido " + servResp);
/*
* done: (Boletín SocketsUDP) Una vez el envío y recepción asumiendo un canal
* confiable (sin pérdidas) esté terminado y probado, debe implementarse un
* mecanismo de retransmisión usando temporizador, en caso de que no se reciba
* respuesta en el plazo de TIMEOUT. En caso de salte el timeout, se debe volver
* a enviar el datagrama y tratar de recibir respuestas, reintentando como
* máximo en MAX_NUMBER_OF_ATTEMPTS ocasiones.
*/
/*
* DONE: (Boletín SocketsUDP) Las excepciones que puedan lanzarse al
* leer/escribir en el socket deben ser capturadas y tratadas en este método. Si
* se produce una excepción de entrada/salida (error del que no es posible
* recuperarse), se debe informar y terminar el programa.
*/
/*
* NOTA: Las excepciones deben tratarse de la más concreta a la más genérica.
* SocketTimeoutException es más concreta que IOException.
*/
if (response != null && response.length == responseData.length) {
System.err.println("Your response is as large as the datagram reception buffer!!\n"
+ "You must extract from the buffer only the bytes that belong to the datagram!");
}
return response;
}
/**
* Método para probar la comunicación con el directorio mediante el envío y
* recepción de mensajes sin formatear ("en crudo")
*
* @return verdadero si se ha enviado un datagrama y recibido una respuesta
*/
public boolean testSendAndReceive() {
/*
* DONE: (Boletín SocketsUDP) Probar el correcto funcionamiento de
* sendAndReceiveDatagrams. Se debe enviar un datagrama con la cadena "ping" y
* comprobar que la respuesta recibida empieza por "pingok". En tal caso,
* devuelve verdadero, falso si la respuesta no contiene los datos esperados.
*/
boolean success = false;
byte[] reqData = new String("ping").getBytes();
byte[] resp = sendAndReceiveDatagrams(reqData);
if (resp != null) {
String respStr = new String(resp, 0 , resp.length);
if (respStr.startsWith("pingok")) {
success = true;
}
}
return success;
}
public String getDirectoryHostname() {
return directoryHostname;
}
public String getClientAddress() {
/* try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
if (ni.isUp() && !ni.isVirtual() && !ni.isLoopback()) {
Enumeration<InetAddress> direccionesInterfaz = ni.getInetAddresses();
while (direccionesInterfaz.hasMoreElements()) {
InetAddress addr = direccionesInterfaz.nextElement();
if (addr instanceof java.net.Inet4Address) return addr.getHostAddress();
}
}
}
} catch (Exception e) { e.printStackTrace(); }
return null;
*/
// esta forma es una ruina
String ip = null;
try {
socket.connect(directoryAddress);
ip = socket.getLocalAddress().getHostAddress();
socket.disconnect();
} catch (Exception e) {}
return ip;
}
/**
* Método para "hacer ping" al directorio, comprobar que está operativo y que
* usa un protocolo compatible. Este método no usa mensajes bien formados.
*
* @return Verdadero si
*/
public boolean pingDirectoryRaw() {
boolean success = false;
byte[] reqData = new String("ping&" + NanoFiles.PROTOCOL_ID).getBytes();
byte[] resp = sendAndReceiveDatagrams(reqData);
if (resp != null) {
String recv = new String(resp, 0, resp.length);
if (recv.equals("welcome")) {
success = true;
}
}
/*
* done: (Boletín EstructuraNanoFiles) Basándose en el código de
* "testSendAndReceive", contactar con el directorio, enviándole nuestro
* PROTOCOL_ID (ver clase NanoFiles). Se deben usar mensajes "en crudo" (sin un
* formato bien definido) para la comunicación.
*
* PASOS: 1.Crear el mensaje a enviar (String "ping&protocolId"). 2.Crear un
* datagrama con los bytes en que se codifica la cadena : 4.Enviar datagrama y
* recibir una respuesta (sendAndReceiveDatagrams). : 5. Comprobar si la cadena
* recibida en el datagrama de respuesta es "welcome", imprimir si éxito o
* fracaso. 6.Devolver éxito/fracaso de la operación.
*/
return success;
}
/**
* Método para "hacer ping" al directorio, comprobar que está operativo y que es
* compatible.
*
* @return Verdadero si el directorio está operativo y es compatible
*/
public boolean pingDirectory() {
boolean success = false;
/*
* done: (Boletín MensajesASCII) Hacer ping al directorio 1.Crear el mensaje a
* enviar (objeto DirMessage) con atributos adecuados (operation, etc.) NOTA:
* Usar como operaciones las constantes definidas en la clase DirMessageOps :
* 2.Convertir el objeto DirMessage a enviar a un string (método toString)
* 3.Crear un datagrama con los bytes en que se codifica la cadena : 4.Enviar
* datagrama y recibir una respuesta (sendAndReceiveDatagrams). : 5.Convertir
* respuesta recibida en un objeto DirMessage (método DirMessage.fromString)
* 6.Extraer datos del objeto DirMessage y procesarlos 7.Devolver éxito/fracaso
* de la operación
*/
DirMessage ping = new DirMessage(DirMessageOps.OPERATION_PING);
String pingStr = ping.toString();
byte[] pingBytes = pingStr.getBytes();
byte[] resp = sendAndReceiveDatagrams(pingBytes);
String respStr = new String(resp, 0, resp.length);
DirMessage respPing = DirMessage.fromString(respStr);
System.out.println("pingDir - " + respPing.getOperation());
success = (respPing.getOperation().equals(DirMessageOps.OPERATION_PING_OK));
return success;
}
/**
* Método para dar de alta como servidor de ficheros en el puerto indicado.
*
* @param serverPort El puerto TCP en el que este peer sirve ficheros a otros
* @return Verdadero si el directorio tiene registrado a este peer como servidor
* y acepta la lista de ficheros, falso en caso contrario.
*/
public boolean registerFileServer(int serverPort) {
boolean success = false;
// TODO: Ver TODOs en pingDirectory y seguir esquema similar
/*
* done: (Boletín MensajesASCII) Hacer ping al directorio 1.Crear el mensaje a
* enviar (objeto DirMessage) con atributos adecuados (operation, etc.) NOTA:
* Usar como operaciones las constantes definidas en la clase DirMessageOps :
* 2.Convertir el objeto DirMessage a enviar a un string (método toString)
* 3.Crear un datagrama con los bytes en que se codifica la cadena : 4.Enviar
* datagrama y recibir una respuesta (sendAndReceiveDatagrams). : 5.Convertir
* respuesta recibida en un objeto DirMessage (método DirMessage.fromString)
* 6.Extraer datos del objeto DirMessage y procesarlos 7.Devolver éxito/fracaso
* de la operación
*/
// este trozo hay que cambiarlo porque qué cojones
// encima tiene algo mal
DirMessage serve = new DirMessage(DirMessageOps.OPERATION_SERVE, NanoFiles.peerNickname, this.getClientAddress(), serverPort);
byte[] serveBytes = serve.toString().getBytes();
byte[] response = sendAndReceiveDatagrams(serveBytes);
String respStr = new String(response, 0, response.length);
DirMessage respServe = DirMessage.fromString(respStr);
success = respServe.getOperation().equals(DirMessageOps.OPERATION_SERVE_OK);
return success;
}
/**
* Método para obtener la lista de ficheros alojados en el directorio. Para cada
* fichero se debe obtener un objeto FileInfo con nombre, tamaño y hash.
*
* @return Los ficheros disponibles en el directorio, o null si el directorio no
* pudo satisfacer nuestra solicitud
*/
public FileInfo[] getFileList() {
// tenemos que empezar a hacer con esta línea
DirMessage requestDirfiles = new DirMessage(DirMessageOps.OPERATION_REQUEST_DIRFILES);
byte[] requestBytes = requestDirfiles.toString().getBytes();
byte[] response = sendAndReceiveDatagrams(requestBytes);
String respStr = new String(response, 0, response.length);
DirMessage respDirfiles = DirMessage.fromString(respStr);
return respDirfiles.getFileList();
// return responseDirfiles.getFileList();
}
public Map<String, InetSocketAddress> getPeerList() {
// Map<String, InetSocketAddress> peers = new LinkedHashMap<String, InetSocketAddress>();
DirMessage requestPeers = new DirMessage(DirMessageOps.OPERATION_REQUEST_SERVER_PEERS);
byte[] requestBytes = requestPeers.toString().getBytes();
byte[] response = sendAndReceiveDatagrams(requestBytes);
String respStr = new String(response, 0, response.length);
DirMessage respPeers = DirMessage.fromString(respStr);
return respPeers.getPeers();
}
public Map<String, InetSocketAddress[]> searchFilesByHash(String hashSubstring) {
Map<String, InetSocketAddress[]> results = new LinkedHashMap<String, InetSocketAddress[]>();
Map<String, InetSocketAddress> peers = getPeerList();
InetSocketAddress miIP = peers.get(NanoFiles.peerNickname);
for (InetSocketAddress addr : peers.values()) {
if (miIP == addr) continue;
try {
NFConnector nfc = new NFConnector(addr);
FileInfo[] peerFiles = nfc.getFileList();
// la longitud tiene que ser EXACTAMENTE 1, si es 0 no hay, si es > 1 es ambiguo
// y si resulta que dos peers tienen un mismo subhash sin tener el mismo hash?
// mando que no se ha podido descargar? comparo contra el hash del primero?
FileInfo[] peerFilesFound = FileInfo.lookupHashSubstring(peerFiles, hashSubstring);
for (FileInfo fi : peerFilesFound) {
InetSocketAddress[] peersWithHash = results.getOrDefault(fi.fileHash, null);
if (peersWithHash == null) {
peersWithHash = new InetSocketAddress[1];
peersWithHash[0] = addr;
} else {
peersWithHash = Arrays.copyOf(peersWithHash, peersWithHash.length + 1);
peersWithHash[peersWithHash.length - 1] = addr;
}
results.put(fi.fileHash, peersWithHash);
}
} catch (IOException e) { e.printStackTrace(); }
}
return results;
}
public DownloadedFile downloadFileFromDirectory(String hashSubstring) {
byte[] fileData = null;
String filename = null;
long filesize = -1;
String filehash = null;
FileInfo[] list = this.getFileList();
FileInfo[] foundFile = FileInfo.lookupHashSubstring(list, hashSubstring);
// TODO: crear nuevo mensaje dirdl y requestdirdl
// similar a un mensaje peerdl
// pasarle hashSubstring a DirMessage y dejar que vaya desde ahí
// implementar toda la lógica en DirMessage
return new DownloadedFile(filename, filesize, fileData, filehash);
}
/**
* Método para darse de baja como servidor de ficheros.
*
* @return Verdadero si el directorio tiene registrado a este peer como servidor
* y ha dado de baja sus ficheros.
*/
public boolean unregisterFileServer() {
boolean success = false;
DirMessage unregisterServer = new DirMessage(DirMessageOps.OPERATION_STOP_SERVE, NanoFiles.peerNickname);
byte[] unregisterBytes = unregisterServer.toString().getBytes();
byte[] response = sendAndReceiveDatagrams(unregisterBytes);
String respStr = new String(response, 0, response.length);
DirMessage respUnregister = DirMessage.fromString(respStr);
success = respUnregister.getOperation().equals(DirMessageOps.OPERATION_STOP_SERVE_OK);
return success;
}
}