gRPC:一个高性能、开源的通用 RPC 框架,基于标准的 HTTP/2 进行传输,默认采用 Protocol Buffers 序列化结构化数据。本文将介绍如何从零搭建一个 Golang 的 gRPC 服务。

准备工作

本文所述的搭建环境基于滴滴云提供的 CentOS 7.2 标准镜像

安装 Golang

下载最新版本的 Golang 安装包

https://golang.org/dl/
1
2
$ wget https://dl.google.com/go/go1.11.2.linux-amd64.tar.gz
 

解压安装包

1
2
$ tar zxvf go1.11.2.linux-amd64.tar.gz
 

配置环境变量

1
2
3
4
5
6
7
8
9
$ mkdir /home/dc2-user/gopath
$ sudo vim /etc/profile.d/go.sh
 
export GOROOT=/home/dc2-user/go
export GOPATH=/home/dc2-user/gopath
export PATH=$GOROOT/bin:$GOPATH/bin:$PATH
 
$ source /etc/profile.d/go.sh
 

检查安装结果

1
2
$ go version && go env
 

出现以下信息则表明安装成功

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
26
27
28
go version go1.11.2 linux/amd64
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/dc2-user/.cache/go-build"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/dc2-user/gopath"
GOPROXY=""
GORACE=""
GOROOT="/home/dc2-user/go"
GOTMPDIR=""
GOTOOLDIR="/home/dc2-user/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build425133327=/tmp/go-build -gno-record-gcc-switches"
 

安装 Protocol Buffers

下载最新版本的 Protobuf 安装包

1
2
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-all-3.6.1.tar.gz
 

解压安装包

1
2
$ tar zxvf protobuf-all-3.6.1.tar.gz
 

安装 Protobuf

1
2
3
$ cd protobuf-3.6.1/
$ ./configure && make && sudo make install
 

安装 Protobuf Golang 插件

1
2
$ go get -u -v github.com/golang/protobuf/protoc-gen-go
 

检查安装结果

1
2
3
4
5
$ protoc --version && which protoc-gen-go
 
libprotoc 3.6.1
~/gopath/bin/protoc-gen-go
 

安装 gRPC

网络环境允许的同学安装 gRPC 非常方便,直接执行以下命令即可安装完成:

1
2
$ go get -u -v google.golang.org/grpc
 
