安装Protobuf 🔗
- 配置环境变量,macos如下:
export PATH=$PATH:$JAVA_HOME/bin:$(go env GOPATH)/bin:/Users/abner/tools/platform-tools:/Users/abner/tools/apache-maven-3.9.11/bin:/Users/abner/tools/protoc-32.0-osx-x86_64/bin
执行source ~/.zshrc
- 最后终端输入
protoc验证
- 安装gRPC核心库
go get google.golang.org/grpc
- 上面安装的是protocol编译器,他可以生成不同语言代码。接着安装go语言的代码生成工具
protoc-gen-go,其他语言自行研究
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
proto文件编写 🔗
//这是在说明使用的是proto3语法
syntax = "proto3";
//这事在说明生成的路径,这里的.表示当前目录,service代码生成的go文件包名是service
option go_package = ".;service";
// 定义服务Greeter,这个服务中有一个rpc方法SayHello,请求参数是HelloRequest,返回参数是HelloReply。一个文件可以定义多个service服务,可以把一组相关的
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// 请求参数
message HelloRequest {
string reqName = 1;
}
// 响应参数
message HelloReply {
string respMsg = 1;
}
编写完上面内容后,在文件目录执行
protoc --go_out=. greeter.proto
protoc --go-grpc_out=. greeter.proto
proto文件介绍 🔗
message
message: protobuf中定义一个消息类型是通过关键字message字段指定的。类似c++中的class,java中的class,go中的struct,一个proto文件中可以定义多个消息类型
字段规则
required: 表示字段必须赋值,否则编译不过。在proto3中,required字段已弃用 optional: 表示字段可以不赋值。在proto3中,required、optional字段已都已经默认optional repeated: 表示字段可以重复赋值,在go中会被定义为切片
消息号
在消息体的定义中,每个字段都必须要有一个唯一的编号,标识从[1,2^29-1]范围内的一个整数
嵌套消息
可以在其他消息类型中定义使用消息,例如
message Father {
message Son {
string name = 1;
int32 age = 2;
repeated int32 weight = 3;
}
repeated Son sons = 1;
}
如果要在外部使用这个消息类型,需要Father.Son的形式使用。如:
message Family {
Father.Son one = 1;
}
服务定义
在proto文件中定义服务,服务中可以定义多个方法,每个方法都对应一个RPC方法。例如:
service GoodsService {
rpc Serarch(SearchRequest) returns (SearchResponse)
}
服务端编写 🔗
server.go
package main
import (
"context"
"demo/grpc/pb"
"fmt"
"net"
"runtime"
"time"
"google.golang.org/grpc"
)
const port = ":50051"
type ServerDemo struct {
pb.UnimplementedGreeterServer
}
func (s *ServerDemo) SayHello(ctx context.Context, req *pb.HelloRequest) (resp *pb.HelloReply, err error) {
msg := fmt.Sprintf("hello,%s[rpc-server,os=%s,nowNanosecond=%d]",
req.ReqName, runtime.GOOS, time.Now().Nanosecond())
return &pb.HelloReply{RespMsg: msg}, nil
}
func main() {
listen, err := net.Listen("tcp", port)
if err != nil {
panic(err)
}
newServer := grpc.NewServer()
pb.RegisterGreeterServer(newServer, &ServerDemo{})
err = newServer.Serve(listen)
fmt.Println(err)
}
客户端编写 🔗
client.go
package main
import (
"context"
"demo/grpc/pb"
"fmt"
"log"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultName = "kaola"
)
func main() {
conn, e := grpc.Dial(address, grpc.WithInsecure())
if e != nil {
panic(e)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
reply, e := c.SayHello(context.Background(), &pb.HelloRequest{ReqName: defaultName})
if e != nil {
log.Fatal(e)
}
fmt.Println(reply.RespMsg)
}
认证 🔗
gRPC默认内置了两种认证方式:
- SSL/TLS认证方式
- 基于Token的认证方式 同时,gRPC提供了接口用于扩展自定义认证方式 项目路面结构
|—— hello/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // 服务端
|—— proto/
|—— hello/
|—— hello.proto // proto描述文件
|—— hello.pb.go // proto编译后文件
SSL/TLS认证 🔗
这里直接扩展hello项目,实现TLS认证机制
首先需要准备证书,在hello目录新建keys目录用于存放证书文件。
- 证书制作
制作私钥 (.key)
# Key considerations for algorithm "RSA" ≥ 2048-bit
$ openssl genrsa -out server.key 2048
# Key considerations for algorithm "ECDSA" ≥ secp384r1
# List ECDSA the supported curves (openssl ecparam -list_curves)
$ openssl ecparam -genkey -name secp384r1 -out server.key
自签名公钥(x509) (PEM-encodings .pem|.crt)
$ openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
自定义信息
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:XxXx
Locality Name (eg, city) []:XxXx
Organization Name (eg, company) [Internet Widgits Pty Ltd]:XX Co. Ltd
Organizational Unit Name (eg, section) []:Dev
Common Name (e.g. server FQDN or YOUR name) []:server name
Email Address []:xxx@xxx.com
目录结构
|—— hello-tls/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // 服务端
|—— keys/ // 证书目录
|—— server.key
|—— server.pem
|—— proto/
|—— hello/
|—— hello.proto // proto描述文件
|—— hello.pb.go // proto编译后文件
示例代码 修改服务端代码:server/main.go
package main
import (
"fmt"
"net"
pb "github.com/jergoo/go-grpc-example/proto/hello"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService Hello服务
var HelloService = helloService{}
// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := new(pb.HelloResponse)
resp.Message = fmt.Sprintf("Hello %s.", in.Name)
return resp, nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}
// TLS认证
creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
// 实例化grpc Server, 并开启TLS认证
s := grpc.NewServer(grpc.Creds(creds))
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
grpclog.Println("Listen on " + Address + " with TLS")
s.Serve(listen)
}
服务端在实例化grpc Server时,可配置多种选项,TLS认证是其中之一
客户端添加TLS认证:client/main.go
package main
import (
pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入proto包
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
func main() {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
conn, err := grpc.Dial(Address, grpc.WithTransportCredentials(creds))
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := pb.NewHelloClient(conn)
// 调用方法
req := &pb.HelloRequest{Name: "gRPC"}
res, err := c.SayHello(context.Background(), req)
if err != nil {
grpclog.Fatalln(err)
}
grpclog.Println(res.Message)
}
客户端添加TLS认证的方式和服务端类似,在创建连接Dial时,同样可以配置多种选项,后面的示例中会看到更多的选项。
Token认证 🔗
再进一步,继续扩展hello-tls项目,实现TLS + Token认证机制,也可以不需要TLS,仅使用token。下面的例子使用TLS + Token认证机制 目录结构
|—— hello_token/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // 服务端
|—— keys/ // 证书目录
|—— server.key
|—— server.pem
|—— proto/
|—— hello/
|—— hello.proto // proto描述文件
|—— hello.pb.go // proto编译后文件
示例代码
先修改客户端实现:client/main.go
package main
import (
pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入proto包
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
// OpenTLS 是否开启TLS认证
OpenTLS = true
)
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return OpenTLS
}
func main() {
var err error
var opts []grpc.DialOption
if OpenTLS {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
// 使用自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
conn, err := grpc.Dial(Address, opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := pb.NewHelloClient(conn)
// 调用方法
req := &pb.HelloRequest{Name: "gRPC"}
res, err := c.SayHello(context.Background(), req)
if err != nil {
grpclog.Fatalln(err)
}
grpclog.Println(res.Message)
}
这里我们定义了一个customCredential结构,并实现了两个方法GetRequestMetadata和RequireTransportSecurity。这是gRPC提供的自定义认证方式,每次RPC调用都会传输认证信息。customCredential其实是实现了grpc/credential包内的PerRPCCredentials接口。每次调用,token信息会通过请求的metadata传输到服务端。下面具体看一下服务端如何获取metadata中的信息。
修改server/main.go中的SayHello方法:
package main
import (
"fmt"
"net"
pb "github.com/jergoo/go-grpc-example/proto/hello"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata" // 引入grpc meta包
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService ...
var HelloService = helloService{}
// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
// 解析metada中的信息并验证
md, ok := metadata.FromContext(ctx)
if !ok {
return nil, grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appid string
appkey string
)
if val, ok := md["appid"]; ok {
appid = val[0]
}
if val, ok := md["appkey"]; ok {
appkey = val[0]
}
if appid != "101010" || appkey != "i am key" {
return nil, grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
}
resp := new(pb.HelloResponse)
resp.Message = fmt.Sprintf("Hello %s.\nToken info: appid=%s,appkey=%s", in.Name, appid, appkey)
return resp, nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
// TLS认证
creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
// 实例化grpc Server, 并开启TLS认证
s := grpc.NewServer(grpc.Creds(creds))
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
grpclog.Println("Listen on " + Address + " with TLS + Token")
s.Serve(listen)
}
服务端可以从context中获取每次请求的metadata,从中读取客户端发送的token信息并验证有效性。
google.golang.org/grpc/credentials/oauth包已实现了用于Google API的oauth和jwt验证的方法,使用方法可以参考官方文档。在实际应用中,我们可以根据自己的业务需求实现合适的验证方式。
Interceptor 拦截器 🔗
grpc服务端和客户端都提供了interceptor功能,功能类似middleware,很适合在这里处理验证、日志等流程。
在自定义Token认证的示例中,认证信息是由每个服务中的方法处理并认证的,如果有大量的接口方法,这种姿势就太不优雅了,每个接口实现都要先处理认证信息。这个时候interceptor就可以用来解决了这个问题,在请求被转到具体接口之前处理认证信息,一处认证,到处无忧。 在客户端,我们增加一个请求日志,记录请求相关的参数和耗时等等。修改hello_token项目实现: 目录结构
|—— hello_interceptor/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // 服务端
|—— keys/ // 证书目录
|—— server.key
|—— server.pem
|—— proto/
|—— hello/
|—— hello.proto // proto描述文件
|—— hello.pb.go // proto编译后文件
示例代码 服务端interceptor: hello_interceptor/server/main.go
package main
import (
"fmt"
"net"
pb "github.com/jergoo/go-grpc-example/proto/hello"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes" // grpc 响应状态码
"google.golang.org/grpc/credentials" // grpc认证包
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata" // grpc metadata包
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService Hello服务
var HelloService = helloService{}
// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := new(pb.HelloResponse)
resp.Message = fmt.Sprintf("Hello %s.", in.Name)
return resp, nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("Failed to listen: %v", err)
}
var opts []grpc.ServerOption
// TLS认证
creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem", "../../keys/server.key")
if err != nil {
grpclog.Fatalf("Failed to generate credentials %v", err)
}
opts = append(opts, grpc.Creds(creds))
// 注册interceptor
opts = append(opts, grpc.UnaryInterceptor(interceptor))
// 实例化grpc Server
s := grpc.NewServer(opts...)
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
grpclog.Println("Listen on " + Address + " with TLS + Token + Interceptor")
s.Serve(listen)
}
// auth 验证Token
func auth(ctx context.Context) error {
md, ok := metadata.FromContext(ctx)
if !ok {
return grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
}
var (
appid string
appkey string
)
if val, ok := md["appid"]; ok {
appid = val[0]
}
if val, ok := md["appkey"]; ok {
appkey = val[0]
}
if appid != "101010" || appkey != "i am key" {
return grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
}
return nil
}
// interceptor 拦截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
err := auth(ctx)
if err != nil {
return nil, err
}
// 继续处理请求
return handler(ctx, req)
}
实现客户端interceptor:hello_intercepror/client/main.go
package main
import (
"time"
pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入proto包
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
// OpenTLS 是否开启TLS认证
OpenTLS = true
)
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return OpenTLS
}
func main() {
var err error
var opts []grpc.DialOption
if OpenTLS {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem", "server name")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
// 指定自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
// 指定客户端interceptor
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.Dial(Address, opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := pb.NewHelloClient(conn)
// 调用方法
req := &pb.HelloRequest{Name: "gRPC"}
res, err := c.SayHello(context.Background(), req)
if err != nil {
grpclog.Fatalln(err)
}
grpclog.Println(res.Message)
}
// interceptor 客户端拦截器
func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
grpclog.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
return err
}
内置Trace 🔗
grpc内置了客户端和服务端的请求追踪,基于golang.org/x/net/trace包实现,默认是开启状态,可以查看事件和请求日志,对于基本的请求状态查看调试也是很有帮助的,客户端与服务端基本一致,这里以服务端开启trace server为例,修改hello项目服务端代码:
目录结构
|—— hello_trace/
|—— client/
|—— main.go // 客户端
|—— server/
|—— main.go // 服务端
|—— proto/
|—— hello/
|—— hello.proto // proto描述文件
|—— hello.pb.go // proto编译后文件
示例代码
package main
import (
"fmt"
"net"
"net/http"
pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入编译生成的包
"golang.org/x/net/context"
"golang.org/x/net/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService Hello服务
var HelloService = helloService{}
// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
resp := new(pb.HelloResponse)
resp.Message = fmt.Sprintf("Hello %s.", in.Name)
return resp, nil
}
func main() {
listen, err := net.Listen("tcp", Address)
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
// 实例化grpc Server
s := grpc.NewServer()
// 注册HelloService
pb.RegisterHelloServer(s, HelloService)
// 开启trace
go startTrace()
grpclog.Println("Listen on " + Address)
s.Serve(listen)
}
func startTrace() {
trace.AuthRequest = func(req *http.Request) (any, sensitive bool) {
return true, true
}
go http.ListenAndServe(":50051", nil)
grpclog.Println("Trace listen on 50051")
}
这里我们开启一个http服务监听50051端口,用来查看grpc请求的trace信息
服务端事件查看 🔗
访问:localhost:50051/debug/events,结果如图:
可以看到服务端注册的服务和服务正常启动的事件信息。
请求日志信息查看 🔗
访问:localhost:50051/debug/requests,结果如图:
这里可以显示最近的请求状态,包括请求的服务、参数、耗时、响应,对于简单的状态查看还是很方便的,默认值显示最近10条记录。