基于grpc-go实现远程调用 | 青训营

65 阅读9分钟

本文将介绍如何基于grpc-go实现服务的远程调用

不使用其他rpc框架的原因是grpc-go的实现相对更简单(而且在github ;go语言rpc框架榜单排名较高)

database部分代码就不写了,需要的可以看我的上一篇文章。

源代码:github.com/OrionLi/use…

proto文件

// user.proto
syntax = "proto3";
option go_package = "/userpb";
package user;
import "google/protobuf/any.proto";

service UserService {
  rpc CreateUser(CreateUserRequest) returns (BaseResponse) {}
  rpc GetUser(GetUserRequest) returns (BaseResponse) {}
}

message CreateUserRequest {
  string username = 1;
  string account = 2;
  string password = 3;
}

message GetUserRequest {
  uint32 id = 1;
}

message BaseResponse {
  google.protobuf.Any data = 1;
}

message User {
  uint32 id = 1;
  string username = 2;
  string account = 3;
  string password = 4;
}

这段代码是一个使用 Protocol Buffers (protobuf) 定义的用户服务接口和数据结构。

首先,syntax = "proto3"; 表示该文件使用的是 proto3 语法。

option go_package = "/userpb"; 表示生成的 Go 语言代码的包名为 userpb

package user; 定义了当前文件的包名为 user,这个包名用于在生成的代码中进行组织。

import "google/protobuf/any.proto"; 引入了 google/protobuf/any.proto 文件,这个文件定义了 Any 类型,用于在消息中存储任意类型的数据。

service UserService 定义了一个名为 UserService 的服务,该服务包含两个 RPC 方法:CreateUserGetUser。每个方法都有输入参数和返回结果。

message CreateUserRequest 定义了一个消息类型,用于在创建用户时传递参数。它包含三个字段:usernameaccountpassword,这些字段分别表示用户名、账号和密码。

message GetUserRequest 定义了一个消息类型,用于在获取用户信息时传递参数。它只包含一个字段 id,表示用户的ID。

message BaseResponse 定义了一个消息类型,用于作为所有方法的返回结果。它含有一个 data 字段,类型为 google.protobuf.Any,用于存储任意类型的数据。

message User 定义了一个消息类型,表示用户的信息。它包含四个字段:idusernameaccountpassword,分别表示用户的ID、用户名、账号和密码。

service文件

package services

import (
    "errors"
    "gorm.io/gorm"
    "user-center-go/database"
    pb "user-center-go/proto/userpb"
)

type UserService interface {
    CreateUser(user *pb.User) (*pb.User, error)
    GetUserByID(id uint) (*pb.User, error)
    GetUserByUsername(username string) (*pb.User, error)
    UpdateUser(user *pb.User) (*pb.User, error)
    DeleteUser(id uint) error
}

type userService struct{}

func NewUserService() UserService {
    return &userService{}
}

func (s *userService) CreateUser(user *pb.User) (*pb.User, error) {
    err := database.DB.Create(user).Error
    if err != nil {
       return nil, err
    }
    return user, nil
}

func (s *userService) GetUserByID(id uint) (*pb.User, error) {
    var user pb.User
    err := database.DB.First(&user, id).Error
    if err != nil {
       if errors.Is(err, gorm.ErrRecordNotFound) {
          return nil, nil // 用户不存在
       }
       return nil, err
    }
    return &user, nil
}

func (s *userService) GetUserByUsername(username string) (*pb.User, error) {
    var user pb.User
    err := database.DB.Where("username = ?", username).First(&user).Error
    if err != nil {
       if errors.Is(err, gorm.ErrRecordNotFound) {
          return nil, nil // 用户不存在
       }
       return nil, err
    }
    return &user, nil
}

func (s *userService) UpdateUser(user *pb.User) (*pb.User, error) {
    err := database.DB.Save(user).Error
    if err != nil {
       return nil, err
    }
    return user, nil
}

func (s *userService) DeleteUser(id uint) error {
    err := database.DB.Delete(&pb.User{}, id).Error
    if err != nil {
       return err
    }
    return nil
}

这段代码定义了一个名为 UserService 的接口,并提供了一些实现了该接口的方法。

首先,import 语句导入了所需的包。

UserService 是一个接口,它定义了一些操作用户的方法,包括创建用户、根据 ID 获取用户、根据用户名获取用户、更新用户和删除用户。

userService 是一个结构体类型,实现了 UserService 接口。它没有任何字段或状态,只是用来组织和提供实现接口方法的功能。

