Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 18 из 25 ~16 мин +50 XP

Рефлексия сервера

grpc reflection — это сервис, который превращает живой gRPC-сервер в собственную документацию: добавьте один вызов reflection.Register(s), и сервер сможет по запросу отдать список своих сервисов и бинарные дескрипторы proto-файлов, из которых собраны все схемы. На этом механизме стоит grpcurl — «curl для gRPC», главная утилита отладки: она спрашивает у сервера схемы сама, превращает ваш JSON в бинарное protobuf-сообщение на проводе и печатает ответ. Семнадцатый урок дал серверу здоровье — grpc.health.v1.Health отвечает, жив ли он; восемнадцатый даёт ему голос — сервер рассказывает, кто он вообще такой. Здесь вы разберёте, что именно регистрирует reflection.Register и почему появляются сразу две версии сервиса; как устроен протокол ServerReflectionInfo — двунаправленный стрим с пятью видами запросов; что отдаёт list_services и почему в списке видны сами reflection-сервисы; как читать вывод describe и вызывать методы с JSON и заголовками из десятого урока; как ведёт себя protojson-маппинг на int64, bytes и enum; что делать, если рефлексии на сервере нет; и почему в проде её часто выключают — по соображениям безопасности.

Содержание урока · 16 разделов

grpc reflection: каталог, который сервер рассказывает о себе#

У любого gRPC-сервера есть досадное свойство: он говорит на бинарном протоколе, и пока у вас нет proto-файлов, сгенерированных заглушек и под рукой нужных версий, вы не можете отправить ему ни одного запроса — даже чтобы проверить, что сервис вообще жив. Серверную рефлексию придумали именно для этого: стандартный сервис gRPC, добавив который, сервер начинает отдавать сведения о собственных схемах по самому же gRPC. Док пакета reflection в grpc-go формулирует назначение в одну строку: «Package reflection implements server reflection service». Механика проста и рекурсивна по красоте: рефлексия — это обычный gRPC-сервис поверх вашего сервера, и он отвечает на вопросы «какие у тебя сервисы?» и «какие у них схемы?» тем же транспортом, что и бизнес-вызовы. Спросить эти вопросы можно из кода, но главный потребитель — утилита grpcurl из экосистемы: её README честно называет её «basically curl for gRPC servers» и объясняет проблему, которую она решает: «gRPC servers use a binary encoding on the wire», поэтому «they are basically impossible to interact with using regular curl» — обычный curl, не умеющий HTTP/2 и protobuf, бесполезен. Решение — «This program accepts messages using JSON encoding, which is much more friendly for both humans and scripts»: grpcurl принимает данные в JSON, а схему для перекодирования берёт у сервера через рефлексию. Полезно сразу развести роли двух «встроенных» сервисов из соседних уроков: health из семнадцатого отвечает на вопрос «работаешь ли ты» и знает только статусы, а рефлексия отвечает на вопрос «что ты умеешь» — имена сервисов, методы, типы сообщений. Первая нужна оркестратору, вторая — человеку за терминалом, и включают их обычно вместе.

Сервер: reflection.Register рядом с health

GO
// cmd/orders/main.go — тот же сервер, что в семнадцатом уроке,
// плюс один вызов, который включает рефлексию.
import (
        "net"

        "google.golang.org/grpc"
        "google.golang.org/grpc/reflection"

        pb "shop/gen/shop/v1"
        grpc_health_v1 "google.golang.org/grpc/health/grpc_health_v1"
)

s := grpc.NewServer()
pb.RegisterOrderServiceServer(s, &server{}) // бизнес-сервис
grpc_health_v1.RegisterHealthServer(s, healthSrv) // здоровье из урока 17

// Рефлексия: сервер отдаёт список сервисов и дескрипторы схем.
// Регистрировать можно в любой момент до Serve, как и остальные.
reflection.Register(s)

// В проде рефлексию часто гейтят явно — регистрация здесь ваш выбор:
// if cfg.EnableReflection {
//         reflection.Register(s) // например, только вне прода
// }

lis, err := net.Listen("tcp", ":50051")
if err != nil {
        log.Fatal(err)
}
log.Print("listening :50051")
s.Serve(lis)

Сравните с сервером из семнадцатого урока: ничего нового не нужно — ни отдельного порта, ни генерации кода, ни внешних зависимостей. reflection.Register принимает интерфейс GRPCServer — регистратор и поставщик сведений о сервисах, — и *grpc.Server реализует его из коробки. Всё, что рефлексия будет рассказывать о схемах, она берёт из того же реестра, куда ваши сгенерированные заглушки уже положили дескрипторы при инициализации пакетов proto. Один вызов — и сервер стал самодостаточным для отладки.

Что именно регистрируется: две версии сразу#

