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

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

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

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

Протоколы: gRPC, gRPC-Web, Connect

Три протокола — один и тот же сервер. Разбираем connect протоколы на уровне проводов: как handler выбирает протокол по Content-Type, из чего состоят пятибайтовые фреймы gRPC, куда gRPC-Web прячет трейлеры, почему собственный протокол Connect отвечает честными HTTP-статусами и умеет GET-кеш, и как выглядит выбор «grpc web vs connect» на практике.

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

Третий урок закончился обещанием: разобрать, как один и тот же handler обслуживает curl с JSON, классических gRPC-клиентов и браузер. Обещание в силе, и сегодня мы спускаемся на уровень HTTP-проводов — это сердце курса, потому что здесь лежит ответ на вопрос «что такое Connect». Возьмите проект из урока об установке: сервер NoteService на localhost:8080 из третьего урока уже умеет всё, о чём пойдёт речь, — ничего дописывать не придётся, только смотреть в терминал и curl. План такой: сначала дисциплина диспетчеризации — по какому признаку handler различает connect протоколы; затем анатомия классического gRPC — пятибайтовые конверты, HTTP/2, трейлеры; дальше gRPC-Web — что именно он перепрятал и когда нужен Envoy; потом собственный протокол Connect — тело без конвертов, HTTP-статусы вместо трейлеров и GET-кеш для процедур без побочных эффектов; и в финале — таблица выбора «grpc web vs connect» и разбор того, как три протокола живут на одном сервере, не мешая друг другу. Это connect протоколы в одном уроке: прочитав его, вы сможете предсказать любой ответ сервера по одному заголовку запроса.

Диспетчер по Content-Type: один handler, три обработчика#

Главный факт урока: протокол выбирается не сборкой, не конфигом и не отдельным портом, а заголовком Content-Type каждого запроса. Сгенерированный handler, который вернул NewNoteServiceHandler, — это три обработчика под одним фасадом: собственный протокол Connect, классический gRPC и gRPC-Web. Каждый HTTP-запрос handler смотрит на Content-Type и передаёт его профильному обработчику: application/grpc — gRPC; application/grpc-web и его варианты — gRPC-Web; application/json и application/proto — собственный протокол Connect. Для стриминговых процедур те же роли играют application/connect+proto и application/connect+json. Диспетчеризация занимает микросекунды и не создаёт ни горутин, ни буферов заранее: это обычный switch по строке заголовка. Почему это возможно? Потому что все три протокола согласованы сверху: один и тот же путь /notes.v1.NoteService/GetNote, одни и те же message-типы, один и тот же словарь из шестнадцати кодов ошибок. Различаются они только упаковкой: как сериализуется сообщение, где едут метаданные и где объявляется итог вызова. Именно поэтому схему не нужно трогать при смене протокола — первым уроком мы это зафиксировали, теперь видите механику.

Content-Type → протокол: таблица диспетчеризации#

Content-Type запросаПротоколФормат телаТиповой клиент
application/grpcклассический gRPCфреймы protobufgrpc-go, grpcurl, чужие сервисы
application/grpc+protoклассический gRPCфреймы protobufявное указание кодека
application/grpc-webgRPC-Webфреймы protobufстарые браузерные клиенты, Envoy
application/grpc-web+protogRPC-Webфреймы protobufбраузерные клиенты grpc-web
application/grpc-web+jsongRPC-Webфреймы JSONредкий, но рабочий вариант
application/jsonпротокол ConnectJSON без конвертовcurl, браузеры, devtools
application/protoпротокол Connectprotobuf без конвертовConnect-клиент, экономия трафика
application/connect+protoпротокол Connect (стримы)фреймы protobufстриминговые вызовы
application/connect+jsonпротокол Connect (стримы)фреймы JSONстриминговые вызовы

Протокол Connect: curl-вызов в чистом виде

GO
$ curl -i \
    --header "Content-Type: application/json" \
    --data '{"id": "n1"}' \
    http://localhost:8080/notes.v1.NoteService/GetNote

HTTP/1.1 200 OK
Content-Type: application/json

{"id":"n1","title":"Первая заметка"}

Это тот же вызов, что мы делали в третьем уроке, но теперь вы знаете, что произошло внутри: заголовок application/json выбрал обработчик собственного протокола, тело запроса — просто сериализованный GetNoteRequest без конвертов, а итог вызова объявлен HTTP-статусом. Никаких фреймов, никаких трейлеров, никаких полей-невидимок: 200 — «вызов удался», 404 — not_found, 429 — resource_exhausted. Именно эта честность делает протокол Connect лучшим другом curl, devtools и нагрузочных прокси: каждое слово на проводе читается без спецификации.

