# Мессенджер на 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).