Первая неожиданность ждёт в списке сервисов после Register: их станет на два больше. Док функции объясняет почему: «Register registers the server reflection service on the given gRPC server. Both the v1 and v1alpha versions are registered» — grpc-go регистрирует сразу обе версии сервиса рефлексии: grpc.reflection.v1.ServerReflection и grpc.reflection.v1alpha.ServerReflection. История тут важна для понимания вывода grpcurl: протокол рефлексии существует давно как v1alpha, а строгое семантически-версионированное v1 появилось позже, и чтобы старые клиенты не сломались, сервер держит оба варианта. В каноническом proto-файле v1alpha об этом сказано прямо: «Warning: this entire file is deprecated. Use this instead» — и рядом option deprecated = true, но исчезать v1alpha не торопится, потому что инструменты разных экосистем до сих пор говорят именно на ней. Если вам точно не нужна обратная совместимость, есть отдельная функция с честным предостережением: «RegisterV1 registers only the v1 version of the server reflection service», дальше док советует: «Many clients may only support v1alpha so most users should use Register instead, at least until clients have upgraded» — большинство должно использовать Register, пока клиенты не обновились. Внутри обе версии — это одна и та же реализация: конструктор NewServerV1 собирает структуру ServerReflectionServer с тремя полями. Поле Services — «The source of advertised RPC services. If not specified, the reflection server will report an empty list when asked to list services» — источник имён сервисов, обычно сам grpc.Server; док уточняет, что список можно и подрезать: «can be customized by wrapping a grpc.Server or using an alternate implementation that returns a custom set of service names» — удобно, если в списке не должно быть внутренних сервисов. Два других поля — резолверы: DescriptorResolver, по умолчанию protoregistry.GlobalFiles (глобальный реестр дескрипторов, куда сгенерированные пакеты кладут свои схемы), и ExtensionResolver, по умолчанию protoregistry.GlobalTypes — реестр расширений. Оба можно подменить своими — рефлексия честно работает через интерфейсы.

Протокол: один двунаправленный стрим вместо методов#

Второе отличие рефлексии от привычных unary-сервисов: у неё один-единственный метод, и это двунаправленный стрим. Комментарий в canonical proto объясняет дизайн: «The reflection service is structured as a bidirectional stream, ensuring all related requests go to a single server» — все связанные запросы гарантированно попадают на один сервер, потому что живут в одном соединении. Метод называется ServerReflectionInfo: клиент пишет сообщения ServerReflectionRequest, сервер отвечает ServerReflectionResponse, и делается это в цикле — recv, обработка, send, пока клиент не закроет стрим. Реализация в grpc-go описывает вопросы честно, по одному доку на каждый: «ListServices returns the names of services this server exposes» — а для символьного поиска proto предписывает формат: «This field should be a fully-qualified symbol name (e.g. <package>.<service>[.<method>] or <package>.<type>)». У запроса есть поле host — легаси, которое сервер просто возвращает обратно в ответе в поле valid_host, — и oneof message_request из пяти вариантов, а у ответа симметричный oneof message_response. Обработчик в реализации grpc-go до смешного лаконичен: switch по типу запроса, и на каждый вариант — либо собранный ответ, либо ErrorResponse с кодом и текстом. В proto это поле описано с обещанием совместимости: «This field uses the error codes defined in grpc::StatusCode» — коды те же, что вы разбираете в обработке ошибок из тринадцатого урока, только едут внутри обычного сообщения, а не в статусе RPC. Это важная деталь: «не найдено» в рефлексии — не ошибка стрима, а нормальный ответ ErrorResponse с кодом NotFound, стрим продолжает работать, и клиент может спросить что-то ещё. Единственный случай, когда стрим умирает настоящей ошибкой, — невалидный запрос без распознанного поля oneof: обработчик выходит с status.Errorf(codes.InvalidArgument, «invalid MessageRequest: %v», ...) — недопустимое сообщение роняет весь вызов, а не превращается в ответ об ошибке. Ответы сервера при этом всегда эхом несут original_request — исходный запрос клиента — так что отладчик видит пару вопрос-ответ целиком.

Пять запросов рефлексии и что приходит в ответ#

Запрос (поле oneof)Что просит клиентОтвет сервера
list_servicesСписок зарегистрированных сервисовListServiceResponse: только полные имена, отсортированы; для незнакомого запроса — ErrorResponse
file_by_filenameФайл по имени пути, как в компиляторе (например shop/v1/order.proto)FileDescriptorResponse с байтами дескриптора и зависимостей или ErrorResponse NotFound
file_containing_symbolФайл, где объявлен символ: сервис, метод или типFileDescriptorResponse; символ не найден — ErrorResponse NotFound
file_containing_extensionФайл, где объявлено расширение типа с заданным номером поляFileDescriptorResponse или ErrorResponse NotFound
all_extension_numbers_of_typeВсе номера расширений указанного типаExtensionNumberResponse с base_type_name и номерами; неизвестный тип — NotFound
неизвестный вариантПоле oneof не распознано вовсеСтрим падает InvalidArgument: «invalid MessageRequest: %v»

Проверка: что вернёт list_services

