## 編寫一個 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
~~~