Мессенджер на gRPC
Опишите gRPC-интерфейс и реализуйте сервер и клиент мессенджера с одним общим чатом. Сервер и клиент общаются по gRPC, пользователь обращается к клиенту по HTTP.
У сервера два RPC-метода: SendMessage отправляет сообщение в чат, ReadMessages открывает подписку на новые сообщения. Сервер должен обрабатывать несколько запросов одновременно, в том числе принимать сообщения при открытых подписках.
Клиент при запуске открывает подписку и сохраняет сообщения от сервера в буфере в порядке получения. Через HTTP пользователь отправляет сообщения и забирает содержимое буфера.
На схеме показаны сервер и два клиента. Тесты обращаются к клиентам от имени двух пользователей:
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, например 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 опишите структуру решения, какие компоненты вы реализовали и как работают методы сервера и клиента. Без отчёта тесты запускаются, но защита не проводится и решение не засчитывается — см. общие правила сдачи.
Баллы за компонент начисляются, только если прошли все его тесты: 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 по общей инструкции. Для Python-заготовки и локального запуска тестов установите зависимости:
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:
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:
# 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 заготовки.
Тестирование решения
Публичные тесты проверяют протокол, сервер и клиент. Сложные конкурентные сценарии вы разберёте на защите.
Полная проверка
Рекомендуемый запуск в окружении тестирующей системы:
docker run --privileged --pull always --rm -v ./solution:/hw/solution distsys.ru/course/grpc-messenger:latest
Сервер и клиент собираются и проверяются независимо: ошибка сборки одного не мешает проверить другой.
Отдельные компоненты через Docker Compose
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
После изменения кода пересоберите соответствующий образ и повторите проверку. После изменения протокола заново сгенерируйте код и выполните:
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:
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:
# Терминал 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-запросов к запущенному клиенту:
curl -X POST localhost:8080/sendMessage -d '{"author": "alice", "text": "hey"}'
curl -X POST localhost:8080/getAndFlushMessages
Сдача решения
Подготовьте solution/readme.md и отправьте решение по общей инструкции. В журнале проверки будут вывод сборки, результаты тестов (после строки === RUN TESTS) и логи контейнеров.
Полезные материалы
grpcurl
grpcurl позволяет вызывать gRPC-методы из терминала. Для Linux и Windows скачайте архив для своей ОС и архитектуры со страницы релизов, распакуйте его и добавьте каталог с исполняемым файлом в PATH. В macOS: brew install grpcurl.
Для проверки сервера откройте подписку в одном терминале, а в другом отправьте сообщение:
# Терминал 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 и официальные примеры.
- Документация Python:
threading,queue,asyncio. - Для работы с несколькими терминалами при желании можно использовать tmux.