NewUserService() 是一个工厂函数,用于创建一个新的 UserService 实例。

接下来是各个方法的实现:

  • CreateUser 方法用于创建用户,它接收一个 pb.User 类型的指针作为参数,将该用户存储到数据库中并返回创建的用户对象。
  • GetUserByID 方法用于根据用户 ID 获取用户信息,它接收一个 id 参数,通过查询数据库获取对应的用户信息,并返回用户对象。
  • GetUserByUsername 方法用于根据用户名获取用户信息,它接收一个 username 参数,通过查询数据库获取对应的用户信息,并返回用户对象。
  • UpdateUser 方法用于更新用户信息,它接收一个 pb.User 类型的指针作为参数,将更新后的用户信息存储到数据库中,并返回更新后的用户对象。
  • DeleteUser 方法用于删除用户,它接收一个 id 参数,根据用户的 ID 删除对应的用户信息。

在这些方法中,database.DB 是一个数据库连接对象,通过该对象来执行数据库操作。gorm 是一个流行的 Go 语言 ORM 库,用于操作关系型数据库。

这段代码实现了用户服务的基本功能,可以用于创建、获取、更新和删除用户信息。

handler

package handlers

import (
    "context"
    "fmt"
    "google.golang.org/protobuf/types/known/anypb"
    pb "user-center-go/proto/userpb"
    "user-center-go/services" // 替换为你的service文件所在的包路径
)

// 只提供grpc服务

type Server struct {
    UserService services.UserService
}

func (s *Server) CreateUser(_ context.Context, req *pb.CreateUserRequest) (*pb.BaseResponse, error) {
    user := &pb.User{
       Username: req.GetUsername(),
       Account:  req.GetAccount(),
       Password: req.GetPassword(),
    }

    createdUser, err := s.UserService.CreateUser(user)
    if err != nil {
       return nil, err
    }

    userResult, _ := anypb.New(createdUser)
    res := &pb.BaseResponse{
       Data: userResult,
    }

    return res, nil
}

func (s *Server) GetUser(_ context.Context, req *pb.GetUserRequest) (*pb.BaseResponse, error) {
    userID := req.GetId()

    user, err := s.UserService.GetUserByID(uint(userID))
    if err != nil {
       return nil, err
    }
    if user == nil {
       return nil, fmt.Errorf("user not found")
    }

    userResult, _ := anypb.New(user)
    res := &pb.BaseResponse{
       Data: userResult,
    }

    return res, nil
}

这段代码定义了一个名为 Server 的结构体类型,并提供了一些方法用于处理 gRPC 请求。

首先,import 语句导入了所需的包。

Server 是一个结构体类型,其中包含一个 UserService 类型的字段 UserService,用于处理用户相关的服务。

接下来是两个方法的实现:

  • CreateUser 方法接收一个 context.Context 对象和一个 pb.CreateUserRequest 对象作为参数,用于创建用户。它从请求对象中获取用户名、账号和密码,然后调用 UserServiceCreateUser 方法创建用户,并将返回的结果存储在 createdUser 变量中。如果创建用户的过程中发生错误,将返回错误;否则,将创建的用户对象转换为 anypb.Any 类型,并将其作为数据存储在 pb.BaseResponse 结构体中,并将该结构体作为响应返回。
  • GetUser 方法接收一个 context.Context 对象和一个 pb.GetUserRequest 对象作为参数,用于获取用户信息。它从请求对象中获取用户 ID,然后调用 UserServiceGetUserByID 方法根据用户 ID 获取用户信息,并将返回的结果存储在 user 变量中。如果获取用户信息的过程中发生错误,将返回错误;如果用户不存在,将返回一个自定义的错误;否则,将用户对象转换为 anypb.Any 类型,并将其作为数据存储在 pb.BaseResponse 结构体中,并将该结构体作为响应返回。

这段代码实现了一个 gRPC 服务,通过处理不同的请求,调用相应的 UserService 方法来执行用户相关的操作,并将结果封装到 pb.BaseResponse 结构体中作为响应返回。

服务端启动类

package main

import (
    "google.golang.org/grpc"
    "log"
    "net"
    "user-center-go/database"
    "user-center-go/handlers"
    pb "user-center-go/proto/userpb"
    "user-center-go/services"
)

