## 编写一个 gRPC 服务 > gRPC 服务其实也是一个 Console 命令行,只是在 Command 中启动了一个 gRPC 服务器而已 首先我们使用 `mix` 命令创建一个 gRPC 项目骨架: ~~~ mix grpc --name=hello ~~~ 通过前面我们对 Console 命令行程序结构的了解,我们先看一下骨架 `manifest/commands` 目录配置的命令: ~~~ package commands import ( "github.com/mix-go/console" "github.com/mix-go/grpc-skeleton/commands" ) var ( Commands []console.CommandDefinition ) func init() { Commands = append(Commands, console.CommandDefinition{ Name: "grpc:server", Usage: "Server demo", Options: []console.OptionDefinition{ { Names: []string{"d", "daemon"}, Usage: "Run in the background", }, }, Command: &commands.GrpcServerCommand{}, }, console.CommandDefinition{ Name: "grpc:client", Usage: "Client demo", Command: &commands.GrpcClientCommand{}, }, ) } ~~~ 从上面我们可以看到定义了 `grpc:server`、`grpc:client` 两个命令: - `grpc:server` 关联了 `commands.GrpcServerCommand` 结构体,里面是服务器的代码 - `grpc:client` 关联了 `commands.GrpcClientCommand` 结构体,里面是客户端的代码 ## 定义 `.proto` 数据结构 在编写 gRPC 服务之前,我们需要先创建一个 `protos/user.proto` 文件: - `.proto` 是 [gRPC](https://github.com/grpc/grpc) 通信的数据结构文件,采用 [protobuf](https://github.com/protocolbuffers/protobuf) 协议 - 下面的定义了一个新增用户的 RPC 接口和相关的数据结构 ~~~ syntax = "proto3"; package go.micro.grpc.user; option go_package = ".;protos"; service User { rpc Add(AddRequest) returns (AddResponse) {} } message AddRequest { string Name = 1; } message AddResponse { int32 error_code = 1; string error_message = 2; int64 user_id = 3; } ~~~ 然后我们需要安装 gRPC 相关的编译程序: - https://www.cnblogs.com/oolo/p/11840305.html#%E5%AE%89%E8%A3%85-grpc 接下来我们开始编译 proto 文件: - 编译成功后会在当前目录生成 `protos/user.pb.go` 文件 ~~~ cd protos protoc --go_out=plugins=grpc:. user.proto ~~~ ## 服务器 然后我们回来看一下 `grpc:server` 关联的 `commands.GrpcServerCommand` 结构体,我们打开骨架 `commands/web.go` 的源码查看: - GrpcServerCommand 结构体中启动了一个 listener 端口监听器 - 还捕获信号,做了服务器的 Shutdown 处理 - `grpc.NewServer()` 创建了一个服务器,并注册了一个 `services.UserService` 服务 - 最后服务器通过 listener 启动 ~~~ package commands import ( "github.com/mix-go/grpc-skeleton/globals" pb "github.com/mix-go/grpc-skeleton/protos" "github.com/mix-go/grpc-skeleton/services" "google.golang.org/grpc" "net" "os" "os/signal" "strings" "syscall" ) const Addr = ":8080" type GrpcServerCommand struct { } func (t *GrpcServerCommand) Main() { logger := globals.Logger() // listen listener, err := net.Listen("tcp", Addr) if err != nil { panic(err) } // signal ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) go func() { <-ch logger.Info("Server shutdown") if err := listener.Close(); err != nil { panic(err) } }() // server s := grpc.NewServer() pb.RegisterUserServer(s, &services.UserService{}) // run welcome() logger.Infof("Server run %s", Addr) if err := s.Serve(listener); err != nil && !strings.Contains(err.Error(), "use of closed network connection") { panic(err) } } ~~~ 然后创建一个 `services.UserService` 结构体,文件路径为 `services/user.go`: - 该服务实现了 `protos/user.pb.go` 文件中定义的 `UserServer` interface,因此才能在前面的 `pb.RegisterUserServer(s, &services.UserService{})` 中成功注册 - `Add` 方法中接收了 `protos/user.pb.go` 文件中定义的 `pb.AddRequest`,同时使用 gorm 在 users 表中插入了一个新记录,并通过定义的 `pb.AddResponse` 返回了响应信息。 ~~~ package services import ( "context" "github.com/mix-go/grpc-skeleton/globals" "github.com/mix-go/grpc-skeleton/models" pb "github.com/mix-go/grpc-skeleton/protos" "time" ) type UserService struct { } func (t *UserService) Add(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) { db := globals.DB() user := models.User{ Name: in.Name, CreateAt: time.Now(), } if err := db.Create(&user).Error; err != nil { return nil, err } resp := pb.AddResponse{ ErrorCode: 0, ErrorMessage: "", UserId: user.ID, } return &resp, nil } ~~~ 上面的代码中使用了 `models.User` 模型,该文件定义在 `models/users.go`: - 结构体中的备注指定了字段关联的数据库字段名称,表名可自行增加前缀等 ~~~ package models import "time" type User struct { ID int `gorm:"primary_key"` Name string `gorm:"column:name"` CreateAt time.Time `gorm:"column:create_at"` } func (User) TableName() string { return "users" } ~~~ 上面使用的 `globals.DB()` 都是骨架中定义好的全局方法,方法内部是采用 `mix-go/bean` 库的依赖注入容器获取的全局 GORM 实例,改实例的依赖配置在 `manifest/beans/db.go` 文件中: - 文件中的依赖配置定义了使用 `gorm.Open` 实例化,`bean.SINGLETON` 定义了这个实例化后的对象是单例模式,`ConstructorArgs` 字段定义了实例化时传入的构造参数,这里传入的 `DATABASE_DSN` 是从环境变量中获取的,也就是说如果我们要修改连接信息,我们还需要到 `.env` 环境配置文件中修改。 ~~~ package beans import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "github.com/mix-go/bean" "github.com/mix-go/dotenv" ) func DB() { Beans = append(Beans, bean.BeanDefinition{ Name: "db", Reflect: bean.NewReflect(gorm.Open), Scope: bean.SINGLETON, ConstructorArgs: bean.ConstructorArgs{"mysql", dotenv.Getenv("DATABASE_DSN").String()}, }, ) } ~~~ ## 客户端 我们看一下 `grpc:client` 关联的 `commands.GrpcClientCommand` 结构体,我们打开骨架 `commands/client.go` 的源码查看: - 首先我们通过 `grpc.DialContext` 方法创建了一个连接 - 然后我们通过 `pb.NewUserClient` 创建了一个客户端对象 - 接下来通过客户端对象调用 `cli.Add` 方法返回响应信息 ~~~ package commands import ( "context" "fmt" pb "github.com/mix-go/grpc-skeleton/protos" "google.golang.org/grpc" "time" ) type GrpcClientCommand struct { } func (t *GrpcClientCommand) Main() { ctx, _ := context.WithTimeout(context.Background(), time.Duration(5)*time.Second) conn, err := grpc.DialContext(ctx, Addr, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { panic(err) } defer func() { _ = conn.Close() }() cli := pb.NewUserClient(conn) req := pb.AddRequest{ Name: "xiaoliu", } resp, err := cli.Add(ctx, &req) if err != nil { panic(err) } fmt.Println(fmt.Sprintf("Add User: %d", resp.UserId)) } ~~~ ## 编译与测试 > 也可以在 Goland Run 里配置 Program arguments 直接编译执行,[Goland 使用] 章节有详细介绍 接下来我们编译上面的程序: ~~~ // linux & macOS go build -o bin/go_build_main_go main.go // win go build -o bin/go_build_main_go.exe main.go ~~~ 首先在命令行启动 `grpc:server` 服务器: ~~~ $ bin/go_build_main_go grpc:server ___ ______ ___ _ /__ ___ _____ ______ / __ `__ \/ /\ \/ /__ __ `/ __ \ / / / / / / / /\ \/ _ /_/ // /_/ / /_/ /_/ /_/_/ /_/\_\ \__, / \____/ /____/ Server Name: mix-grpc Listen Addr: :8080 System Name: darwin Go Version: 1.13.4 Framework Version: 1.0.20 time=2020-11-09 15:08:17.544 level=info msg=Server run :8080 file=server.go:46 ~~~ 然后开启一个新的终端,执行下面的客户端命令与上面的服务器通信 ~~~ $ bin/go_build_main_go grpc:client Add User: 1200 ~~~