среда, 31 марта 2021 г.

Базовое руководство по gRPC в Go

Это руководство представляет собой базовое введение в работу с gRPC в Golang.

Изучив этот пример, вы:

  • Определите службу в файле .proto.
  • Сгенерируйте серверный и клиентский код с помощью protocol buffer компилятора.
  • Используйте Go gRPC API, чтобы написать простой клиент и сервер для вашей службы.

Обратите внимание, что в примере в этом руководстве используется версия proto3 языка protocol buffers.

Зачем использовать gRPC?

Наш пример представляет собой простое приложение для сопоставления маршрутов, которое позволяет клиентам получать информацию об объектах на своем маршруте, создавать сводку своего маршрута и обмениваться информацией о маршруте, такой как обновления трафика, с сервером и другими клиентами.

С помощью gRPC мы можем один раз определить нашу службу в файле .proto и создать клиентов и серверы на любом из поддерживаемых языков gRPC, которые, в свою очередь, могут быть запущены в различных средах, от серверов в большом центре обработки данных до вашего собственного планшета - вся сложность общение между разными языками и средами осуществляется за вас через gRPC. Мы также получаем все преимущества работы с protocol buffers, включая эффективную сериализацию, простой IDL и легкое обновление интерфейса.

Настройка

У вас уже должны быть установлены инструменты, необходимые для генерации кода клиентского и серверного интерфейса.

Получите пример кода

Код примера является частью репозитория grpc-go.

Загрузите репозиторий в виде zip-файла и разархивируйте его или клонируйте репозиторий:

$ git clone -b v1.35.0 https://github.com/grpc/grpc-go

Перейдите в каталог с примером:

$ cd grpc-go/examples/route_guide

Определение сервисы

Нашим первым шагом является определение службы gRPC и типов запросов и ответов метода с использованием protocol buffers. Полный файл .proto находится в routeguide/route_guide.proto.

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

service RouteGuide {
   ...
}

Затем вы определяете методы rpc внутри определения службы, указывая их типы запроса и ответа. gRPC позволяет определить четыре вида методов службы, все из которых используются в службе RouteGuide:

1. Простой RPC, при котором клиент отправляет запрос на сервер с помощью заглушки и ждет ответа, как при обычном вызове функции.

// Получает объект в заданной позиции.
rpc GetFeature(Point) returns (Feature) {}

2. RPC с потоковой передачей на стороне сервера, при котором клиент отправляет запрос на сервер и получает поток для обратного чтения последовательности сообщений. Клиент читает из возвращенного потока, пока не кончатся сообщения. Как вы можете видеть в нашем примере, вы указываете метод потоковой передачи на стороне сервера, помещая ключевое слово stream перед типом ответа.

// Получает Features, доступные в данном Rectangle. 
// Результаты передается в потоковом режиме, 
// а не возвращается сразу (например, в ответном сообщении с
// повторяющимся полем), так как прямоугольник 
// может покрывать большую площадь и содержать
// огромное количество функций.
rpc ListFeatures(Rectangle) returns (stream Feature) {}

3. RPC с потоковой передачей на стороне клиента, при котором клиент записывает последовательность сообщений и отправляет их на сервер, снова используя предоставленный поток. Как только клиент закончит писать сообщения, он ждет, пока сервер прочитает их все и вернет свой ответ. Вы указываете метод потоковой передачи на стороне клиента, помещая ключевое слово stream перед типом запроса.

// Принимает поток Points по пройденному маршруту, возвращая
// RouteSummary по завершении обхода.
rpc RecordRoute(stream Point) returns (RouteSummary) {}

4. Двунаправленный потоковый RPC, при котором обе стороны отправляют последовательность сообщений, используя поток чтения-записи. Два потока работают независимо, поэтому клиенты и серверы могут читать и писать в любом порядке: например, сервер может дождаться получения всех клиентских сообщений, прежде чем писать свои ответы, или он может поочередно читать сообщение, а затем писать сообщение, или какая-то другая комбинация чтения и записи. Порядок сообщений в каждом потоке сохраняется. Вы указываете этот тип метода, помещая ключевое слово stream перед запросом и ответом.

