Програми в мрежата
Проблем
Касата и складът
Касата в магазина трябва да пита склада колко кафе е останало, а складовата програма работи на друг компютър. Файл не помага: двете програми не виждат дисковете си. Трябва едната да попита, а другата да отговори, веднага, по мрежата.
Така работи почти всичко около теб: браузърът пита сървъра за страница, приложението на банката пита банката за баланса, играта праща ходовете ти на другите играчи.
Идеята
Адрес, порт и два начина за пращане
За да стигнат данните до програма на друг компютър, трябват две неща:
- IP адрес: номерът на компютъра в мрежата, например
192.168.1.20или142.250.185.78. Собственият ти компютър винаги е127.0.0.1, с имеlocalhost. - Порт: номер от 0 до 65 535 за програмата на този компютър. Адресът е сградата, портът е апартаментът. HTTPS ползва 443, а ние ще ползваме 5101 и нагоре.
Програмата, която чака да я потърсят, е сървър. Тази, която търси, е клиент. Как пътуват данните, решава протоколът:
| TCP | UDP | |
|---|---|---|
| Връзка | първо се свързват, после си говорят | без връзка, всеки пакет е отделно писмо |
| Доставка | всичко стига, в същия ред, или разбираш, че не е | пакетът може да се загуби или да дойде в друг ред |
| Скорост | по-бавен | по-бърз |
| За какво | уеб, имейл, плащания, файлове | глас и видео на живо, игри, сензори |
socket.По-точно, за любопитните
- Мрежата се описва на слоеве. Моделът OSI има седем: физически (битове по кабела или въздуха), канален (връзка до съседното устройство), мрежов (IP адреси и маршрути), транспортен (TCP, UDP), сесиен, представителен и приложен (HTTP, имейл). Интернет на практика ползва по-простия модел TCP/IP с четири слоя. Програмата ти работи на най-горния: Python и операционната система поемат всичко отдолу.
- IP доставя отделни пакети до адрес, но не обещава, че ще стигнат. TCP стъпва върху IP и номерира пакетите, за да подреди и поиска наново загубените.
- Протоколи, които ще срещнеш: HTTP и HTTPS (уеб; HTTPS е HTTP, криптиран с TLS, по-старото име е SSL), WebSocket (двупосочна връзка за чат и известия в браузъра), FTP (файлове), SSH (сигурен достъп до отдалечен компютър).
Виж го
Сървърът
Складовият сървър получава име на продукт и отговаря с цената:
PRICES = {"кафе": 7.50, "чай": 3.20}
def serve(server): client, address = server.accept() with client, client.makefile(encoding="utf-8") as reader: product = reader.readline().rstrip("\n") print("Сървърът получи:", product) price = PRICES.get(product) answer = "няма такъв продукт" if price is None else f"{product}: {price} €" client.sendall(f"{answer}\n".encode())serverе сокет, който слуша на порт (ще го отворим в следващата карта).server.accept()чака, докато се свърже клиент, и връща двойка: сокет за връзката с него и адреса му.client.makefile(encoding="utf-8")дава файл за четене от сокета, както при файловете:readline()чете ред, заедно с"\n"накрая.rstrip("\n")го маха.client.sendall(байтове)праща. По мрежата минават байтове, затова текстът се превръща с.encode()(в UTF-8)."\n"в края казва на другата страна, че редът е свършил. Ще видим защо е важно.withзатваря връзката и файла накрая.
Виж го
Клиентът
socket.create_connection((адрес, порт)) и ползва същите makefile и sendall. Тук сървърът и клиентът са в една програма: сървърът работи в своя нишка, а главната програма е клиентът. Така можеш да ги пробваш без втори компютър:server = socket.create_server(("localhost", 5101))threading.Thread(target=serve, args=(server,)).start()
with ( socket.create_connection(("localhost", 5101)) as sock, sock.makefile(encoding="utf-8") as reader,): sock.sendall("кафе\n".encode()) print("Клиентът получи:", reader.readline().rstrip("\n"))server.close()Сървърът получи: кафеКлиентът получи: кафе: 7.5 €socket.create_server(("localhost", 5101)) в главната програма, преди нишката да тръгне. Така, когато клиентът се свърже, сървърът вече слуша. Скобите след with позволяват два ресурса на отделни редове.Двете страни се редуват: клиентът пише ред и чака отговор с readline(), а сървърът чете реда и отговаря. Ако сменят местата си и двете чакат да четат, ще чакат завинаги.
server = socket.create_server(("localhost", 5101))threading.Thread(target=serve, args=(server,)).start()
with ( socket.create_connection(("localhost", 5101)) as sock, sock.makefile(encoding="utf-8") as reader,): sock.sendall("кафе\n".encode()) print("Клиентът получи:", reader.readline().rstrip("\n"))server.close()Стъпка по стъпка, с думи
- Сървърът отваря порт 5101 и започва да слуша.
- В своя нишка сървърът вика accept() и чака клиент.
- Клиентът се свързва. accept() връща сокет и двете страни могат да пишат и да четат.
- Клиентът пише ред: „кафе“. Той минава по връзката до сървъра.
- Клиентът чака на readline(). Сървърът прочита реда и го печата.
- Сървърът намира цената и пише обратно: „кафе: 7.5 €“.
- readline() на клиента връща отговора и той го печата.
- with затваря сокета, а server.close() затваря порта.
По-точно, за любопитните
- Цялата програма, с
import-ите:
import socketimport threading
PRICES = {"кафе": 7.50, "чай": 3.20}
def serve(server): client, address = server.accept() with client, client.makefile(encoding="utf-8") as reader: product = reader.readline().rstrip("\n") print("Сървърът получи:", product) price = PRICES.get(product) answer = "няма такъв продукт" if price is None else f"{product}: {price} €" client.sendall(f"{answer}\n".encode())
server = socket.create_server(("localhost", 5101))threading.Thread(target=serve, args=(server,)).start()
with ( socket.create_connection(("localhost", 5101)) as sock, sock.makefile(encoding="utf-8") as reader,): sock.sendall("кафе\n".encode()) print("Клиентът получи:", reader.readline().rstrip("\n"))server.close()- В два отделни файла и два терминала е същото: пускаш първо сървъра, после клиента. Ако на порта няма сървър,
create_connectionхвърляConnectionRefusedError. Ако портът вече е зает от друга програма,create_serverхвърляOSError: [Errno 98] Address already in use. - Текстът минава по мрежата като байтове.
.encode()иmakefile(encoding="utf-8")ползват UTF-8, затова кирилицата пристига цяла.
Пробвай
Чакай клиент
Кой метод чака, докато се свърже клиент?
server = socket.create_server(("localhost", 5101))
client, address = server.()accept() блокира, докато някой не се свърже, и връща сокет за връзката с него. Сървърът може да го вика много пъти, за много клиенти.Идеята
Много клиенти наведнъж
accept() не се вика и другите чакат. Затова истинският сървър прави безкраен цикъл: приема клиент, дава го на нова нишка и веднага се връща към accept():def accept_all(server): number = 0 while True: try: client, address = server.accept() except OSError: return number += 1 threading.Thread(target=handle, args=(client, number)).start()
def handle(client, number): with client, client.makefile(encoding="utf-8") as reader: for line in reader: line = line.rstrip("\n") print(f"Клиент {number}: {line}") client.sendall(f"Ехо: {line}\n".encode())handle отговаря на всеки ред, докато клиентът не затвори връзката: тогава for line in reader свършва. Два клиента един след друг:server = socket.create_server(("localhost", 5102))threading.Thread(target=accept_all, args=(server,), daemon=True).start()
for order in ["маса 4: две кафета", "маса 7: сметката"]: with ( socket.create_connection(("localhost", 5102)) as sock, sock.makefile(encoding="utf-8") as reader, ): sock.sendall(f"{order}\n".encode()) print(reader.readline().rstrip("\n"))server.close()Клиент 1: маса 4: две кафетаЕхо: маса 4: две кафетаКлиент 2: маса 7: сметкатаЕхо: маса 7: сметкатаПо-точно, за любопитните
daemon=Trueправи нишката „фонова“: програмата свършва, когато свърши главната програма, без да чака безкрайния цикъл. Истинският сървър не я прави фонова: той трябва да работи, докато не го спреш.with client, …затваря вече създадения сокет накрая.- При хиляди клиенти нишка за всеки е скъпо. Тогава приемащият цикъл дава клиентите на басейн от нишки от предишния урок:
pool.submit(handle, client, number).
Пробвай
Двамата чакат
Клиентът праща „кафе“ и чака отговор. Сървърът е същият, но програмата увисва и не печата нищо. На кой ред е грешката?
Избери реда с грешката.
"\n". Сървърът чете с readline(), а той връща чак когато дойде краят на реда. Сървърът чака края на реда, а клиентът чака отговор. Поправката е sock.sendall("кафе\n".encode()).Идеята
UDP: писма без връзка
makefile: пращаш байтове до адрес и порт през сокет от вида SOCK_DGRAM (datagram, отделен пакет):receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)receiver.bind(("localhost", 5103))
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sensor: for reading in ["кухня 21.5", "склад 4.0", "хладилник 3.2"]: sensor.sendto(reading.encode(), ("localhost", 5103))
for i in range(3): data, address = receiver.recvfrom(256) print(f"Температура: {data.decode()} °C")receiver.close()Температура: кухня 21.5 °CТемпература: склад 4.0 °CТемпература: хладилник 3.2 °C- Получателят слуша на порт 5103 с
bind(("localhost", 5103)). Изпращачът не викаbind: системата му дава свободен порт. sendto(байтове, (адрес, порт))праща един пакет.recvfrom(256)чака пакет до 256 байта и връща байтовете и адреса на изпращача.- Никой не потвърждава, че пакетът е стигнал. На
localhostне се губят, но по истинска мрежа може.
По-точно, за любопитните
Ако получателят трябва да отговори, праща нов пакет до адреса, който е върнал recvfrom: receiver.sendto(отговор, address).
Пакетът е ограничен по размер (под 64 KB, а по интернет е разумно до около 1 400 байта), затова за файлове UDP не става.
Идеята
HTTP: езикът на уеба
Браузърът, когато отваряш страница, прави TCP връзка до сървъра и праща текстова HTTP заявка: „дай ми /“. Сървърът връща отговор: статус, заглавки и тяло (HTML, JSON, картинка). В Python това прави urllib.request:
import urllib.request
with urllib.request.urlopen("https://example.com") as response: print("Статус:", response.status) print("Тип:", response.headers["content-type"]) body = response.read().decode() print("Заглавие:", body[body.index("<title>") + 7 : body.index("</title>")])Статус: 200Тип: text/html; charset=utf-8Заглавие: Example Domainurlopen(адрес)праща заявкаGETи чака отговора.withзатваря връзката накрая.response.read()дава тялото като байтове, а.decode()го превръща в текст.- Статусът казва как е минало:
200е успех,404няма такава страница,500е грешка на сървъра. При статус 4xx и 5xxurlopenхвърляurllib.error.HTTPError.
Проверката на кода на сайта е без интернет, затова тази програма е показана с изхода от пускане с мрежа. Без мрежа urlopen хвърля urllib.error.URLError.
По-точно, за любопитните
http.server). Така можеш да пробваш urlopen и без интернет, на localhost:class Stock(BaseHTTPRequestHandler): def do_GET(self): if self.path != "/stock": self.send_error(404) return answer = "кафе: 12 бр.".encode() self.send_response(200) self.send_header("Content-Length", str(len(answer))) self.end_headers() self.wfile.write(answer)
def log_message(self, format, *args): pass # без ред в конзолата за всяка заявка
server = HTTPServer(("localhost", 5104), Stock)threading.Thread(target=server.serve_forever, daemon=True).start()
for path in ["/stock", "/prices"]: try: with urllib.request.urlopen(f"http://localhost:5104{path}") as response: print(path, "->", response.status, response.read().decode()) except urllib.error.HTTPError as e: print(path, "->", e.code, e.reason)server.shutdown()/stock -> 200 кафе: 12 бр./prices -> 404 Not FoundGET сървърът вика do_GET. За /prices той връща 404 със send_error, а urlopen го хвърля като HTTPError. log_message е празен, за да не печата сървърът ред за всяка заявка. В истински проекти за уеб сървър се ползват библиотеки като Flask, Django или FastAPI. Те са построени върху същите сокети.Упражнение
Сървъри на localhost
1. Ехо сървър. Сървърът връща всеки ред с „Ехо: “ отпред, докато не получи „край“. Тогава отговаря „Довиждане“ и затваря връзката. Клиентът праща „здравей“, „как си“ и „край“ и печата отговорите:
Ехо: здравейЕхо: как сиДовиждане2. Сървър с команди. ГЛАВНИ текст връща текста с главни букви, ЧАС връща текущия час като 14:05 (datetime.now().strftime("%H:%M")), а всичко друго връща „Непозната команда: “ и командата. Часът се мени, затова клиентът проверява само вида му с регулярен израз:
ПОРЪЧКА 42 Е ГОТОВАЧасът има вид ЧЧ:ММ: TrueНепозната команда: ВРЕМЕБорис получи: Здрасти, Борис! Свободен ли си в 18:00?Ана получи: Да, ще дойда.Борис получи: Супер, до скоро.Ана получи: Чао!Тест