Skip to content

Latest commit

 

History

History
577 lines (444 loc) · 29.4 KB

part40-rus.md

File metadata and controls

577 lines (444 loc) · 29.4 KB

Определяем gRPC API и генерируем Go код с помощью protobuf

Оригинал

Привет, ребята. На последней лекции, мы изучили некоторые теоретические основы gRPC. Теперь пришло время попрактиковаться!

Определяем RPC API

Первым шагом при разработке gRPC веб-сервиса является определение API RPC и его структуры запроса/ответа с помощью protobuf. Затем используйте это protobuf определение для генерации кода сервера и клиента на выбранном вами конкретном языке программирования. И это именно то, чем мы будем заниматься на этой лекции. На странице документации gRPC вы можете найти инструкцию как это сделать в разделе Быстрый старт. В первую очередь нужно установить компилятор Protocol Buffer. Если вы используете Linux или Mac, вы можете легко сделать это с помощью менеджера пакетов. Для Windows вам нужно будет загрузить предварительно скомпилированный двоичный файл со страницы релизов protoc. Поскольку я на Mac OS, я установлю его с помощью Homebrew. Так что давайте выполним

brew install protobuf

в терминале. После успешной установки мы можем просмотреть версию программы с помощью этой команды:

protoc --version
libprotoc 3.19.4

Теперь, чтобы генерировать Golang код, нам нужно установить еще два плагина для компилятора. Во-первых, плагин protoc-gen-go поможет нам генерировать Go код для любого сообщения запроса/ответа, определенного в protobuf.

go install google.golang.org/protobuf/cmd/[email protected]

Вы можете узнать установленную версию с помощью этой команды:

protoc-gen-go --version
protoc-gen-go v1.28.0

Здесь я устанавливаю последнюю версию на данный момент, это версия 1.28.

Второй плагин, который мы должны установить, это protoc-gen-go-grpc. Как следует из его названия, он поможет нам сгенерировать Go код, работающий с gRPC фреймворком.

go install google.golang.org/grpc/cmd/[email protected]
go: downloading google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
go: downloading google.golang.org/grpc v1.2.1

Версия, которую я только что установил, — 1.2, последняя на момент написания этой лекции.

protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.2.0

Когда вы будете читать эту лекцию, последняя версия может отличаться, и сгенерированные коды могут отличаться от моих. Однако, я думаю, как только вы поймёте идею, лежащую в основе, вы сможете легко адаптировать свой код для работы с любыми новыми изменениями, произошедшими в будущем.

Ещё, возможно, вам придётся добавить папку go bin в PATH вашей системы, чтобы компилятор protoc мог найти установленные плагины.

export PATH="$PATH:$(go env GOPATH)/bin"

Я уже сделал это раньше при установке Go, поэтому пропущу этот шаг.

Хорошо, теперь, когда все инструменты установлены, мы можем начать писать код. На этой лекции я покажу вам, как написать protobuf определения для двух API: API для создания нового пользователя и API для входа в систему, чтобы получить токен доступа и refresh токен.

Пишем protobuf определения для API создания нового пользователя

Я собираюсь создать новую папку с названием proto для хранения всех файлов protobuf. Поскольку наши API будут работать с пользователями, я начну с определения объекта User. Итак, давайте создадим файл user.proto внутри папки proto.

Во-первых, мы пропишем syntax = "proto3", чтобы сообщить компилятору, что мы будем использовать синтаксис protobuf версии 3 для определения наших сообщений. Существует также protobuf версии 2, но я думаю, что лучше использовать последнюю версию.

syntax = "proto3";

На данный момент у нас ещё нет подсветки синтаксиса. Чтобы включить её, мы должны установить плагин vscode-proto3.

Как только этот плагин будет установлен и включен, мы увидим подсветку синтаксиса в нашем proto файле.

Далее я объявлю пакет для этого proto файла. Давайте назовём его pb. Пакет — это просто способ сгруппировать вместе несколько proto файлов относящихся к одной тематике.

package pb;

Затем мы должны указать одну option, чтобы сообщить protoc, для какого пакета Go мы хотим, чтобы он генерировал Golang код. По сути, это должен быть подпакет корневого модуля, который мы указали в файле go.mod: github.com/techschool/simplebank.

Поэтому я скопирую его в файл user.proto и добавлю суффикс /pb в конце.

option go_package = "github.com/techschool/simplebank/pb";

Благодаря этому сгенерированный Go код будет помещён в папку pb, которую я только что создал здесь.