Анатомия gRPC: конверты из пяти байт#

Теперь классика. gRPC определён поверх HTTP/2, и каждое сообщение в нём едет в конверте: один байт флага сжатия плюс четыре байта длины big-endian, и только затем байты сериализованного сообщения. Пять служебных байтов на каждое сообщение — это цена, за которую протокол получает независимость от кодека и поддержку сжатия на уровне сообщения. Метаданные в gRPC — не HTTP-заголовки, а отдельный механизм HTTP/2: request-заголовки уезжают в HEADERS-кадрах, а итог вызова — в трейлерах, специальных кадрах после тела. Успешный unary-вызов выглядит как HTTP 200 с телом из фреймов и трейлером grpc-status: 0; неудачный — как HTTP 200 с трейлерами grpc-status: 5 и grpc-message: not found. Да, вы не ослышались: в классическом gRPC ошибка едет с кодом 200. HTTP-статус в этом протоколе почти ничего не значит — единственный источник правды grpc-status в трейлерах. Отсюда и жёсткое требование транспорта: HTTP/2 обязателен, потому что трейлеров на HTTP/1.1 не бывает. В браузерном Fetch API трейлеры недоступны — помните тупик из первого урока? Вот его техническая причина: JavaScript физически не может прочитать grpc-status, каким бы сервер ни был.

Протокол gRPC: тот же вызов через grpcurl

GO
$ grpcurl -plaintext \
    -import-path proto -proto notes/v1/notes.proto \
    -d '{"id": "n1"}' \
    localhost:8080 notes.v1.NoteService/GetNote

{
  "id": "n1",
  "title": "Первая заметка"
}

grpcurl — швейцарский нож gRPC-мира, и наш Connect-сервер отвечает ему без единой правки: grpcurl отправляет application/grpc, диспетчер отдаёт запрос gRPC-обработчику, тот разбирает пятибайтовые конверты. Флаг -plaintext нужен для локального cleartext-сервера, а -import-path и -proto — потому что reflection у нас не включён: без них grpcurl не знает схемы. Если включить пакет reflection из экосистемы Connect, схемы станут известны серверу, и вызов упростится до grpcurl -plaintext -d '...' localhost:8080 notes.v1.NoteService/GetNote. Заметьте в выводе: JSON здесь — косметика grpcurl, по проводу ехали protobuf-фреймы.

Запрос пришёл на наш handler с заголовком Content-Type: application/grpc-web+json. Что произойдёт?

gRPC-Web: трейлеры, спрятанные в тело#

gRPC-Web — это gRPC, адаптированный под браузерные ограничения: он работал поверх HTTP/1.1 и не требовал доступа к трейлерам, которых в браузере нет. Как? Трейлеры переехали из HTTP/2-кадров в конец тела ответа: последним элементом идёт текстовый фрейм grpc-status: 0 (или кодом ошибки), и клиентская библиотека вычитывает его из тела. На практике gRPC-Web почти всегда едет с protobuf-кодеком — бинарным фреймованным потоком, поэтому «человекочитаемость» здесь заканчивается на заголовках. Историческая раскладка такая: классический gRPC-сервер не понимает gRPC-Web, поэтому перед ним ставили переводчик — Envoy с grpc-web-фильтром, который переупаковывал запросы и ответы; это и есть «gRPC-Web требует прокси», фраза из первого урока. Connect-сервер переводчик не нужен: gRPC-Web-обработчик встроен в handler, и запрос application/grpc-web+proto обслуживается тем же кодом, что и application/grpc. При этом gRPC-Web сохраняет главный рудимент родителя: итог вызова не в HTTP-статусе — ответ обычно 200, а grpc-status вычитывается из тела, и лог-системы на уровне HTTP видят «200 OK» даже на ошибку. Это не дефект реализации, а наследие протокола: HTTP/1.1-совместимость стоит того, чтобы жертвовать статусами.

Куда gRPC-Web кладёт итоговый код вызова — grpc-status?

Собственный протокол Connect: unary без конвертов#