Сервер регистрирует OrderService и health, затем вызывает reflection.Register(s). Клиент открыл стрим к grpc.reflection.v1 и отправил запрос list_services. Что придёт в ListServiceResponse?

Дескрипторы: сервер отдаёт proto-файлы байтами#

list_services даёт только имена, и весь магический комфорт grpcurl начинается на втором типе запросов — file_containing_symbol и file_by_filename. Ответ — FileDescriptorResponse, и в proto он описан подчеркнуто честно: «Serialized FileDescriptorProto messages. We avoid taking a dependency on descriptor.proto, which uses proto2 only features, by making them opaque bytes instead» — поле это просто массив байт: сериализованные FileDescriptorProto, намеренно непрозрачные, чтобы протокол не зависел от версии descriptor.proto. Клиент сам их разбирает — grpcurl строит из байтов живые схемы и дальше работает с ними как будто у него были ваши proto-файлы. Сколько файлов придёт? Внутренняя реализация grpc-go описывает функцию дословно так: «FileDescWithDependencies returns a slice of serialized fileDescriptors in wire format ([]byte). The fileDescriptors will include fd and all the transitive dependencies of fd with names not in sentFileDescriptors» — сервер отдаёт не один файл, а весь транзитивный граф импортов: описали в order.proto импорт google/timestamp.proto — приедут оба. Map sentFileDescriptors — локальная переменная стрима: сервер помнит, что уже отправил в этом соединении, и не шлёт повторно, что прямо разрешено протоколом: «The reflection service is allowed to avoid sending FileDescriptorProtos that were previously sent in response to earlier requests in the stream». Ищет сервер символ через FindDescriptorByName из реестра, и символ — это не только имя типа: док функции говорит «The given symbol can be a type, a service or a method» — годится и сервис, и метод вида shop.v1.OrderService/GetOrder в записи с точками, и enum, и поле. Не нашлось — ErrorResponse с кодом NotFound и текстом ошибки реестра; файл с путём-плейсхолдером (импортирован, но сам не слинкован в бинарник) трактуется как отсутствующий. Практический нюанс про file_by_filename: путь — это то, под каким именем файл зарегистрирован в реестре дескрипторов, как правило путь от корня proto-модуля, например shop/v1/order.proto, а не расположение на диске сервера; угадать его легко через describe, где grpcurl показывает имена файлов.

Второй запрос в том же стриме

Прочитайте код и выберите, что он выведет:

// Один открытый стрим к ServerReflectionInfo. Клиент шлёт подряд два запроса.
//
// №1: file_containing_symbol = "shop.v1.OrderService"
//     -> сервер отвечает FileDescriptorResponse:
//        [order.proto, common.proto, google/protobuf/timestamp.proto]  (3 файла)
//
// №2: ровно тот же запрос file_containing_symbol = "shop.v1.OrderService"
//     -> что вернёт сервер во ВТОРОМ ответе?

grpcurl: curl для gRPC-сервера#

Теперь инструмент. grpcurl — CLI на Go из fullstorydev, ставится из релизов brew'ем, snap'ом или go install, и его README задаёт ожидания точно: «It's basically curl for gRPC servers». Ключевая мысль, которую стоит проговорить вслух: «In fact, the way the tool transforms JSON request data into a binary encoded protobuf is using that very same schema» — grpcurl кодирует ваш JSON в protobuf по той самой схеме, которую вы ему дадите; если сервер не отвечает рефлексией, README продолжает, «you will either need the proto source files that define the service or need protoset files that grpcurl can use». То есть без схем grpcurl слеп: схемы либо приходят с сервера (рефлексия — путь по умолчанию), либо даются флагами. Командный интерфейс — три глагола из usage: «%s [flags] [address] [list|describe] [symbol]». С list всё просто: адрес и verb, и на выходе отсортированные имена сервисов. С describe шире: «If 'describe' is indicated, the descriptor for the given symbol is shown. The symbol should be a fully-qualified service, enum, or message name. If no symbol is given then the descriptors for all exposed or known services are shown» — без символа describe печатает всё, с символом — конкретную схему, включая методы, поля с номерами и типами, enum-значения. Третий случай — когда ни list, ни describe не указаны: «the symbol must be a fully-qualified method name in 'service/method' or 'service.method' format. In this case, the request body will be used to invoke the named method» — это уже вызов, о нём через блок. Флаг -plaintext: «Use plain-text HTTP/2 when connecting to server (no TLS)» — для локальной отладки без сертификатов, как в двенадцатом уроке; для TLS-сервера понадобится -cacert/-cert/-key. Ещё две удобные мелочи: -max-time — «The maximum total time the operation can take, in seconds. This sets a timeout on the gRPC context», дедлайн для всего вызова, и -max-msg-sz — потолок входящего сообщения, «defaults to 4,194,304 (4 megabytes)». И, разумеется, важно понимать диагноз при его отсутствии: если сервер без рефлексии, grpcurl падает с ошибкой ровно такого текста — «server does not support the reflection API».