Хорошо, теперь мы можем определить сообщение User следующим образом. Внутри фигурных скобок нам нужно будет указать все поля, которые будет содержать объект User, а именно Username, FullName, Email, PasswordChangedAt, CreatedAt. Точно такие же, которые в настоящее время определены в структуре userResponse.

type userResponse struct {
	Username          string    `json:"username"`
	FullName          string    `json:"full_name"`
	Email             string    `json:"email"`
	PasswordChangedAt time.Time `json:"password_changed_at"`
	CreatedAt         time.Time `json:"created_at"`
}

Чтобы определить поле сообщения proto3, мы используем такой синтаксис: начинаем с типа данных поля, например, string. Затем пишем название поля, то есть username, за которым следует символ равенства и номер поля.

message User {
  string username = 1;
}

Этот номер поля очень важен в сообщении protobuf, потому что он будет использоваться для уникальной идентификации поля при сериализации или десериализации сообщения в двоичном формате. Вы можете использовать любые целые числа от 1 до 2^29-1, за исключением некоторых зарезервированных, которые вы можете найти на странице официальной документации protobuf.

Вы также можете получить более подробную информацию о различных поддерживаемых типах данных на этой странице. Ссылку также можно найти в описании к этому видео.

Итак, вернёмся к сообщению User, следующее поле также является строкой: full_name. И я назначу ему номер поля равный 2.

message User {
  string username = 1;
  string full_name = 2;
}

Третье поле — это строка email с номером поля 3.

message User {
  string username = 1;
  string full_name = 2;
  string email = 3;
}

Затем следует поле password_changed_at, которое должно быть меткой времени. Метка времени не является встроенным типом данных protobuf, но это всем известный тип данных, и Google уже добавил его в свою стандартную библиотеку. Таким образом, мы можем сослаться на него как google.protobuf.Timestamp, тогда имя поля будет равно password_changed_at, а номер поля — 4.

message User {
  string username = 1;
  string full_name = 2;
  string email = 3;
  google.protobuf.Timestamp password_changed_at = 4;
}

Здесь появилась красная волнистая линия под типом данных, потому что он не является встроенным. Поэтому нам нужно импортировать его из библиотеки Google, а именно: "google/protobuf/timestamp.proto". После этого импорта ошибка исчезнет.

import "google/protobuf/timestamp.proto";

Хорошо, теперь аналогичным образом я добавлю последнее поле в сообщение User, поле created_at, которое также является меткой времени, и его номер будет равен 5.

message User {
  string username = 1;
  string full_name = 2;
  string email = 3;
  google.protobuf.Timestamp password_changed_at = 4;
  google.protobuf.Timestamp created_at = 5;
}

Здесь вы видите, что я использую последовательность из целых чисел от 1 до 5 для номеров полей. Вам не обязательно так делать, но это стандартный способ определения полей. Обратите внимание, что для кодирования чисел от 1 до 15 требуется всего один байт, а для бóльших чисел требуется два или более байтов. Таким образом, используя небольшие числа, мы также экономим место в памяти и, таким образом, уменьшаем размер сообщения при кодировании.

Итак, мы определили объект User, используя protocol buffer

Теперь пришло время определить наш первый RPC: API для создания пользователя. Я добавлю новый файл под названием rpc_create_user.proto внутрь папки proto. Затем давайте вставим в него содержимое файла user.proto, потому что мы собираемся повторно использовать большую его часть. Мы оставим нетронутыми все команды syntax, package, import и option. Но давайте изменим название сообщения на CreateUserRequest.

Это будет объект, содержащий запрос от клиента на создание нового пользователя. В нём должно быть четыре обязательных поля: username, password, full_name и email. Поэтому я оставлю первые три поля этой структуры User, удалю остальные и добавлю новое поле с типом string для password.

message CreateUserRequest {
  string username = 1;
  string full_name = 2;
  string email = 3;
  string password = 4;
}

Нам не нужен этот импорт здесь,

import "google/protobuf/timestamp.proto";

так как в сообщении нет поля с меткой времени.

Итак, теперь у нас есть сообщение CreateUserRequest. Далее я определю ещё одно сообщение для хранения CreateUserResponse. Это сообщение, конечно же, будет содержать информацию о созданном пользователе. Таким образом, там будет поле user с типом User и номером 1. Это наш собственный, пользовательский тип данных, поэтому мы должны импортировать его из файла user.proto.

Но почему не исчезает красная волнистая линия под этим import?

