Ir al contenido
wannacode

Reservas de hotel

En resumen: Un proyecto de reservas de hotel: un servidor y muchos clientes reservan, consultan y anulan habitaciones a la vez, y cuando dos personas quieren la misma habitación en el mismo instante, solo una lo consigue.

Problema

Una habitación, dos confirmaciones

Un hotel pequeño tiene cuatro habitaciones. Las reservas llegan de todas partes: del sitio web, de la recepción, de las agencias de viajes. Un viernes por la tarde, dos agencias reservan la última habitación doble en el mismo segundo. Las dos reciben una confirmación. El sábado llegan dos familias a la habitación.

Vamos a construir un sistema en el que el servidor guarda las habitaciones y las reservas, y los clientes envían solicitudes por la red. Un cliente puede:

  • ver las habitaciones libres;
  • consultar una habitación;
  • reservar una habitación a su nombre;
  • anular su propia reserva.

Y cuando dos clientes reservan la misma habitación a la vez, solo uno de ellos lo consigue. Lo demostraremos con un programa que da la misma salida en cada ejecución.

Construimos de abajo arriba: el modelo sin red, las reglas y los errores, el protocolo, el servidor y el cliente, las solicitudes simultáneas y, al final, todo en un solo programa.

Más precisamente, para curiosos
El modelo es el mismo en los cuatro lenguajes del curso. En Python, como en Java, los clientes envían líneas de texto por un socket, y cada cliente está en su propio hilo. Un cerrojo protege las habitaciones: with self._lock.

La idea

La habitación

Una habitación tiene un número, un tipo, un precio por noche y un estado: libre u ocupada.

class Room {
private final int number;
private final String type;
private final int price;
private boolean free = true;
public Room(int number, String type, int price) {
this.number = number;
this.type = type;
this.price = price;
}
public int getNumber() {
return number;
}
public String getType() {
return type;
}
public int getPrice() {
return price;
}
public boolean isFree() {
return free;
}
public void occupy() {
free = false;
}
public void release() {
free = true;
}
@Override
public String toString() {
return number + ", " + type + ", " + price + " € por noche, " + (free ? "libre" : "ocupada");
}
}
  • El número, el tipo y el precio no cambian. Solo cambia el estado, y solo a través de dos métodos con nombres claros: occupy y release.
  • La habitación no sabe quién se aloja en ella. La habitación sale hacia cualquier cliente que pregunte, y el nombre del huésped no debe salir del hotel.
Más precisamente, para curiosos
Los campos empiezan por _: eso significa «no tocar desde fuera». Python no lo prohíbe, pero es la convención. El número, el tipo y el precio se leen con @property sin paréntesis (room.number), y cambiar uno desde fuera (room.number = 5) se detiene con AttributeError, porque la propiedad no tiene setter. El tipo de la habitación es kind, porque type es una función integrada de Python. Para más de dos estados se usa una enumeración: class RoomState(Enum) del módulo enum.

Míralo

El hotel y la reserva

El hotel guarda las habitaciones en una lista y los huéspedes en un diccionario «número de habitación → nombre»:
private final List<Room> rooms = new ArrayList<>();
private final Map<Integer, String> guests = new HashMap<>();
public void add(Room room) {
rooms.add(room);
}
public List<Room> freeRooms() {
return rooms.stream().filter(Room::isFree).toList();
}
public Room check(int number) {
return find(number);
}

_find es privado (con _, por convención): busca la habitación por número o lanza una excepción. Así nadie de fuera puede agarrar una habitación y ocuparla saltándose las reglas:

private Room find(int number) {
for (Room room : rooms) {
if (room.getNumber() == number) {
return room;
}
}
throw new RoomNotFoundException("No existe la habitación " + number + ".");
}

La reserva comprueba el nombre, busca la habitación y comprueba si está libre. Solo entonces la ocupa:

public void reserve(int number, String guest) {
if (guest.isBlank()) {
throw new HotelException("El nombre del huésped es obligatorio.");
}
Room room = find(number);
if (!room.isFree()) {
throw new RoomTakenException("La habitación " + number + " ya está ocupada.");
}
room.occupy();
guests.put(number, guest);
}
Hotel hotel = new Hotel();
hotel.add(new Room(101, "individual", 60));
hotel.add(new Room(102, "doble", 85));
hotel.add(new Room(201, "doble", 95));
hotel.add(new Room(202, "suite", 140));
for (Room room : hotel.freeRooms()) {
System.out.println(room);
}
hotel.reserve(102, "Ana Torres");
System.out.println("Después de la reserva: " + hotel.check(102));
System.out.println("Habitaciones libres: " + hotel.freeRooms().size());
Salida
101, individual, 60 € por noche, libre
102, doble, 85 € por noche, libre
201, doble, 95 € por noche, libre
202, suite, 140 € por noche, libre
Después de la reserva: 102, doble, 85 € por noche, ocupada
Habitaciones libres: 3

La idea

La anulación y las reglas

No toda solicitud se puede cumplir: la habitación está ocupada, no existe la habitación 305, un huésped anula la reserva de otro. Son rechazos según las reglas, así que son excepciones propias con un padre común:

class HotelException extends RuntimeException {
public HotelException(String message) {
super(message);
}
}
class RoomNotFoundException extends HotelException {
public RoomNotFoundException(String message) {
super(message);
}
}
class RoomTakenException extends HotelException {
public RoomTakenException(String message) {
super(message);
}
}

La anulación comprueba que la habitación esté ocupada y que la anule el mismo huésped:

public void cancel(int number, String guest) {
Room room = find(number);
if (room.isFree()) {
throw new HotelException("La habitación " + number + " no está reservada.");
}
if (!guests.get(number).equals(guest)) {
throw new HotelException("La habitación " + number + " está reservada por otro huésped.");
}
room.release();
guests.remove(number);
}

El enunciado del proyecto pide comprobar el sistema en distintos escenarios: reservar, consultar y anular, tanto cuando son válidos como cuando no lo son. Cada escenario es una línea de la salida:

Hotel hotel = new Hotel();
hotel.add(new Room(101, "individual", 60));
hotel.add(new Room(102, "doble", 85));
hotel.add(new Room(201, "doble", 95));
hotel.add(new Room(202, "suite", 140));
hotel.reserve(102, "Ana Torres");
System.out.println("1. Habitación libre: " + hotel.check(102));
try {
hotel.reserve(102, "Bruno Silva");
} catch (RoomTakenException e) {
System.out.println("2. Habitación ocupada: " + e.getMessage());
}
try {
hotel.cancel(102, "Bruno Silva");
} catch (HotelException e) {
System.out.println("3. Reserva ajena: " + e.getMessage());
}
hotel.cancel(102, "Ana Torres");
System.out.println("4. Anulación: " + hotel.check(102));
try {
hotel.cancel(102, "Ana Torres");
} catch (HotelException e) {
System.out.println("5. Segunda anulación: " + e.getMessage());
}
try {
hotel.reserve(305, "Ana Torres");
} catch (RoomNotFoundException e) {
System.out.println("6. No existe esa habitación: " + e.getMessage());
}
try {
hotel.reserve(201, " ");
} catch (HotelException e) {
System.out.println("7. Sin nombre: " + e.getMessage());
}
Salida
1. Habitación libre: 102, doble, 85 € por noche, ocupada
2. Habitación ocupada: La habitación 102 ya está ocupada.
3. Reserva ajena: La habitación 102 está reservada por otro huésped.
4. Anulación: 102, doble, 85 € por noche, libre
5. Segunda anulación: La habitación 102 no está reservada.
6. No existe esa habitación: No existe la habitación 305.
7. Sin nombre: El nombre del huésped es obligatorio.

La excepción concreta dice cuál es el problema. HotelError atrapa todos los rechazos de una vez: el servidor atrapará solo esa y la convertirá en una respuesta para el cliente.

Más precisamente, para curiosos
  • No hay excepciones comprobadas: ningún método declara qué lanza. Por eso la recepción las atrapará en un solo lugar.
  • Escenarios como estos son tests naturales. Con unittest cada uno se convierte en un método de un TestCase, y with self.assertRaises(RoomTakenError): alrededor de hotel.reserve(102, "Bruno Silva") comprueba la excepción.

Pruébalo

¿Quién está en la 201?

¿Qué imprimirá el programa?

