Содержание урока · 14 разделов
- Диспетчер по Content-Type: один handler, три обработчика
- Content-Type → протокол: таблица диспетчеризации
- Анатомия gRPC: конверты из пяти байт
- gRPC-Web: трейлеры, спрятанные в тело
- Собственный протокол Connect: unary без конвертов
- GET-кеш: idempotency_level и что это вам даёт
- grpc web vs connect: таблица выбора
- Три протокола на одном сервере: сравнительная карта
- Совместимость: один контракт, любое число клиентов
- Один mux, много сервисов: маршрутизация не зависит от протокола
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Третий урок закончился обещанием: разобрать, как один и тот же 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 | фреймы protobuf | grpc-go, grpcurl, чужие сервисы |
| application/grpc+proto | классический gRPC | фреймы protobuf | явное указание кодека |
| application/grpc-web | gRPC-Web | фреймы protobuf | старые браузерные клиенты, Envoy |
| application/grpc-web+proto | gRPC-Web | фреймы protobuf | браузерные клиенты grpc-web |
| application/grpc-web+json | gRPC-Web | фреймы JSON | редкий, но рабочий вариант |
| application/json | протокол Connect | JSON без конвертов | curl, браузеры, devtools |
| application/proto | протокол Connect | protobuf без конвертов | 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/GetNotegrpc 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 оставьте как опцию совместимости, которая включена всегда и без настройки.
Три протокола на одном сервере: сравнительная карта#
| Свойство | gRPC | gRPC-Web | Connect |
|---|---|---|---|
| Транспорт | только HTTP/2 | HTTP/1.1 или HTTP/2 | HTTP/1.1 или HTTP/2 |
| Unary на проводе | фреймы 5 байт + тело | фреймы 5 байт + тело | чистое тело без конвертов |
| Итог вызова | трейлер grpc-status | текстовый фрейм в теле | HTTP-статус ответа |
| Неудачный вызов выглядит | HTTP 200 + grpc-status: 5 | HTTP 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(¬esServer{}); 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 для стримов.
Путь одного curl-запроса через диспетчер
Расставьте по порядку события между curl-командой и JSON-ответом на localhost:8080.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Когда вызов падает, сопротивляйтесь искушению дебажить его клиентской библиотекой — спускайтесь на уровень провода. Вызовите метод curl-ом в протоколе Connect: вы сразу увидите HTTP-статус, Content-Type и JSON ошибки — три факта, которых достаточно в девяти случаях из десяти. Если curl работает, а клиент нет, проблема в клиентской части: протокол, транспорт, метаданные. Браузерная песочница сайта сетевых вызовов не исполняет, поэтому эта привычка отрабатывается только на вашем локальном сервере из второго урока — и это лучший повод его поднять.
Карточки: три протокола
Что дальше по курсу#
Теперь вы знаете 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 и собственный?
По заголовку 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.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Fyne: десктопные и мобильные GUI на Go
Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.
- Создавать окна и интерфейсы из виджетов
- Управлять компоновкой: Box, Grid, Form
- Рисовать примитивами canvas и строить свои виджеты
- Связывать данные через binding