Третий обработчик обслуживает протокол, который Buf спроектировал с нуля, учтя ошибки предшественников — дальше будем звать его просто «протокол Connect». Unary-вызов на нём максимально плоский: POST по пути /package.Service/Method, тело — сериализованное сообщение без конвертов, Content-Type application/json или application/proto объявляет кодек. Итог вызова объявляется настоящим HTTP-статусом по таблице кодов из первого урока: not_found — 404, invalid_argument — 400, resource_exhausted — 429, unimplemented — 501, unavailable — 503. На ошибку тело — маленький JSON с полями code, message и опциональным details; на успех — обычный сериализованный ответ. Метаданные — обычные HTTP-заголовки в обе стороны, никаких HEADERS-кадров и текстовых фреймов-трейлеров. Стриминговые процедуры устроены чуть сложнее: там сообщения едут фреймами, потому что потоку нужна делимитация, и кодек объявляется конвертированным типом application/connect+proto или application/connect+json — но даже в стримах итог вызова остаётся в HTTP-статусе для серверного стриминга и в финальном фрейме-конце для двунаправленного. Дизайн-цель протокола сформулирована в его спецификации честно: RPC должен читаться человеком в devtools и жить на любом HTTP-транспорте, включая HTTP/1.1, без прокси-переводчиков.

Протокол Connect: ошибка на проводе

GO
$ curl -i \
    --header "Content-Type: application/json" \
    --data '{"id": "missing"}' \
    http://localhost:8080/notes.v1.NoteService/GetNote

HTTP/1.1 404 Not Found
Content-Type: application/json

{"code":"not_found","message":"заметка не найдена"}

Та же схема из шестого урока, увиденная с проводной стороны: connect.NewError(CodeNotFound, ...) на сервере превратился в HTTP 404 и JSON с code и message. Сравните с классическим gRPC, где тот же код ехал бы трейлером grpc-status: 5 при HTTP 200, и с gRPC-Web, где он лежал бы в конце тела. Одно и то же событие «заметки нет» — три упаковки; код шестнадцати значений один и тот же, поэтому клиенты на любом стеке ветвятся одинаково.

Процедура помечена NO_SIDE_EFFECTS, вызов ушёл GET-ом. Чем ответит сервер?

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

// Схема notes.v1, метод GetNote:
rpc GetNote (GetNoteRequest) returns (Note) {
  option idempotency_level = NO_SIDE_EFFECTS;
}

// Клиент:
$ curl -i "http://localhost:8080/notes.v1.NoteService/GetNote?connect=v1&encoding=json&message=eyJpZCI6Im4xIn0%3D&base64=1"

GET-кеш: idempotency_level и что это вам даёт#

Раз вызов без побочных эффектов уезжает GET-ом по обычному URL, его начинает обслуживать вся HTTP-инфраструктура кеширования: браузерный HTTP-кеш, прокси корпоративной сети, CDN на границе, а также ретраи становятся безопасными — повторить GET можно без страха задвоить запись. В gRPC GET-вызовы существовали только как редко реализованная экзотика спецификации, в gRPC-Web их нет вовсе, у Connect это первоклассный путь. Правила гигиены: помечайте NO_SIDE_EFFECTS только действительно чистые процедуры — чтения, подсчёты, статусы; метод с пометкой обязан не иметь последствий, иначе кеш превратит первый вызов в единственный настоящий. Учтите и границу: GET-оптимизация — собственность протокола Connect; клиенты на gRPC и gRPC-Web будут ходить тем же методом POST, и это нормально — контракт от этого не ломается, пометка просто игнорируется теми обработчиками. И да, кешу нужна помощь с заголовками Cache-Control — их ставит ваш middleware или CDN-политика; протокол гарантирует форму запроса, а политику устаревания определяете вы.

Бинарный gRPC-Web-запрос прилетел прямо на Connect-handler без Envoy. Что будет?

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

// Наш Connect-сервер на localhost:8080. Envoy и других прокси НЕТ.
$ curl -i \
    --header "Content-Type: application/grpc-web+proto" \
    --data-binary @request.bin \
    http://localhost:8080/notes.v1.NoteService/GetNote

grpc web vs connect: таблица выбора#

Соберём всё в одну карту — она отвечает на вопрос, который вы будете задавать себе в каждом новом проекте. Протокол gRPC выбирайте, когда оба конца ваши и нативные: сервис-сервис внутри периметра, клиенты на Go/Kotlin/Java, HTTP/2 гарантирован от начала до конца. gRPC-Web — когда в цепочке есть браузер, но сервер остаётся классическим gRPC, и прокси-переводчик уже стоит или стоит того по другим причинам; это протокол совместимости, а не выбора. Собственный протокол Connect — дефолт для новых проектов: он обслуживается тем же handler-ом, читается curl-ом, живёт на HTTP/1.1, даёт браузеру то же самое без прокси и добавляет GET-кеш для чистых процедур. Спор «grpc web vs connect» на практике решается одним вопросом: есть ли у вас уже классический gRPC-бэкенд и Envoy? Если да — gRPC-Web достался бесплатно; если вы начинаете с чистого листа — берите протокол Connect и не оглядывайтесь, а gRPC оставьте как опцию совместимости, которая включена всегда и без настройки.