// Принимает поток RouteNotes, 
// отправленный во время прохождения маршрута,
// при получении других RouteNotes 
// (например, от других пользователей).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

Наш файл .proto также содержит определения типов сообщений protocol buffer для всех типов запросов и ответов, используемых в наших методах сервиса - например, вот тип сообщения Point:

// Точки представлены парами широта-долгота в представлении E7
// (градусы умножены на 10**7 и 
// округлены до ближайшего целого числа).
// Широта должна быть в диапазоне +/- 90 градусов, 
// а долгота должна быть в диапазоне
// диапазон +/- 180 градусов (включительно).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Генерация клиентского и серверного кода

Затем нам нужно сгенерировать клиентский и серверный интерфейсы gRPC из нашего определения службы .proto. Мы делаем это с помощью protocol buffer компилятора protoc со специальным плагином gRPC Go.

В каталоге examples/route_guide выполните следующую команду:

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

Выполнение этой команды создает следующие файлы в каталоге routeguide:

  • route_guide.pb.go, который содержит весь protocol buffer код для заполнения, сериализации и извлечения типов сообщений запроса и ответа.
  • route_guide_grpc.pb.go, который содержит следующее:
    • Тип интерфейса (или заглушка) для вызовов клиентов с помощью методов, определенных в службе RouteGuide.
    • Тип интерфейса для реализации серверами, также с методами, определенными в службе RouteGuide.

Создание сервера

Сначала давайте посмотрим, как мы создаем сервер RouteGuide. Если вас интересует только создание клиентов gRPC, вы можете пропустить этот раздел и сразу перейти к созданию клиента (хотя вам все равно это может показаться интересным!).

Чтобы наша служба RouteGuide выполняла свою работу, нужно сделать две части:

  • Реализация интерфейса службы, созданного на основе нашего определения службы: выполнение фактической "работы" нашей службы.
  • Запуск сервера gRPC для прослушивания запросов от клиентов и их отправки в нужную реализацию службы.

Вы можете найти пример сервера RouteGuide в server/server.go. Давайте подробнее рассмотрим, как он работает.

Реализация RouteGuide

Как видите, у нашего сервера есть тип структуры routeGuideServer, который реализует сгенерированный интерфейс RouteGuideServer:

type routeGuideServer struct {
        ...
}
...

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
        ...
}
...

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
        ...
}
...

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
        ...
}
...

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
        ...
}
...

Простой RPC

RouteGuideServer реализует все наши методы сервиса. Давайте сначала рассмотрим простейший тип, GetFeature, который просто получает Point от клиента и возвращает информацию о соответствующей функции из своей базы данных в Feature.

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
    }
  }
  // Функция не найдена, возвращаем безымянную функцию
  return &pb.Feature{Location: point}, nil
}

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

RPC потоковой передачи на стороне сервера

Теперь давайте посмотрим на один из наших потоковых RPC. ListFeatures - это потоковый RPC на стороне сервера, поэтому нам нужно отправить обратно несколько функций нашему клиенту.

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
  for _, feature := range s.savedFeatures {
    if inRange(feature.Location, rect) {
      if err := stream.Send(feature); err != nil {
        return err
      }
    }
  }
  return nil
}

Как видите, вместо получения простых объектов запроса и ответа в параметрах нашего метода на этот раз мы получаем объект запроса (прямоугольник (Rectangle), в котором наш клиент хочет найти функции) и специальный объект RouteGuide_ListFeaturesServer для записи наших ответов.

В этом методе мы заполняем столько объектов Feature, сколько нам нужно вернуть, записывая их в RouteGuide_ListFeaturesServer, используя его метод Send(). Наконец, как и в нашем простом RPC, мы возвращаем нулевую ошибку, чтобы сообщить gRPC, что мы закончили писать ответы. Если в этом вызове произойдет какая-либо ошибка, мы вернем ошибку, отличную от нуля; уровень gRPC преобразует его в соответствующий статус RPC для отправки по сети.

Клиентская потоковая передача RPC

