Files
hse-2026/homework/02-grpc-messenger/readme.md
T

297 lines
21 KiB
Markdown
Raw Normal View History

2026-09-17 16:34:16 +03:00
# Мессенджер на 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).