hotel = Hotel()
hotel.add(Room(101, "individual", 60))
hotel.add(Room(102, "doble", 85))
hotel.add(Room(201, "doble", 95))
hotel.add(Room(202, "suite", 140))
hotel.reserve(101, "Ana Torres")
hotel.reserve(201, "Bruno Silva")
hotel.cancel(101, "Ana Torres")
try:
hotel.reserve(201, "Vera Gil")
print("Vera está en la 201")
except RoomTakenError:
print("Rechazo para Vera")
print(len(hotel.free_rooms()))
Respuesta

La idea

El protocolo: una línea «COMANDO:texto»

Cada mensaje es una línea COMANDO:texto, y el servidor responde con una línea:

El cliente envía El servidor responde
LIBRES OK: y las habitaciones libres, separadas por ;
CONSULTAR:102 OK:102, doble, 85 € por noche, ocupada
RESERVAR:102;Ana Torres OK:La habitación 102 es tuya, Ana Torres. o ERROR: y el motivo
ANULAR:102;Ana Torres OK:La reserva de la habitación 102 está anulada. o ERROR: y el motivo
SALIR ADIÓS, y el servidor cierra la conexión

Las palabras del protocolo son constantes en un solo lugar (en Python una constante es una convención: letras mayúsculas, y nadie la cambia):

class Protocol:
FREE = "LIBRES"
CHECK = "CONSULTAR"
RESERVE = "RESERVAR"
CANCEL = "ANULAR"
EXIT = "SALIR"
OK = "OK"
ERROR = "ERROR"
BYE = "ADIÓS"

La recepción traduce una línea del protocolo en una llamada al hotel y de vuelta:

