Хотелски резервации
Проблем
Една стая, две потвърждения
Малък хотел има четири стаи. Резервации идват отвсякъде: от сайта, от рецепцията, от туристически агенции. Една петъчна вечер две агенции резервират последната двойна стая в една и съща секунда. И двете получават потвърждение. В събота в стаята пристигат две семейства.
Ще направим система, в която сървърът пази стаите и резервациите, а клиентите пращат заявки по мрежата. Клиентът може:
- да види свободните стаи;
- да провери една стая;
- да резервира стая на свое име;
- да анулира своята резервация.
А когато двама клиенти резервират една стая едновременно, само единият успява. Това ще докажем с програма, която дава един и същ изход при всяко пускане.
Строим отдолу нагоре: моделът без мрежа, правилата и грешките, протоколът, сървърът и клиентът, едновременните заявки и накрая всичко в една програма.
По-точно, за любопитните
with self._lock.Идеята
Стаята
Стаята има номер, тип, цена за нощ и състояние: свободна или заета.
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 + " € на нощ, " + (free ? "свободна" : "заета"); }}public class Room{ public int Number { get; } public string Type { get; } public int Price { get; } public bool IsFree { get; set; } = true;
public Room(int number, string type, int price) { Number = number; Type = type; Price = price; }
public void Occupy() { IsFree = false; }
public void Release() { IsFree = true; }
public override string ToString() { return Number + ", " + Type + ", " + Price + " € на нощ, " + (IsFree ? "свободна" : "заета"); }}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 = "свободна" if self._free else "заета" return f"{self._number}, {self._kind}, {self._price} € на нощ, {state}"class Room { #number; #type; #price; #free = true;
constructor(number, type, price) { this.#number = number; this.#type = type; this.#price = price; }
get number() { return this.#number; }
get type() { return this.#type; }
get price() { return this.#price; }
get isFree() { return this.#free; }
occupy() { this.#free = false; }
release() { this.#free = true; }
toString() { const state = this.#free ? "свободна" : "заета"; return `${this.#number}, ${this.#type}, ${this.#price} € на нощ, ${state}`; }}- Номерът, типът и цената не се менят. Мени се само състоянието, и то с два метода с ясни имена:
occupyиrelease. - Кой е отседнал, стаята не знае. Стаята стига до всеки клиент, който пита, а името на госта не бива да излиза от хотела.
По-точно, за любопитните
_: това значи „не пипай отвън“. Python не го забранява, но така се пише. Номерът, типът и цената се четат през @property без скоби (room.number), а смяна отвън (room.number = 5) спира с AttributeError, защото свойството няма setter. Типът на стаята е kind, защото type е вградена функция на Python. За повече от две състояния се ползва енумерация: class RoomState(Enum) от модула enum.Виж го
Хотелът и резервацията
private final List<Room> rooms = new ArrayList<>();private final Map<Integer, String> guests = new HashMap<>();private readonly List<Room> rooms = new List<Room>();private readonly Dictionary<int, string> guests = new Dictionary<int, string>();def __init__(self): self._rooms = [] self._guests = {}#rooms = [];#guests = new Map();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);}public void Add(Room room){ rooms.Add(room);}
public List<Room> FreeRooms(){ return rooms.Where(room => room.IsFree).ToList();}
public Room Check(int number){ return Find(number);}def add(self, room): self._rooms.append(room)
def free_rooms(self): return [room for room in self._rooms if room.is_free]
def check(self, number): return self._find(number)add(room) { this.#rooms.push(room);}
freeRooms() { return this.#rooms.filter((room) => room.isFree);}
check(number) { return this.#find(number);}_find е частен (с _, по договорка): търси стаята по номер или хвърля изключение. Така никой отвън не може да вземе стая и да я заеме покрай правилата:
private Room find(int number) { for (Room room : rooms) { if (room.getNumber() == number) { return room; } } throw new RoomNotFoundException("Няма стая " + number + ".");}private Room Find(int number){ Room room = rooms.FirstOrDefault(r => r.Number == number); if (room == null) { throw new RoomNotFoundException("Няма стая " + number + "."); } return room;}def _find(self, number): for room in self._rooms: if room.number == number: return room raise RoomNotFoundError(f"Няма стая {number}.")#find(number) { const room = this.#rooms.find((r) => r.number === number); if (room === undefined) { throw new RoomNotFoundError(`Няма стая ${number}.`); } return room;}Резервацията проверява името, намира стаята и проверява дали е свободна. Едва тогава я заема:
public void reserve(int number, String guest) { if (guest.isBlank()) { throw new HotelException("Името на госта е задължително."); } Room room = find(number); if (!room.isFree()) { throw new RoomTakenException("Стая " + number + " вече е заета."); } room.occupy(); guests.put(number, guest);}public void Reserve(int number, string guest){ if (string.IsNullOrWhiteSpace(guest)) { throw new HotelException("Името на госта е задължително."); } Room room = Find(number); if (!room.IsFree) { throw new RoomTakenException("Стая " + number + " вече е заета."); } room.Occupy(); guests[number] = guest;}def reserve(self, number, guest): if not guest.strip(): raise HotelError("Името на госта е задължително.") room = self._find(number) if not room.is_free: raise RoomTakenError(f"Стая {number} вече е заета.") room.occupy() self._guests[number] = guestreserve(number, guest) { if (guest.trim() === "") { throw new HotelError("Името на госта е задължително."); } const room = this.#find(number); if (!room.isFree) { throw new RoomTakenError(`Стая ${number} вече е заета.`); } room.occupy(); this.#guests.set(number, guest);}Hotel hotel = new Hotel();hotel.add(new Room(101, "единична", 60));hotel.add(new Room(102, "двойна", 85));hotel.add(new Room(201, "двойна", 95));hotel.add(new Room(202, "апартамент", 140));
for (Room room : hotel.freeRooms()) { System.out.println(room);}hotel.reserve(102, "Анна Петрова");System.out.println("След резервацията: " + hotel.check(102));System.out.println("Свободни стаи: " + hotel.freeRooms().size());101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3Hotel hotel = new Hotel();hotel.Add(new Room(101, "единична", 60));hotel.Add(new Room(102, "двойна", 85));hotel.Add(new Room(201, "двойна", 95));hotel.Add(new Room(202, "апартамент", 140));
foreach (Room room in hotel.FreeRooms()){ Console.WriteLine(room);}hotel.Reserve(102, "Анна Петрова");Console.WriteLine("След резервацията: " + hotel.Check(102));Console.WriteLine("Свободни стаи: " + hotel.FreeRooms().Count);101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))
for room in hotel.free_rooms(): print(room)hotel.reserve(102, "Анна Петрова")print(f"След резервацията: {hotel.check(102)}")print(f"Свободни стаи: {len(hotel.free_rooms())}")101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3const hotel = new Hotel();hotel.add(new Room(101, "единична", 60));hotel.add(new Room(102, "двойна", 85));hotel.add(new Room(201, "двойна", 95));hotel.add(new Room(202, "апартамент", 140));
for (const room of hotel.freeRooms()) { console.log(String(room));}hotel.reserve(102, "Анна Петрова");console.log(`След резервацията: ${hotel.check(102)}`);console.log(`Свободни стаи: ${hotel.freeRooms().length}`);101, единична, 60 € на нощ, свободна102, двойна, 85 € на нощ, свободна201, двойна, 95 € на нощ, свободна202, апартамент, 140 € на нощ, свободнаСлед резервацията: 102, двойна, 85 € на нощ, заетаСвободни стаи: 3Идеята
Анулиране и правилата
Не всяка заявка е изпълнима: стаята е заета, номер 305 няма, гостът анулира чужда резервация. Това са откази по правилата, затова са свои изключения с общ родител:
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); }}public class HotelException : Exception{ public HotelException(string message) : base(message) { }}
public class RoomNotFoundException : HotelException{ public RoomNotFoundException(string message) : base(message) { }}
public class RoomTakenException : HotelException{ public RoomTakenException(string message) : base(message) { }}class HotelError(Exception): pass
class RoomNotFoundError(HotelError): pass
class RoomTakenError(HotelError): passclass HotelError extends Error { constructor(message) { super(message); this.name = "HotelError"; }}
class RoomNotFoundError extends HotelError { constructor(message) { super(message); this.name = "RoomNotFoundError"; }}
class RoomTakenError extends HotelError { constructor(message) { super(message); this.name = "RoomTakenError"; }}Анулирането проверява, че стаята е заета и че я анулира същият гост:
public void cancel(int number, String guest) { Room room = find(number); if (room.isFree()) { throw new HotelException("Стая " + number + " не е резервирана."); } if (!guests.get(number).equals(guest)) { throw new HotelException("Стая " + number + " е резервирана от друг гост."); } room.release(); guests.remove(number);}public void Cancel(int number, string guest){ Room room = Find(number); if (room.IsFree) { throw new HotelException("Стая " + number + " не е резервирана."); } if (guests[number] != guest) { throw new HotelException("Стая " + number + " е резервирана от друг гост."); } room.Release(); guests.Remove(number);}def cancel(self, number, guest): room = self._find(number) if room.is_free: raise HotelError(f"Стая {number} не е резервирана.") if self._guests[number] != guest: raise HotelError(f"Стая {number} е резервирана от друг гост.") room.release() del self._guests[number]cancel(number, guest) { const room = this.#find(number); if (room.isFree) { throw new HotelError(`Стая ${number} не е резервирана.`); } if (this.#guests.get(number) !== guest) { throw new HotelError(`Стая ${number} е резервирана от друг гост.`); } room.release(); this.#guests.delete(number);}Условието на проекта иска системата да се провери в различни сценарии: резервация, проверка и анулиране, и когато са правилни, и когато не са. Всеки сценарий е ред в изхода:
Hotel hotel = new Hotel();hotel.add(new Room(101, "единична", 60));hotel.add(new Room(102, "двойна", 85));hotel.add(new Room(201, "двойна", 95));hotel.add(new Room(202, "апартамент", 140));
hotel.reserve(102, "Анна Петрова");System.out.println("1. Свободна стая: " + hotel.check(102));try { hotel.reserve(102, "Борис Илиев");} catch (RoomTakenException e) { System.out.println("2. Заета стая: " + e.getMessage());}try { hotel.cancel(102, "Борис Илиев");} catch (HotelException e) { System.out.println("3. Чужда резервация: " + e.getMessage());}hotel.cancel(102, "Анна Петрова");System.out.println("4. Анулиране: " + hotel.check(102));try { hotel.cancel(102, "Анна Петрова");} catch (HotelException e) { System.out.println("5. Второ анулиране: " + e.getMessage());}try { hotel.reserve(305, "Анна Петрова");} catch (RoomNotFoundException e) { System.out.println("6. Няма такава стая: " + e.getMessage());}try { hotel.reserve(201, " ");} catch (HotelException e) { System.out.println("7. Без име: " + e.getMessage());}1. Свободна стая: 102, двойна, 85 € на нощ, заета2. Заета стая: Стая 102 вече е заета.3. Чужда резервация: Стая 102 е резервирана от друг гост.4. Анулиране: 102, двойна, 85 € на нощ, свободна5. Второ анулиране: Стая 102 не е резервирана.6. Няма такава стая: Няма стая 305.7. Без име: Името на госта е задължително.Hotel hotel = new Hotel();hotel.Add(new Room(101, "единична", 60));hotel.Add(new Room(102, "двойна", 85));hotel.Add(new Room(201, "двойна", 95));hotel.Add(new Room(202, "апартамент", 140));
hotel.Reserve(102, "Анна Петрова");Console.WriteLine("1. Свободна стая: " + hotel.Check(102));try{ hotel.Reserve(102, "Борис Илиев");}catch (RoomTakenException e){ Console.WriteLine("2. Заета стая: " + e.Message);}try{ hotel.Cancel(102, "Борис Илиев");}catch (HotelException e){ Console.WriteLine("3. Чужда резервация: " + e.Message);}hotel.Cancel(102, "Анна Петрова");Console.WriteLine("4. Анулиране: " + hotel.Check(102));try{ hotel.Cancel(102, "Анна Петрова");}catch (HotelException e){ Console.WriteLine("5. Второ анулиране: " + e.Message);}try{ hotel.Reserve(305, "Анна Петрова");}catch (RoomNotFoundException e){ Console.WriteLine("6. Няма такава стая: " + e.Message);}try{ hotel.Reserve(201, " ");}catch (HotelException e){ Console.WriteLine("7. Без име: " + e.Message);}1. Свободна стая: 102, двойна, 85 € на нощ, заета2. Заета стая: Стая 102 вече е заета.3. Чужда резервация: Стая 102 е резервирана от друг гост.4. Анулиране: 102, двойна, 85 € на нощ, свободна5. Второ анулиране: Стая 102 не е резервирана.6. Няма такава стая: Няма стая 305.7. Без име: Името на госта е задължително.hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))
hotel.reserve(102, "Анна Петрова")print(f"1. Свободна стая: {hotel.check(102)}")try: hotel.reserve(102, "Борис Илиев")except RoomTakenError as e: print(f"2. Заета стая: {e}")try: hotel.cancel(102, "Борис Илиев")except HotelError as e: print(f"3. Чужда резервация: {e}")hotel.cancel(102, "Анна Петрова")print(f"4. Анулиране: {hotel.check(102)}")try: hotel.cancel(102, "Анна Петрова")except HotelError as e: print(f"5. Второ анулиране: {e}")try: hotel.reserve(305, "Анна Петрова")except RoomNotFoundError as e: print(f"6. Няма такава стая: {e}")try: hotel.reserve(201, " ")except HotelError as e: print(f"7. Без име: {e}")1. Свободна стая: 102, двойна, 85 € на нощ, заета2. Заета стая: Стая 102 вече е заета.3. Чужда резервация: Стая 102 е резервирана от друг гост.4. Анулиране: 102, двойна, 85 € на нощ, свободна5. Второ анулиране: Стая 102 не е резервирана.6. Няма такава стая: Няма стая 305.7. Без име: Името на госта е задължително.const hotel = new Hotel();hotel.add(new Room(101, "единична", 60));hotel.add(new Room(102, "двойна", 85));hotel.add(new Room(201, "двойна", 95));hotel.add(new Room(202, "апартамент", 140));
hotel.reserve(102, "Анна Петрова");console.log(`1. Свободна стая: ${hotel.check(102)}`);try { hotel.reserve(102, "Борис Илиев");} catch (e) { if (!(e instanceof RoomTakenError)) throw e; console.log(`2. Заета стая: ${e.message}`);}try { hotel.cancel(102, "Борис Илиев");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`3. Чужда резервация: ${e.message}`);}hotel.cancel(102, "Анна Петрова");console.log(`4. Анулиране: ${hotel.check(102)}`);try { hotel.cancel(102, "Анна Петрова");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`5. Второ анулиране: ${e.message}`);}try { hotel.reserve(305, "Анна Петрова");} catch (e) { if (!(e instanceof RoomNotFoundError)) throw e; console.log(`6. Няма такава стая: ${e.message}`);}try { hotel.reserve(201, " ");} catch (e) { if (!(e instanceof HotelError)) throw e; console.log(`7. Без име: ${e.message}`);}1. Свободна стая: 102, двойна, 85 € на нощ, заета2. Заета стая: Стая 102 вече е заета.3. Чужда резервация: Стая 102 е резервирана от друг гост.4. Анулиране: 102, двойна, 85 € на нощ, свободна5. Второ анулиране: Стая 102 не е резервирана.6. Няма такава стая: Няма стая 305.7. Без име: Името на госта е задължително.Конкретното изключение казва какъв е проблемът. HotelError хваща всички откази наведнъж: сървърът ще хваща само него и ще го превръща в отговор за клиента.
По-точно, за любопитните
- Проверени изключения няма: никой метод не обявява какво хвърля. Затова рецепцията ще ги хване на едно място.
- Сценарии като тези са естествени тестове. С
unittestвсеки става метод наTestCase, аwith self.assertRaises(RoomTakenError):околоhotel.reserve(102, "Борис Илиев")проверява изключението.
Пробвай
Кой е в 201
Какво ще отпечата програмата?
hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))
hotel.reserve(101, "Анна Петрова")hotel.reserve(201, "Борис Илиев")hotel.cancel(101, "Анна Петрова")try: hotel.reserve(201, "Вера Колева") print("Вера е в 201")except RoomTakenError: print("Отказ за Вера")print(len(hotel.free_rooms()))Анна заема 101 и после я освобождава. Борис заема 201, затова Вера получава RoomTakenError и редът „Вера е в 201“ не се изпълнява. Заета е само 201, свободни са три стаи.
Идеята
Протоколът: ред „КОМАНДА:текст“
Всяко съобщение е един ред КОМАНДА:текст, а сървърът отговаря с един ред:
| Клиентът праща | Сървърът отговаря |
|---|---|
СВОБОДНИ |
ОК: и свободните стаи, разделени с ; |
ПРОВЕРИ:102 |
ОК:102, двойна, 85 € на нощ, заета |
РЕЗЕРВИРАЙ:102;Анна Петрова |
ОК:Стая 102 е твоя, Анна Петрова. или ГРЕШКА: и причината |
АНУЛИРАЙ:102;Анна Петрова |
ОК:Резервацията за стая 102 е анулирана. или ГРЕШКА: и причината |
КРАЙ |
ДОВИЖДАНЕ и сървърът затваря връзката |
Думите на протокола са константи на едно място (в Python константата е договорка: главни букви и никой не я сменя):
class Protocol: FREE = "СВОБОДНИ" CHECK = "ПРОВЕРИ" RESERVE = "РЕЗЕРВИРАЙ" CANCEL = "АНУЛИРАЙ" EXIT = "КРАЙ"
OK = "ОК" ERROR = "ГРЕШКА" BYE = "ДОВИЖДАНЕ"Рецепцията превежда ред от протокола в извикване на хотела и обратно:
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"Непозната команда {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}:Стая {number} е твоя, {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}:Резервацията за стая {number} е анулирана."
@staticmethoddef _number_and_guest(text): parts = text.split(";", 1) if len(parts) != 2: raise HotelError("Пиши номер и име, например „102;Анна Петрова“.") return parts
@staticmethoddef _number(text): try: return int(text) except ValueError: raise HotelError("Номерът на стаята е цяло число.")
@staticmethoddef _join(rooms): if not rooms: return "Няма свободни стаи." return "; ".join(str(room) for room in rooms)line.partition(":") дели реда на три: текста до първото :, самото : и останалото. Без : командата е целият ред, а текстът е празен. match сравнява командата с всяка константа, а case _ хваща всичко останало.Рецепцията не помни нищо за клиента: всеки ред носи всичко нужно, дори името на госта. Затова една рецепция стига за всички клиенти. Проверява се без мрежа:
hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))
reception = Reception(hotel)lines = [ "РЕЗЕРВИРАЙ:102;Анна Петрова", "ПРОВЕРИ:102", "РЕЗЕРВИРАЙ:102;Борис Илиев", "АНУЛИРАЙ:102", "ПРОВЕРИ:сто и две", "ПОКАЖИ",]for line in lines: print(f"> {line}") print(reception.handle(line))> РЕЗЕРВИРАЙ:102;Анна ПетроваОК:Стая 102 е твоя, Анна Петрова.> ПРОВЕРИ:102ОК:102, двойна, 85 € на нощ, заета> РЕЗЕРВИРАЙ:102;Борис ИлиевГРЕШКА:Стая 102 вече е заета.> АНУЛИРАЙ:102ГРЕШКА:Пиши номер и име, например „102;Анна Петрова“.> ПРОВЕРИ:сто и двеГРЕШКА:Номерът на стаята е цяло число.> ПОКАЖИГРЕШКА:Непозната команда ПОКАЖИ.По-точно, за любопитните
-
Командите могат да са енумерация:
class Command(StrEnum)от модулаenum. Тогава са отделен тип със затворен списък от стойности. В курса са константи, защото енумерации не сме учили. - Протоколът е без състояние: всеки ред е сам за себе си. Това го прави лесен за проверка и за повече клиенти. Ако клиентът трябваше първо да каже „аз съм Анна“, рецепцията щеше да е по една на клиент, както сесията в автокъщата.
Виж го
Сървърът
Всеки свързан клиент получава свой ClientHandler, а рецепцията е обща:
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"Връзката с клиента прекъсна: {e}")makefile прави от сокета файл: редовете се четат с for, а се пишат с print(..., file=writer). flush=True праща реда веднага, а не когато буферът се напълни. with (...) затваря връзката и двата файла накрая, както и да е излязъл методът.
HotelServer приема клиентите в своя нишка и дава всеки на басейн от 10 нишки (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()- Десет клиенти се обслужват наведнъж. Единадесетият е приет и чака в опашката на басейна, докато някой не си тръгне.
- И десетте нишки викат един и същ
Hotel. Затова хотелът ще трябва да пази стаите си. Как, след малко. - Нишката, която приема клиентите, е
daemon: тя не държи програмата жива, когато всичко останало е свършило.
Виж го
Клиентът
send е два метода, с текст и без. В Python е един, а text=None значи, че текстът може да се пропусне. close() казва КРАЙ и затваря двата файла и връзката: сокетът се затваря наистина, едва когато и файловете от 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()Сървърът и клиентът в една програма:
hotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))server = HotelServer(5321, hotel)server.start()
client = HotelClient("localhost", 5321)print(client.send(Protocol.RESERVE, "102;Анна Петрова"))print(client.send(Protocol.CHECK, "102"))print(client.send(Protocol.FREE))print(client.send(Protocol.CANCEL, "102;Анна Петрова"))print(client.send(Protocol.CHECK, "305"))client.close()server.stop()ОК:Стая 102 е твоя, Анна Петрова.ОК:102, двойна, 85 € на нощ, заетаОК:101, единична, 60 € на нощ, свободна; 201, двойна, 95 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободнаОК:Резервацията за стая 102 е анулирана.ГРЕШКА:Няма стая 305.Идеята
Двама за една стая
| Агенция А | Агенция Б | |
|---|---|---|
| 1 | проверява: 102 е свободна | |
| 2 | проверява: 102 е свободна | |
| 3 | заема 102 за своя гост | |
| 4 | заема 102 за своя гост и записва него |
И двете получават потвърждение. Няма грешка в нито един ред код, грешката е в това, че стъпките на двете заявки се преплитат.
Решението: в хотела влиза само една заявка наведнъж. Тогава „провери и заеми“ е неделимо: никой не може да влезе между проверката и заемането. В Python това прави ключалка в хотела,threading.RLock(), и with self._lock: около тялото на всеки публичен метод: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("Името на госта е задължително.") room = self._find(number) if not room.is_free: raise RoomTakenError(f"Стая {number} вече е заета.") room.occupy() self._guests[number] = guestdef reserve(self, number, guest): with self._lock: if not guest.strip(): raise HotelError("Името на госта е задължително.") room = self._find(number) if not room.is_free: raise RoomTakenError(f"Стая {number} вече е заета.") room.occupy() self._guests[number] = guestСтъпка по стъпка, с думи
- Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
- Първо без with self._lock. Това е един възможен ред: заявката на Анна проверява и вижда „свободна“.
- Заявката на Борис проверява, преди Анна да е заела стаята: и тя вижда „свободна“.
- Анна заема стаята и записва името си.
- Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
- Отначало, с with self._lock: в хотела влиза само една нишка наведнъж.
- Да кажем, че първа е Анна: тя взима ключалката. Проверката и заемането са една стъпка, никой не влиза между тях.
- Борис стига до хотела и чака, докато ключалката се освободи.
- Ключалката е на Борис. Проверката вече вижда „заета“.
- RoomTakenError. Рецепцията го връща като ред с грешка, а Борис може да пита за друга стая.
- Програмата от урока брои успелите: по 1 за всяка стая. Кой е бил пръв, е различно при всяко пускане.
Доказателството: двама клиенти резервират 102 наведнъж, после двадесет агенции резервират 201 наведнъж. Отговорите се събират, а програмата печата колко са успели:
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 reservedhotel = Hotel()hotel.add(Room(101, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))server = HotelServer(5322, hotel)server.start()
two = ["Анна Петрова", "Борис Илиев"]print(f"Двама за стая 102, успешни: {race(5322, 102, two)}")
agencies = [f"Агенция {i + 1}" for i in range(20)]print(f"Двадесет за стая 201, успешни: {race(5322, 201, agencies)}")
desk = HotelClient("localhost", 5322)print(desk.send(Protocol.FREE))desk.close()server.stop()Двама за стая 102, успешни: 1Двадесет за стая 201, успешни: 1ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободнаmap чака отговорите и ги връща в реда на гостите, а race брои успелите.Кой е спечелил стаята, е различно при всяко пускане, затова програмата не го печата. Колко са успели, е винаги едно и също: по един за всяка стая.
По-точно, за любопитните
- Без заключването програмата пак почти винаги би казала 1: прозорецът между проверката и заемането е малък. Но „почти винаги“ значи, че веднъж на хиляди резервации в стаята пристигат двама. Такъв бъг не се хваща с пускане, а с мислене: кои данни са общи и кой ги мени наведнъж. GIL на Python не пази „провери и заеми“: нишката може да бъде сменена между два реда. Ако между проверката и заемането има нещо бавно, например заявка към база данни, другата нишка влиза почти сигурно.
-
self._lockзаключва целия хотел. Докато една нишка резервира 102, друга чака дори за 201. При четири стаи това е добре. При хиляди се заключва по стая, с ключалка за всяка. Ключалката еRLock, защото нишката, която я държи, може да я вземе пак. Това трябва в упражнението: смяната на стая викаcancelиreserve, които също заключват. С обикновенLockвторият опит би чакал вечно: нишката чака сама себе си. - В истинска система стаите са в база данни и пазенето е там: транзакция или условие в самата заявка (
UPDATE rooms SET free = false WHERE number = 102 AND free = true). Тогава важи и при няколко сървъра, а не само в една програма.
Пробвай
Заключено, но не съвсем
with self._lock. Рецепцията вика първо проверката, после заемането. При много заявки наведнъж понякога пак двама получават една стая. На кой ред е грешката?Избери реда с грешката.
reserve, както в нашия Hotel.Виж го
Целият хотел
Тестовите сценарии от условието, този път по мрежата, с двама клиенти, свързани наведнъж. Накрая двама резервират 201 в един миг:
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, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))server = HotelServer(5323, hotel)server.start()
anna = HotelClient("localhost", 5323)boris = HotelClient("localhost", 5323)say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова")say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")say("Борис", boris, Protocol.CHECK, "102")say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев")say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова")say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")say("Анна", anna, Protocol.CHECK, "305")anna.close()boris.close()
two = ["Анна Петрова", "Вера Колева"]print(f"Двама за стая 201 наведнъж, успешни: {race(5323, 201, two)}")desk = HotelClient("localhost", 5323)print(desk.send(Protocol.FREE))desk.close()server.stop()Анна > РЕЗЕРВИРАЙ:102;Анна ПетроваАнна < ОК:Стая 102 е твоя, Анна Петрова.Борис > РЕЗЕРВИРАЙ:102;Борис ИлиевБорис < ГРЕШКА:Стая 102 вече е заета.Борис > ПРОВЕРИ:102Борис < ОК:102, двойна, 85 € на нощ, заетаБорис > АНУЛИРАЙ:102;Борис ИлиевБорис < ГРЕШКА:Стая 102 е резервирана от друг гост.Анна > АНУЛИРАЙ:102;Анна ПетроваАнна < ОК:Резервацията за стая 102 е анулирана.Борис > РЕЗЕРВИРАЙ:102;Борис ИлиевБорис < ОК:Стая 102 е твоя, Борис Илиев.Анна > ПРОВЕРИ:305Анна < ГРЕШКА:Няма стая 305.Двама за стая 201 наведнъж, успешни: 1ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободнаПо-точно, за любопитните
Цялата програма, от началото до края:
import socketimport threadingfrom 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 = "свободна" if self._free else "заета" return f"{self._number}, {self._kind}, {self._price} € на нощ, {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("Името на госта е задължително.") room = self._find(number) if not room.is_free: raise RoomTakenError(f"Стая {number} вече е заета.") 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"Стая {number} не е резервирана.") if self._guests[number] != guest: raise HotelError(f"Стая {number} е резервирана от друг гост.") room.release() del self._guests[number]
def _find(self, number): for room in self._rooms: if room.number == number: return room raise RoomNotFoundError(f"Няма стая {number}.")
class HotelError(Exception): pass
class RoomNotFoundError(HotelError): pass
class RoomTakenError(HotelError): pass
class Protocol: FREE = "СВОБОДНИ" CHECK = "ПРОВЕРИ" RESERVE = "РЕЗЕРВИРАЙ" CANCEL = "АНУЛИРАЙ" EXIT = "КРАЙ"
OK = "ОК" ERROR = "ГРЕШКА" BYE = "ДОВИЖДАНЕ"
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"Непозната команда {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}:Стая {number} е твоя, {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}:Резервацията за стая {number} е анулирана."
@staticmethod def _number_and_guest(text): parts = text.split(";", 1) if len(parts) != 2: raise HotelError("Пиши номер и име, например „102;Анна Петрова“.") return parts
@staticmethod def _number(text): try: return int(text) except ValueError: raise HotelError("Номерът на стаята е цяло число.")
@staticmethod def _join(rooms): if not rooms: return "Няма свободни стаи." 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"Връзката с клиента прекъсна: {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, "единична", 60))hotel.add(Room(102, "двойна", 85))hotel.add(Room(201, "двойна", 95))hotel.add(Room(202, "апартамент", 140))server = HotelServer(5323, hotel)server.start()
anna = HotelClient("localhost", 5323)boris = HotelClient("localhost", 5323)say("Анна", anna, Protocol.RESERVE, "102;Анна Петрова")say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")say("Борис", boris, Protocol.CHECK, "102")say("Борис", boris, Protocol.CANCEL, "102;Борис Илиев")say("Анна", anna, Protocol.CANCEL, "102;Анна Петрова")say("Борис", boris, Protocol.RESERVE, "102;Борис Илиев")say("Анна", anna, Protocol.CHECK, "305")anna.close()boris.close()
two = ["Анна Петрова", "Вера Колева"]print(f"Двама за стая 201 наведнъж, успешни: {race(5323, 201, two)}")desk = HotelClient("localhost", 5323)print(desk.send(Protocol.FREE))desk.close()server.stop()Упражнение
Продължи сам
Работи с цялата програма от предишната карта.
1. Смяна на стая. Гостът се мести от една стая в друга с ПРЕМЕСТИ:102;202;Анна Петрова. Отговорът е ОК:Анна Петрова е в стая 202. Ако новата стая е заета, гостът остава в старата. Смяната е един метод в Hotel, в with self._lock: никой не бива да заеме новата стая между освобождаването на старата и заемането на новата.
ОК:Стая 102 е твоя, Анна Петрова.ОК:Стая 201 е твоя, Борис Илиев.ГРЕШКА:Стая 201 вече е заета.ОК:Анна Петрова е в стая 202.ОК:101, единична, 60 € на нощ, свободна; 102, двойна, 85 € на нощ, свободнаОще идеи
- Нощувки и сума. Резервацията е за брой нощи, а потвърждението казва сумата: „3 нощи, 255 €“.
- Дати. Стаята се резервира за дни, например от 14 до 16 декември. Две резервации за една стая не бива да се застъпват. Свободна ли е стаята, зависи от датите.
- Моите резервации.
МОИ:Анна Петроваказва кои стаи са на този гост. - Списък на чакащите. Ако стаята е заета, гостът се записва да чака. Когато се анулира, тя отива при първия в списъка.
-
Клиент от клавиатурата. Втора програма чете команди от клавиатурата с
input()и ги праща на сървъра, пуснат в друг терминал.
Тест