gRPC в Golang

Архитектура микросервисов — один из предпочтительных методов создания масштабируемых и надежных приложений. Она предполагает разбиение больших приложений на более мелкие компоненты, которые четко определены, выполняют конкретную задачу и используют наборы интерфейсов прикладного программирования (API) для их взаимодействия.

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

В этой статье мы рассмотрим, что такое gRPC и как начать создавать службу управления пользователями с использованием gRPC, MongoDB и Golang.

Что такое gRPC?

gRPC — это современная коммуникационная платформа, которая может работать в любой среде и помогает эффективно подключать сервисы. Он был представлен в 2015 году и управляется Cloud Native Computing Platform (CNCF). Помимо эффективного соединения сервисов в распределенной системе, мобильных приложений, внешнего интерфейса и серверной части и т. д., он поддерживает проверку работоспособности, балансировку нагрузки, трассировку и аутентификацию.

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

Определение услуги

gRPC использует буферы протокола в качестве языка описания интерфейса, аналогичного JSON, и предоставляет такие функции, как аутентификация, отмена, таймауты и т. д.

Легкий и производительный

Определения gRPC на 30 процентов меньше, чем определения JSON, и работают в 5–7 раз быстрее, чем традиционный REST API.

Поддержка нескольких платформ

gRPC не зависит от языка и автоматически генерирует код для языков, поддерживаемых клиентом и сервером.

Масштабируемый

От среды разработчика до производства gRPC предназначен для масштабирования запросов на миллионы лер-секунд.

Начало

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

Предварительные условия

Чтобы полностью усвоить концепции, представленные в этом руководстве, необходимо следующее:

  • Базовое понимание Go
  • Базовое понимание протокольного буфера
  • Установлен компилятор протокольного буфера.
  • Учетная запись MongoDB для размещения базы данных. Регистрация совершенно бесплатна .
  • Postman или любое приложение для тестирования gRPC.

Настройка проекта и зависимостей

Чтобы начать, нам нужно перейти в нужный каталог и выполнить команду ниже в нашем терминале:

cargo new grpc_go && cd grpc_go

Эта команда создает проект Golang под названием grpc_go и переходит в каталог проекта.

Далее нам нужно инициализировать модуль Go для управления зависимостями проекта, выполнив следующую команду:

go mod init grpc_go

Эта команда создаст go.mod файл для отслеживания зависимостей проекта.

Переходим к установке необходимых зависимостей с помощью:

go get google.golang.org/grpc go.mongodb.org/mongo-driver/mongo github.com/joho/godotenv google.golang.org/protobuf

google.golang.org/grpc— это реализация gRPC на Golang.

go.mongodb.org/mongo-driver/mongo— драйвер для подключения к MongoDB.

github.com/joho/godotenv— это библиотека для управления переменными среды.

google.golang.org/protobuf— это реализация протокольных буферов в Golang.

Определение буфера протокола управления пользователями и компиляция

Для начала нам нужно определить буфер протокола для представления всех операций и ответов, задействованных в службе управления пользователями. Для этого сначала нам нужно создать proto папку в корневом каталоге, а в этой папке создать файл user.proto и добавить фрагмент ниже:

syntax = "proto3";
package user;
option go_package = "grpc_go/proto";

service UserService {
    rpc GetUser (UserRequest) returns (UserResponse);
    rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
    rpc UpdateUser (UpdateUserRequest) returns (UpdateUserResponse);
    rpc DeleteUser (DeleteUserRequest) returns (DeleteUserResponse);
    rpc GetAllUsers (Empty) returns (GetAllUsersResponse);
}

message UserRequest {
    string id = 1;
}

message UserResponse {
    string id = 1;
    string name = 2;
    string location = 3;
    string title = 4;
}

message CreateUserRequest {
    string name = 2;
    string location = 3;
    string title = 4;
}

message CreateUserResponse {
    string data = 1;
}

message UpdateUserRequest {
    string id = 1;
    string name = 2;
    string location = 3;
    string title = 4;
}

message UpdateUserResponse {
    string data = 1;
}

message DeleteUserRequest {
    string id = 1;
}

message DeleteUserResponse {
    string data = 1;
}

message Empty {}

message GetAllUsersResponse {
    repeated UserResponse users = 1;
}

Приведенный выше фрагмент делает следующее:

  • Указывает использование proto3 синтаксиса
  • Объявляется user как имя пакета
  • Использует go_package возможность определить путь импорта пакета и место хранения сгенерированного кода.
  • Создает service для создания, чтения, редактирования и удаления (CRUD) пользователя и соответствующие ему ответы в виде messages.

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

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

Приведенная выше команда использует компилятор Protocol Buffer для генерации кода сервера и клиента Golang путем указания относительной части и использования user.proto файла.

Чтобы избежать ошибок, мы должны убедиться, что добавили Golang в путь.

При успешной компиляции мы должны увидеть user_grpc.pb.go файлы user.pb.go, добавленные в proto папку. Эти файлы содержат код сервера и клиента, созданный gRPC. В этой статье мы будем использовать только серверный код.

