Начало работы с gRPC-Go

1. Введение

В этой лабораторной работе вы будете использовать gRPC-Go для создания клиента и сервера, которые составят основу приложения для сопоставления маршрутов, написанного на Go.

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

Служба определена в файле Protocol Buffers, который будет использоваться для генерации шаблонного кода для клиента и сервера, чтобы они могли взаимодействовать друг с другом, экономя ваше время и усилия при реализации этой функциональности.

Сгенерированный код учитывает не только сложности взаимодействия между сервером и клиентом, но также сериализацию и десериализацию данных.

Чему вы научитесь

  • Как использовать Protocol Buffers для определения API сервиса.
  • Как создать клиент и сервер на основе gRPC из определения Protocol Buffers с использованием автоматической генерации кода.
  • Понимание клиент-серверного взаимодействия с помощью gRPC.

Эта практическая работа предназначена для разработчиков на Go, впервые использующих gRPC или желающих освежить свои знания, а также для всех, кто интересуется разработкой распределённых систем. Опыт работы с gRPC не требуется.

2. Прежде чем начать

Предпосылки

Убедитесь, что у вас установлено следующее:

  • Набор инструментов Go версии 1.24.5 или более поздней. Инструкции по установке см. в руководстве по началу работы с Go.
  • Компилятор буфера протокола protoc версии 3.27.1 или более поздней. Инструкции по установке см. в руководстве по установке компилятора.
  • Плагины компилятора буфера протокола для Go и gRPC. Чтобы установить эти плагины, выполните следующие команды:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Обновите переменную PATH , чтобы компилятор буфера протокола мог найти плагины:

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

Получить код

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

Загрузите этот исходный код в виде архива .ZIP с GitHub и распакуйте его содержимое.

Кроме того, готовый исходный код доступен на GitHub, если вы не хотите вводить реализацию вручную.

3. Определите услугу

Первым шагом будет определение службы gRPC приложения, её метода RPC и типов сообщений запросов и ответов с помощью Protocol Buffers . Ваша служба будет предоставлять:

  • Метод RPC, называемый GetFeature , который реализует сервер и вызывает клиент.
  • Типы сообщений Point и Feature представляют собой структуры данных, которыми обмениваются клиент и сервер при использовании метода GetFeature . Клиент предоставляет координаты карты в качестве Point в своём запросе GetFeature к серверу, а сервер отвечает соответствующим Feature , описывающим то, что находится в этих координатах.

Этот метод RPC и его типы сообщений будут определены в файле routeguide/route_guide.proto предоставленного исходного кода.

Буферы протоколов обычно называются protobuf. Подробнее о терминологии gRPC см. в разделе «Основные концепции, архитектура и жизненный цикл gRPC».

Типы сообщений

В файле routeguide/route_guide.proto исходного кода сначала определите тип сообщения Point . Point представляет собой пару координат (широта-долгота) на карте. В этой практической работе используйте целые числа в качестве координат:

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Цифры 1 и 2 — это уникальные идентификационные номера для каждого из полей в структуре message .

Затем определите тип сообщения Feature . Feature использует string поле для имени или почтового адреса объекта, расположенного в месте, указанном Point :

message Feature {
  // The name or address of the feature.
  string name = 1;

  // The point where the feature is located.
  Point location = 2;
}

Метод обслуживания

Файл route_guide.proto имеет структуру service с именем RouteGuide , которая определяет один или несколько методов, предоставляемых службой приложения.

Добавьте метод rpc GetFeature в определение RouteGuide . Как объяснялось ранее, этот метод ищет название или адрес местоположения по заданному набору координат, поэтому GetFeature должен возвращать Feature для заданной Point :

service RouteGuide {
  // Definition of the service goes here

  // Obtains the feature at a given position.
  rpc GetFeature(Point) returns (Feature) {}
}

Это унарный метод RPC: простой RPC , при котором клиент отправляет запрос серверу и ждет ответа, как при локальном вызове функции.

4. Сгенерируйте клиентский и серверный код.

Затем сгенерируйте шаблонный код gRPC для клиента и сервера из файла .proto с помощью компилятора Protocol Buffer. В каталоге routeguide выполните:

protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       route_guide.proto

Эта команда генерирует следующие файлы:

  • route_guide.pb.go , который содержит функции для создания типов сообщений приложения и доступа к их данным.
  • route_guide_grpc.pb.go , который содержит функции, используемые клиентом для вызова удаленного метода gRPC службы, и функции, используемые сервером для предоставления этой удаленной службы.

Далее мы реализуем метод GetFeature на стороне сервера, чтобы сервер мог ответить, когда клиент отправляет запрос.

5. Реализуйте услугу

Функция GetFeature на стороне сервера выполняет основную работу: она принимает сообщение Point от клиента и возвращает в сообщении Feature соответствующую информацию о местоположении из списка известных мест. Вот реализация функции в server/server.go :

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // No feature was found, return an unnamed feature
  return &pb.Feature{Location: point}, nil
}

При вызове этого метода после запроса от удалённого клиента функции передаются объект Context описывающий RPC-вызов, и объект буфера протокола Point из этого клиентского запроса. Функция возвращает объект буфера протокола Feature для искомого местоположения и при необходимости error .