Что ж, потому что по умолчанию protoc будет искать импортируемый файл в текущей папке, которую мы открываем в Visual Studio Code, но наши proto файлы на самом деле находятся в папке proto. Итак, чтобы исправить эту ошибку, мы должны сообщить об этом protoc. Давайте откроем плагин vscode-proto3. Здесь, в его документации, мы найдем фрагмент с настройками для protoc.

{
  "protoc": {
    "path": "/path/to/protoc",
    "compile_on_save": false,
    "options": [
      "--proto_path=protos/v3",
      "--proto_path=protos/v2",
      "--proto_path=${workspaceRoot}/proto",
      "--proto_path=${env.GOPATH}/src",
      "--java_out=gen/java"
    ]
  }
}

Давайте скопируем их, затем откроем Code / Preferences / Settings, поищем по ключевому слову proto3 и нажмём Edit в settings.json.

Теперь я вставлю настройки, скопированные ранее, удалю внешние фигурные скобки, затем там мы можем удалить /path/to/protoc и compile_on_save. Оставьте только options и proto-path. Все наши proto файлы хранятся в папке proto, поэтому я пропишу это здесь. И мы можем удалить все остальные пути. Вот так.

{
  "protoc": {
    "options": [
      "--proto_path=proto",
    ]
  }
}

Затем сохраните файл и вернитесь к коду.

Теперь мы видим, что все красные волнистые линии исчезли. Превосходно!

Имейте в виду, что здесь мы просто определяем структуру данных запроса и ответа API. На следующем шаге нам нужно будет объявить gRPC службу и добавить RPC определение для этого API.

Я сделаю это в отдельном файле под названием service_simple_bank.proto. Команды в верхней части для объявления синтаксиса и пакетов protobuf должны быть такими же, как и в других файлах proto.

syntax = "proto3";

package pb;

import "user.proto";

option go_package = "github.com/techschool/simplebank/pb";

Но на этот раз мы собираемся определить сервис SimpleBank вместо сообщения, как в других файлах. И внутри этого сервиса я определю первый RPC с названием CreateUser. В качестве входных данных он примет объект CreateUserRequest и вернет CreateUserResponse. Это определение RPC заканчивается парой фигурных скобок. Мы узнаем, как задать некоторые пользовательские настройки для RPC внутри этих фигурных скобок, в последующих лекциях. Пока что давайте оставим их пустыми и ничего писать внутри не будем.

service SimpleBank {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
  }
}

Здесь снова появились красные волнистые линии под объектами запроса и ответа. Вы уже наверняка знаете, что надо сделать, да? Нам нужно импортировать файл rpc_create_user.proto, так как именно в нем определены объекты запроса и ответа.

import "user.proto";
import "rpc_create_user.proto";

И мы можем избавиться от этого импорта файла user.proto

Пишем protobuf определения для API входа пользователя в систему

Итак, теперь у нас всё готово! Мы успешно определили gRPC API для создания нового пользователя. Можете ли вы сделать то же самое для API входа пользователя в систему? Сейчас вы можете поставить видео на паузу и сделать это самостоятельно. Мы займёмся этим очень скоро. И кстати, вы ещё помните 4 типа gRPC, о которых мы говорили в предыдущей лекции по теоретическим основам? Догадались ли вы к какому типу gRPC относится CreateUser API? Да, это самый простой тип gRPC: унарный gRPC, потому что клиент просто отправляет один запрос и получает один ответ.

Итак, удалось ли вам определить RPC API для входа пользователя в систему?

Он также относится к унарному gRPC, как и CreateUser API. Во-первых, давайте создадим новый файл под названием rpc_login_user.proto внутри папки proto.

Затем я скопирую всё содержимое из файла rpc_create_user.proto в этот новый файл. Теперь мы должны изменить названия сообщений на LoginUserRequest и LoginUserResponse. Для входа в систему нам нужны только username и password, поэтому я удалю все остальные поля и изменю номер поля password на 2. Это всё что нужно изменить для сообщения запроса.

syntax = "proto3";

package pb;

import "user.proto";

option go_package = "github.com/techschool/simplebank/pb";

message LoginUserRequest {
  string username = 1;
  string password = 2;
}

Для сообщения ответа от сервера, помимо информации о user, нам понадобятся ещё несколько полей для сессии, токена доступа и refresh токена. Итак, давайте добавим новое поле string session_id = 2, string access_token = 3 и string refresh_token = 4. А затем два поля с метками времени для хранения времени истечения срока действия токенов.

Я скопирую строку с импортом пакета google/protobuf/timestamp.proto из файла user.proto. Итак, поле access_token_expires_at будет типа google.protobuf.Timestamp с номером 5. Наконец, пропишем тот же тип для refresh_token_expires_at и назначим для него номер поля — 6.