Три протокола на одном сервере: сравнительная карта#

СвойствоgRPCgRPC-WebConnect
Транспорттолько HTTP/2HTTP/1.1 или HTTP/2HTTP/1.1 или HTTP/2
Unary на проводефреймы 5 байт + телофреймы 5 байт + телочистое тело без конвертов
Итог вызоватрейлер grpc-statusтекстовый фрейм в телеHTTP-статус ответа
Неудачный вызов выглядитHTTP 200 + grpc-status: 5HTTP 200 + фрейм в концеHTTP 404 + JSON ошибки
Браузер напрямуюнет: трейлеров нет в JSда, но трейлеры из телада, без оговорок
Envoy/проксине нуженнужен перед gRPC-серверомне нужен
curl-дружелюбностьпочти нет: нужен grpcurlнет: бинарные фреймыда: читается целиком
GET-кешнетнетда: NO_SIDE_EFFECTS
Типовой сценарийсервис-сервис, строгий ingressбраузер + готовый gRPC-бэкендновые API, curl, браузер

Совместимость: один контракт, любое число клиентов#

Диспетчеризация по Content-Type даёт свойство, ради которого всё затевалось: контракт один, клиентов сколько угодно. Пометим это на примере notes.v1, который мы собираем с первого урока: хендлер GetNote возвращает одну и ту же заметку — но curl получает JSON со статусом 404 на промахе, grpc-клиент из курса по gRPC-серверам — трейлер grpc-status: 5, а TypeScript-клиент девятого урока — объект ошибки с кодом not_found. Ни одна строка серверного кода не знает, кто на другом конце: обработчики протоколов переводят один и тот же *connect.Error в формат своего провода. Отсюда три практических вывода. Первый: версии схем эволюционируют независимо от протоколов — совместимость живёт в protobuf-поле за полем. Второй: метрики и логи на уровне HTTP стоит снимать с Content-Type — вы увидите, каким протоколом реально пользуются, и увидите, что gRPC-клиентов у вашего Connect-сервера может не оказаться вовсе. Третий: переезд клиента с gRPC на протокол Connect — это строка в его конфигурации, а не серверная эпопея: handler принял бы его завтра без деплоя.

Браузерное приложение на TypeScript ходит в ваш новый сервис на Connect. Какой протокол поедет по умолчанию и что для этого нужно?

Один mux, много сервисов: маршрутизация не зависит от протокола#

Последний кирпич — маршрутизация. Путь каждого вызова — это строка /package.Service/Method из схемы, и регистрация сервиса в ServeMux не зависит от того, каким протоколом к нему поедут: path, handler := notesv1connect.NewNoteServiceHandler(&notesServer{}); mux.Handle(path, handler). Хотите второй сервис — второй mux.Handle рядом, как мы делали с ElizaService в третьем уроке; пути не пересекаются, потому что включают полное имя сервиса. Важно понять уровень, на котором развилка: mux различает сервисы по пути, а handler внутри одного сервиса различает протоколы по Content-Type — два независимых измерения маршрутизации, и каждое решается своим механизмом. Поэтому «три обработчика на одном сервере» — не фигура речи: на одном порту живут все три протокола для каждого сервиса, и ни один из них не конфликтует с middleware, health-чеками и статикой, потому что всё это обычный net/http. Никакого отдельного порта «для gRPC», никакого отдельного listener-а: если вы видели такие конструкции в чистом grpc-go, здесь они не нужны — и это одна из самых недооценённых экономик курса.

Соберите три вызова одного метода

Восстановите Content-Type каждого вызова: JSON для протокола Connect, grpc-web для браузерного варианта, connect+json для стримов.

# 1. Протокол Connect, unary, человекочитаемо: curl --header "Content-Type: application/" \ --data '{"id": "n1"}' \ http://localhost:8080/notes.v1.NoteService/GetNote # 2. gRPC-Web, protobuf-фреймы (браузерный вариант): --header "Content-Type: application/+proto" # 3. Протокол Connect, стриминговая процедура: --header "Content-Type: application/"

Путь одного curl-запроса через диспетчер

Расставьте по порядку события между curl-командой и JSON-ответом на localhost:8080.

Строки

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

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

Карточки: три протокола

Термин

Диспетчер Content-Type

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