В методе заполните объект Feature соответствующей информацией для заданной Point , а затем return его вместе с nil ошибкой, чтобы сообщить gRPC, что вы завершили работу с RPC и что объект Feature можно вернуть клиенту.

Метод GetFeature требует создания и регистрации объекта routeGuideServer , чтобы запросы клиентов на поиск местоположения можно было направлять в эту функцию. Это делается в main() :

func main() {
  flag.Parse()
  lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  grpcServer := grpc.NewServer(opts...)

  s := &routeGuideServer{}
  s.loadFeatures()
  pb.RegisterRouteGuideServer(grpcServer, s)
  grpcServer.Serve(lis)
}

Вот что происходит в main() , шаг за шагом:

  1. Укажите TCP-порт для прослушивания запросов удалённых клиентов с помощью lis, err := net.Listen(...) . По умолчанию приложение использует TCP-порт 50051 , указанный переменной port или параметром --port в командной строке при запуске сервера. Если TCP-порт не удаётся открыть, приложение завершает работу с фатальной ошибкой.
  2. Создайте экземпляр сервера gRPC с помощью grpc.NewServer(...) , назвав этот экземпляр grpcServer .
  3. Создайте указатель на routeGuideServer , структуру, представляющую службу API приложения, назвав указатель s.
  4. Используйте s.loadFeatures() для заполнения массива s.savedFeatures местоположениями, которые можно найти с помощью GetFeature .
  5. Зарегистрируйте службу API на сервере gRPC, чтобы вызовы RPC к GetFeature направлялись в соответствующую функцию.
  6. Вызовите Serve() на сервере с данными нашего порта, чтобы выполнить блокирующее ожидание клиентских запросов; это продолжается до тех пор, пока процесс не будет завершен или не будет вызван Stop() .

Функция loadFeatures() получает сопоставления координат и местоположения из server/testdata.go .

6. Создайте клиента

Теперь отредактируйте client/client.go , в котором вы будете реализовывать клиентский код.

Для вызова методов удалённой службы сначала необходимо создать канал gRPC для взаимодействия с сервером. Мы создаём его, передавая строку целевого URI сервера (в данном случае это просто адрес и номер порта) в grpc.NewClient() в функции main() клиента следующим образом:

conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
        log.Fatalf("fail to dial: %v", err)
}
defer conn.Close()

Адрес сервера, определяемый переменной serverAddr , по умолчанию равен localhost:50051 и может быть переопределен с помощью переключателя --addr в командной строке при запуске клиента.

Если клиенту необходимо подключиться к службе, требующей учётных данных аутентификации, например, TLS или JWT, он может передать объект DialOptions в качестве параметра методу grpc.NewClient , содержащему необходимые учётные данные. Служба RouteGuide не требует никаких учётных данных.

После настройки канала gRPC нам понадобится клиентская заглушка для выполнения RPC-вызовов через вызовы функций Go. Мы получаем эту заглушку с помощью метода NewRouteGuideClient , предоставленного файлом route_guide_grpc.pb.go , сгенерированным из файла .proto приложения.

import (pb "github.com/grpc-ecosystem/codelabs/getting_started_unary/routeguide")

client := pb.NewRouteGuideClient(conn)

Методы обслуживания вызовов

В gRPC-Go RPC работают в блокирующем/синхронном режиме, что означает, что вызов RPC ждет ответа сервера и либо возвращает ответ, либо ошибку.

Простой RPC

Вызов простого RPC GetFeature почти так же прост, как вызов локального метода, в данном случае client.GetFeature :

point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

// Call GetFeature method on the client.
feature, err := client.GetFeature(context.TODO(), point)
if err != nil {
  log.Fatalf("client.GetFeature failed: %v", err)
}

Клиент вызывает метод созданной ранее заглушки. Для параметров метода клиент создаёт и заполняет объект буфера протокола запроса Point . Вы также передаёте объект context.Context , который позволяет при необходимости изменить поведение RPC, например, установить ограничение по времени вызова или отменить RPC в процессе выполнения. Если вызов не возвращает ошибку, клиент может прочитать информацию об ответе сервера из первого возвращаемого значения:

log.Println(feature)

В целом функция main() клиента должна выглядеть так:

func main() {
        flag.Parse()

        // Set up a connection to the gRPC server.
        conn, err := grpc.NewClient("dns:///"+*serverAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("fail to dial: %v", err)
        }
        defer conn.Close()

        // Create a new RouteGuide stub.
        client := pb.NewRouteGuideClient(conn)

        point := &pb.Point{Latitude: 409146138, Longitude: -746188906}
        log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

        // Call GetFeature method on the client.
        feature, err := client.GetFeature(context.TODO(), point)
        if err != nil {
                log.Fatalf("client.GetFeature failed: %v", err)
        }
        log.Println(feature)
}

7. Попробуйте

Убедитесь, что сервер и клиент работают друг с другом корректно, выполнив следующие команды в рабочем каталоге приложения:

  1. Запустите сервер в одном терминале:
cd server
go run .
  1. Запустите клиент с другого терминала:
cd client
go run .

Вы увидите примерно такой вывод, при этом временные метки опущены для ясности:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>

8. Что дальше?