grpcurl: list и describe живого сервера

GO
# Список сервисов: только имена, отсортированы.
$ grpcurl -plaintext localhost:50051 list
grpc.health.v1.Health
grpc.reflection.v1.ServerReflection
grpc.reflection.v1alpha.ServerReflection
shop.v1.OrderService

# Схема сервиса: методы, входные и выходные типы.
$ grpcurl -plaintext localhost:50051 describe shop.v1.OrderService
shop.v1.OrderService is a service:
{
  "name": "shop.v1.OrderService",
  "method": [
    {
      "name": "GetOrder",
      "inputType": ".shop.v1.GetOrderRequest",
      "outputType": ".shop.v1.Order"
    }
  ]
}

# Схема сообщения: поля, номера, типы — как в исходном proto.
$ grpcurl -plaintext localhost:50051 describe shop.v1.Order
# (вывод сокращён; ещё есть describe без символа — всё сразу)

Формат вывода describe — человекочитаемый разбор дескриптора, который grpcurl собрал из байтов, полученных через file_containing_symbol. Обратите внимание на inputType с ведущей точкой: так записываются полные имена типов внутри FileDescriptorProto, и именно в таком виде символы принимаются на вход describe. В этом выводе же удобно подсматривать имена файлов для file_by_filename и camelCase-имена полей для JSON — две самые частые причины вернуться к describe посреди отладки.

Вызов метода: -d JSON и адрес service/method#

Настоящий workflow отладки — вызов. Адресация описана в usage дословно: «If neither verb is present, the symbol must be a fully-qualified method name in 'service/method' or 'service.method' format» — слэш или точка, оба варианта grpcurl понимает, в отличие от wire-формата, где разделитель всегда слэш: на проводе путь /shop.v1.OrderService/GetOrder вы видели в четвёртом уроке. Тело запроса — флаг -d: «Data for request contents. If the value is '@' then the request contents are read from stdin» — JSON строкой или из stdin, что удобно для многострочных сообщений и скриптов. Если тело не дали, grpcurl не падает: «If no body is given but one is required (i.e. the method is unary or server-streaming), an empty instance of the method's request type will be sent» — уйдёт пустое сообщение, отличный способ проверить валидацию на сервере. Стримовые методы тоже поддержаны, и это одна из причин предпочесть grpcurl самодельным скриптам: для client- и bidi-стримов несколько JSON-сообщений склеиваются подряд (а в формате text — разделяются служебным байтом), для bidi в интерактивном терминале grpcurl читает stdin построчно и отвечает по мере поступления. Помните из седьмого-девятого уроков, что стрим — это один вызов с несколькими сообщениями: grpcurl покажет каждый ответ отдельным JSON-блоком. Типичный цикл отладки выглядит так: list, чтобы убедиться, что сервер вообще тот; describe сервиса и метода, чтобы снять имена полей; вызов с -d; разбор ошибки в JSON — и повтор. Ошибка кода (не-Nil статус) печатается в stderr, и по умолчанию тело ответа в этом случае не показывается — включите -format-error, чтобы увидеть и сообщение статуса в том же JSON-формате. Для метода из десятого урока с метаданными в ответе пригодится -v: verbose-режим печатает заголовки ответа и трейлеры, где сервер оставляет время или идентификатор трассировки.

protojson: как JSON ложится на protobuf#

grpcurl принимает и печатает JSON по правилам canonical JSON mapping из proto3-спецификации — тому набору правил, который в Go реализован в protojson, а исторически — в предшественнике jsonpb, которым grpcurl и пользуется внутри. Базовых правил немного, и их стоит выучить как таблицу умножения, потому что они объясняют девять десятых ошибок при вызовах. Первое: имена полей. На выходе используется JSON-имя поля — по умолчанию camelCase, полученный из snake_case имени в proto (у поля order_id JSON-имя orderId); на вход принимаются оба варианта — и camelCase, и оригинальное proto-имя, так что «order_id: 7» и «orderId: 7» эквивалентны. Второе: 64-битные целые. int64, sint64, uint64, fixed64 сериализуются как JSON-строки в кавычках: int64-значение сорока двух приедет строкой «42», а не числом — не потерять точность на больших числах; в proto с полем int64 отправить число без кавычек grpcurl обычно тоже простит, но принимать от JSON-ответов строку — привычка правильная. Третье: bytes — base64-строка, стандартная или URL-safe. Четвёртое: enum — строковое имя значения на вход и выход; числовой номер принимается на входе, но в ответах ждите имена. Пятое: WellKnownTypes имеют особые JSON-формы: Timestamp — RFC3339-строка, Duration — «3.5s», Any — объект с @type, Empty — {}. Шестое: неизвестные поля — ошибка: разбор остановится с сообщением про unknown field, если не включить флаг -allow-unknown-fields — «When true, the request contents, if 'json' format is used, allows unknown fields to be present. They will be ignored when parsing the request». Это удобно, когда отлаживаете ответ клиента с лишними полями, но по умолчанию строгость — защита от опечаток. Седьмое: пустые поля в ответе опускаются; если нужен полный вид — флаг -emit-defaults: «Emit default values for JSON-encoded responses». Все эти правила едины и для protojson в вашем Go-коде, если вы когда-нибудь сериализуете сообщения в JSON руками: тот же camelCase, те же строки для int64, тот же base64 — выучив grpcurl, вы выучили protojson.