func main() {
    // 初始化数据库连接
    database.InitSqliteDB()

    // 创建UserService实例
    userService := services.NewUserService()

    // 创建gRPC服务器
    grpcServer := grpc.NewServer()

    // 注册UserService服务
    pb.RegisterUserServiceServer(grpcServer, &handlers.Server{UserService: userService})

    // 监听指定端口
    listener, err := net.Listen("tcp", ":50051")
    if err != nil {
       log.Fatalf("Failed to listen: %v", err)
    }

    // 启动gRPC服务器
    if err := grpcServer.Serve(listener); err != nil {
       log.Fatalf("Failed to serve: %v", err)
    }
}

这段代码是一个服务端的主程序代码,它完成以下几个主要任务:

  1. 调用 database.InitSqliteDB() 来初始化数据库连接,确保数据库可用。

  2. 创建一个 UserService 实例,即 services.NewUserService(),用于处理用户相关的服务。

  3. 创建一个 gRPC 服务器实例 grpcServer,通过调用 grpc.NewServer() 来创建。

  4. 注册 UserService 服务,使得客户端可以通过 gRPC 调用相应的方法。通过调用 pb.RegisterUserServiceServer(grpcServer, &handlers.Server{UserService: userService})userService 注册到 grpcServer 上。

  5. 通过调用 net.Listen("tcp", ":50051") 监听指定的端口(这里是 50051),并返回一个 net.Listener 对象。

  6. 最后,通过调用 grpcServer.Serve(listener) 启动 gRPC 服务器,并开始监听客户端的请求。

整个过程可以概括为:初始化数据库连接,创建实例和服务器,注册服务,监听端口,并启动服务器等待客户端请求的到来。

客户端启动类

package main

import (
    "context"
    "fmt"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "log"
    "time"
    pb "user-center-go/proto/userpb" // 替换为你的 proto 文件的包路径
)

func main() {
    // 设置 gRPC 服务器的地址和端口
    address := "localhost:50051"

    // 创建与 gRPC 服务器的连接
    conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
       log.Fatalf("Failed to connect to gRPC server: %v", err)
    }
    defer conn.Close()

    // 创建 gRPC 客户端
    client := pb.NewUserServiceClient(conn)

    // 循环获取用户输入的 ID
    for {
       // 获取用户输入的 ID
       var id int64
       fmt.Print("Enter the user ID (0 to exit): ")
       _, err = fmt.Scan(&id)
       if err != nil {
          log.Fatalf("Failed to read user ID: %v", err)
       }

       if id == 0 {
          // 输入 0 表示退出循环
          break
       }

       // 发起 gRPC 调用
       ctx, cancel := context.WithTimeout(context.Background(), time.Second)
       defer cancel()

       // 调用 GetUserByID 方法
       getUserByIDRequest := &pb.GetUserRequest{
          Id: uint32(id),
       }
       getUserByIDResponse, err := client.GetUser(ctx, getUserByIDRequest)
       if err != nil {
          log.Fatalf("Failed to get user by ID: %v", err)
       }

       // 处理返回结果...
       log.Printf("User by ID: %v", getUserByIDResponse)
    }
}

这段代码是一个 gRPC 客户端程序,用于向服务端发送请求并获取返回结果。

首先,import 语句导入了所需的包。

main 函数是程序的入口函数,其中包含了以下几个步骤:

  1. 设置 gRPC 服务器的地址和端口,这里设置为 localhost:50051

  2. 创建与 gRPC 服务器的连接,使用 grpc.Dial 方法建立连接。这里使用 grpc.WithTransportCredentials(insecure.NewCredentials()) 来设置连接的安全凭证,其中 insecure.NewCredentials() 表示使用不安全的连接。在实际生产环境中,应该使用合适的安全凭证来保护连接。

  3. 创建 gRPC 客户端,使用 pb.NewUserServiceClient(conn) 方法创建一个新的 gRPC 客户端实例,其中 conn 是前面建立的连接。

  4. 进入一个循环,获取用户输入的 ID。用户可以不断输入 ID 来查询用户信息,当输入 0 时表示退出循环。

  5. 发起 gRPC 调用。首先使用 context.WithTimeout 方法创建一个带有超时时间的上下文,这里设置超时时间为 1 秒。然后调用 client.GetUser 方法来向服务端发起 GetUserByID 请求,传入用户输入的 ID。

  6. 处理返回结果,通过 getUserByIDResponse 获取服务端返回的数据,并进行处理。这里只是简单地使用 log.Printf 打印出来,实际应用中可以根据业务需求进行相应的处理。

总之,这段代码创建了一个 gRPC 客户端,与服务端建立连接后,通过循环获取用户输入的 ID,并向服务端发起 GetUserByID 请求,然后处理返回结果。