Содержание урока · 15 разделов
- Что такое Connect RPC
- Почему gRPC не работает в браузере
- gRPC-Web: обходной путь с прокси
- gRPC, gRPC-Web и Connect: сравнение
- Решение Connect: один обработчик на net/http
- Тот же protobuf, другой транспорт-слой
- Три протокола одним обработчиком
- Connect-протокол: HTTP-статусы вместо трейлеров
- Коды ошибок RPC и их HTTP-статусы
- protoc-gen-connect-go и buf: proto-first кодогенерация
- Где Connect уместен, а где лишний
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Начну с ситуации, в которой оказывается почти каждый, кто прошёл курс по gRPC. Сервис на google.golang.org/grpc радует: контракт в .proto, заглушки сгенерированы, дедлайны и стриминг работают, всё тестировалось. Потом приходит фронтенд-команда и спрашивает: «А можно нам те же методы прямо из браузера?» И здесь понижение температуры: нельзя. Браузерный JavaScript не умеет говорить по gRPC — ни fetch, ни XMLHttpRequest не дают доступа к HTTP/2-трейлерам, где gRPC хранит итоговый статус вызова. Дальше начинается черёд костылей: ручной REST-фасад, который дублирует логику и расходится с контрактом, или прокси Envoy с протоколом gRPC-Web, у которого свой список оговорок. В 2022 году компания Buf — та самая, что сделала тулчейн buf и Buf Schema Registry — открыла Connect: библиотеку, построенную ровно вокруг этой боли. Этот урок — connectrpc обзор в одном месте: сначала разберём, почему gRPC и браузер несовместимы, затем посмотрим на решение — один обработчик на net/http, который понимает Connect, gRPC и gRPC-Web одновременно, — а в конце честно обсудим, где Connect уместен, а где он лишний.
Что такое Connect RPC#
Connect — «slim library for building browser and gRPC-compatible HTTP APIs», как формулируют авторы: тонкая библиотека для построения браузерных и gRPC-совместимых HTTP API. Рецепт работы прежний, proto-first: пишете короткую Protocol Buffers-схему, реализуете логику приложения, а библиотека генерирует код, который делает сериализацию, маршрутизацию, компрессию и согласование форматов, плюс типизированный клиент. Для Go это модуль connectrpc.com/connect, официально рекомендуемый конструктор API поверх protobuf. Проект открыт в 2022 году под лицензией Apache 2.0 и развивается компанией Buf; кроме Go, есть реализации для TypeScript (пакет для браузера и Node), Kotlin, Swift, Dart и Python — экосистема уже мультиязычная, а не «Go-only». Ключевая идея всего фреймворка умещается в одну фразу из документации: handlers и clients поддерживают три протокола — gRPC, gRPC-Web и собственный протокол Connect. Схема одна, сервер один, порты и клиенты — по вкусу. В этом курсе мы изучим полный путь: установку с buf, первый сервис, протоколы, ошибки, interceptors, стриминг и браузерных клиентов.
Почему gRPC не работает в браузере#
Чтобы понять ценность Connect, нужно на минуту спуститься на уровень HTTP. gRPC ездит строго по HTTP/2 и активно использует его трейлеры — заголовки, которые сервер отправляет после тела ответа. Это не деталь реализации, а ядро дизайна: итоговый статус вызова (grpc-status) и сообщение об ошибке gRPC кладёт именно в трейлеры. У обычных HTTP-серверов статус живёт в первой строке ответа и известен до тела, у gRPC — в самом конце, потому что часть ошибок выявляется только когда ответ уже частично отослан. Теперь браузер. JavaScript-код не работает с HTTP напрямую: он говорит через Fetch API, а тот не отдаёт коду ни трейлеры, ни сырые HTTP/2-кадры. Сотрудники команды Connect в FAQ формулируют это жёстко и точно: многие HTTP-реализации, включая браузеры, трейлеры до сих пор не поддерживают — и потому никакой код в браузере не может говорить по протоколу gRPC. Заметьте, картина получается дурацкая: ответ физически пришёл, тело даже распарсилось, а фронтенд не может узнать, чем закончился вызов — успехом или ошибкой. Для API это не баг, а полный отказ: система, в которой нельзя узнать исход запроса, не годится.
gRPC-Web: обходной путь с прокси#
Первая попытка лечить это — протокол gRPC-Web, придуманный самой gRPC-командой. Идея простая до безобразия: вместо трейлеров закодировать служебную информацию в конец тела ответа. Трейлеров нет — браузеру они и не нужны. Но у gRPC-Web есть две оговорки, которые вы вспоминаете в самый неподходящий момент. Первая: чистые gRPC-серверы (grpc-go, grpc-java) по умолчанию gRPC-Web не говорят, поэтому между браузером и сервисом ставят прокси — обычно Envoy, — который переводит gRPC-Web в gRPC и обратно. Лишний hop, лишний деплой, лишняя точка, где что-то пойдёт не так. Вторая оговорка: gRPC-Web поддерживает только бинарный формат protobuf, поэтому в devtools браузера вы видите кашу из байтов вместо читаемого JSON — отлаживать неудобно. Со стримингом тоже не без клиники: серверный стриминг из браузера работает, а вот клиентский и двунаправленный — с серьёзными ограничениями, о которых честно предупреждают документации. Подытожу по-своему: gRPC-Web — это полумера, которая решает проблему трейлеров, но платит прокси и удобством разработки. Именно от этой полумеры Connect и отталкивается.
gRPC, gRPC-Web и Connect: сравнение#
| Критерий | gRPC | gRPC-Web | Connect |
|---|---|---|---|
| Транспорт | только HTTP/2 | HTTP/1.1 и HTTP/2 | HTTP/1.1 и HTTP/2 (bidi — только HTTP/2) |
| Трейлеры | обязательны: grpc-status живёт там | нет, закодированы в тело | не используются вовсе |
| Браузер напрямую | нет | да, если сервер умеет gRPC-Web | да, JSON из коробки |
| Прокси для чистого gRPC-сервера | не нужен | нужен, обычно Envoy | не нужен: handler сам понимает gRPC |
| Отладка curl | почти нет, нужен grpcurl | трудно: только бинарный формат | да: JSON, читаемые статусы, живые пути |
| GET и HTTP-кеш | нет | нет | да, для методов без побочных эффектов |
Почему браузерный JavaScript не может вызвать обычный gRPC-сервер напрямую?
Решение Connect: один обработчик на net/http#
Вот здесь и появляется обещание фреймворка: один сгенерированный обработчик обслуживает все три протокола — gRPC, gRPC-Web и Connect — различая их по заголовку Content-Type. Не три сервера на трёх портах и не прокси в придачу: один http.Handler в вашем mux. Под капотом Connect — только Protocol Buffers и стандартная библиотека Go: никакой собственной реализации HTTP, никакого нового API балансировки и разрешения имён. В README это звучит так: всё, что вы знаете о net/http, остаётся верным, и любой пакет, работающий с http.Server, http.Client или http.Handler, работает и с Connect. Практическое следствие сложно переоценить: ваши middleware, трейсинг, метрики, httptest-тесты, настройки таймаутов и graceful shutdown — всё стандартное — подключаются к Connect-сервису без адаптеров. Контракт по-прежнему живёт в .proto-файле, код генерируется компилятором, типы проверяются на сборке — вся gRPC-эргономика сохранена, сменился только транспортный слой. Именно поэтому переезд с gRPC на Connect часто оказывается меньшей операцией, чем кажется по названию курса.
Контракт: тот же protobuf, что и в gRPC
GOsyntax = "proto3";
package notes.v1;
option go_package = "example.com/notes/gen/notes/v1;notesv1";
message Note {
string id = 1;
string title = 2;
string body = 3;
}
message GetNoteRequest {
string id = 1;
}
message CreateNoteRequest {
string title = 1;
string body = 2;
}
service NoteService {
rpc GetNote (GetNoteRequest) returns (Note);
rpc CreateNote (CreateNoteRequest) returns (Note);
}Анатомия знакома каждому, кто видел gRPC: syntax, package, go_package, messages с пронумерованными полями, service с rpc-методами. Никаких connect-специфичных аннотаций, никаких новых ключевых слов. Этот файл — обычный protobuf: его можно скормить protoc-gen-go и генератору gRPC, а можно — плагину Connect. Схема не знает, какой фреймворк её переведёт в код.
Тот же protobuf, другой транспорт-слой#
Проще всего понять Connect через сравнение с тем, что вы уже знаете. Если вы проходили курс по gRPC, вы собирали сервер так: net.Listen открывает TCP-порт, grpc.NewServer создаёт сервер, RegisterOrderServiceServer привязывает реализацию к сервису, Serve блокируется в цикле приёма — весь этот разбор в уроке про первый gRPC-сервер. В Connect эти слои меняются: вместо специализированного транспорта — обычный http.Handler, который монтируется в стандартный ServeMux и слушается вашим http.Server. Что при этом остаётся нетронутым: сами .proto-файлы, плагин protoc-gen-go (message-типы ни при чём — это про сериализацию), бинарный формат protobuf на проводе, модель кодов ошибок gRPC и весь словарь видов вызовов. Стриминг тоже на месте: если в курсе gRPC вы писали серверный стриминг WatchOrders — один запрос, поток событий — то в Connect то же самое выражается тем же словом stream в схеме; соответствующий разбор — в уроке про серверный стриминг курса gRPC. Формула короткая: тот же protobuf, другой транспорт-слой. Логика приложения, если она не вросла в grpc.Server, переезжает почти без изменений.
Сервер: обычный net/http, никаких grpc.NewServer
GOpackage main
import (
"context"
"log"
"net/http"
"connectrpc.com/connect"
notesv1 "example.com/notes/gen/notes/v1"
notesv1connect "example.com/notes/gen/notes/v1/notesv1connect"
)
// Реализация сервиса: та же структура, что и у gRPC-сервера.
type notesServer struct {
// Встроенный тип отвечает за совместимость при росте схемы.
notesv1connect.UnimplementedNoteServiceHandler
}
func (s *notesServer) GetNote(ctx context.Context, req *notesv1.GetNoteRequest) (*notesv1.Note, error) {
// Здесь будет поиск в базе, пока возвращаем заглушку.
return ¬esv1.Note{Id: req.Id, Title: "Первая заметка"}, nil
}
func main() {
mux := http.NewServeMux()
// Конструктор возвращает путь ("/notes.v1.NoteService/")
// и http.Handler, который понимает все три протокола.
path, handler := notesv1connect.NewNoteServiceHandler(¬esServer{})
mux.Handle(path, handler)
log.Fatal(http.ListenAndServe("localhost:8080", mux))
}Смотрите на main: http.NewServeMux, монтирование handler-а, ListenAndServe — это обычный net/http, который вы пишете с первого дня в Go. grpc.NewServer и net.Listen отсюда исчезли, их заменил стандартный mux. Единственная connect-специфика — импорт connectrpc.com/connect и сгенерированный пакет notesv1connect с суффиксом connect: его создаёт плагин protoc-gen-connect-go, о нём ниже. Полный разбор этого файла с клиентом — в третьем уроке курса, пока важно запомнить форму: сгенерированный handler монтируется как обычный HTTP-роут.
Браузерный клиент вызвал GetNote с Content-Type: application/json. Что вернёт сервер?
Прочитайте код и выберите, что он выведет:
func (s *notesServer) GetNote(ctx context.Context, req *notesv1.GetNoteRequest) (*notesv1.Note, error) {
// Заметки с таким id нет — возвращаем не ошибку Go, а код RPC.
return nil, connect.NewError(connect.CodeNotFound, errors.New("заметка не найдена"))
}Три протокола одним обработчиком#
Как же один handler различает трёх клиентов? По Content-Type, и правило выбора простое. Заголовки application/proto или application/json — это собственный протокол Connect: тело запроса и ответа без обёрток, осмысленные HTTP-статусы. Заголовок application/grpc (+ суффикс кодека) — классический gRPC: он приезжает на HTTP/2, и handler обслуживает его по спецификации gRPC, включая trailers и стриминг. Заголовок application/grpc-web+proto — gRPC-Web, тот самый протокол для браузеров с закодированными в тело трейлерами; Connect подаёт его нативно, без Envoy. Последний пункт стоит проговорить отдельно, потому что это прямое сравнение из FAQ проекта: серверы, построенные на Connect-Go, понимают gRPC-Web сами, а большинство реализаций gRPC от Google — нет, им нужен прокси. Практический итог для команды: старые gRPC-клиенты продолжают работать без перекомпиляции схем, фронтенд ходит JSON-ом из браузера, новый Go-клиент выбирает Connect-протокол — и всё это на одном порту, в одном деплое, под одним балансировщиком. Health-проверки и server reflection в Go-версии доступны отдельными пакетами, они тоже совместимы с gRPC.
Вызов curl: никакого специального клиента
GOcurl \
--header "Content-Type: application/json" \
--data '{"sentence": "I feel happy."}' \
https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/SayЭто официальный живой демо-сервер проекта, можете выполнить команду прямо сейчас — ответ вернётся обычным JSON с HTTP 200. Путь собирается из схемы: /package.Service/Method, то есть /connectrpc.eliza.v1.ElizaService/Say. Свои сервисы дёргаются точно так же — на локальном сервере замените адрес на localhost. Для сравнения: чистый gRPC так не умеет, для ручных вызовов там нужен grpcurl и включённый reflection. Латинская буква в URL, русские комментарии в коде — смешение нормально: контракты интернациональны.
На ваш Connect-сервер пришёл запрос с Content-Type: application/grpc от старого gRPC-клиента. Что произойдёт?
Connect-протокол: HTTP-статусы вместо трейлеров#
Собственный протокол Connect задуман по трём правилам, которые авторы выписали в спецификации: быть читаемым обычными HTTP-инструментами, оставаться концептуально близким к gRPC и зависеть только от широко поддерживаемых фич HTTP. Для unary-вызовов результат похож на урезанный REST: POST на путь, выведенный из схемы (/notes.v1.NoteService/GetNote), тело — валидный protobuf или JSON без бинарной обёртки, а HTTP-статус ответа несёт смысл. Для методов без побочных эффектов схему помечают опцией idempotency_level = NO_SIDE_EFFECTS — и клиент отправляет обычный GET, который кешируется стандартной HTTP-инфраструктурой; gRPC такого подарка не делает. Стриминговые вызовы устроены чуть сложнее: Content-Type меняется на application/connect+json или application/connect+proto, каждое сообщение оборачивается несколькими байтами фрейминга, HTTP-статус всегда 200, а ошибка приходит в конце тела — по механике это ближе к gRPC-Web, только трейлеров нет и на HTTP/1.1 всё живёт. Исключение одно: двунаправленный стриминг требует HTTP/2, это ограничение протокола, а не библиотеки.
Коды ошибок RPC и их HTTP-статусы#
| Код | HTTP-статус | Типовой смысл |
|---|---|---|
| invalid_argument | 400 Bad Request | запрос некорректен независимо от состояния системы |
| unauthenticated | 401 Unauthorized | нет валидных учётных данных |
| permission_denied | 403 Forbidden | данные есть, доступа нет |
| not_found | 404 Not Found | запрошенный ресурс не существует |
| already_exists | 409 Conflict | создание ресурса, который уже есть |
| resource_exhausted | 429 Too Many Requests | исчерпан лимит или квота |
| unavailable | 503 Service Unavailable | временная недоступность, стоит ретраить |
| unimplemented | 501 Not Implemented | метод не реализован или не включён |
| deadline_exceeded | 504 Gateway Timeout | дедлайн истёк до завершения вызова |
К Connect-серверу подключили чистый grpc-go клиент (тот же .proto). Чем закончится вызов GetNote?
Прочитайте код и выберите, что он выведет:
// Клиент сгенерирован grpc-go по той же схеме notes.v1,
// без изменения .proto и без настроек прокси.
conn, err := grpc.NewClient("localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()))
// ...
client := pb.NewNoteServiceClient(conn)
note, err := client.GetNote(ctx, &pb.GetNoteRequest{Id: "1"})Из браузера серверный стриминг работает нормально, а вот клиентский и двунаправленный — ограничены возможностями Fetch API: браузеры не дают полноценного отправляющего потока. Команда Connect честно фиксирует это в документации, а её TypeScript-библиотека превращает клиентский стриминг в half-duplex. Планируете чат или загрузку файлов частями из браузера — закладывайте дизайн под это ограничение заранее.
protoc-gen-connect-go и buf: proto-first кодогенерация#
Connect сохраняет главную дисциплину gRPC — proto-first: первична схема, код не пишется руками. Генерация двухступенчатая. Первый плагин — знакомый protoc-gen-go: он создаёт notes.pb.go с message-типами, и это ровно тот же файл, что и в gRPC-проекте. Второй — protoc-gen-connect-go: он создаёт notes.connect.go, где лежат интерфейс NoteServiceHandler с методом Unimplemented-заглушкой, конструктор NewNoteServiceHandler и типизированный клиент NewNoteServiceClient. Имя пакета получает суффикс connect — notesv1connect, — и по нему легко ориентироваться в импортах. Порядок установки такой: go get connectrpc.com/connect в проект, два плагина локально — и protoc с двумя --_out флагами. Но существует и современный путь: buf от компании-автора Connect заменяет хороводы protoc-флагов одним файлом buf.gen.yaml, добавляет линт схем, проверку breaking changes и реестр схем BSR. Это отдельная история, которой мы займёмся в следующем уроке про установку — там соберём notes.v1 из этого урока в рабочий код.
Восстановите ядро Connect-сервера: создание mux, монтирование сгенерированного handler-а и запуск стандартного HTTP-сервера.
Какой плагин генерирует файл notes.connect.go с handler-ами и клиентом?
Соберите по порядку путь от .proto-схемы до первого запроса curl к Connect-сервису.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Где Connect уместен, а где лишний#
Мой расклад по сценариям. Первый и самый убедительный: у вас Go-бэкенд и фронтенд в браузере — Connect убирает весь слой костылей: ни REST-фасада, который отстаёт от контракта, ни Envoy, ни второй схемы. Второй: существующий gRPC-продукт, у которого появился браузерный клиент — переезд наименьшей крови, потому что gRPC-протокол поддерживается handler-ом и старые клиенты даже не заметят смены. Третий: публичные API, где хочется типизацию protobuf, но и curl-отладку жалко — JSON по умолчанию закрывает обе потребности. Четвёртый — новый проект с TypeScript-клиентом: для меня Connect стал выбором по умолчанию, а не экспериментом. Когда он лишний? Если у вас чисто внутренние gRPC-системы, где всё уже работает: Envoy стоит, браузера нет, миграция ради миграции не окупается. Если главное требование — REST-совместимость для чужих интеграций, честнее взять REST или grpc-gateway, чем протокол, который лишь похож на REST. И если команда на два месяца, а сервис один — возможно, хватит и обычного JSON API. Connect силён там, где сходятся protobuf, браузер и желание жить на net/http.
Код уроков — настоящий Go с protobuf-кодогенерацией: сетевой код и protoc браузерная песочница сайта исполнить не может, поэтому runner-блоков в курсе нет — для практики нужен локальный Go и инструменты, установку которых разберём в следующем уроке. Интерактивы (тесты, «предскажи вывод», сборка по строкам, карточки) работают прямо на странице и дают XP, а код с построчными разборами можно копировать в свой проект.
Карточки: словарь Connect
Что дальше по курсу#
Следующий урок — «Установка и настройка» (02-ustanovka): go get connectrpc.com/connect, плагины protoc-gen-go и protoc-gen-connect-go, первый buf.gen.yaml. Схему notes.v1 из этого урока мы доведём до генерируемого кода, и дальше по курсу она будет расти вместе с нами: третий урок соберёт первый Connect-сервис целиком, четвёртый разберёт протоколы на проводе, шестой — ошибки и connect.NewError, седьмой — interceptors, девятый — браузерных клиентов на TypeScript. Программа всех двадцати пяти уроков, от установки до итогового проекта, живёт на странице курса. Если protobuf для вас в новинку — начните с курса по gRPC: там протокол сериализации разобран глубже, чем это нужно для Connect.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Connect от Buf — библиотека для gRPC-совместимых HTTP API: та же .proto-схема и protobuf, но транспорт — обычный net/http.
- Причина появления: браузер не умеет gRPC — Fetch API не отдаёт трейлеры, где живёт итоговый статус grpc-status.
- gRPC-Web решает проблему трейлеров, но требует прокси (Envoy) и оставляет только бинарный формат.
- Один сгенерированный handler на net/http обслуживает три протокола: Connect, gRPC, gRPC-Web — по Content-Type.
- Connect-протокол: POST/GET по пути /package.Service/Method, JSON или protobuf, осмысленные HTTP-статусы, not_found → 404.
- Стриминг поддержан; из браузера полноценно работает серверный, клиентский и bidi ограничены Fetch API.
- Миграция с gRPC мягкая: старые клиенты работают, схема не меняется, меняется плагин генерации.
Домашнее задание
Возьмите любой сервис из курса по gRPC и выпишите в две колонки: что переедет в Connect без изменений (схема, message-типы, коды ошибок, стриминг в схеме) и что придётся переделать (плагин генерации, регистрация сервера, транспорт). Затем выполните curl-команду из урока на живой демо-странице connectrpc.com и убедитесь, что RPC бывает читаемым без специальных клиентов.
Личные заметкиПусто
Вопросы и ответы по уроку
Что такое Connect RPC простыми словами?
Библиотека RPC от Buf: контракт в protobuf, сгенерированные типизированные клиенты, а на сервере — обычный HTTP-handler на net/http, который понимает gRPC, gRPC-Web и собственный протокол Connect одновременно.
Connect заменяет gRPC?
Не заменяет, а обнимает: та же схема, те же коды ошибок и стриминг, плюс протокол, который работает в браузере и читается curl-ом. Существующие gRPC-клиенты продолжают работать без изменений.
Чем Connect отличается от gRPC-Web?
gRPC-Web — только браузерный протокол с бинарным форматом, которому чистому gRPC-серверу нужен прокси. Connect — и протокол, и библиотека: без прокси, с JSON по умолчанию, и при этом полноценная совместимость с gRPC.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Fyne: десктопные и мобильные GUI на Go
Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.
- Создавать окна и интерфейсы из виджетов
- Управлять компоновкой: Box, Grid, Form
- Рисовать примитивами canvas и строить свои виджеты
- Связывать данные через binding