Что напечатает grpcurl на такой ответ

Прочитайте код и выберите, что он выведет:

// proto (фрагмент):
// message Order {
//   int64  order_id = 1;
//   string sku      = 2;
//   bytes  invoice  = 3;
//   Status status   = 4;   // enum: ACTIVE = 0; PAID = 1; VOID = 2;
// }
//
// Сервер вернул сообщение Order:
//   order_id = 7
//   sku      = "go-book"
//   invoice  = []byte("INV-1")
//   status   = PAID
//
// Что напечатает grpcurl (формат json)?

grpcurl: invoke с телом и заголовком авторизации

GO
# Unary-вызов: адрес service/method, тело в -d, заголовок в -H.
$ grpcurl -plaintext \
    -d '{"id": "7"}' \
    -H 'authorization: Bearer eyJhbGciOi...' \
    localhost:50051 shop.v1.OrderService/GetOrder
{
  "id": "7",
  "sku": "go-book",
  "status": "PAID"
}

# Пустое тело: уйдёт нулевое сообщение — проверка валидации сервера.
$ grpcurl -plaintext -max-time 2 \
    localhost:50051 shop.v1.OrderService/GetOrder

# Здоровье из семнадцатого урока — тем же способом:
$ grpcurl -plaintext -d '{}' localhost:50051 grpc.health.v1.Health/Check
{
  "status": "SERVING"
}

Ключ -H здесь — мост из десятого урока про метаданные: то, что клиент передавал через metadata.AppendToOutgoingContext, в grpcurl делается флагом. Строки-значения заголовков уходят как есть, бинарные значения ждут суффикс -bin в имени, как на обычном wire. Обратите внимание на третью команду: health — такой же gRPC-сервис, и его Check вызывается через рефлексию наравне с бизнес-методами; так вы за одну команду проверяете и транспорт, и состояние сервера.

Проверка: как grpcurl адресует метод

Какие варианты адреса метода grpcurl примет для вызова (без глаголов list/describe)?

Заголовки и токены: -H, -rpc-header и связка с метаданными#

Отладка защищённого сервиса — три четверти работы с grpcurl, и здесь у утилиты три разных флага, которые важно не перепутать. Первый и главный — -H: «Additional headers in 'name: value' format. May specify more than one via multiple flags. These headers will also be included in reflection requests to a server» — заголовки уходят и в ваши RPC, и в reflection-запросы. Это принципиально для связки с десятым уроком: если серверная авторизация живёт в перехватчике, который читает authorization из метаданных каждого вызова, то без токена умрёт и сам describe — рефлексия на вашем сервере ходит через тот же перехватчик. Формально это видно в выводе: reflection-запрос — такой же RPC с путём /grpc.reflection.v1.ServerReflection/ServerReflectionInfo, интерсепторы применяются к нему на общих основаниях; так что -H 'authorization: Bearer ...' — не украшение, а билет внутрь. Второй флаг — -rpc-header: «Additional RPC headers ... will only be used when invoking the requested RPC method. They are excluded from reflection requests» — строго для вызова метода. Когда это нужно? Когда серверный интерсептор рефлексии не должен видеть ваши заголовки, или когда заголовок конфликтует с чем-то в reflection-запросах, или вы просто хотите чистый эксперимент: с -rpc-header list работает без токена, а invoke требует его. Третий — -reflect-header, симметричный: «will only be used during reflection requests and will be excluded when invoking the requested RPC method» — только для рефлексии. Распределение по трём флагам даёт полный контроль: общее — -H, только RPC — -rpc-header, только reflection — -reflect-header. Транспортная часть из двенадцатого урока переносится один в один: -plaintext против -cacert/-cert/-key (взаимный TLS grpcurl тоже умеет), а -insecure с честным предупреждением «(NOT SECURE!)» — только для лабораторий. Пара практических привычек: сначала проверьте доступ без токена (ожидание — честная ошибка Unauthenticated, если авторизация настроена), затем с токеном; и держите -max-time, чтобы зависший вызов не висел терминал.

Проверка: какой флаг заголовков выбрать

Сервер проверяет токен в перехватчике для всех вызовов, включая reflection. Вам нужно, чтобы grpcurl мог и посмотреть схемы, и вызвать метод. Какой флаг передаст authorization в оба вида запросов?

Если рефлексии нет: proto-файлы и protoset#

