package com.rzg.zombieland.comunes.comunicacion;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.rzg.zombieland.comunes.controlador.Controlador;
import com.rzg.zombieland.comunes.controlador.Controlador.ComandoDesconocidoException;
import com.rzg.zombieland.comunes.controlador.ControladorFactory;
import com.rzg.zombieland.comunes.misc.Log;
import com.rzg.zombieland.comunes.misc.ZombielandException;
/**
* Clase que se ocupa de la comunicaci�n con un cliente en particular.
* Una vez que se arranca el hilo, se escucha constantemente. Enviar una petici�n desde otro hilo
* es thread-safe (bueno, supuestamente). Ver comentarios en implementaci�n para conocer detalles
* del protocolo.
* @author nicolas
*
*/
public class HiloEscucha extends Thread implements EnviaPeticiones {
// El socket sobre el que se escucha al cliente.
private Socket socket;
// Indica si el hilo se est� ejecutando.
private boolean corriendo;
// F�brica de controladores.
private ControladorFactory controladorFactory;
// Mapea las peticiones para su respuesta.
private Map<UUID, Peticion<?, ?>> mapaPeticiones;
// Lista a la que se le avisa cuando ocurren distintos eventos en el hilo de escucha.
private List<HiloListener> listeners;
// True si se debe notificar a los listeners del corte de la conexi�n cuando se cierra.
private boolean notificarAlCierre;
/**
* Comienza a escuchar en el socket dado, delegando las peticiones a la f�brica de
* controladores. El socket ya debe estar abierto.
* @param socket - el socket con el que se escuchar� al cliente.
* @param controladorFactory - f�brica de controladores inyectada.
*/
public HiloEscucha(Socket socket, ControladorFactory controladorFactory) {
super("HiloEscucha: " + socket.getInetAddress());
corriendo = true;
notificarAlCierre = true;
Log.debug("Estableciendo nueva conexi�n con " + socket.getInetAddress());
this.socket = socket;
this.controladorFactory = controladorFactory;
mapaPeticiones = new HashMap<UUID, Peticion<?, ?>>();
this.listeners = new ArrayList<HiloListener>();
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// Obtengo un reader de entrada.
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()))) {
while (corriendo) {
// El hilo se detendr� ac� hasta que el otro extremo env�e un c�digo de petici�n.
int codigo = in.read();
// Este bloque es sincronizado para que si otro hilo intenta enviar una
// respuesta sobre esta instancia de escucha mientras nosotros estamos
// escribiendo en el socket, los mensajes no se intercalen y rompan el
// protocolo.
synchronized (this) {
Log.debug("Recibiendo datos en HiloEscucha " + this + ". C�digo: " + codigo);
// -1 indica que el otro extremo cerr� la conexi�n.
if (codigo == -1) {
Log.debug("Cerrando hilo escucha: lleg� el -1");
if (notificarAlCierre) {
Log.debug("Notificando listeners");
notificarCierre();
}
socket.close();
return;
}
// Si llegamos ac�, la petici�n no viene de una respuesta. Obtenemos el ID
// generado por el cliente de la petici�n para que la pueda identificar.
String uuid = in.readLine();
String contenido = in.readLine();
// Si el servidor env�a una respuesta, resolvemos la petici�n que ten�amos
// relacionada. Primero buscamos la buscamos en el mapa de peticiones de
// acuerdo al ID que nos envi� el otro extremo, luego la quitamos y le enviamos
// la respuesta para que la procese.
if (codigo == Enviable.RESPUESTA) {
Log.debug("Procesando respuesta:");
Log.debug(contenido);
mapaPeticiones.remove(UUID.fromString(uuid)).
procesarRespuesta(contenido);
continue;
}
// Obtengo un controlador que es quien tomar� las acciones apropiadas para
// la petici�n, como mover a un personaje o registrar un jugador.
Log.debug("Contenido:");
// La pr�xima l�nea es el cuerpo de la petici�n. Su formato var�a de
// acuerdo al tipo de petici�n y lo sabe manejar el controlador.
Log.debug(contenido);
// Obtenemos la respuesta a partir del controlador.
String respuesta;
try {
Controlador controlador = controladorFactory.crear(codigo);
respuesta = controlador.procesar(contenido);
} catch (ComandoDesconocidoException e) {
// Esto no deber�a suceder jam�s de los jamases, a menos que se corte la
// conexi�n en el medio del env�o del c�digo de comando... en cualquier caso,
// debemos poder manejar el error.
Log.error("Se envi� un comando desconocido: " + e.getMessage());
e.printStackTrace();
respuesta = Enviable.LINEA_ERROR;
}
Log.debug("Respuesta:");
Log.debug(respuesta);
// En esta secci�n se puede ver el protocolo de env�o de respuestas.
// Escribimos el c�digo de las respuestas para que el cliente pueda
// identificar el mensaje apropiadamente.
// Escribimos la respuesta en el b�fer de salida...
out.write(Enviable.RESPUESTA);
// Escribimos el mismo UUID que nos dio el otro extremo.
out.println(uuid);
out.println(respuesta);
// ...y nos aseguramos de que se limpie de una vez para evitar deadlocks
// en los tests.
out.flush();
}
}
// Ya no estamos m�s corriendo. Cerramos el socket y nos vamos a dormir.
socket.close();
} catch (SocketException e) {
Log.info("Cerrando hilo de escucha " + getName() + ":");
Log.info("Motivo: " + e.getMessage());
if (notificarAlCierre)
notificarCierre();
return;
} catch (IOException e) {
Log.error("Error en hilo de escucha " + getName() + ":");
Log.error(e.getMessage());
e.printStackTrace();
}
}
/**
* Env�a la petici�n al otro lado de la conexi�n.
* @param peticion
* @throws ZombielandException
*/
@Override
public void enviarPeticion(Peticion<?, ?> peticion) throws ZombielandException {
try {
// Antes de enviar la petici�n, la almacenamos en un mapa identificada por un ID
// generado aleatoriamente para poder identificar su respuesta. Es importante notar que
// la vuelta de la petici�n desde el otro extremo se maneja en otro hilo, por lo que la
// respuesta debe resolverse as�ncronamete mediante una promesa.
// Ver comentarios en run() sobre el synchronized.
synchronized (this) {
mapaPeticiones.put(peticion.getID(), peticion);
PrintWriter salida = new PrintWriter(socket.getOutputStream());
Log.debug("Enviando petici�n " + peticion.getCodigoPeticion());
// El protocolo de env�o de peticiones es sencillo: c�digo, ID, contenido.
salida.write(peticion.getCodigoPeticion());
salida.println(peticion.getID().toString());
salida.println(peticion.getMensajePeticion());
salida.flush();
}
} catch (IOException e) {
Log.error("Error de conexi�n:");
Log.error(e.getMessage());
e.printStackTrace();
throw new ZombielandException("No se pudo comunicar con el otro extremo al enviar una petici�n");
}
}
/**
* Cierra el hilo de escucha.
* @param notificar - true si debe notificar a los escuchadores, false de lo contrario.
*/
public void cerrar(boolean notificar) {
corriendo = false;
notificarAlCierre = notificar;
try {
socket.close();
} catch (IOException e) {
}
}
private void notificarCierre() {
synchronized (listeners) {
for (HiloListener listener : listeners)
listener.hiloCerrado(this);
}
}
public void addListener(HiloListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
/**
* @return la f�brica de controladores.
*/
public ControladorFactory getFactory() {
return controladorFactory;
}
}