Взаимодействие на основе патчей. Работа с сетью
TODO в 2025-м эта лекция закончилась за час — возможно, стоит питонью часть делать подробнее (не копипастить код коротких примеров, а писать их в прямом эфире)?
Ещё про работу с историей
Немного лайфхаков:
Отложенные правки: git-stash (Статья на Skillbox)
Потерянные и случайно убитые коммиты: git-reflog
Адресация истории от последнего (подписанного) тега: git-describe
Применение изменений из произвольных коммитов: git-cherry-pick
Статья на Хабре про стратегии git merge
…
Работа с патчами и наборами патчей
Немного о формате
(в частности, diff -u)
- понятие контекста
- fuzzy контекст
Пример: (python3 -c "import calendar; print(calendar.__file__)"):
1 $ cp /usr/lib64/python3.13/calendar.py . 2 $ sed 's/WEDNESDAY/ADDAMS/' calendar.py > calendar1.py 3 $ sed 's/# Exception/# Exception:\n# /' calendar.py > calendar2.py 4 $ diff -u calendar.py calendar1.py > patch1 5 $ diff -u calendar.py calendar2.py > patch2 6 $ patch --verbose < patch2 7 $ patch --verbose < patch1
Похоже на git diff!
BTW, [g]vimdiff и прочие mergetool-ы умеют распознавать синтаксис patch-файла
Патчи и Git:
git-format-patch и git-am / git-apply
Замечание: git apply / git am не умеет в fuzzy (и правильно!), но в некоторых случаях срабатывает git am -3
⇒ иногда уместнее patch -u или patch --git
Если пересекаются сами изменения, а не контекст — только mergetool.
- Серия коммитов ⇒ серия патчей, условная нестрогость порядка
Патч или набор с точки зрения GIT — это сериализация коммитов, превращение их в пригодный для передачи формат.
Пример взаимодействия:
1 $ cp /usr/lib64/python3.13/calendar.py .
2 $ git add .
3 $ git commit -a -m initial
4 $ git tag root
5 $ sed -i 's/WEDNESDAY/ADDAMS/' calendar.py
6 $ git commit -a -m "Addams"
7 $ sed -i 's/= 2/= 13/' calendar.py
8 $ git commit -a -m "Thirteen"
9 $ git format-patch HEAD~2
10 $ git reset --hard HEAD~2
11 $ git am *.patch
- Так работает, а вот в обратном порядке патчи не наложатся
BTW: difflib
Простейший сетевой сервер
Простейший низкоуровневый сервер с помощью socket
1 import socket
2
3 HOST, PORT = 'localhost', 1337
4 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
5 s.bind((HOST, PORT))
6 s.listen()
7 while True:
8 conn, addr = s.accept()
9 with conn:
10 print('Connected by', addr)
11 while data := conn.recv(1024):
12 conn.sendall(data.swapcase())
Флаг SO_REUSEADDR устанавливается для того, чтобы система освободила сокет немедленно после его закрытия. В некоторых случаях (TODO каких?) она может так не делать.
Главный недостаток: в каждый момент обрабатывает не более одного подключения
В действительности всю логику после установления соединения (s.accept()) надо выполнять в отдельном процессе / / потоке / корутине:
1 import socket
2 import multiprocessing
3
4 def serve(conn, addr):
5 with conn:
6 print('Connected by', addr)
7 while data := conn.recv(1024):
8 conn.sendall(data.swapcase())
9
10 HOST, PORT = 'localhost', 1337
11 with socket.create_server((HOST, PORT)) as s:
12 s.listen()
13 while True:
14 conn, addr = s.accept()
15 multiprocessing.Process(target=serve, args=(conn, addr)).start()
Тупой аналог netcat с помощью socket:
1 import sys
2 import socket
3
4 host = "localhost" if len(sys.argv) < 2 else sys.argv[1]
5 port = 1337 if len(sys.argv) < 3 else int(sys.argv[2])
6 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
7 s.connect((host, port))
8 while msg := sys.stdin.buffer.readline():
9 s.sendall(msg)
10 print(s.recv(1024).rstrip().decode())
Обязателен «ответ» после «запроса» (send() и receive() чередуются, именно поэтому в большинстве старых протоколов так принято делать!)
В общем случае программа сильно сложнее. Мы будем пользоваться netcat, netcat или ntsh
Asyncio Streams
Недостаток threading — необходимость следить за thread safe и синхронизация по доступу к данным; недостаток multiprocessing — требует отдельной дисциплины по передаче обработанных данных из процесса в процесс.
Выход: asyncio. Посмотрим в примеры. В частности, echo-сервер:
1 import asyncio
2
3 async def echo(reader, writer):
4 while data := await reader.readline():
5 writer.write(data.swapcase())
6 writer.close()
7 await writer.wait_closed()
8
9 async def main():
10 server = await asyncio.start_server(echo, '0.0.0.0', 1337)
11 async with server:
12 await server.serve_forever()
13
14 asyncio.run(main())
Это asyncio (вспоминаем):
await вместо yield from и в целом Python async
явный yield выбрасывает корутину в скрытый управляющий цикл,
В примере тоже строго чередуются send() и receive()
- Сервер принимает несколько TCP-соединений
- обрабатываются они не в отдельных тредах (и не в отдельных процессах), а асинхронно в одном потоке вычислений
- ⇒ любое исключение в одной из запущенных корутин останавливает весь сервер
- → Из двух зол выбрано меньшее: все исключения молча игнорируются
- обрабатываются они не в отдельных тредах (и не в отдельных процессах), а асинхронно в одном потоке вычислений
Используем Streams для написания «общего чата».
- Входящий поток от одного соединения сервер ретранслирует на все остальные
- Поскольку нет многопоточности, гонки могут возникнуть только на неатомарных операциях
Например, stream — это последовательность байтов, и мы используем .readline() для того, чтобы считывать из неё по одной строке. А вот клиент, использующий socket.recv(1024), примет несколько строк разом.
Ретрансляцию сделаем с помощью asyncio.Queue для каждого клиента
ID клиента — это IP+порт (.get_extra_info('peername'))
- Нет простого способа узнать, есть ли входящие данные в Stream
⇒ асинхронно запустим send() (из Stream) и receive() (из Queue), какой успеет первым, такой и обработаем
- При отключении клиента остановим оба необработанных Task-а (иначе они останутся в планировщике)
1 #!/usr/bin/env python3
2 import asyncio
3
4 clients = {}
5
6 async def chat(reader, writer):
7 me = "{}:{}".format(*writer.get_extra_info('peername'))
8 print(me)
9 clients[me] = asyncio.Queue()
10 send = asyncio.create_task(reader.readline())
11 receive = asyncio.create_task(clients[me].get())
12 while not reader.at_eof():
13 done, pending = await asyncio.wait([send, receive], return_when=asyncio.FIRST_COMPLETED)
14 for q in done:
15 if q is send:
16 send = asyncio.create_task(reader.readline())
17 for out in clients.values():
18 if out is not clients[me]:
19 await out.put(f"{me} {q.result().decode().strip()}")
20 elif q is receive:
21 receive = asyncio.create_task(clients[me].get())
22 writer.write(f"{q.result()}\n".encode())
23 await writer.drain()
24 send.cancel()
25 receive.cancel()
26 print(me, "DONE")
27 del clients[me]
28 writer.close()
29 await writer.wait_closed()
30
31 async def main():
32 server = await asyncio.start_server(chat, '0.0.0.0', 1337)
33 async with server:
34 await server.serve_forever()
35
36 asyncio.run(main())
Д/З
Почитать про asyncio
…в частности про разработку и отладку для asyncio
Про читать про Streams и поупражняться в них
- Превратить «общий чат» в «коровий» следующим образом:
- Вводимые строки состоят из команды с возможными параметрами
Вместо get_extra_info('peername') уникальным идентификатором пользователя является название коровы из python-cowsay, под которым он регистрируется
- Пока пользователь не зарегистрировался, он не имеет право ни писать, ни получать сообщения
Сообщения оформляются с помощью cowsay() из модуля python-cowsay
- Команды:
who — просмотр зарегистрированных пользователей
cows — просмотр свободных имён коров
login название_коровы — зарегистрироваться под именем название_коровы
say название_коровы текст сообщения — послать сообщение пользователю название_коровы
yield текст сообщения — послать сообщение всем зарегистрированным пользователям
quit — отключиться
Вместо клиента можно пользоваться либо системным netcat, либо скриптом из простого модуля netcat, либо чуть более продвинутым ntsh
Разработку вести согласно дисциплине оформления коммитов в подкаталоге 05_DiffPatchNet отчётного репозитория по Д/З
Предполагается, что модуль python-cowsay устанавливается в окружение с помощью pipenv, в каталоге должен присутствовать соответствующий Pipfile