Теперь давайте посмотрим на нечто более сложное: на клиентский метод потоковой передачи RecordRoute, где мы получаем поток Points от клиента и возвращаем один RouteSummary с информацией об их поездке. Как видите, на этот раз у метода вообще нет параметра запроса. Вместо этого он получает поток RouteGuide_RecordRouteServer, который сервер может использовать как для чтения, так и для записи сообщений - он может получать клиентские сообщения с помощью метода Recv() и возвращать свой единственный ответ с помощью метода SendAndClose().

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
  var pointCount, featureCount, distance int32
  var lastPoint *pb.Point
  startTime := time.Now()
  for {
    point, err := stream.Recv()
    if err == io.EOF {
      endTime := time.Now()
      return stream.SendAndClose(&pb.RouteSummary{
        PointCount:   pointCount,
        FeatureCount: featureCount,
        Distance:     distance,
        ElapsedTime:  int32(endTime.Sub(startTime).Seconds()),
      })
    }
    if err != nil {
      return err
    }
    pointCount++
    for _, feature := range s.savedFeatures {
      if proto.Equal(feature.Location, point) {
        featureCount++
      }
    }
    if lastPoint != nil {
      distance += calcDistance(lastPoint, point)
    }
    lastPoint = point
  }
}

В теле метода мы используем метод Recv() RouteGuide_RecordRouteServer для многократного чтения в запросах нашего клиента к объекту запроса (в данном случае Point) до тех пор, пока больше не будет сообщений: серверу необходимо проверить ошибку, возвращаемую из Read() после каждого вызова. Если это nil, поток все еще в порядке, и он может продолжить чтение; если это io.EOF, поток сообщений закончился и сервер может вернуть свой RouteSummary. Если он имеет любое другое значение, мы возвращаем ошибку "как есть", чтобы уровень gRPC преобразовал ее в статус RPC.

Двунаправленный потоковый RPC

Наконец, давайте посмотрим на наш двунаправленный потоковый RPC RouteChat().

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      return nil
    }
    if err != nil {
      return err
    }
    key := serialize(in.Location)
                ... // ищем заметки для отправки клиенту
    for _, note := range s.routeNotes[key] {
      if err := stream.Send(note); err != nil {
        return err
      }
    }
  }
}

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

Синтаксис для чтения и записи здесь очень похож на наш метод потоковой передачи клиента, за исключением того, что сервер использует метод потока Send(), а не SendAndClose(), поскольку он записывает несколько ответов. Хотя каждая сторона всегда будет получать сообщения друг друга в том порядке, в котором они были написаны, и клиент, и сервер могут читать и писать в любом порядке - потоки работают полностью независимо.

Запуск сервера

После того, как мы реализовали все наши методы, нам также необходимо запустить сервер gRPC, чтобы клиенты действительно могли использовать наш сервис. В следующем фрагменте показано, как мы это делаем для нашей службы RouteGuide:

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...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)

Чтобы собрать и запустить сервер:

1. Укажите порт, который мы хотим использовать для прослушивания клиентских запросов, используя:

lis, err := net.Listen(...).

2. Создайте экземпляр сервера gRPC с помощью grpc.NewServer(...).

3. Зарегистрируйте нашу реализацию сервиса на сервере gRPC.

4. Вызовите Serve() на сервере с данными нашего порта, чтобы выполнить блокирующее ожидание, пока процесс не будет убит или не будет вызван Stop().

Создание клиента

В этом разделе мы рассмотрим создание клиента Go для нашей службы RouteGuide. Вы можете увидеть полный пример клиентского кода в examples/route_guide/client/client.go.

Создание заглушки

Чтобы вызвать методы сервиса, нам сначала нужно создать канал gRPC для связи с сервером. Мы создаем его, передавая адрес сервера и номер порта в grpc.Dial() следующим образом:

var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
  ...
}
defer conn.Close()

Вы можете использовать DialOptions для установки учетных данных аутентификации (например, учетных данных TLS, GCE или JWT) в grpc.Dial, когда они требуются службе. Сервис RouteGuide не требует никаких учетных данных.

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

client := pb.NewRouteGuideClient(conn)

Вызов сервисных методов

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

Простой RPC

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

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
  ...
}

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

log.Println(feature)

RPC потоковой передачи на стороне сервера

Здесь мы вызываем серверный метод потоковой передачи ListFeatures, который возвращает поток географических объектов. Если вы уже прочитали "Создание сервера", некоторые из них могут показаться вам очень знакомыми - потоковые RPC реализованы одинаковым образом с обеих сторон.

