grpc-go学习

学学 grpc-go,顺便整整优化

简单应答服务

安装idl转换工具

1
2
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc

然后写一份proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注意要是 proto3 ,只有3才支持 service 语法
syntax = "proto3";

// 这里的go_package是指定生成go代码时,第一行的package名。这里可以用你项目的mod路径+生成后代码所在的相对路径
option go_package = "grpcserver/repo";

message Request {
string msg = 1;
}

message Response {
string msg = 1;
}

service Greeter {
rpc Echo(Request) returns (Response) {}
}

编写完进入代码路径,执行

1
protoc.exe --go_out=. --go_opt=paths=source_relative  --go-grpc_out=. --go-grpc_opt=paths=source_relative .\server.proto

就可以生成 *.pb.go*_grpc.pb.go 两个源码文件,接下来实现相关方法。

1
2
3
4
5
6
7
8
9
10
11
12
import (
"context"
"grpcserver/repo" // 导入包时,包名和proto文件里面的package一样
)

type GreeterImpl struct {
repo.UnimplementedGreeterServer
}

func (*GreeterImpl) Echo(ctx context.Context, in *repo.Request) (*repo.Response, error) {
return &repo.Response{Msg: in.Msg}, nil
}

实现加入的 repo.UnimplementedGreeterServer ,是grpc-go后面版本新增加的,默认实现了所有接口的对象。默认方法是返回一个错误,显示接口未实现,保证主调方用新pb调用服务端旧协议时,出现奇怪的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GreeterServer is the server API for Greeter service.
// All implementations must embed UnimplementedGreeterServer
// for forward compatibility
type GreeterServer interface {
Echo(context.Context, *Request) (*Response, error)
mustEmbedUnimplementedGreeterServer() // 这是个匿名函数,所以自己的实现必须导入 UnimplementedGreeterServer 才能编译通过
}

// UnimplementedGreeterServer must be embedded to have forward compatible implementations.
type UnimplementedGreeterServer struct {
}

// 这里默认实现了一个 Echo,返回方法未实现的错误。在我们自己实现的代码中,Echo会覆盖这一个
func (UnimplementedGreeterServer) Echo(context.Context, *Request) (*Response, error) {
return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented")
}
func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {}

接下来就是监听并服务了。grpc有两种监听方式,一种是直接listen一个tcp socket,另外一种是通过 ServeHTTP ,和http server 一起工作。下面是直接listen tcp的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"grpcserver/repo"
"grpcserver/service"
"net"

"google.golang.org/grpc"
)

func main() {
sock, err := net.Listen("tcp", "127.0.0.1:9999") // 监听端口
if err != nil {
panic(err)
}
server := grpc.NewServer()
repo.RegisterGreeterServer(server, &service.GreeterImpl{})
if err := server.Serve(sock); err != nil {
panic(err)
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"context"
"fmt"
"grpcserver/repo"

"google.golang.org/grpc"
)

func main() {
conn, err := grpc.Dial("127.0.0.1:9999", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
client := repo.NewGreeterClient(conn)
resp, err := client.Echo(context.Background(), &repo.Request{
Msg: "hello",
})
if err != nil {
panic(err)
}
fmt.Println(resp)
}

到这里,一个简单的 Echo 服务就搭好了。

这里不难看出,每次都是Dial一个新的链接,然后发起请求,并没有复用链接,所以其实有优化空间。

流式服务

在pb的 request 和 response 前面加个 stream ,就是流接口定义

1
2
3
4
service Greeter {
rpc Echo(Request) returns (Response) {}
rpc StreamEcho(stream Request) returns (stream Response) {}
}

服务端新增个接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (*GreeterImpl) StreamEcho(s repo.Greeter_StreamEchoServer) error {
var wg sync.WaitGroup
wg.Add(1)
go func() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
s.Send(&repo.Response{
Msg: fmt.Sprintf("hello %d", i),
})
}
wg.Done()
}()
wg.Wait()
return nil
}

client 写个循环 recv

1
2
3
4
5
6
7
8
9
10
11
c, err := client.StreamEcho(context.Background())
if err != nil {
panic(err)
}
for {
msg, err := c.Recv()
if err != nil {
break
}
fmt.Println(msg)
}

配合nginx

一般会用nginx或其他工具作为中间层,tls加密放在中间层,所以grpc服务这边可以使用非加密的h2c提供服务

1
2
3
4
5
6
7
8
9
func main() {
s := grpc.NewServer()
repo.RegisterGreeterServer(s, &service.GreeterImpl{})
// 先申请一个 handler ,方法指向serverHTTP
handlerFunc := http.HandlerFunc(s.ServeHTTP)
// 然后用h2c获取handler,这样就可以提供一个没有加密的grpc服务了
handler := h2c.NewHandler(handlerFunc, &http2.Server{})
http.ListenAndServe("127.0.0.1:9999", handler)
}