message LoginUserResponse {
  User user = 1;
  string session_id = 2;
  string access_token = 3;
  string refresh_token = 4;
  google.protobuf.Timestamp access_token_expires_at = 5;
  google.protobuf.Timestamp refresh_token_expires_at = 6;
}

Хорошо, теперь пора определить RPC в файле service_simple_bank.proto. Давайте импортируем rpc_login_user.proto, затем продублируем RPC CreateUser. Я изменю название второго RPC на LoginUser. Входное сообщение изменим на LoginUserRequest, а исходящее — на LoginUserResponse. И это, по сути, всё, что надо сделать!

import "rpc_login_user.proto";

service SimpleBank {
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {}
  rpc LoginUser(LoginUserRequest) returns (LoginUserResponse) {}
}

Мы закончили с определением CreateUser и LoginUser API для нашего gRPC сервиса Simple Bank.

Теперь наступает важный шаг: создание Golang кода из этого определения сервиса. На самом деле сделать это довольно просто, так как у нас уже установлены все необходимые инструменты.

В Makefile я добавлю новую команду под названием proto. Затем давайте вернёмся на страницу Quick Start веб-сайта документации gRPC.

protoc команду для генерации кода можно найти в этом разделе.

Давайте скопируем её и вставим в наш Makefile. Однако нам придется немного изменить эту команду.

proto:
	protoc --go_out=. --go_opt=paths=source_relative \
        --go-grpc_out=. --go-grpc_opt=paths=source_relative \
        helloworld/helloworld.proto

Во-первых, давайте добавим параметр proto_path, чтобы указать путь к нашим proto файлам, которые находятся внутри папки proto. Затем с помощью ключа go_out пропишем путь к папке, куда бы мы хотели поместить сгенерированный Golang код, в нашем случае это папка pb. Оставим ключ go_opt=paths=source_relative без изменений, а go_grpc_out поменяем на ту же папку pb. Не будем менять go-grpc_opt=paths=source_relative. Наконец, последним аргументом должно быть путь к нашим proto файлам, то есть proto/*.proto, потому что мы хотим сгенерировать код для всех proto файлов в этой папке.

proto:
	protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
        --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
        proto/*.proto

Вот и всё! Давайте добавим команду proto в список PHONY. Затем откройте терминал и выполните:

make proto
protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
        --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
        proto/*.proto

Ура! Она успешно выполнена!

Теперь, если мы вернемся в Visual Studio Code и откроем папку pb, мы увидим несколько сгенерированных файлов с расширением .pb.go.

Каждый файл соответствует собственному proto файлу, который мы создали в папке proto.

Таким образом были созданы следующие файлы rpc_create_user, rpc_login_user, service_simple_bank, and user.

Также в папке находится файл service_simple_bank_grpc.pb.go. Этот файл, содержит серверные и клиентские gRPC интерфейсы или заглушки, на основе которых мы позже напишем настоящую реализацию сервисов. Прямо сейчас возникли некоторые ошибки с импортируемыми пакетами.

Они связаны с тем, что пакет Golang gRPC ещё не установлен на нашем компьютере. Мы можем исправить их, просто выполнив

go mod tidy
go: downloading google.golang.org/grpc v1.43.0
go: downloading google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa
go: downloading golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2

в терминале.

Go mod найдёт недостающие пакеты и скачает их за нас.

И, вуаля, все красные волнистые линии под импортами пропали.

Мы успешно сгенерировали код Golang для нашего gRPC сервиса.

Но перед тем как закончить, давайте добавим ещё одну команду в make proto:

proto:
	rm -f pb/*.go
	protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \
        --go-grpc_out=pb --go-grpc_opt=paths=source_relative \
        proto/*.proto

Эта команда rm удалит все существующие Golang файлы в папке pb перед запуском следующей команды для повторной генерации новых кодов. Почему мы должны это делать?

Потому что в этом случае мы избавимся от лишних файлов. Если мы захотим удалить некоторые proto файлы, соответствующие им сгенерированные Golang файлы будут удалены этой командой rm.

И на этом мы закончим сегодняшнюю лекцию. Теперь вы знаете, как написать protobuf определение для gRPC API сервиса и сгенерировать из него Golang код.

На следующей лекции мы узнаем как использовать этот сгенерированный код для реализации обработчика gRPC API сервиса на стороне сервера.

Большое спасибо за время, потраченное на чтение! Желаю вам получать удовольствие от обучения и до встречи на следующей лекции!