grpc-gateway使用

1,166 阅读2分钟

初始化项目

$ go mod init github.com/rcrick/grpc-gatewat
module github.com/rcrick/grpc-gatewat

go 1.16

require (
	golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
	golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect
	golang.org/x/text v0.3.7 // indirect
	google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect
	google.golang.org/grpc v1.40.0 // indirect
	google.golang.org/protobuf v1.27.1 // indirect
)

在接下来启动server的时候,可能会有缺少go依赖的报错,直接按照提示运行go get XXXX 就好

创建相关文件夹

.
├── client
├── go.mod
├── proto
└── server

编写proto

创建proto/hello.proto文件

syntax = "proto3";

package proto;

option go_package = "github.com/rcrick/grpc-gatewat/proto";

service HelloService {
    rpc SayHello(HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

编译proto文件\color{#005bb7}{编译proto文件}

$ cd proto
$ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative hello.proto

会在proto目录下生成hello.pb.gohello_grpc.pb.go

.
├── client
├── go.mod
├── proto
│   ├── hello.pb.go
│   ├── hello.proto
│   └── hello_grpc.pb.go
└── server
    └── main.go

注意:\color{red}{注意:} 如果不加--go_opt=paths=source_relative--go-grpc_opt=paths=source_relative 那么编译生成的文件则会放到go_package指定的路径下, 不建议这种做法

$ protoc --go_out=.  --go-grpc_out=.   hello.proto

结果如下

.
├── client
├── go.mod
├── proto
│   ├── github.com
│   │   └── rcrick
│   │       └── grpc-gatewat
│   │           └── proto
│   │               ├── hello.pb.go
│   │               └── hello_grpc.pb.go

编写server

package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"google.golang.org/grpc"

	pb "github.com/rcrick/grpc-gatewat/proto"
)

type server struct {
	pb.UnimplementedHelloServiceServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Receive msg: %s", in.GetName())
	return &pb.HelloReply{Message: fmt.Sprintf("You send server: %s", in.GetName())}, nil
}

const PORT = "8000"

func main() {
	s := grpc.NewServer()
	pb.RegisterHelloServiceServer(s, &server{})

	lis, err := net.Listen("tcp", ":"+PORT)
	if err != nil {
		log.Fatal(err)
	}

	log.Fatal(s.Serve(lis))
}

编写client

package main

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

	"google.golang.org/grpc"

	pb "github.com/rcrick/grpc-gatewat/proto"
)

const PORT = "8000"

func main() {
	conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	client := pb.NewHelloServiceClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	res, err := client.SayHello(ctx, &pb.HelloRequest{Name: "I'm client"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(res.GetMessage())
}

验证结果

启动server

$ cd server
$ go run main.go

启动client

$ cd client
$ go run main.go
$ You send server: I'm client

支持result api

方法参考官方文档

修改hello.proto文件

syntax = "proto3";

package proto;

// new add
import "google/api/annotations.proto";

option go_package = "github.com/rcrick/grpc-gatewat/proto";

service HelloService {
    rpc SayHello(HelloRequest) returns (HelloReply) {
        // new add
        option (google.api.http) = {
            get: "/v1/hello"
        };
    }
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

因为引用了google/api/annotations.proto[🔗,而annotations.proto又引用了google/api/http.proto🔗, 所以我直接在proto下创建文件夹google/api/,并把annotations.protohttp.proto拷贝到这里

$ cd proto
$ mkdir -p google/api/
$ cd google/api/
$ curl -O https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto
$ curl -O https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto

此时项目结构如下:

.
├── client
│   └── main.go
├── go.mod
├── go.sum
├── proto
│   ├── google
│   │   └── api
│   │       ├── annotations.proto
│   │       └── http.proto
│   └── hello.proto
└── server
    └── main.go

再次编译proto文件

$ cd proto
$ protoc  --go_out . --go_opt paths=source_relative \
--go-grpc_out . --go-grpc_opt paths=source_relative \
--grpc-gateway_out . --grpc-gateway_opt paths=source_relative hello.proto

如果不将依赖文件放到本地,会有如下报错:

$ google/api/annotations.proto: File not found.
$ hello.proto:6:1: Import "google/api/annotations.proto" was not found or had errors.

会生成三个文件hello.pb.gw.go,hello.pb.go,hello_grpc.pb.go 下载依赖

go get "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
.
├── client
│   └── main.go
├── go.mod
├── go.sum
├── proto
│   ├── google
│   │   └── api
│   │       ├── annotations.proto
│   │       └── http.proto
│   ├── hello.pb.go
│   ├── hello.pb.gw.go
│   ├── hello.proto
│   └── hello_grpc.pb.go
└── server
    └── main.go

修改server/main.go

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"net/http"

	"google.golang.org/grpc"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	pb "github.com/rcrick/grpc-gatewat/proto"
)

type server struct {
	pb.UnimplementedHelloServiceServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Receive msg: %s", in.GetName())
	return &pb.HelloReply{Message: fmt.Sprintf("You send server: %s", in.GetName())}, nil
}

const PORT = "8000"

func main() {
	s := grpc.NewServer()
	pb.RegisterHelloServiceServer(s, &server{})

	lis, err := net.Listen("tcp", ":"+PORT)
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		log.Fatal(s.Serve(lis))
	}()

	// Create a client connection to the gRPC server we just started
	// This is where the gRPC-Gateway proxies the requests
	conn, err := grpc.DialContext(
		context.Background(),
		":"+PORT,
		grpc.WithInsecure(),
		grpc.WithBlock())
	if err != nil {
		log.Fatal(err)
	}

	gwmux := runtime.NewServeMux()
	err = pb.RegisterHelloServiceHandler(context.Background(), gwmux, conn)
	if err != nil {
		log.Fatal(err)
	}

	gwServer := &http.Server{
		Addr: ":8001",
		Handler: gwmux,
	}
	log.Println("Serving gRPC-Gateway on http://localhost:8001/")
	log.Fatalln(gwServer.ListenAndServe())
}

验证

$ cd server
$ go run main
$ 2021/09/03 01:13:51 Receive msg:

使用请求参数进行验证

我是用的是go-proto-validators

修改hello.proto

syntax = "proto3";

package proto;

import "google/api/annotations.proto";
// new add
import "validator/validator.proto";

option go_package = "github.com/rcrick/grpc-gatewat/proto";

service HelloService {
    rpc SayHello(HelloRequest) returns (HelloReply) {
        // new add
        option (google.api.http) = {
            post: "/v1/hello"
            body: "*"
        };
    }
}
// new add
message HelloRequest {
    int32 number =1 [(validator.field) = {int_gt: 0, int_lt: 100}];
    string name = 2 [(validator.field) = {length_gt: 10}];
}

message HelloReply {
    string message = 1;
}

go-proto-validators里文档里写的用法是import "github.com/mwitkow/go-proto-validators/validator.proto"; 在我这里会报错,所以我自己新建一个目录,并将validator.proto放到本地,代码改成了import "validator/validator.proto";

$ cd proto
$ mkdir validator
$ cd validator
$ curl -O https://raw.githubusercontent.com/mwitkow/go-proto-validators/master/validator.proto

编译proto

$ protoc    --go_out . --go_opt paths=source_relative \ 
 --go-grpc_out . --go-grpc_opt paths=source_relative \ 
 --grpc-gateway_out . --grpc-gateway_opt paths=source_relative \ 
 --govalidators_out=. --govalidators_opt paths=source_relative  hello.proto

多生成了一个文件hello.validator.pb.go

.
├── client
│   └── main.go
├── go.mod
├── go.sum
├── proto
│   ├── github.com
│   ├── google
│   │   └── api
│   │       ├── annotations.proto
│   │       └── http.proto
│   ├── hello.pb.go
│   ├── hello.pb.gw.go
│   ├── hello.proto
│   ├── hello.validator.pb.go
│   ├── hello_grpc.pb.go
│   └── validator
│       └── validator.proto
└── server
    └── main.go

验证validators

启动server 使用curl进行验证

$ curl -X POST --form 'number="0"' --form 'name=" 34ddddddddddd2"'  'http://localhost:8001/v1/hello'
$ {"code":3, "message":"invalid character '-' in numeric literal", "details":[]

以上报错的原因是接口要求入参为json格式的,我是用的是form,所以改成json格式试一下

$ curl -X POST --data-raw '{"number": 0,"name": "34ddddddddddd2"}' 'http://localhost:8001/v1/hello'
$ {"code":2, "message":"invalid field Number: value '0' must be greater than '0'", "details":[]}

因为number要求大于0,所以再改下post的数据

$ curl -X POST --data-raw '{"number": 10,"name": "34ddddddddddd2"}' 'http://localhost:8001/v1/hello
$ {"message":"You send server: 34ddddddddddd2"}

成功.