Хотелски резервации
Проблем
Една стая, две потвърждения
Малък хотел има четири стаи. Резервации идват отвсякъде: от сайта, от рецепцията, от туристически агенции. Една петъчна вечер две агенции резервират последната двойна стая в една и съща секунда. И двете получават потвърждение. В събота в стаята пристигат две семейства.
Ще направим система, в която сървърът пази стаите и резервациите, а клиентите пращат заявки по мрежата. Клиентът може:
- да види свободните стаи;
- да провери една стая;
- да резервира стая на свое име;
- да анулира своята резервация.
А когато двама клиенти резервират една стая едновременно, само единият успява. Това ще докажем с програма, която дава един и същ изход при всяко пускане.
Строим отдолу нагоре: моделът без мрежа, правилата и грешките, протоколът, сървърът и клиентът, едновременните заявки и накрая всичко в една програма.
По-точно, за любопитните
HttpClient. В двата езика една и съща ключова идея пази стаите: synchronized.Идеята
Стаята
Стаята има номер, тип, цена за нощ и състояние: свободна или заета.
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. - Кой е отседнал, стаята не знае. Стаята стига до всеки клиент, който пита, а името на госта не бива да излиза от хотела.
По-точно, за любопитните
boolean free. Може и с текст („свободна“, „заета“, „в ремонт“), но тогава всяко сравнение е с низ и сгрешена буква е бъг. За повече от две състояния се ползва енумерация: enum RoomState { FREE, TAKEN, REPAIR }.Виж го
Хотелът и резервацията
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. Без име: Името на госта е задължително.Конкретното изключение казва какъв е проблемът. HotelException хваща всички откази наведнъж: сървърът ще хваща само него и ще го превръща в отговор за клиента.
По-точно, за любопитните
-
Изключенията наследяват
RuntimeException, затова методите не пишатthrows.run()на нишка и ламбдите не могат да хвърлят проверени изключения. - Сценарии като тези са естествени тестове. С JUnit всеки става метод с
@TestиassertThrows(RoomTakenException.class, () -> hotel.reserve(102, "Борис Илиев")).
Пробвай
Кой е в 201
Какво ще отпечата програмата?
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(101, "Анна Петрова");hotel.reserve(201, "Борис Илиев");hotel.cancel(101, "Анна Петрова");try { hotel.reserve(201, "Вера Колева"); System.out.println("Вера е в 201");} catch (RoomTakenException e) { System.out.println("Отказ за Вера");}System.out.println(hotel.freeRooms().size());Анна заема 101 и после я освобождава. Борис заема 201, затова Вера получава RoomTakenException и редът „Вера е в 201“ не се изпълнява. Заета е само 201, свободни са три стаи.
Идеята
Протоколът: ред „КОМАНДА:текст“
Всяко съобщение е един ред КОМАНДА:текст, а сървърът отговаря с един ред:
| Клиентът праща | Сървърът отговаря |
|---|---|
СВОБОДНИ |
ОК: и свободните стаи, разделени с ; |
ПРОВЕРИ:102 |
ОК:102, двойна, 85 € на нощ, заета |
РЕЗЕРВИРАЙ:102;Анна Петрова |
ОК:Стая 102 е твоя, Анна Петрова. или ГРЕШКА: и причината |
АНУЛИРАЙ:102;Анна Петрова |
ОК:Резервацията за стая 102 е анулирана. или ГРЕШКА: и причината |
КРАЙ |
ДОВИЖДАНЕ и сървърът затваря връзката |
Думите на протокола са константи на едно място:
class Protocol { public static final String FREE = "СВОБОДНИ"; public static final String CHECK = "ПРОВЕРИ"; public static final String RESERVE = "РЕЗЕРВИРАЙ"; public static final String CANCEL = "АНУЛИРАЙ"; public static final String EXIT = "КРАЙ";
public static final String OK = "ОК"; public static final String ERROR = "ГРЕШКА"; public static final String BYE = "ДОВИЖДАНЕ";}Рецепцията превежда ред от протокола в извикване на хотела и обратно:
class Reception { private final Hotel hotel;
public Reception(Hotel hotel) { this.hotel = hotel; } ...}public String handle(String line) { int colon = line.indexOf(':'); String command = colon < 0 ? line : line.substring(0, colon); String text = colon < 0 ? "" : line.substring(colon + 1); try { switch (command) { case Protocol.FREE: return Protocol.OK + ":" + join(hotel.freeRooms()); case Protocol.CHECK: return Protocol.OK + ":" + hotel.check(number(text)); case Protocol.RESERVE: return reserve(text); case Protocol.CANCEL: return cancel(text); default: throw new HotelException("Непозната команда " + command + "."); } } catch (HotelException e) { return Protocol.ERROR + ":" + e.getMessage(); }}private String reserve(String text) { String[] parts = numberAndGuest(text); int number = number(parts[0]); hotel.reserve(number, parts[1]); return Protocol.OK + ":Стая " + number + " е твоя, " + parts[1] + ".";}
private String cancel(String text) { String[] parts = numberAndGuest(text); int number = number(parts[0]); hotel.cancel(number, parts[1]); return Protocol.OK + ":Резервацията за стая " + number + " е анулирана.";}
private static String[] numberAndGuest(String text) { String[] parts = text.split(";", 2); if (parts.length != 2) { throw new HotelException("Пиши номер и име, например „102;Анна Петрова“."); } return parts;}
private static int number(String text) { try { return Integer.parseInt(text.trim()); } catch (NumberFormatException e) { throw new HotelException("Номерът на стаята е цяло число."); }}
private static String join(List<Room> rooms) { if (rooms.isEmpty()) { return "Няма свободни стаи."; } return String.join("; ", rooms.stream().map(Room::toString).toList());}Рецепцията не помни нищо за клиента: всеки ред носи всичко нужно, дори името на госта. Затова една рецепция стига за всички клиенти. Проверява се без мрежа:
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));
Reception reception = new Reception(hotel);String[] lines = { "РЕЗЕРВИРАЙ:102;Анна Петрова", "ПРОВЕРИ:102", "РЕЗЕРВИРАЙ:102;Борис Илиев", "АНУЛИРАЙ:102", "ПРОВЕРИ:сто и две", "ПОКАЖИ"};for (String line : lines) { System.out.println("> " + line); System.out.println(reception.handle(line));}> РЕЗЕРВИРАЙ:102;Анна ПетроваОК:Стая 102 е твоя, Анна Петрова.> ПРОВЕРИ:102ОК:102, двойна, 85 € на нощ, заета> РЕЗЕРВИРАЙ:102;Борис ИлиевГРЕШКА:Стая 102 вече е заета.> АНУЛИРАЙ:102ГРЕШКА:Пиши номер и име, например „102;Анна Петрова“.> ПРОВЕРИ:сто и двеГРЕШКА:Номерът на стаята е цяло число.> ПОКАЖИГРЕШКА:Непозната команда ПОКАЖИ.По-точно, за любопитните
-
Командите могат да са енумерация:
enum Command { FREE, CHECK, RESERVE, CANCEL, EXIT }. Тогава са отделен тип със затворен списък от стойности. В курса са константи, защото енумерации не сме учили. - Протоколът е без състояние: всеки ред е сам за себе си. Това го прави лесен за проверка и за повече клиенти. Ако клиентът трябваше първо да каже „аз съм Анна“, рецепцията щеше да е по една на клиент, както сесията в автокъщата.
Виж го
Сървърът
Всеки свързан клиент получава свой ClientHandler, а рецепцията е обща:
class ClientHandler implements Runnable { private final Socket socket; private final Reception reception;
public ClientHandler(Socket socket, Reception reception) { this.socket = socket; this.reception = reception; }
@Override public void run() { try (socket; BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { String line; while ((line = in.readLine()) != null && !line.equals(Protocol.EXIT)) { out.println(reception.handle(line)); } out.println(Protocol.BYE); } catch (IOException e) { System.out.println("Връзката с клиента прекъсна: " + e.getMessage()); } }}HotelServer приема клиентите в своя нишка и дава всеки на басейн от 10 нишки:
class HotelServer { private final ServerSocket serverSocket; private final ExecutorService pool = Executors.newFixedThreadPool(10); private final Reception reception;
public HotelServer(int port, Hotel hotel) throws IOException { this.serverSocket = new ServerSocket(port); this.reception = new Reception(hotel); }
public void start() { Thread acceptor = new Thread(() -> acceptAll()); acceptor.setDaemon(true); acceptor.start(); }
private void acceptAll() { while (true) { try { Socket client = serverSocket.accept(); pool.execute(new ClientHandler(client, reception)); } catch (IOException e) { return; } } }
public void stop() throws IOException { serverSocket.close(); pool.shutdown(); }}- Десет клиенти се обслужват наведнъж. Единадесетият е приет и чака в опашката на басейна, докато някой не си тръгне.
- И десетте нишки викат един и същ
Hotel. Затова хотелът ще трябва да пази стаите си. Как, след малко.
Виж го
Клиентът
close() казва КРАЙ и затваря връзката:class HotelClient { private final Socket socket; private final BufferedReader in; private final PrintWriter out;
public HotelClient(String host, int port) throws IOException { socket = new Socket(host, port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); }
public String send(String command, String text) throws IOException { out.println(command + ":" + text); return in.readLine(); }
public String send(String command) throws IOException { out.println(command); return in.readLine(); }
public void close() throws IOException { send(Protocol.EXIT); socket.close(); }}Сървърът и клиентът в една програма:
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));HotelServer server = new HotelServer(5321, hotel);server.start();
HotelClient client = new HotelClient("localhost", 5321);System.out.println(client.send(Protocol.RESERVE, "102;Анна Петрова"));System.out.println(client.send(Protocol.CHECK, "102"));System.out.println(client.send(Protocol.FREE));System.out.println(client.send(Protocol.CANCEL, "102;Анна Петрова"));System.out.println(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 за своя гост и записва него |
И двете получават потвърждение. Няма грешка в нито един ред код, грешката е в това, че стъпките на двете заявки се преплитат.
Решението: в хотела влиза само една заявка наведнъж. Тогава „провери и заеми“ е неделимо: никой не може да влезе между проверката и заемането. В Java това правиsynchronized на всеки публичен метод на Hotel:public synchronized 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 synchronized 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);}Стъпка по стъпка, с думи
- Стая 102 е свободна. Анна Петрова и Борис Илиев я резервират в един и същ миг.
- Първо без synchronized. Това е един възможен ред: заявката на Анна проверява и вижда „свободна“.
- Заявката на Борис проверява, преди Анна да е заела стаята: и тя вижда „свободна“.
- Анна заема стаята и записва името си.
- Борис също заема стаята и записва своето име. Двамата имат потвърждение за една стая.
- Отначало, със synchronized: в хотела влиза само една нишка наведнъж.
- Да кажем, че първа е Анна: тя взима ключалката. Проверката и заемането са една стъпка, никой не влиза между тях.
- Борис стига до хотела и чака, докато ключалката се освободи.
- Ключалката е на Борис. Проверката вече вижда „заета“.
- RoomTakenException. Рецепцията го връща като ред с грешка, а Борис може да пита за друга стая.
- Програмата от урока брои успелите: по 1 за всяка стая. Кой е бил пръв, е различно при всяко пускане.
Доказателството: двама клиенти резервират 102 наведнъж, после двадесет агенции резервират 201 наведнъж. Отговорите се събират, а програмата печата колко са успели:
static String reserve(int port, int room, String guest) { try { HotelClient client = new HotelClient("localhost", port); String reply = client.send(Protocol.RESERVE, room + ";" + guest); client.close(); return reply; } catch (IOException e) { return Protocol.ERROR + ":" + e.getMessage(); }}
static int race(int port, int room, String[] guests) throws InterruptedException { String[] replies = new String[guests.length]; List<Thread> clients = new ArrayList<>(); for (int i = 0; i < guests.length; i++) { int n = i; Thread client = new Thread(() -> replies[n] = reserve(port, room, guests[n])); clients.add(client); client.start(); } for (Thread client : clients) { client.join(); } int reserved = 0; for (String reply : replies) { if (reply.startsWith(Protocol.OK)) { reserved++; } } return reserved;}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));HotelServer server = new HotelServer(5322, hotel);server.start();
String[] two = { "Анна Петрова", "Борис Илиев" };System.out.println("Двама за стая 102, успешни: " + race(5322, 102, two));
String[] agencies = new String[20];for (int i = 0; i < agencies.length; i++) { agencies[i] = "Агенция " + (i + 1);}System.out.println("Двадесет за стая 201, успешни: " + race(5322, 201, agencies));
HotelClient desk = new HotelClient("localhost", 5322);System.out.println(desk.send(Protocol.FREE));desk.close();server.stop();Двама за стая 102, успешни: 1Двадесет за стая 201, успешни: 1ОК:101, единична, 60 € на нощ, свободна; 202, апартамент, 140 € на нощ, свободнаmain чака всички с join и брои успелите.Кой е спечелил стаята, е различно при всяко пускане, затова програмата не го печата. Колко са успели, е винаги едно и също: по един за всяка стая.
По-точно, за любопитните
- Без заключването програмата пак почти винаги би казала 1: прозорецът между проверката и заемането е малък. Но „почти винаги“ значи, че веднъж на хиляди резервации в стаята пристигат двама. Такъв бъг не се хваща с пускане, а с мислене: кои данни са общи и кой ги мени наведнъж.
-
synchronizedна метод заключва целия обектHotel. Докато една нишка резервира 102, друга чака дори за 201. При четири стаи това е добре. При хиляди стаи се заключва по стая, напримерsynchronized (room) { ... }. - В истинска система стаите са в база данни и пазенето е там: транзакция или условие в самата заявка (
UPDATE rooms SET free = false WHERE number = 102 AND free = true). Тогава важи и при няколко сървъра, а не само в една програма.
Пробвай
Заключено, но не съвсем
synchronized. Рецепцията вика първо проверката, после заемането. При много заявки наведнъж понякога пак двама получават една стая. На кой ред е грешката?Избери реда с грешката.
reserve, както в нашия Hotel.Виж го
Целият хотел
Тестовите сценарии от условието, този път по мрежата, с двама клиенти, свързани наведнъж. Накрая двама резервират 201 в един миг:
static void say(String who, HotelClient client, String command, String text) throws IOException { String reply = text.isEmpty() ? client.send(command) : client.send(command, text); System.out.println(who + " > " + command + (text.isEmpty() ? "" : ":" + text)); System.out.println(who + " < " + reply);}
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));HotelServer server = new HotelServer(5323, hotel);server.start();
HotelClient anna = new HotelClient("localhost", 5323);HotelClient boris = new 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();
String[] two = { "Анна Петрова", "Вера Колева" };System.out.println("Двама за стая 201 наведнъж, успешни: " + race(5323, 201, two));HotelClient desk = new HotelClient("localhost", 5323);System.out.println(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 java.io.*;import java.net.*;import java.util.*;import java.util.concurrent.*;
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 ? "свободна" : "заета"); }}
class Hotel { private final List<Room> rooms = new ArrayList<>(); private final Map<Integer, String> guests = new HashMap<>();
public synchronized void add(Room room) { rooms.add(room); }
public synchronized List<Room> freeRooms() { return rooms.stream().filter(Room::isFree).toList(); }
public synchronized Room check(int number) { return find(number); }
public synchronized 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 synchronized 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); }
private Room find(int number) { for (Room room : rooms) { if (room.getNumber() == number) { return room; } } throw new RoomNotFoundException("Няма стая " + number + "."); }}
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); }}
class Protocol { public static final String FREE = "СВОБОДНИ"; public static final String CHECK = "ПРОВЕРИ"; public static final String RESERVE = "РЕЗЕРВИРАЙ"; public static final String CANCEL = "АНУЛИРАЙ"; public static final String EXIT = "КРАЙ";
public static final String OK = "ОК"; public static final String ERROR = "ГРЕШКА"; public static final String BYE = "ДОВИЖДАНЕ";}
class Reception { private final Hotel hotel;
public Reception(Hotel hotel) { this.hotel = hotel; }
public String handle(String line) { int colon = line.indexOf(':'); String command = colon < 0 ? line : line.substring(0, colon); String text = colon < 0 ? "" : line.substring(colon + 1); try { switch (command) { case Protocol.FREE: return Protocol.OK + ":" + join(hotel.freeRooms()); case Protocol.CHECK: return Protocol.OK + ":" + hotel.check(number(text)); case Protocol.RESERVE: return reserve(text); case Protocol.CANCEL: return cancel(text); default: throw new HotelException("Непозната команда " + command + "."); } } catch (HotelException e) { return Protocol.ERROR + ":" + e.getMessage(); } }
private String reserve(String text) { String[] parts = numberAndGuest(text); int number = number(parts[0]); hotel.reserve(number, parts[1]); return Protocol.OK + ":Стая " + number + " е твоя, " + parts[1] + "."; }
private String cancel(String text) { String[] parts = numberAndGuest(text); int number = number(parts[0]); hotel.cancel(number, parts[1]); return Protocol.OK + ":Резервацията за стая " + number + " е анулирана."; }
private static String[] numberAndGuest(String text) { String[] parts = text.split(";", 2); if (parts.length != 2) { throw new HotelException("Пиши номер и име, например „102;Анна Петрова“."); } return parts; }
private static int number(String text) { try { return Integer.parseInt(text.trim()); } catch (NumberFormatException e) { throw new HotelException("Номерът на стаята е цяло число."); } }
private static String join(List<Room> rooms) { if (rooms.isEmpty()) { return "Няма свободни стаи."; } return String.join("; ", rooms.stream().map(Room::toString).toList()); }}
class ClientHandler implements Runnable { private final Socket socket; private final Reception reception;
public ClientHandler(Socket socket, Reception reception) { this.socket = socket; this.reception = reception; }
@Override public void run() { try (socket; BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { String line; while ((line = in.readLine()) != null && !line.equals(Protocol.EXIT)) { out.println(reception.handle(line)); } out.println(Protocol.BYE); } catch (IOException e) { System.out.println("Връзката с клиента прекъсна: " + e.getMessage()); } }}
class HotelServer { private final ServerSocket serverSocket; private final ExecutorService pool = Executors.newFixedThreadPool(10); private final Reception reception;
public HotelServer(int port, Hotel hotel) throws IOException { this.serverSocket = new ServerSocket(port); this.reception = new Reception(hotel); }
public void start() { Thread acceptor = new Thread(() -> acceptAll()); acceptor.setDaemon(true); acceptor.start(); }
private void acceptAll() { while (true) { try { Socket client = serverSocket.accept(); pool.execute(new ClientHandler(client, reception)); } catch (IOException e) { return; } } }
public void stop() throws IOException { serverSocket.close(); pool.shutdown(); }}
class HotelClient { private final Socket socket; private final BufferedReader in; private final PrintWriter out;
public HotelClient(String host, int port) throws IOException { socket = new Socket(host, port); in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new PrintWriter(socket.getOutputStream(), true); }
public String send(String command, String text) throws IOException { out.println(command + ":" + text); return in.readLine(); }
public String send(String command) throws IOException { out.println(command); return in.readLine(); }
public void close() throws IOException { send(Protocol.EXIT); socket.close(); }}
public class Main { static void say(String who, HotelClient client, String command, String text) throws IOException { String reply = text.isEmpty() ? client.send(command) : client.send(command, text); System.out.println(who + " > " + command + (text.isEmpty() ? "" : ":" + text)); System.out.println(who + " < " + reply); }
static String reserve(int port, int room, String guest) { try { HotelClient client = new HotelClient("localhost", port); String reply = client.send(Protocol.RESERVE, room + ";" + guest); client.close(); return reply; } catch (IOException e) { return Protocol.ERROR + ":" + e.getMessage(); } }
static int race(int port, int room, String[] guests) throws InterruptedException { String[] replies = new String[guests.length]; List<Thread> clients = new ArrayList<>(); for (int i = 0; i < guests.length; i++) { int n = i; Thread client = new Thread(() -> replies[n] = reserve(port, room, guests[n])); clients.add(client); client.start(); } for (Thread client : clients) { client.join(); } int reserved = 0; for (String reply : replies) { if (reply.startsWith(Protocol.OK)) { reserved++; } } return reserved; }
public static void main(String[] args) throws Exception { 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)); HotelServer server = new HotelServer(5323, hotel); server.start();
HotelClient anna = new HotelClient("localhost", 5323); HotelClient boris = new 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();
String[] two = { "Анна Петрова", "Вера Колева" }; System.out.println("Двама за стая 201 наведнъж, успешни: " + race(5323, 201, two)); HotelClient desk = new HotelClient("localhost", 5323); System.out.println(desk.send(Protocol.FREE)); desk.close(); server.stop(); }}Упражнение
Продължи сам
Работи с цялата програма от предишната карта.
1. Смяна на стая. Гостът се мести от една стая в друга с ПРЕМЕСТИ:102;202;Анна Петрова. Отговорът е ОК:Анна Петрова е в стая 202. Ако новата стая е заета, гостът остава в старата. Смяната е един метод в Hotel, synchronized: никой не бива да заеме новата стая между освобождаването на старата и заемането на новата.
ОК:Стая 102 е твоя, Анна Петрова.ОК:Стая 201 е твоя, Борис Илиев.ГРЕШКА:Стая 201 вече е заета.ОК:Анна Петрова е в стая 202.ОК:101, единична, 60 € на нощ, свободна; 102, двойна, 85 € на нощ, свободнаОще идеи
- Нощувки и сума. Резервацията е за брой нощи, а потвърждението казва сумата: „3 нощи, 255 €“.
- Дати. Стаята се резервира за дни, например от 14 до 16 декември. Две резервации за една стая не бива да се застъпват. Свободна ли е стаята, зависи от датите.
- Моите резервации.
МОИ:Анна Петроваказва кои стаи са на този гост. - Списък на чакащите. Ако стаята е заета, гостът се записва да чака. Когато се анулира, тя отива при първия в списъка.
-
Клиент от клавиатурата. Втора програма чете команди от клавиатурата със
Scannerи ги праща на сървъра, пуснат в друг терминал.
Тест