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) пользователя и соответствующие ему ответы в видеmessage
s.
Наконец, нам нужно скомпилировать 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 в нашем приложении
После завершения процесса компиляции мы можем начать использовать сгенерированный код в нашем приложении.
Настройка и интеграция базы данных
Сначала нам нужно настроить базу данных и коллекцию в 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://[::1]:8080
URL-адрес, выберите опцию «Импортировать файл .proto» и загрузите user.proto
файл, который мы создали ранее.
После этого соответствующий метод будет заполнен, и мы сможем протестировать его соответствующим образом.
Мы также можем убедиться, что наш сервер gRPC работает, проверив нашу коллекцию MongoDB.
Заключение
В этом посте обсуждалось, что такое gRPC, его роль в создании масштабируемых приложений и как начать работу с создания службы управления пользователями с помощью Golang и MongoDB. Помимо того, что обсуждалось выше, gRPC предлагает надежные методы аутентификации, обработки ошибок, производительности и т. д.