файлы, созданные gRPC

Использование сгенерированного кода из gRPC в нашем приложении

После завершения процесса компиляции мы можем начать использовать сгенерированный код в нашем приложении.

Настройка и интеграция базы данных

Сначала нам нужно настроить базу данных и коллекцию в MongoDB, как показано ниже:

настройка базы данных

Нам также нужно получить строку подключения к базе данных, нажав кнопку «Подключиться» и изменив значение «Драйвер» на Go.

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

MONGOURI=mongodb+srv://<YOUR USERNAME HERE>:<YOUR PASSWORD HERE>@cluster0.e5akf.mongodb.net/<DATABASE NAME>?retryWrites=true&w=majority

Пример правильно заполненной строки подключения ниже:

MONGOURI=mongodb+srv://malomz:malomzPassword@cluster0.e5akf.mongodb.net/projectMngt?retryWrites=true&w=majority

В-третьих, нам нужно создать вспомогательную функцию для загрузки переменной среды с помощью библиотеки github.com/joho/godoten. Для этого нам нужно создать configs папку в корневом каталоге; здесь создайте env.goфайл и добавьте фрагмент ниже:

package configs

import (
    "log"
    "os"
    "github.com/joho/godotenv"
)

func EnvMongoURI() string {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }
    return os.Getenv("MONGOURI")
}

В-четвертых, нам нужно создать модель для представления данных нашего приложения. Для этого нам нужно создать user_model.go файл в той же configs папке и добавить фрагмент ниже:

package configs

import "go.mongodb.org/mongo-driver/bson/primitive"

type User struct {
    Id       primitive.ObjectID `json:"id,omitempty"`
    Name     string             `json:"name,omitempty" validate:"required"`
    Location string             `json:"location,omitempty" validate:"required"`
    Title    string             `json:"title,omitempty" validate:"required"`
}

Наконец, нам нужно создать db.go файл для реализации логики нашей базы данных в той же configs папке и добавить фрагмент ниже:

package configs

import (
    "context"
    "fmt"
    "log"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type dbHandler interface {
    GetUser(id string) (*User, error)
    CreateUser(user User) (*mongo.InsertOneResult, error)
    UpdateUser(id string, user User) (*mongo.UpdateResult, error)
    DeleteUser(id string) (*mongo.DeleteResult, error)
    GetAllUsers() ([]*User, error)
}

type DB struct {
    client *mongo.Client
}

func NewDBHandler() dbHandler {
    client, err := mongo.NewClient(options.Client().ApplyURI(EnvMongoURI()))
    if err != nil {
        log.Fatal(err)
    }

    ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
    err = client.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }

    //ping the database
    err = client.Ping(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Connected to MongoDB")

    return &DB{client: client}
}

func colHelper(db *DB) *mongo.Collection {
    return db.client.Database("projectMngt").Collection("User")
}

func (db *DB) CreateUser(user User) (*mongo.InsertOneResult, error) {
    col := colHelper(db)
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    newUser := User{
        Id:       primitive.NewObjectID(),
        Name:     user.Name,
        Location: user.Location,
        Title:    user.Title,
    }
    res, err := col.InsertOne(ctx, newUser)

    if err != nil {
        return nil, err
    }

    return res, err
}

func (db *DB) GetUser(id string) (*User, error) {
    col := colHelper(db)
    var user User
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    objId, _ := primitive.ObjectIDFromHex(id)

    err := col.FindOne(ctx, bson.M{"_id": objId}).Decode(&user)

    if err != nil {
        return nil, err
    }

    return &user, err
}

func (db *DB) UpdateUser(id string, user User) (*mongo.UpdateResult, error) {
    col := colHelper(db)
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    objId, _ := primitive.ObjectIDFromHex(id)

    update := bson.M{"name": user.Name, "location": user.Location, "title": user.Title}
    result, err := col.UpdateOne(ctx, bson.M{"_id": objId}, bson.M{"$set": update})

    if err != nil {
        return nil, err
    }

    return result, err
}

func (db *DB) DeleteUser(id string) (*mongo.DeleteResult, error) {
    col := colHelper(db)
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    objId, _ := primitive.ObjectIDFromHex(id)

    result, err := col.DeleteOne(ctx, bson.M{"_id": objId})

    if err != nil {
        return nil, err
    }

    return result, err
}

func (db *DB) GetAllUsers() ([]*User, error) {
    col := colHelper(db)
    var users []*User
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    results, err := col.Find(ctx, bson.M{})

    if err != nil {
        return nil, err
    }

    for results.Next(ctx) {
        var singleUser *User
        if err = results.Decode(&singleUser); err != nil {
            return nil, err
        }
        users = append(users, singleUser)
    }

    return users, err
}

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Строки 15–21 : определяет dbHandler интерфейс, который описывает все связанные функции в нашей службе управления пользователями.
  • Строки 23–25 : Создает DBструктуру со col свойством, которое будет реализовывать dbHandlerинтерфейс.
  • Строки 27–48 : Создает NewDBHandler функцию-конструктор, которая связывает DBструктуру и dbHandlerинтерфейс, который она реализует, инициализируя подключение к базе данных к MongoDB и возвращая соответствующий ответ.
  • Строки 50–52 : Создает colHelper функцию, которая принимает соединение с базой данных, указывая имя базы данных и связанную коллекцию.
  • Строки 54–145 : Создает необходимые методы CreateUser, GetUser, UpdateUser, DeleteUserи GetAllUsersс DBприемником указателя и возвращает соответствующие ответы. Методы также используют соответствующие методы MongoDB для выполнения необходимых операций.

Интеграция логики базы данных с кодом, созданным с помощью gRPC.

Настроив логику нашей базы данных, мы можем использовать методы для создания обработчиков наших приложений. Для этого нам нужно создать service папку; здесь создайте user_service.go файл и добавьте фрагмент ниже:

package services

import (
    "context"
    "grpc_go/configs"
    pb "grpc_go/proto"
)

var db = configs.NewDBHandler()

type UserServiceServer struct {
    pb.UnimplementedUserServiceServer
}

func (service *UserServiceServer) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
    resp, err := db.GetUser(req.Id)

    if err != nil {
        return nil, err
    }

    return &pb.UserResponse{Id: resp.Id.String(), Name: resp.Name, Location: resp.Location, Title: resp.Title}, nil
}