1
2
3
4
Fetching https://google.golang.org/grpc?go-get=1
https fetch failed: Get https://google.golang.org/grpc?go-get=1: dial tcp 216.239.37.1:443: i/o timeout
package google.golang.org/grpc: unrecognized import path "google.golang.org/grpc" (https fetch: Get https://google.golang.org/grpc?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
 

如果出现以上问题,则可以按照下面的方式进行安装:

在 GOPATH 下创建 google.golang.org 目录

1
2
3
$ mkdir -p $GOPATH/src/google.golang.org/
$ cd $GOPATH/src/google.golang.org/
 

下载 gRPC 最新代码并解压

1
2
3
4
$ wget https://github.com/grpc/grpc-go/archive/master.tar.gz
$ tar zxvf master.tar.gz
$ mv grpc-go-master/ grpc
 

安装 gRPC

1
2
$ go install google.golang.org/grpc
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
grpc/internal/transport/controlbuf.go:27:2: cannot find package "golang.org/x/net/http2" in any of:
    /home/dc2-user/go/src/golang.org/x/net/http2 (from $GOROOT)
    /home/dc2-user/gopath/src/golang.org/x/net/http2 (from $GOPATH)
grpc/internal/transport/controlbuf.go:28:2: cannot find package "golang.org/x/net/http2/hpack" in any of:
    /home/dc2-user/go/src/golang.org/x/net/http2/hpack (from $GOROOT)
    /home/dc2-user/gopath/src/golang.org/x/net/http2/hpack (from $GOPATH)
grpc/server.go:36:2: cannot find package "golang.org/x/net/trace" in any of:
    /home/dc2-user/go/src/golang.org/x/net/trace (from $GOROOT)
    /home/dc2-user/gopath/src/golang.org/x/net/trace (from $GOPATH)
grpc/internal/channelz/types_linux.go:26:2: cannot find package "golang.org/x/sys/unix" in any of:
    /home/dc2-user/go/src/golang.org/x/sys/unix (from $GOROOT)
    /home/dc2-user/gopath/src/golang.org/x/sys/unix (from $GOPATH)
grpc/status/status.go:37:2: cannot find package "google.golang.org/genproto/googleapis/rpc/status" in any of:
    /home/dc2-user/go/src/google.golang.org/genproto/googleapis/rpc/status (from $GOROOT)
    /home/dc2-user/gopath/src/google.golang.org/genproto/googleapis/rpc/status (from $GOPATH)
 

如果在安装过程中出现以上错误,表明 gRPC 依赖的库缺失,则需按照错误提示逐步补全安装其依赖库

安装 golang.org/x/*

golang.org/x/github.com/golang/golang.org/x/*
1
2
3
4
5
6
7
8
#!/bin/bash
MODULES="crypto net oauth2 sys text tools"
for module in ${MODULES}
do
    wget https://github.com/golang/${module}/archive/master.tar.gz -O ${GOPATH}/src/golang.org/x/${module}.tar.gz
    cd ${GOPATH}/src/golang.org/x && tar zxvf ${module}.tar.gz && mv ${module}-master/ ${module}
done
 

安装 google.golang.org/genproto

google.golang.org/genprotogithub.com/google/go-genproto
1
2
3
$ wget https://github.com/google/go-genproto/archive/master.tar.gz -O ${GOPATH}/src/google.golang.org/genproto.tar.gz
$ cd ${GOPATH}/src/google.golang.org && tar zxvf genproto.tar.gz && mv go-genproto-master genproto
 
go install google.golang.org/grpc

构建一个简单的 gRPC 服务

helloworld.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";
 
option go_package = "github.com/grpc/example/helloworld";
 
package helloworld;
 
// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}
 
// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}
 
// The response message containing the greetings
message HelloReply {
  string message = 1;
}
 

编译 .proto 文件

1
2
3
4
5
6
7
8
9
10
11
12
$ protoc helloworld.proto --go_out=output
$ tree .
.
├── helloworld.proto
└── output
    └── github.com
        └── grpc
            └── example
                └── helloworld
                    └── helloworld.pb.go
5 directories, 2 files
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ head -n 15 output/github.com/grpc/example/helloworld/helloworld.pb.go
 
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto
 
package helloworld
 
import (
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    math "math"
)
 
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
 
.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
$ protoc helloworld.proto --go_out=plugins=grpc:output
$ tree .
.
├── helloworld.proto
└── output
    └── github.com
        └── grpc
            └── example
                └── helloworld
                    └── helloworld.pb.go
 
5 directories, 2 files
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ head output/github.com/grpc/example/helloworld/helloworld.pb.go -n 15
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto
 
package helloworld
 
import (
    context "context"
    fmt "fmt"
    proto "github.com/golang/protobuf/proto"
    grpc "google.golang.org/grpc"
    math "math"
)
 
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
 

编写 client.go 与 server.go

client.goserver.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tree .
.
├── client.go
├── helloworld.proto
├── output
│   └── github.com
│       └── grpc
│           └── example
│               └── helloworld
│                   └── helloworld.pb.go
└── server.go
 
5 directories, 4 files
 

编写 client.go

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
 
import (
    "context"
    "log"
    "os"
    "time"
 
    "google.golang.org/grpc"
    pb "./output/github.com/grpc/example/helloworld"
)
 
const (
    address     = "localhost:50051"
    defaultName = "world"
)
 
func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)
 
    // Contact the server and print out its response.
    name := defaultName
    if len(os.Args) > 1 {
        name = os.Args[1]
    }
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}
 

编写 server.go

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
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
 
import (
    "context"
    "log"
    "net"
 
    "google.golang.org/grpc"
    pb "./output/github.com/grpc/example/helloworld"
    "google.golang.org/grpc/reflection"
)
 
const (
    port = ":50051"
)
 
// server is used to implement helloworld.GreeterServer.
type server struct{}
 
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
 
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    // Register reflection service on gRPC server.
    reflection.Register(s)
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}
 

运行 gRPC 服务

打开两个会话窗口,在其中之一执行:

1
2
$ go run server.go
 

在另一个会话窗口运行:

1
2
3
$ go run client.go gRPC
2018/12/09 18:05:22 Greeting: Hello gRPC
 

自此一个简单的 gRPC 服务就搭建起来了。

构建一个安全的 gRPC 服务

.proto

增加 TLS

生成服务端公私钥

1
2
3
$ openssl genrsa -out server.key 2048
$ openssl req -x509 -key server.key -out server.pem
 

目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ tree .
.
├── client.go
├── helloworld.proto
├── output
│   └── github.com
│       └── grpc
│           └── example
│               └── helloworld
│                   └── helloworld.pb.go
├── server.go
├── server.key
└── server.pem
 
5 directories, 6 files
 

改写 server.go

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main
 
import (
        "context"
        "crypto/tls"
        "log"
        "net"
 
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials"
        pb "./output/github.com/grpc/example/helloworld"
        "google.golang.org/grpc/reflection"
)
 
const (
        port = ":50051"
)
 
// server is used to implement helloworld.GreeterServer.
type server struct{}
 
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
 
func main() {
        cert, err := tls.LoadX509KeyPair("./server.pem", "./server.key")
        if err != nil {
                log.Fatalf("failed to load key pair: %s", err)
        }
        lis, err := net.Listen("tcp", port)
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
        opts := []grpc.ServerOption{
                // Enable TLS for all incoming connections.
                grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
        }
        s := grpc.NewServer(opts...)
        pb.RegisterGreeterServer(s, &server{})
        // Register reflection service on gRPC server.
        reflection.Register(s)
        if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
        }
}
 

改写 client.go

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main
 
import (
        "context"
        "crypto/tls"
        "log"
        "os"
        "time"
 
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials"
        pb "./output/github.com/grpc/example/helloworld"
)
 
const (
        address     = "localhost:50051"
        defaultName = "world"
)
 
func main() {
        opts := []grpc.DialOption{
                // credentials.
                grpc.WithTransportCredentials(
                        credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}),
                ),
        }
        // Set up a connection to the server.
        conn, err := grpc.Dial(address, opts...)
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)
 
        // Contact the server and print out its response.
        name := defaultName
        if len(os.Args) > 1 {
                name = os.Args[1]
        }
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.Message)
}
 

运行 gRPC 服务

打开两个会话窗口,在其中之一执行:

1
2
$ go run server.go
 

在另一个会话窗口运行:

1
2
3
$ go run client.go tls_gRPC
2018/12/09 21:19:07 Greeting: Hello tls_gRPC
 

增加 OAuth2 鉴权

改写 server.go

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package main
 
import (
        "context"
        "crypto/tls"
        "log"
        "net"
        "strings"
 
        "google.golang.org/grpc"
        "google.golang.org/grpc/codes"
        "google.golang.org/grpc/credentials"
        pb "./output/github.com/grpc/example/helloworld"
        "google.golang.org/grpc/reflection"
        "google.golang.org/grpc/metadata"
        "google.golang.org/grpc/status"
)
 
const (
        port = ":50051"
)
 
var (
        errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
        errInvalidToken    = status.Errorf(codes.Unauthenticated, "invalid token")
)
 
// server is used to implement helloworld.GreeterServer.
type server struct{}
 
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
 
func main() {
        cert, err := tls.LoadX509KeyPair("./server.pem", "./server.key")
        if err != nil {
                log.Fatalf("failed to load key pair: %s", err)
        }
        lis, err := net.Listen("tcp", port)
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
        opts := []grpc.ServerOption{
                // The following grpc.ServerOption adds an interceptor for all unary
                // RPCs. To configure an interceptor for streaming RPCs, see:
                // https://godoc.org/google.golang.org/grpc#StreamInterceptor
                grpc.UnaryInterceptor(ensureValidToken),
                // Enable TLS for all incoming connections.
                grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
        }
        s := grpc.NewServer(opts...)
        pb.RegisterGreeterServer(s, &server{})
        // Register reflection service on gRPC server.
        reflection.Register(s)
        if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
        }
}
 
// valid validates the authorization.
func valid(authorization []string) bool {
        if len(authorization) < 1 {
                return false
        }
        token := strings.TrimPrefix(authorization[0], "Bearer ")
        // Perform the token validation here. For the sake of this example, the code
        // here forgoes any of the usual OAuth2 token validation and instead checks
        // for a token matching an arbitrary string.
        if token != "some-secret-token" {
                return false
        }
        return true
}
 
// ensureValidToken ensures a valid token exists within a request's metadata. If
// the token is missing or invalid, the interceptor blocks execution of the
// handler and returns an error. Otherwise, the interceptor invokes the unary
// handler.
func ensureValidToken(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
                return nil, errMissingMetadata
        }
        // The keys within metadata.MD are normalized to lowercase.
        // See: https://godoc.org/google.golang.org/grpc/metadata#New
        if !valid(md["authorization"]) {
                return nil, errInvalidToken
        }
        // Continue execution of handler after ensuring a valid token.
        return handler(ctx, req)
}
 

改写 client.go

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main
 
import (
        "context"
        "crypto/tls"
        "log"
        "os"
        "time"
 
        "golang.org/x/oauth2"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials"
        "google.golang.org/grpc/credentials/oauth"
        pb "./output/github.com/grpc/example/helloworld"
)
 
const (
        address     = "localhost:50051"
        defaultName = "world"
)
 
func main() {
        perRPC := oauth.NewOauthAccess(fetchToken())
        opts := []grpc.DialOption{
                // In addition to the following grpc.DialOption, callers may also use
                // the grpc.CallOption grpc.PerRPCCredentials with the RPC invocation
                // itself.
                // See: https://godoc.org/google.golang.org/grpc#PerRPCCredentials
                grpc.WithPerRPCCredentials(perRPC),
                // oauth.NewOauthAccess requires the configuration of transport
                // credentials.
                grpc.WithTransportCredentials(
                        credentials.NewTLS(&tls.Config{InsecureSkipVerify: true}),
                ),
        }
        // Set up a connection to the server.
        conn, err := grpc.Dial(address, opts...)
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)
 
        // Contact the server and print out its response.
        name := defaultName
        if len(os.Args) > 1 {
                name = os.Args[1]
        }
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.Message)
}
 
// fetchToken simulates a token lookup and omits the details of proper token
// acquisition. For examples of how to acquire an OAuth2 token, see:
// https://godoc.org/golang.org/x/oauth2
func fetchToken() *oauth2.Token {
        return &oauth2.Token{
                AccessToken: "some-secret-token",
        }
}
 
golang.org/x/oauth2cloud.google.com/go/compute/metadatagithub.com/googleapis/google-cloud-go
1
2
3
4
$ mkdir -p ${GOPATH}/src/cloud.google.com/
$ wget https://github.com/googleapis/google-cloud-go/archive/master.tar.gz -O ${GOPATH}/src/cloud.google.com/go.tar.gz
$ cd ${GOPATH}/src/cloud.google.com/ && tar zxvf go.tar.gz && mv google-cloud-go-master go
 

运行 gRPC 服务

打开两个会话窗口,在其中之一执行:

1
2
$ go run server.go
 

在另一个会话窗口运行:

1
2
3
$ go run client.go oauth2_tls_gRPC
2018/12/09 21:27:56 Greeting: Hello oauth2_tls_gRPC
 

自此一个安全的 gRPC 服务就搭建起来了。