Сервер без рефлексии — не тупик, просто grpcurl пойдёт в схему с диска. Диагноз вы увидите сразу: «server does not support the reflection API» — это константа ErrReflectionNotSupported из кода grpcurl, которую утилита отдаёт, когда reflection-клиент получил отказ. Первый вариант — исходники: флаг -proto задаёт proto-файл, док читается как инструкция: «The name of a proto source file. Source files given will be used to determine the RPC schema instead of querying for it from the remote server via the gRPC reflection API», а импорты резолвятся через -import-path: «Paths will be searched in the order given. If no import paths are given, all files (including all imports) must be provided as -proto flags» — либо все пути импортов, либо все файлы перечислены явно. Это то самое место, где важен proto import path из третьего урока: grpcurl собирает дескрипторы так же, как protoc, и требует те же пути. Второй вариант — protoset: «The name of a file containing an encoded FileDescriptorSet. This file's contents will be used to determine the RPC schema instead of querying for it from the remote server via the gRPC reflection API» — бинарный файл с набором дескрипторов, который собирает protoc --descriptor_set_out=order.protoset shop/v1/order.proto (при желании с --include_imports, чтобы зависимости приехали в файл, — иначе их придётся подкладывать отдельными -proto). Protoset удобнее в CI: один бинарный артефакт вместо пачки файлов и путей, версия схем зашита в релиз. Есть и гибрид: «-use-reflection ... Defaults to true unless a -proto or -protoset option is provided. If -use-reflection is used in combination with a -proto or -protoset flag, the provided descriptor sources will be used in addition to server reflection to resolve messages and extensions» — список сервисов при этом берётся с сервера, а типы — с диска; полезно, когда сервер знает сервисы, но какие-то зависимости в рефлексию не слинкованы. И заметьте симметрию: у grpcurl есть -protoset-out — «With the list and describe verbs, the listed or described elements and their transitive dependencies will be written to the named file» — можно один раз скачать схемы с живого сервера с рефлексией и дальше ходить в его оффлайн-копию, в том числе на сервер, где рефлексию выключили.

Соберите: сервер с рефлексией и вызов без неё

Впишите имя функции регистрации, имя protoset-файла и флаг plaintext.

// main.go — включаем рефлексию рядом с health из семнадцатого урока import "google.golang.org/grpc/reflection" // ... RegisterOrderServiceServer и RegisterHealthServer выше reflection.(s) // регистрирует сразу v1 и v1alpha // За пределами сервера: соберём protoset из исходников proto: // protoc --proto_path=. --descriptor_set_out= shop/v1/order.proto // Вызов против сервера БЕЗ рефлексии, по protoset: // grpcurl - -protoset order.protoset -d '{"id": "7"}' \ // localhost:50051 shop.v1.OrderService/GetOrder

Безопасность: что рефлексия рассказывает наружу#

Теперь цена вопроса. Рефлексия отдаёт полную карту вашей API-поверхности любому, кто смог открыть соединение: имена всех сервисов и методов, все сообщения со всеми полями и номерами, enum-значения, опции, пути файлов. Для отладки это подарок; для атаки — разведка: вместо слепого перебора метод противник за один describe получает точные имена полей и типы, с которыми можно строить корректные запросы. Критично понимать: у reflection-сервиса нет никакого встроенного доступа-контроля — это обычный gRPC-сервис, зарегистрированный вашим вызовом Register, и защищать его вы должны теми же средствами, что остальной сервер: перехватчик авторизации из одиннадцатого урока, TLS из двенадцатого, сетевые границы. Отсюда рабочие правила прод-гигиены. Правило первое: гейтите регистрацию явно — не вызывайте reflection.Register в проде вовсе или вызывайте по флагу окружения, который включается только на стендах; поскольку регистрация — ваш код, выключатель тоже ваш. Правило второе: если рефлексия нужна в проде (некоторые инспекционные панели и отладочные прокси без неё слепы), держите её за авторизацией: перехватчик, проверяющий токен админа для путей grpc.reflection.v1 и v1alpha, — и не забудьте, что клиенты должны слать токен и в reflection-запросы, то есть grpcurl с -H. Правило третье: сеть важнее кода — отдельный админ-порт или доступ только из внутреннего контура, как и для health из семнадцатого урока: обе «служебные» поверхности закрываются одинаково. Правило четвёртое: вспомните про ServerOptions — список сервиса можно подрезать, обернув провайдер имён, если наружу не должны попадать даже имена внутренних сервисов. И последнее: рефлексия не отдаёт данные — только схемы; утечка схемы неприятна, но это не утечка пользовательских данных, и взвешивать риск стоит именно так.