Handler выбирает протокол по заголовку: application/grpc — gRPC, application/grpc-web — gRPC-Web, application/json и application/proto — протокол Connect. Для стримов — application/connect+json и connect+proto.

1 / 6

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

Теперь вы знаете connect протоколы не понаслышке: диспетчер по Content-Type, конверты gRPC, трейлеры-в-теле gRPC-Web, честные статусы и GET-кеш протокола Connect — и понимаете, почему один handler покрывает всё это без единой настройки. Следующий урок — клиенты: как из Go-кода выбрать протокол осознанно — опции connect.WithGRPC() и connect.WithGRPCWeb(), требования к HTTP/2-транспорту, метаданные и таймауты. Дальше — ошибки в полную силу (шестой урок) и перехватчики (седьмой), которые видят протокол вызова через req.Spec(). Программа всех двадцати пяти уроков — на странице курса. Домашняя работа урока: прогнать три вызова GetNote из этого урока — curl-ом, grpcurl-ом и браузерным fetch с application/grpc-web+proto — и записать в заметки, чем отличаются ответы на уровне статусов; это войдёт в ваш тестовый арсенал проекта.

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

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

Вывод

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

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

  • Протокол выбирается по Content-Type каждого запроса: application/grpc — gRPC, application/grpc-web — gRPC-Web, application/json или application/proto — протокол Connect.
  • Сгенерированный handler — три обработчика под одним фасадом: диспетчеризация стоит микросекунды и не требует ни конфигов, ни отдельных портов.
  • gRPC: сообщения в пятибайтовых конвертах (флаг сжатия + длина), итог вызова в трейлере grpc-status, HTTP 200 даже при ошибке, HTTP/2 обязателен.
  • gRPC-Web: трейлеры переехали в конец тела, работает на HTTP/1.1, но перед классическим gRPC-сервером нужен Envoy; Connect обслуживает его нативно.
  • Протокол Connect: unary — тело без конвертов, ошибки — HTTP-статус плюс JSON {code, message, details}, метаданные — обычные заголовки.
  • NO_SIDE_EFFECTS превращает unary-вызов в GET с сообщением в query string — HTTP-кеш, CDN и безопасные ретраи становятся доступными.
  • Маршрутизация двухуровневая: ServeMux различает сервисы по пути /package.Service/Method, handler различает протоколы по Content-Type.

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

Поднимите сервер из третьего урока и вызовите GetNote тремя способами из этого урока: curl-ом с Content-Type application/json (ожидайте 200 и JSON-заметку), grpcurl-ом с -import-path и -proto (ожидайте тот же ответ в его форматировании) и повторите промах с несуществующим id обоими способами (404 с code: not_found против grpc-status: 5). Затем добавьте в notes.proto методу GetNote опцию option idempotency_level = NO_SIDE_EFFECTS;, перегенерируйте buf generate и повторите вызов GET-ом с параметрами connect=v1 и encoding=json — убедитесь, что ответ тот же. Запишите в заметки, где именно каждый протокол объявил итог вызова: статус, трейлер или фрейм в теле — эта таблица станет вашей шпаргалкой по отладке.

Личные заметкиПусто
Хранятся только в вашем браузере (Connect · Протоколы: gRPC, gRPC-Web, Connect)

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

Как Connect различает протоколы gRPC, gRPC-Web и собственный?

По заголовку Content-Type каждого запроса: application/grpc уходит обработчику классического gRPC, application/grpc-web — обработчику gRPC-Web, application/json и application/proto — собственному протоколу Connect. Все три обработчика встроены в один сгенерированный handler и обслуживаются без настройки.

Нужен ли Envoy для gRPC-Web перед Connect-сервером?

Нет. Обработчик gRPC-Web встроен в Connect-handler, поэтому браузерные клиенты ходят напрямую. Envoy нужен только перед классическим gRPC-сервером, который не умеет читать трейлеры, закодированные в тело ответа.

Что выбрать: grpc web vs connect?

Если сервер уже классический gRPC и Envoy стоит — gRPC-Web достаётся бесплатно. Для новых проектов берите собственный протокол Connect: он работает на HTTP/1.1, читается curl-ом, отдаёт честные HTTP-статусы, поддерживает GET-кеш для NO_SIDE_EFFECTS и обслуживается тем же handler-ом, что и gRPC.

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

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

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

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

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

Fyne25 уроков~13 чпрактика25 уроков уже доступны

Fyne: десктопные и мобильные GUI на Go

Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.

  • Создавать окна и интерфейсы из виджетов
  • Управлять компоновкой: Box, Grid, Form
  • Рисовать примитивами canvas и строить свои виджеты
  • Связывать данные через binding