class Reception:
def __init__(self, hotel):
self._hotel = hotel
...
def handle(self, line):
command, _, text = line.partition(":")
try:
match command:
case Protocol.FREE:
return f"{Protocol.OK}:{self._join(self._hotel.free_rooms())}"
case Protocol.CHECK:
return f"{Protocol.OK}:{self._hotel.check(self._number(text))}"
case Protocol.RESERVE:
return self._reserve(text)
case Protocol.CANCEL:
return self._cancel(text)
case _:
raise HotelError(f"Comando desconocido {command}.")
except HotelError as e:
return f"{Protocol.ERROR}:{e}"
def _reserve(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.reserve(number, guest)
return f"{Protocol.OK}:La habitación {number} es tuya, {guest}."
def _cancel(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.cancel(number, guest)
return f"{Protocol.OK}:La reserva de la habitación {number} está anulada."
@staticmethod
def _number_and_guest(text):
parts = text.split(";", 1)
if len(parts) != 2:
raise HotelError("Escribe número y nombre, por ejemplo «102;Ana Torres».")
return parts
@staticmethod
def _number(text):
try:
return int(text)
except ValueError:
raise HotelError("El número de la habitación es un número entero.")
@staticmethod
def _join(rooms):
if not rooms:
return "No hay habitaciones libres."
return "; ".join(str(room) for room in rooms)
line.partition(":") divide la línea en tres: el texto hasta el primer :, el propio : y el resto. Sin :, el comando es la línea entera y el texto está vacío. match compara el comando con cada constante, y case _ atrapa todo lo demás.

La recepción no recuerda nada del cliente: cada línea lleva todo lo que necesita, incluso el nombre del huésped. Por eso basta una recepción para todos los clientes. Puedes comprobarlo sin red:

hotel = Hotel()
hotel.add(Room(101, "individual", 60))
hotel.add(Room(102, "doble", 85))
hotel.add(Room(201, "doble", 95))
hotel.add(Room(202, "suite", 140))
reception = Reception(hotel)
lines = [
"RESERVAR:102;Ana Torres",
"CONSULTAR:102",
"RESERVAR:102;Bruno Silva",
"ANULAR:102",
"CONSULTAR:ciento dos",
"MOSTRAR",
]
for line in lines:
print(f"> {line}")
print(reception.handle(line))
Salida
> RESERVAR:102;Ana Torres
OK:La habitación 102 es tuya, Ana Torres.
> CONSULTAR:102
OK:102, doble, 85 € por noche, ocupada
> RESERVAR:102;Bruno Silva
ERROR:La habitación 102 ya está ocupada.
> ANULAR:102
ERROR:Escribe número y nombre, por ejemplo «102;Ana Torres».
> CONSULTAR:ciento dos
ERROR:El número de la habitación es un número entero.
> MOSTRAR
ERROR:Comando desconocido MOSTRAR.
Más precisamente, para curiosos
  • Los comandos podrían ser una enumeración: class Command(StrEnum) del módulo enum. Entonces son un tipo aparte con una lista cerrada de valores. En el curso son constantes, porque no hemos visto las enumeraciones.
  • El protocolo no tiene estado: cada línea vale por sí misma. Eso lo hace fácil de comprobar y fácil para atender a más clientes. Si el cliente tuviera que decir primero «soy Ana», haría falta una recepción por cliente, como la sesión del concesionario de coches.

Míralo

El servidor

Cada cliente conectado recibe su propio ClientHandler, y la recepción es compartida:

class ClientHandler:
def __init__(self, connection, reception):
self._connection = connection
self._reception = reception
def run(self):
try:
with (
self._connection,
self._connection.makefile("r", encoding="utf-8") as reader,
self._connection.makefile("w", encoding="utf-8") as writer,
):
for line in reader:
line = line.rstrip("\n")
if line == Protocol.EXIT:
break
print(self._reception.handle(line), file=writer, flush=True)
print(Protocol.BYE, file=writer, flush=True)
except OSError as e:
print(f"Se cortó la conexión con el cliente: {e}")

makefile convierte el socket en un archivo: las líneas se leen con for y se escriben con print(..., file=writer). flush=True envía la línea enseguida, no cuando se llena el búfer. with (...) cierra la conexión y los dos archivos al final, salga como salga el método.

HotelServer acepta a los clientes en su propio hilo y pasa cada uno a un grupo de 10 hilos (ThreadPoolExecutor):

class HotelServer:
def __init__(self, port, hotel):
self._server_socket = socket.create_server(("localhost", port))
self._pool = ThreadPoolExecutor(max_workers=10)
self._reception = Reception(hotel)
def start(self):
acceptor = threading.Thread(target=self._accept_all, daemon=True)
acceptor.start()
def _accept_all(self):
while True:
try:
client, _ = self._server_socket.accept()
except OSError:
return
self._pool.submit(ClientHandler(client, self._reception).run)
def stop(self):
self._server_socket.close()
self._pool.shutdown()
  • Se atienden diez clientes a la vez. El undécimo es aceptado y espera en la cola del grupo hasta que alguien se vaya.
  • Los diez hilos llaman al mismo Hotel. Así que el hotel tendrá que proteger sus habitaciones. Cómo, en un momento.
  • El hilo que acepta a los clientes es un daemon: no mantiene vivo el programa cuando todo lo demás ha terminado.

Míralo

El cliente

El cliente envía una línea y lee la respuesta. En Java send son dos métodos, con texto y sin texto. En Python es uno, y text=None significa que el texto se puede omitir. close() dice SALIR y cierra los dos archivos y la conexión: el socket solo se cierra de verdad cuando también se cierran los archivos de makefile.
class HotelClient:
def __init__(self, host, port):
self._connection = socket.create_connection((host, port))
self._in = self._connection.makefile("r", encoding="utf-8")
self._out = self._connection.makefile("w", encoding="utf-8")
def send(self, command, text=None):
line = command if text is None else f"{command}:{text}"
print(line, file=self._out, flush=True)
return self._in.readline().rstrip("\n")
def close(self):
self.send(Protocol.EXIT)
self._in.close()
self._out.close()
self._connection.close()

El servidor y el cliente en un solo programa:

hotel = Hotel()
hotel.add(Room(101, "individual", 60))
hotel.add(Room(102, "doble", 85))
hotel.add(Room(201, "doble", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5351, hotel)
server.start()
client = HotelClient("localhost", 5351)
print(client.send(Protocol.RESERVE, "102;Ana Torres"))
print(client.send(Protocol.CHECK, "102"))
print(client.send(Protocol.FREE))
print(client.send(Protocol.CANCEL, "102;Ana Torres"))
print(client.send(Protocol.CHECK, "305"))
client.close()
server.stop()
Salida
OK:La habitación 102 es tuya, Ana Torres.
OK:102, doble, 85 € por noche, ocupada
OK:101, individual, 60 € por noche, libre; 201, doble, 95 € por noche, libre; 202, suite, 140 € por noche, libre
OK:La reserva de la habitación 102 está anulada.
ERROR:No existe la habitación 305.

La idea

Dos para una habitación

Una reserva son dos pasos: comprobar que la habitación está libre y luego ocuparla. Si dos solicitudes se ejecutan a la vez, puede pasar esto:
Agencia A Agencia B
1 comprueba: la 102 está libre
2 comprueba: la 102 está libre
3 ocupa la 102 para su huésped
4 ocupa la 102 para su huésped y anota a ese huésped

Las dos reciben una confirmación. No hay ningún error en ninguna línea de código por separado. El error es que los pasos de las dos solicitudes se intercalan.

La solución: solo una solicitud a la vez entra en el hotel. Entonces «comprobar y ocupar» es atómico: nadie puede meterse entre la comprobación y la ocupación de la habitación. En Python lo hace un cerrojo en el hotel, threading.RLock(), con with self._lock: alrededor del cuerpo de cada método público:
def __init__(self):
self._rooms = []
self._guests = {}
self._lock = threading.RLock()
def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("El nombre del huésped es obligatorio.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"La habitación {number} ya está ocupada.")
room.occupy()
self._guests[number] = guest

def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("El nombre del huésped es obligatorio.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"La habitación {number} ya está ocupada.")
room.occupy()
self._guests[number] = guest
Paso a paso, en palabras
  1. La habitación 102 está libre. Ana Torres y Bruno Silva la reservan en el mismo instante.
  2. Primero sin with self._lock. Este es un orden posible: la solicitud de Ana comprueba y ve «libre».
  3. La solicitud de Bruno comprueba antes de que Ana haya ocupado la habitación: también ve «libre».
  4. Ana ocupa la habitación y anota su nombre.
  5. Bruno también ocupa la habitación y anota su nombre. Los dos tienen una confirmación para la misma habitación.
  6. Desde el principio, con with self._lock: en el hotel entra solo un hilo a la vez.
  7. Digamos que Ana es la primera: toma el cerrojo. La comprobación y la ocupación son un solo paso, y nadie se mete entre ellas.
  8. Bruno llega al hotel y espera hasta que el cerrojo quede libre.
  9. Bruno tiene el cerrojo. Ahora la comprobación ve «ocupada».
  10. RoomTakenError. La recepción la devuelve como una línea de error, y Bruno puede pedir otra habitación.
  11. El programa de la lección cuenta las que tienen éxito: 1 por habitación. Quién fue primero cambia en cada ejecución.

La prueba: dos clientes reservan la 102 a la vez, y luego veinte agencias reservan la 201 a la vez. Se recogen las respuestas, y el programa imprime cuántas tuvieron éxito:

def reserve(port, room, guest):
try:
client = HotelClient("localhost", port)
reply = client.send(Protocol.RESERVE, f"{room};{guest}")
client.close()
return reply
except OSError as e:
return f"{Protocol.ERROR}:{e}"
def race(port, room, guests):
with ThreadPoolExecutor(max_workers=len(guests)) as clients:
replies = list(clients.map(lambda guest: reserve(port, room, guest), guests))
reserved = 0
for reply in replies:
if reply.startswith(Protocol.OK):
reserved += 1
return reserved
hotel = Hotel()
hotel.add(Room(101, "individual", 60))
hotel.add(Room(102, "doble", 85))
hotel.add(Room(201, "doble", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5352, hotel)
server.start()
two = ["Ana Torres", "Bruno Silva"]
print(f"Dos para la habitación 102, con éxito: {race(5352, 102, two)}")
agencies = [f"Agencia {i + 1}" for i in range(20)]
print(f"Veinte para la habitación 201, con éxito: {race(5352, 201, agencies)}")
desk = HotelClient("localhost", 5352)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()
Salida
Dos para la habitación 102, con éxito: 1
Veinte para la habitación 201, con éxito: 1
OK:101, individual, 60 € por noche, libre; 202, suite, 140 € por noche, libre
El grupo tiene tantos hilos como huéspedes, así que todas las solicitudes arrancan a la vez. map espera las respuestas y las devuelve en el orden de los huéspedes, y race cuenta las que tuvieron éxito.

Quién se quedó con la habitación cambia en cada ejecución, así que el programa no lo imprime. Cuántas tuvieron éxito es siempre lo mismo: una por habitación.

Más precisamente, para curiosos
  • Sin el bloqueo, el programa casi siempre diría 1 de todos modos: la ventana entre la comprobación y la ocupación de la habitación es pequeña. Pero «casi siempre» significa que una vez entre miles de reservas llegan dos huéspedes a la habitación. Un error así no se atrapa ejecutando el programa, sino pensando: qué datos son compartidos y quién los cambia al mismo tiempo. El GIL de Python no protege «comprobar y ocupar»: el hilo puede cambiarse entre dos líneas. Si entre la comprobación y la ocupación hay algo lento, como una consulta a una base de datos, el otro hilo se mete casi con seguridad.
  • self._lock bloquea todo el hotel. Mientras un hilo reserva la 102, otro espera incluso para la 201. Con cuatro habitaciones está bien. Con miles se bloquea por habitación, con un cerrojo para cada una. El cerrojo es un RLock porque el hilo que lo tiene puede volver a tomarlo. El ejercicio lo necesita: el cambio de habitación llama a cancel y a reserve, que también bloquean. Con un Lock normal el segundo intento esperaría para siempre: el hilo se estaría esperando a sí mismo.
  • En un sistema real las habitaciones están en una base de datos, y la protección ocurre allí: una transacción o una condición en la propia consulta (UPDATE rooms SET free = false WHERE number = 102 AND free = true). Entonces se cumple incluso con varios servidores, no solo dentro de un programa.

Pruébalo

Bloqueado, pero no del todo

Un compañero dividió la reserva en dos métodos, los dos con with self._lock. La recepción llama primero a la comprobación y luego a la ocupación. Con muchas solicitudes a la vez, a veces dos personas siguen consiguiendo la misma habitación. ¿En qué línea está el error?

Elige la línea con el error.

Míralo

El hotel completo

Los escenarios de prueba del enunciado, esta vez por la red, con dos clientes conectados a la vez. Al final dos personas reservan la 201 en el mismo instante:

def say(who, client, command, text):
reply = client.send(command, text)
print(f"{who} > {command}:{text}")
print(f"{who} < {reply}")
hotel = Hotel()
hotel.add(Room(101, "individual", 60))
hotel.add(Room(102, "doble", 85))
hotel.add(Room(201, "doble", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5353, hotel)
server.start()
anna = HotelClient("localhost", 5353)
boris = HotelClient("localhost", 5353)
say("Ana", anna, Protocol.RESERVE, "102;Ana Torres")
say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva")
say("Bruno", boris, Protocol.CHECK, "102")
say("Bruno", boris, Protocol.CANCEL, "102;Bruno Silva")
say("Ana", anna, Protocol.CANCEL, "102;Ana Torres")
say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva")
say("Ana", anna, Protocol.CHECK, "305")
anna.close()
boris.close()
two = ["Ana Torres", "Vera Gil"]
print(f"Dos para la habitación 201 a la vez, con éxito: {race(5353, 201, two)}")
desk = HotelClient("localhost", 5353)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()
Salida
Ana > RESERVAR:102;Ana Torres
Ana < OK:La habitación 102 es tuya, Ana Torres.
Bruno > RESERVAR:102;Bruno Silva
Bruno < ERROR:La habitación 102 ya está ocupada.
Bruno > CONSULTAR:102
Bruno < OK:102, doble, 85 € por noche, ocupada
Bruno > ANULAR:102;Bruno Silva
Bruno < ERROR:La habitación 102 está reservada por otro huésped.
Ana > ANULAR:102;Ana Torres
Ana < OK:La reserva de la habitación 102 está anulada.
Bruno > RESERVAR:102;Bruno Silva
Bruno < OK:La habitación 102 es tuya, Bruno Silva.
Ana > CONSULTAR:305
Ana < ERROR:No existe la habitación 305.
Dos para la habitación 201 a la vez, con éxito: 1
OK:101, individual, 60 € por noche, libre; 202, suite, 140 € por noche, libre
Más precisamente, para curiosos

El programa completo, de principio a fin:

hotel.py
import socket
import threading
from concurrent.futures import ThreadPoolExecutor
class Room:
def __init__(self, number, kind, price):
self._number = number
self._kind = kind
self._price = price
self._free = True
@property
def number(self):
return self._number
@property
def kind(self):
return self._kind
@property
def price(self):
return self._price
@property
def is_free(self):
return self._free
def occupy(self):
self._free = False
def release(self):
self._free = True
def __str__(self):
state = "libre" if self._free else "ocupada"
return f"{self._number}, {self._kind}, {self._price} € por noche, {state}"
class Hotel:
def __init__(self):
self._rooms = []
self._guests = {}
self._lock = threading.RLock()
def add(self, room):
with self._lock:
self._rooms.append(room)
def free_rooms(self):
with self._lock:
return [room for room in self._rooms if room.is_free]
def check(self, number):
with self._lock:
return self._find(number)
def reserve(self, number, guest):
with self._lock:
if not guest.strip():
raise HotelError("El nombre del huésped es obligatorio.")
room = self._find(number)
if not room.is_free:
raise RoomTakenError(f"La habitación {number} ya está ocupada.")
room.occupy()
self._guests[number] = guest
def cancel(self, number, guest):
with self._lock:
room = self._find(number)
if room.is_free:
raise HotelError(f"La habitación {number} no está reservada.")
if self._guests[number] != guest:
raise HotelError(f"La habitación {number} está reservada por otro huésped.")
room.release()
del self._guests[number]
def _find(self, number):
for room in self._rooms:
if room.number == number:
return room
raise RoomNotFoundError(f"No existe la habitación {number}.")
class HotelError(Exception):
pass
class RoomNotFoundError(HotelError):
pass
class RoomTakenError(HotelError):
pass
class Protocol:
FREE = "LIBRES"
CHECK = "CONSULTAR"
RESERVE = "RESERVAR"
CANCEL = "ANULAR"
EXIT = "SALIR"
OK = "OK"
ERROR = "ERROR"
BYE = "ADIÓS"
class Reception:
def __init__(self, hotel):
self._hotel = hotel
def handle(self, line):
command, _, text = line.partition(":")
try:
match command:
case Protocol.FREE:
return f"{Protocol.OK}:{self._join(self._hotel.free_rooms())}"
case Protocol.CHECK:
return f"{Protocol.OK}:{self._hotel.check(self._number(text))}"
case Protocol.RESERVE:
return self._reserve(text)
case Protocol.CANCEL:
return self._cancel(text)
case _:
raise HotelError(f"Comando desconocido {command}.")
except HotelError as e:
return f"{Protocol.ERROR}:{e}"
def _reserve(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.reserve(number, guest)
return f"{Protocol.OK}:La habitación {number} es tuya, {guest}."
def _cancel(self, text):
number_text, guest = self._number_and_guest(text)
number = self._number(number_text)
self._hotel.cancel(number, guest)
return f"{Protocol.OK}:La reserva de la habitación {number} está anulada."
@staticmethod
def _number_and_guest(text):
parts = text.split(";", 1)
if len(parts) != 2:
raise HotelError("Escribe número y nombre, por ejemplo «102;Ana Torres».")
return parts
@staticmethod
def _number(text):
try:
return int(text)
except ValueError:
raise HotelError("El número de la habitación es un número entero.")
@staticmethod
def _join(rooms):
if not rooms:
return "No hay habitaciones libres."
return "; ".join(str(room) for room in rooms)
class ClientHandler:
def __init__(self, connection, reception):
self._connection = connection
self._reception = reception
def run(self):
try:
with (
self._connection,
self._connection.makefile("r", encoding="utf-8") as reader,
self._connection.makefile("w", encoding="utf-8") as writer,
):
for line in reader:
line = line.rstrip("\n")
if line == Protocol.EXIT:
break
print(self._reception.handle(line), file=writer, flush=True)
print(Protocol.BYE, file=writer, flush=True)
except OSError as e:
print(f"Se cortó la conexión con el cliente: {e}")
class HotelServer:
def __init__(self, port, hotel):
self._server_socket = socket.create_server(("localhost", port))
self._pool = ThreadPoolExecutor(max_workers=10)
self._reception = Reception(hotel)
def start(self):
acceptor = threading.Thread(target=self._accept_all, daemon=True)
acceptor.start()
def _accept_all(self):
while True:
try:
client, _ = self._server_socket.accept()
except OSError:
return
self._pool.submit(ClientHandler(client, self._reception).run)
def stop(self):
self._server_socket.close()
self._pool.shutdown()
class HotelClient:
def __init__(self, host, port):
self._connection = socket.create_connection((host, port))
self._in = self._connection.makefile("r", encoding="utf-8")
self._out = self._connection.makefile("w", encoding="utf-8")
def send(self, command, text=None):
line = command if text is None else f"{command}:{text}"
print(line, file=self._out, flush=True)
return self._in.readline().rstrip("\n")
def close(self):
self.send(Protocol.EXIT)
self._in.close()
self._out.close()
self._connection.close()
def say(who, client, command, text):
reply = client.send(command, text)
print(f"{who} > {command}:{text}")
print(f"{who} < {reply}")
def reserve(port, room, guest):
try:
client = HotelClient("localhost", port)
reply = client.send(Protocol.RESERVE, f"{room};{guest}")
client.close()
return reply
except OSError as e:
return f"{Protocol.ERROR}:{e}"
def race(port, room, guests):
with ThreadPoolExecutor(max_workers=len(guests)) as clients:
replies = list(clients.map(lambda guest: reserve(port, room, guest), guests))
reserved = 0
for reply in replies:
if reply.startswith(Protocol.OK):
reserved += 1
return reserved
hotel = Hotel()
hotel.add(Room(101, "individual", 60))
hotel.add(Room(102, "doble", 85))
hotel.add(Room(201, "doble", 95))
hotel.add(Room(202, "suite", 140))
server = HotelServer(5353, hotel)
server.start()
anna = HotelClient("localhost", 5353)
boris = HotelClient("localhost", 5353)
say("Ana", anna, Protocol.RESERVE, "102;Ana Torres")
say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva")
say("Bruno", boris, Protocol.CHECK, "102")
say("Bruno", boris, Protocol.CANCEL, "102;Bruno Silva")
say("Ana", anna, Protocol.CANCEL, "102;Ana Torres")
say("Bruno", boris, Protocol.RESERVE, "102;Bruno Silva")
say("Ana", anna, Protocol.CHECK, "305")
anna.close()
boris.close()
two = ["Ana Torres", "Vera Gil"]
print(f"Dos para la habitación 201 a la vez, con éxito: {race(5353, 201, two)}")
desk = HotelClient("localhost", 5353)
print(desk.send(Protocol.FREE))
desk.close()
server.stop()

Ejercicio

Sigue por tu cuenta

Trabaja con el programa completo de la tarjeta anterior.

1. Cambio de habitación. El huésped se muda de una habitación a otra con MOVER:102;202;Ana Torres. La respuesta es OK:Ana Torres está en la habitación 202. Si la habitación nueva está ocupada, el huésped se queda en la antigua. El cambio es un solo método en Hotel, dentro de with self._lock: nadie debe ocupar la habitación nueva entre la liberación de la antigua y la ocupación de la nueva.

Salida
OK:La habitación 102 es tuya, Ana Torres.
OK:La habitación 201 es tuya, Bruno Silva.
ERROR:La habitación 201 ya está ocupada.
OK:Ana Torres está en la habitación 202.
OK:101, individual, 60 € por noche, libre; 102, doble, 85 € por noche, libre

Más ideas

  • Noches y total. Una reserva es para un número de noches, y la confirmación dice el total: «3 noches, 255 €».
  • Fechas. Una habitación se reserva por días, por ejemplo del 14 al 16 de diciembre. Dos reservas de la misma habitación no deben solaparse. Si la habitación está libre depende de las fechas.
  • Mis reservas. MIAS:Ana Torres dice qué habitaciones son de este huésped.
  • Lista de espera. Si la habitación está ocupada, el huésped se apunta para esperar. Cuando se anula, pasa al primero de la lista.
  • Un cliente desde el teclado. Un segundo programa lee comandos del teclado con input() y los envía al servidor, que se ejecuta en otra terminal.

Test

Ponte a prueba

1. Dos agencias reservan la habitación 102 en el mismo instante, y la reserva no está sincronizada. ¿Qué puede pasar?
2. La comprobación «¿está libre?» y la ocupación de la habitación están en dos métodos sincronizados distintos. ¿Basta con eso?
3. La habitación 102 está ocupada. ¿Qué recibe el cliente que envía RESERVAR:102;Bruno Silva?
4. ¿Por qué el programa imprime cuántas reservas tuvieron éxito y no quién se quedó con la habitación?
5. ¿Por qué los nombres de los huéspedes están en un diccionario del hotel y no en la habitación?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·