Чек-лист: grpc reflection и grpcurl#

  • Рефлексия — стандартный gRPC-сервис: reflection.Register(s) регистрирует сразу grpc.reflection.v1.ServerReflection и grpc.reflection.v1alpha.ServerReflection; нужен только v1 — есть RegisterV1, но клиенты пока чаще говорят на v1alpha.
  • Один метод ServerReflectionInfo — двунаправленный стрим: «all related requests go to a single server»; вопросы — oneof message_request, ответы — oneof message_response.
  • list_services отдаёт только отсортированные полные имена из GetServiceInfo — включая health и сами reflection-сервисы; методов и полей в ответе нет.
  • Схемы едут байтами FileDescriptorProto в FileDescriptorResponse вместе со всеми транзитивными импортами; уже отправленное в стриме сервер вправе не слать повторно.
  • Символ для file_containing_symbol — «a type, a service or a method»; не нашлось — ErrorResponse NotFound внутри стрима; неизвестный oneof роняет стрим InvalidArgument.
  • grpcurl: list — имена, describe — схемы (сервис, метод, тип или всё сразу), вызов — service/method или service.method; тело в -d JSON, пустое тело уходит как нулевое сообщение.
  • protojson-правила: camelCase-имена на выходе и оба имени на входе, int64 — строка, bytes — base64, enum — имя, Timestamp/Duration — особые формы, неизвестные поля — ошибка (-allow-unknown-fields).
  • Заголовки: -H уходит и в reflection, и в RPC; -rpc-header — только в вызов; -reflect-header — только в рефлексию; связка с metadata из десятого урока — прямая.
  • Без рефлексии: -proto с -import-path или protoset из protoc --descriptor_set_out; -use-reflection выключается автоматически при явных источниках схем; ошибка отсутствия — «server does not support the reflection API».
  • Прод: рефлексию выключать или гейтить флагом окружения; нужна — за перехватчиком авторизации и сетью; список имён можно подрезать через ServerOptions.

Workflow отладки живого сервиса через grpcurl

Расставьте шаги первого знакомства с чужим gRPC-сервисом в правильном порядке.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: grpc reflection и grpcurl

Термин

grpc.reflection.v1.ServerReflection

нажмите, чтобы перевернуть
Определение

Стандартный сервис рефлексии с единственным методом ServerReflectionInfo — двунаправленным стримом. Включается reflection.Register(s), который ставит сразу v1 и v1alpha («Both the v1 and v1alpha versions are registered»); v1alpha формально deprecated, но до сих пор нужна старым клиентам.

1 / 6

Что дальше по курсу#

Честная оговорка про формат, одна на урок: в самоучителе есть песочница с интерпретатором Go, но сеть в браузере недоступна — gRPC-сервер, kubelet и grpcurl в браузере не запустить, поэтому runner-ов в этом уроке, как и во всём gRPC-курсе, нет; каждая команда из урока проверяется локально за пару минут. Три эксперимента. Первый: включите reflection.Register на сервере из семнадцатого урока, запустите grpcurl -plaintext localhost:50051 list и убедитесь, что в списке четыре имени — оба reflection-сервиса, health и ваш; затем вызовите Check у grpc.health.v1.Health через grpcurl и сравните с клиентским кодом из прошлого урока. Второй: снимите токен-гейт на сервере перехватчиком из одиннадцатого урока и прогоните матрицу флагов — без заголовка, с -rpc-header, с -H: убедитесь на своих глазах, что list падает без токена только в первых двух случаях. Третий: соберите protoset командой protoc --descriptor_set_out и вызовите метод с -protoset против сервера с выключенной рефлексией — ошибка «server does not support the reflection API» исчезнет, а вывод будет тем же. Дальше по маршруту: девятнадцатый урок — оптимизация proto: как устроена сериализация на низком уровне, почему номера полей важнее имён и что такое backward compatibility при выкатке новых версий схем; рефлексия, кстати, станет вашим микроскопом — grpcurl покажет, как схемы выглядят после добавления полей. Перед ним освежите урок про метаданные — флаги -H и -rpc-header ровно про то же, — и урок про health — list_services покажет его сервис в общем списке. Полный маршрут курса — на странице gRPC-Go.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • grpc reflection — стандартный сервис: reflection.Register(s) ставит сразу grpc.reflection.v1 и v1alpha; нужен только v1 — RegisterV1, но клиенты пока чаще говорят v1alpha.
  • Весь протокол — один двунаправленный стрим ServerReflectionInfo: пять видов запросов в oneof message_request и симметричный oneof message_response; невалидный запрос роняет стрим InvalidArgument.
  • list_services отдаёт только отсортированные имена из GetServiceInfo — включая health и обе версии рефлексии; схемы едут отдельным запросом байтами FileDescriptorProto вместе с транзитивными импортами.
  • grpcurl — curl для gRPC: list, describe по символу и вызов service/method или service.method; тело — JSON в -d, пустое тело уходит нулевым сообщением; без схем — «server does not support the reflection API».
  • protojson-правила: camelCase на выходе и оба имени на входе, int64 — строка, bytes — base64, enum — имя, Timestamp/Duration — особые формы; неизвестные поля — ошибка, лечится -allow-unknown-fields.
  • Заголовки: -H идёт и в reflection, и в RPC, -rpc-header — только в вызов, -reflect-header — только в рефлексию; для сервера с авторизацией на всех путях токен нужен уже на list.
  • Если рефлексии нет — -proto с -import-path или protoset из protoc --descriptor_set_out; -use-reflection выключается при явных источниках схем; grpcurl -protoset-out умеет скачать схемы с живого сервера.
  • В проде рефлексию выключают или гейтят: она отдаёт полную карту схем без встроенного контроля доступа; закрывать — перехватчиком, сетью и подрезанным списком имён через ServerOptions.