rect := &pb.Rectangle{ ... }  // инициализируем pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
  ...
}
for {
    feature, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
    }
    log.Println(feature)
}

Как и в простом RPC, мы передаем методу контекст и запрос. Однако вместо того, чтобы вернуть объект ответа, мы возвращаем экземпляр RouteGuide_ListFeaturesClient. Клиент может использовать поток RouteGuide_ListFeaturesClient для чтения ответов сервера.

Мы используем метод Recv() RouteGuide_ListFeaturesClient для многократного чтения ответов сервера на объект protocol buffer ответа (в данном случае Feature) до тех пор, пока больше не будет сообщений: клиенту необходимо проверять ошибку err, возвращаемую из Recv() после каждого вызов. Если nil, поток все еще в порядке, и он может продолжить чтение; если это io.EOF, значит, поток сообщений закончился; в противном случае должна быть ошибка RPC, которая пропускается через err.

Клиентская потоковая передача RPC

Метод потоковой передачи на стороне клиента RecordRoute похож на метод на стороне сервера, за исключением того, что мы только передаем этому методу контекст и получаем обратно поток RouteGuide_RecordRouteClient, который мы можем использовать как для записи, так и для чтения сообщений.

// Создаем случайное количество случайных точек
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Пройдите не менее двух точек
var points []*pb.Point
for i := 0; i < pointCount; i++ {
  points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
stream, err := client.RecordRoute(context.Background())
if err != nil {
  log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
  if err := stream.Send(point); err != nil {
    log.Fatalf("%v.Send(%v) = %v", stream, point, err)
  }
}
reply, err := stream.CloseAndRecv()
if err != nil {
  log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)

RouteGuide_RecordRouteClient имеет метод Send(), который мы можем использовать для отправки запросов на сервер. После того, как мы закончили писать запросы нашего клиента в поток с помощью Send(), нам нужно вызвать CloseAndRecv() в потоке, чтобы сообщить gRPC, что мы закончили запись и ожидаем ответа. Мы получаем наш статус RPC из ошибки, возвращенной CloseAndRecv(). Если статус равен nil, то первым возвращаемым значением от CloseAndRecv() будет действительный ответ сервера.

Двунаправленный потоковый RPC

Наконец, давайте посмотрим на наш двунаправленный потоковый RPC RouteChat(). Как и в случае с RecordRoute, мы передаем методу только объект контекста и получаем обратно поток, который мы можем использовать как для записи, так и для чтения сообщений. Однако на этот раз мы возвращаем значения через поток нашего метода, в то время как сервер все еще записывает сообщения в свой поток сообщений.

stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func() {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      // read done.
      close(waitc)
      return
    }
    if err != nil {
      log.Fatalf("Failed to receive a note : %v", err)
    }
    log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
  }
}()
for _, note := range notes {
  if err := stream.Send(note); err != nil {
    log.Fatalf("Failed to send a note: %v", err)
  }
}
stream.CloseSend()
<-waitc

Синтаксис для чтения и записи здесь очень похож на наш метод потоковой передачи на стороне клиента, за исключением того, что мы используем метод CloseSend() потока после завершения нашего вызова. Хотя каждая сторона всегда будет получать сообщения другой стороны в том порядке, в котором они были написаны, и клиент, и сервер могут читать и писать в любом порядке - потоки работают полностью независимо.

Запустите пример

Выполните следующие команды из каталога examples/route_guide:

Запускаем сервер:

$ go run server/server.go

С другого терминала запустите клиент:

$ go run client/client.go

Вы увидите такой результат:

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:<>
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
...
name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
Traversing 56 points.
Route summary: point_count:56 distance:497013163
Got message First message at point(0, 1)
Got message Second message at point(0, 2)
Got message Third message at point(0, 3)
Got message First message at point(0, 1)
Got message Fourth message at point(0, 1)
Got message Second message at point(0, 2)
Got message Fifth message at point(0, 2)
Got message Third message at point(0, 3)
Got message Sixth message at point(0, 3)


Читайте также:


Комментариев нет:

Отправить комментарий