Files
hse-2026/homework/02-grpc-messenger/readme.md
T
2026-09-17 16:34:16 +03:00

297 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Мессенджер на gRPC
Опишите gRPC-интерфейс и реализуйте **сервер** и **клиент** мессенджера с одним общим чатом. Сервер и клиент общаются по gRPC, пользователь обращается к клиенту по HTTP.
У сервера два RPC-метода: `SendMessage` отправляет сообщение в чат, `ReadMessages` открывает подписку на новые сообщения. Сервер должен обрабатывать несколько запросов одновременно, в том числе принимать сообщения при открытых подписках.
Клиент при запуске открывает подписку и сохраняет сообщения от сервера в буфере в порядке получения. Через HTTP пользователь отправляет сообщения и забирает содержимое буфера.
На схеме показаны сервер и два клиента. Тесты обращаются к клиентам от имени двух пользователей:
```mermaid
flowchart RL
subgraph Tests
U1{User 1}
U2{User 2}
end
subgraph Clients with HTTP interface
C1(fa:fa-comments Client 1)
C2(fa:fa-comments Client 2)
end
subgraph gRPC server
S(fa:fa-server Server)
end
C1 -- SendMessage --> S
S -. Stream ReadMessages .-> C1
U1 -- POST /sendMessage --> C1
C1 -- Forward messages in /getAndFlushMessages --> U1
C2 -- SendMessage --> S
S -. Stream ReadMessages .-> C2
U2 -- POST /sendMessage --> C2
C2 -- Forward messages in /getAndFlushMessages --> U2
```
## Требования
### Доставка сообщений
- Подписка начинается, когда сервер регистрирует вызов `ReadMessages`, и действует до отмены RPC или закрытия соединения. Сообщения, принятые до регистрации, в подписку не попадают.
- Сервер передаёт каждое сообщение ровно один раз во все подписки, активные в момент его принятия, включая подписку отправителя. Восстанавливать историю после разрывов и перезапусков не нужно.
- Сообщения, общие для нескольких подписчиков, должны идти в одинаковом порядке во всех их потоках. Клиенты сохраняют этот порядок. Для одновременных вызовов `SendMessage` сервер может выбрать любой порядок.
- `sendTime` — серверное время принятия сообщения. В течение одного запуска эти значения должны быть уникальны и строго возрастать в порядке рассылки. Ответ `SendMessage` и сообщение во всех подписках содержат одинаковый `sendTime`.
- Успешный ответ `SendMessage` означает, что сервер принял сообщение. Это не гарантирует, что все клиенты уже его получили.
### HTTP-интерфейс клиента
Пользователи и тесты обращаются к клиенту через два HTTP-метода.
При успехе оба метода возвращают HTTP `200` и JSON. Поле `sendTime` — строка в [JSON-формате `google.protobuf.Timestamp`](https://protobuf.dev/reference/protobuf/google.protobuf/#timestamp), например `2025-09-20T10:58:42.665193557Z`.
```
POST /sendMessage
Отправляет одно сообщение в общий чат.
Тело запроса:
{
"author": "Ivan Ivanov",
"text": "Hey guys"
}
Тело ответа:
{
"sendTime": "..."
}
```
```
POST /getAndFlushMessages
Возвращает накопленные сообщения в порядке получения и очищает буфер.
Тело запроса: нет
Тело ответа:
[{
"author": "Ivan Ivanov",
"text": "Hey guys",
"sendTime": "..."
},{
"author": "Petr Petrov",
"text": "Hey Ivan",
"sendTime": "..."
}]
```
Если буфер пуст, `getAndFlushMessages` сразу возвращает `[]`. Чтение и очистка буфера должны быть атомарными: сообщение, пришедшее во время этой операции, попадает в текущий или следующий ответ. Клиент не должен терять сообщения, выдавать их повторно или менять их порядок.
### gRPC-интерфейс сервера
Тесты проверяют сервер отдельно от клиента. Соблюдайте требования к интерфейсу:
- синтаксис — `proto3`, пакет — `mes_grpc`;
- gRPC-сервис `MessengerServer` содержит два метода: `SendMessage` и `ReadMessages`;
- `SendMessage` — унарный вызов. Запрос содержит одиночные строковые поля `author` и `text`, ответ — одиночное поле `sendTime` типа `google.protobuf.Timestamp`;
- `ReadMessages` принимает один пустой запрос и возвращает поток сообщений. Можно описать свой тип пустого сообщения или взять готовый из библиотеки. Каждое сообщение в потоке содержит одиночные поля `author` и `text` типа `string` и `sendTime` типа `google.protobuf.Timestamp`.
Все перечисленные поля одного сообщения должны допускать одновременное заполнение.
Имена типов сообщений и номера полей выберите самостоятельно — тесты их не фиксируют.
## Оценивание
За задание можно получить 10 баллов:
- **2 балла** — протокол `messenger.proto`, проверяется в `test_proto.py`.
- **4 балла** — сервер, проверяется в `test_server.py`.
- **4 балла** — клиент, проверяется в `test_client.py`.
В отчёте `solution/readme.md` опишите структуру решения, какие компоненты вы реализовали и как работают методы сервера и клиента. Без отчёта тесты запускаются, но защита не проводится и решение не засчитывается — см. [общие правила сдачи](../readme.md#сдача-решения).
Баллы за компонент начисляются, только если прошли все его тесты: 2 или 0 за протокол, 4 или 0 за сервер, 4 или 0 за клиент. Значение `SCORE` в выводе тестов — предварительная оценка. Итоговую оценку преподаватель выставляет после защиты с учётом штрафов ниже.
Система собирает и проверяет сервер и клиент независимо. Если сервер не собирается или не запускается, он получает 0 баллов, но клиент всё равно проверяется, и наоборот. Протокол проверяется отдельно.
При независимом оценивании клиенты работают со служебным gRPC-сервером, построенным по вашему `messenger.proto`. Поэтому для проверки клиента нужен корректный протокол, но ошибки вашего сервера не влияют на баллы за клиент.
На защите можно потерять баллы за следующие ошибки:
- Сервер не может обрабатывать несколько запросов одновременно — 2 балла.
- При конкурентном доступе сервер может потерять или продублировать сообщения, выдать их в разном порядке в потоках `ReadMessages` либо нарушить требования к `sendTime` — 2 балла.
- Клиент теряет, повторно выдаёт или меняет порядок сообщений из потока `ReadMessages` — 2 балла.
За ошибки сервера снимаются только баллы за сервер, за ошибки клиента — только баллы за клиент, не больше 4 баллов в каждом случае. Баллы за протокол сохраняются. На защите нужно разобрать предложенный преподавателем сценарий конкурентного выполнения и объяснить по своему коду, почему решение работает правильно.
## Заготовки для клиента
В `templates` есть заготовки клиента на Python, Python с asyncio и Go. Официальная заготовка — `messenger-py/client`; она проверена для текущего задания. Остальные заготовки относятся к прошлым версиям задания: их можно использовать, но расхождения с условием нужно исправить самостоятельно. Можно выбрать и другой язык — тесты обращаются к решению через HTTP и gRPC.
## Порядок выполнения задания
Выполняйте команды из папки `homework/02-grpc-messenger`. Примеры с переменными окружения написаны для Bash. В Windows используйте WSL или задавайте переменные через PowerShell.
### Подготовка окружения
Установите Python 3.12 или новее и Docker по [общей инструкции](../readme.md#настройка-окружения). Для Python-заготовки и локального запуска тестов установите зависимости:
```bash
python3 -m pip install -r templates/messenger-py/client/requirements.txt -r tests/requirements.txt
grpcurl -version
```
Если `grpcurl` не найден, установите его по инструкции в разделе «Полезные материалы». На Windows используйте `python` вместо `python3`.
### Структура проекта
Разместите решение в папке `solution`. Сохраните пути к трём файлам, которые используют тесты и [docker-compose.yml](docker-compose.yml):
- `client.dockerfile` — сборка и запуск клиента;
- `server.dockerfile` — сборка и запуск сервера;
- `proto/messenger.proto` — описание gRPC-интерфейса; дополните начальный файл.
При сдаче отправляется только папка `solution`. Изменения за её пределами не учитываются.
Для официальной Python-заготовки скопируйте `templates/messenger-py/client` в `solution/client`, а образец `client.dockerfile` — в `solution/client.dockerfile`. Сервер разместите в `solution/server/server.py`. Заготовка использует пакет `solution` и импорты `from solution.proto import messenger_pb2, messenger_pb2_grpc`. В своей реализации можно выбрать другую структуру, сохранив три обязательных пути выше.
### Описание и компиляция gRPC-интерфейса
Опишите сообщения и сервис в `solution/proto/messenger.proto`.
Сгенерируйте код для выбранного языка с помощью `protoc`:
```bash
# Python
python3 -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --grpc_python_out=. solution/proto/messenger.proto
# Go (после установки protoc и плагинов protoc-gen-go и protoc-gen-go-grpc)
protoc -I solution/proto --go_out=solution/proto --go_opt=paths=source_relative --go-grpc_out=solution/proto --go-grpc_opt=paths=source_relative messenger.proto
```
Для Go укажите в `option go_package` путь пакета в вашем модуле; пример есть в Go-заготовке. Закрепите версии генераторов, совместимые с вашей версией Go. Сгенерированные файлы включите в решение или генерируйте при сборке образа с закреплёнными версиями инструментов.
### Реализация сервера и клиента
Сервер реализуйте с нуля; заготовки для него нет. Можно использовать потоки или асинхронный код. Открытые подписки не должны мешать обработке других запросов.
Если используете Python-заготовку клиента, заполните места с пометкой TODO. HTTP-сервер в ней уже реализован. При запуске клиент должен дождаться сервера, открыть `ReadMessages` и принимать сообщения независимо от обработки HTTP-запросов.
Сервер и клиент должны брать настройки из переменных окружения:
| Переменная | Назначение |
| --- | --- |
| `MESSENGER_SERVER_PORT` | Порт gRPC-сервера, по умолчанию `51075` |
| `MESSENGER_SERVER_ADDR` | Адрес gRPC-сервера для подключения клиента |
| `MESSENGER_HTTP_PORT` | Порт HTTP-интерфейса клиента |
Сервер и HTTP-интерфейс клиента должны слушать на `0.0.0.0`.
В `solution/server.dockerfile` и `solution/client.dockerfile` опишите сборку и запуск сервера и клиента. Контекст сборки — папка `solution`. Включите в образы все нужные файлы и зависимости. За образец можно взять Dockerfile заготовки.
## Тестирование решения
Публичные тесты проверяют [протокол](tests/test_proto.py), [сервер](tests/test_server.py) и [клиент](tests/test_client.py). Сложные конкурентные сценарии вы разберёте на защите.
### Полная проверка
Рекомендуемый запуск в окружении тестирующей системы:
```bash
docker run --privileged --pull always --rm -v ./solution:/hw/solution distsys.ru/course/grpc-messenger:latest
```
Сервер и клиент собираются и проверяются независимо: ошибка сборки одного не мешает проверить другой.
### Отдельные компоненты через Docker Compose
```bash
docker compose build tests
# Протокол
docker compose run --rm --no-deps tests --component proto
# Сервер
docker compose build server-tests
docker compose run --rm server-test-runner
# Клиент со служебным сервером
docker compose build client-test1
docker compose run --rm client-tests
```
После изменения кода пересоберите соответствующий образ и повторите проверку. После изменения протокола заново сгенерируйте код и выполните:
```bash
docker compose down
docker compose build tests server-tests client-test1
docker compose run --rm tests
```
Смотрите логи через `docker compose logs`, останавливайте контейнеры командой `docker compose down`. После обновления задания скачайте свежий служебный образ: `docker compose pull client-test-server`.
### Ручная отладка своей связки (необязательно)
Запустите свой сервер и два клиента через Compose:
```bash
docker compose build server client1
docker compose up -d server client1 client2
```
Сервер доступен на `localhost:51075`, клиенты — на `localhost:8080` и `localhost:8081`. Compose запускает клиентов раньше сервера. После правок пересоберите соответствующий образ и повторите `up`; для логов и остановки используйте команды выше.
Без Docker запустите компоненты и тестер в отдельных терминалах из папки задания. Для официальной Python-заготовки и сервера в `solution/server/server.py`:
```bash
# Терминал 1
python3 -m solution.server.server
# Терминал 2
python3 -m solution.client.client
# Терминал 3
MESSENGER_HTTP_PORT=8081 python3 -m solution.client.client
# Терминал 4
python3 tests/main.py
```
Если вы добавили зависимости, установите и их. Чтобы проверить один компонент, передайте тестеру `--component proto`, `--component server` или `--component client`. По умолчанию проверяются все компоненты (`--component all`).
Здесь клиенты работают с вашим сервером, поэтому его ошибки могут повлиять на клиентские тесты. Для независимой проверки клиента используйте Compose со служебным сервером.
Примеры HTTP-запросов к запущенному клиенту:
```bash
curl -X POST localhost:8080/sendMessage -d '{"author": "alice", "text": "hey"}'
curl -X POST localhost:8080/getAndFlushMessages
```
### Сдача решения
Подготовьте `solution/readme.md` и отправьте решение по [общей инструкции](../readme.md#сдача-решения). В журнале проверки будут вывод сборки, результаты тестов (после строки `=== RUN TESTS`) и логи контейнеров.
## Полезные материалы
### grpcurl
[grpcurl](https://github.com/fullstorydev/grpcurl) позволяет вызывать gRPC-методы из терминала. Для Linux и Windows скачайте архив для своей ОС и архитектуры со [страницы релизов](https://github.com/fullstorydev/grpcurl/releases), распакуйте его и добавьте каталог с исполняемым файлом в `PATH`. В macOS: `brew install grpcurl`.
Для проверки сервера откройте подписку в одном терминале, а в другом отправьте сообщение:
```bash
# Терминал 1: поток остаётся открытым; Ctrl+C отменяет вызов
grpcurl -proto solution/proto/messenger.proto -plaintext localhost:51075 mes_grpc.MessengerServer/ReadMessages
# Терминал 2
grpcurl -proto solution/proto/messenger.proto -d '{"author": "alice", "text": "hello"}' -plaintext localhost:51075 mes_grpc.MessengerServer/SendMessage
```
### Конкурентная обработка в Python
При конкурентном доступе к общим изменяемым данным учитывайте возможные гонки. Структуры данных и способы синхронизации выберите самостоятельно.
- [gRPC Basics Tutorial](https://grpc.io/docs/languages/python/basics/) и [официальные примеры](https://github.com/grpc/grpc/blob/master/examples).
- Документация Python: [`threading`](https://docs.python.org/3/library/threading.html), [`queue`](https://docs.python.org/3/library/queue.html), [`asyncio`](https://docs.python.org/3/library/asyncio.html).
- Для работы с несколькими терминалами при желании можно использовать [tmux](https://github.com/tmux/tmux/wiki/Getting-Started).