Домашнее задание

Соберите лабораторию рефлексии на паре сервер/клиент из четвёртого и пятого уроков. Первое: добавьте reflection.Register к серверу OrderService и напишите мини-клиент рефлексии — откройте стрим к grpc.reflection.v1.ServerReflection/ServerReflectionInfo, отправьте list_services и file_containing_symbol для OrderService, распарсите пришедшие байты через protodesc и выведите имена файлов зависимостей; убедитесь, что второй такой же запрос в том же стриме привозит меньше файлов. Второе: прогоните полный workflow grpcurl — list, describe сервиса, describe метода, вызов с -d и пустой вызов без -d — и сравните поведение сервера: что отвечает валидация на нулевое сообщение. Третье: включите авторизацию перехватчиком на все пути, включая reflection, и постройте матрицу: list и invoke без токена, с -rpc-header, с -H; задокументируйте, какие комбинации возвращают Unauthenticated и почему. Четвёртое: соберите protoset через protoc --descriptor_set_out --include_imports, выключите рефлексию на сервере и вызовите метод через -protoset; затем попробуйте -use-reflection вместе с -protoset и объясните по usage, откуда в этом режиме берётся список сервисов. Пятое: напишите JSON с тремя ловушками protojson — int64 числом без кавычек, bytes строкой с произвольной кодировкой, опечаткой в имени поля — и разберите каждую ошибку grpcurl по правилам из урока; повторите с -allow-unknown-fields и объясните, почему для продовой отладки это опасный флаг. Шестое: поднимите два сервера на разных портах — с рефлексией и без — и через grpcurl -protoset-out сохраните схемы первого, чтобы обслужить второй; опишите, какие артефакты и в каком порядке нужны, чтобы восстановить полную схему только из protoset.

Личные заметкиПусто
Хранятся только в вашем браузере (gRPC · Рефлексия сервера)

Вопросы и ответы по уроку

Почему в grpcurl list видны grpc.reflection.v1 и v1alpha, и можно ли их скрыть?

reflection.Register регистрирует обе версии одним вызовом — «Both the v1 and v1alpha versions are registered», — а list_services честно перечисляет всё, что зарегистрировано, включая health и сами reflection-сервисы. Это не баг: рефлексия — такой же сервис на сервере. Скрыть можно тремя способами: перейти на RegisterV1 (останется только v1), обернуть ServiceInfoProvider и вырезать служебные имена из списка через ServerOptions, либо не регистрировать рефлексию вовсе там, где она не нужна. Учтите, что клиенты grpcurl понимают обе версии, так что скрытие v1alpha ради красоты ломает старые инструменты.

grpcurl отвечает «server does not support the reflection API» — что делать?

Сервер без рефлексии не может отдать схемы, а без схем grpcurl не умеет кодировать ваш JSON — README говорит прямо: «the way the tool transforms JSON request data into a binary encoded protobuf is using that very same schema». Два выхода: передать исходники флагами -proto и -import-path (импорты либо все перечислены, либо пути через -import-path) или собрать protoset командой protoc --descriptor_set_out и передать его через -protoset. Проверьте и обратное: если ошибка возникает на сервере, где рефлексия точно включена, убедитесь, что вы используете -plaintext для незащищённого порта, что путь и порт верные и что перехватчик авторизации не отклоняет reflection-запросы с Unauthenticated.

Насколько безопасно оставлять reflection.Register в проде?

По умолчанию — небезопасно: рефлексия без встроенного контроля доступа отдаёт полную карту API (сервисы, методы, поля, типы) любому, кто достиг порта, и экономит противнику фазу разведки. Минимальная гигиена: не вызывать Register в проде или гейтить флагом окружения; если рефлексия нужна (отладочные прокси, инспекционные панели) — закрывать перехватчиком авторизации именно пути grpc.reflection.v1* и grpc.reflection.v1alpha, ограничивать сеть (админ-порт или внутренний контур) и при необходимости подрезать список имён через ServerOptions. Помните и о клиентах: токен должен уезжать и в reflection-запросы, то есть grpcurl с -H, а не с -rpc-header. Сама по себе схема — не данные пользователей, но это карта, по которой данные искать.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Connect25 уроков~12 чпродвинутый25 уроков уже доступны

Connect: современный RPC от Buf

Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.

  • Строить сервисы на connectrpc.com/connect
  • Понимать три протокола и выбирать правильный
  • Настраивать buf: lint, breaking changes, generate
  • Писать interceptors и обрабатывать ошибки