從零開始搭建 gRPC 服務 - Golang 篇(一)
gRPC:一個高效能、開源的通用 RPC 框架,基於標準的 HTTP/2 進行傳輸,預設採用 Protocol Buffers 序列化結構化資料。本文將介紹如何從零搭建一個 Golang 的 gRPC 服務。
準備工作
本文所述的搭建環境基於滴滴雲提供的 CentOS 7.2 標準映象
安裝 Golang
下載最新版本的 Golang 安裝包
gRPC 依賴於 1.6 以上版本,如果對其他版本有訴求可以在 https://golang.org/dl/
選擇下載
$ wget https://dl.google.com/go/go1.11.2.linux-amd64.tar.gz
解壓安裝包
$ tar zxvf go1.11.2.linux-amd64.tar.gz
配置環境變數
$ 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
檢查安裝結果
$ go version && go env
出現以下資訊則表明安裝成功
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 安裝包
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protobuf-all-3.6.1.tar.gz
解壓安裝包
$ tar zxvf protobuf-all-3.6.1.tar.gz
安裝 Protobuf
$ cd protobuf-3.6.1/
$ ./configure && make && sudo make install
安裝 Protobuf Golang 外掛
$ go get -u -v github.com/golang/protobuf/protoc-gen-go
檢查安裝結果
$ protoc --version && which protoc-gen-go
libprotoc 3.6.1
~/gopath/bin/protoc-gen-go
安裝 gRPC
網路環境允許的同學安裝 gRPC 非常方便,直接執行以下命令即可安裝完成:
$ go get -u -v google.golang.org/grpc
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 目錄
$ mkdir -p $GOPATH/src/google.golang.org/
$ cd $GOPATH/src/google.golang.org/
下載 gRPC 最新程式碼並解壓
$ wget https://github.com/grpc/grpc-go/archive/master.tar.gz
$ tar zxvf master.tar.gz
$ mv grpc-go-master/ grpc
安裝 gRPC
$ go install google.golang.org/grpc
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/
下均有 mirror,利用這個我們可以以 GitHub 為跳板來安裝相應依賴。執行以下指令碼則可以自動完成 golang.org/x/*
的安裝
#!/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/genproto
在GitHub 上的 mirror 地址為:github.com/google/go-genproto
$ 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 的安裝,自此環境相關的準備工作完成。
構建一個簡單的 gRPC 服務
以該 helloworld.proto
檔案為例
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 檔案
$ protoc helloworld.proto --go_out=output
$ tree .
.
├── helloworld.proto
└── output
└── github.com
└── grpc
└── example
└── helloworld
└── helloworld.pb.go
5 directories, 2 files
$ 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
檔案中描述的結構與方法翻譯成了 Golang 而已,如果需要將其以 gRPC 的方式提供服務的話,需需要在編譯時指定外掛
$ protoc helloworld.proto --go_out=plugins=grpc:output
$ tree .
.
├── helloworld.proto
└── output
└── github.com
└── grpc
└── example
└── helloworld
└── helloworld.pb.go
5 directories, 2 files
$ 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.go
,一個 server.go
檔案,目錄結構如下:
$ tree .
.
├── client.go
├── helloworld.proto
├── output
│ └── github.com
│ └── grpc
│ └── example
│ └── helloworld
│ └── helloworld.pb.go
└── server.go
5 directories, 4 files
編寫 client.go
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
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 服務
開啟兩個會話視窗,在其中之一執行:
$ go run server.go
在另一個會話視窗執行:
$ go run client.go gRPC
2018/12/09 18:05:22 Greeting: Hello gRPC
自此一個簡單的 gRPC 服務就搭建起來了。
構建一個安全的 gRPC 服務
同樣是上面的 .proto
檔案,我們想要為其增加鑑權與加密傳輸的能力
增加 TLS
生成服務端公私鑰
$ openssl genrsa -out server.key 2048
$ openssl req -x509 -key server.key -out server.pem
目錄結構為:
$ 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
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
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 服務
開啟兩個會話視窗,在其中之一執行:
$ go run server.go
在另一個會話視窗執行:
$ go run client.go tls_gRPC
2018/12/09 21:19:07 Greeting: Hello tls_gRPC
增加 OAuth2 鑑權
改寫 server.go
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
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/oauth2
中依賴了 cloud.google.com/go/compute/metadata
,其在 GitHub 上的 mirror 地址為 github.com/googleapis/google-cloud-go
,於是我們需要按照之前的方式安裝該依賴。
$ 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 服務
開啟兩個會話視窗,在其中之一執行:
$ go run server.go
在另一個會話視窗執行:
$ go run client.go oauth2_tls_gRPC
2018/12/09 21:27:56 Greeting: Hello oauth2_tls_gRPC
自此一個安全的 gRPC 服務就搭建起來了。