297 lines
21 KiB
Markdown
297 lines
21 KiB
Markdown
# Мессенджер на 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).
|