func (service *UserServiceServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
    newUser := configs.User{Name: req.Name, Location: req.Location, Title: req.Title}
    _, err := db.CreateUser(newUser)

    if err != nil {
        return nil, err
    }

    return &pb.CreateUserResponse{Data: "User created successfully!"}, nil
}

func (service *UserServiceServer) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) {
    newUser := configs.User{Name: req.Name, Location: req.Location, Title: req.Title}
    _, err := db.UpdateUser(req.Id, newUser)

    if err != nil {
        return nil, err
    }

    return &pb.UpdateUserResponse{Data: "User updated successfully!"}, nil
}

func (service *UserServiceServer) DeleteUser(ctx context.Context, req *pb.DeleteUserRequest) (*pb.DeleteUserResponse, error) {
    _, err := db.DeleteUser(req.Id)

    if err != nil {
        return nil, err
    }

    return &pb.DeleteUserResponse{Data: "User details deleted successfully!"}, nil
}

func (service *UserServiceServer) GetAllUsers(context.Context, *pb.Empty) (*pb.GetAllUsersResponse, error) {
    resp, err := db.GetAllUsers()
    var users []*pb.UserResponse

    if err != nil {
        return nil, err
    }

    for _, v := range resp {
        var singleUser = &pb.UserResponse{
            Id:       v.Id.String(),
            Name:     v.Name,
            Location: v.Location,
            Title:    v.Title,
        }
        users = append(users, singleUser)
    }

    return &pb.GetAllUsersResponse{Users: users}, nil
}

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Инициализирует базу данных с помощью NewDBHandler функции конструктора
  • Создает объект UserServiceServer, реализующий интерфейс, созданный gRPC, UserServiceServer внутри user_grpc.pb.goфайла.
  • Создает необходимые методы, передавая UserServiceServer структуру как указатель и возвращая соответствующие ответы, сгенерированные gRPC.

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

После этого мы можем создать сервер gRPC приложения, создав main.goфайл в корневом каталоге и добавив фрагмент ниже:

package main

import (
    "log"
    "net"

    pb "grpc_go/proto"
    "grpc_go/services"
    "google.golang.org/grpc"
)
func main() {
    lis, err := net.Listen("tcp", "[::1]:8080")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    grpcServer := grpc.NewServer()
    service := &services.UserServiceServer{}

    pb.RegisterUserServiceServer(grpcServer, service)
    err = grpcServer.Serve(lis)

    if err != nil {
        log.Fatalf("Error strating server: %v", err)
    }
}

Приведенный выше фрагмент делает следующее:

  • Импортирует необходимые зависимости
  • Указывает порт приложения с помощью встроенного net пакета
  • Создает экземпляр сервера gRPC, используя NewServer метод, и указывает связанную службу, используя UserServiceServer структуру.
  • Зарегистрируйте реализацию службы на сервере gRPC.
  • Запускает сервер с использованием Serve метода, передавая необходимый порт и соответствующим образом обрабатывая ошибки.

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

go run main.go

Тестирование с помощью Postman

Когда наш сервер запущен и работает, мы можем протестировать наше приложение, создав новый запрос gRPC .

Создавать новое

Выберите запрос gRPC.

Введите grpc://[::1]:8080 URL-адрес, выберите опцию «Импортировать файл .proto» и загрузите user.proto файл, который мы создали ранее.

URL-адрес и загрузка файла прототипа

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

getAllUsers

getAUser

Мы также можем убедиться, что наш сервер gRPC работает, проверив нашу коллекцию MongoDB.

Коллекция

Заключение

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