Protobuf
安装Protoc
Go
参考: 1.windows下安装golang使用protobuf - Go语言中文网 - Golang中文社区 (studygolang.com) 2.【转】windows 下 goprotobuf 的安装与使用 - 苍洱 - 博客园 (cnblogs.com)
1.安装 protoc
- 在该链接下下 载protoc-3.3.0-win32.zip的包
 - 将文件解压到某一文件夹
 - 将解压出来的文件夹下的 /bin 路径添加到环境变量path中
 
2、下载protobuf模块以及插件
            
            
              shell
              
              
            
          
          # protoc-gen-go是用来将protobuf的的代码转换成go语言代码的一个插件
$ go get -u github.com/golang/protobuf/protoc-gen-go
# proto是protobuf在golang中的接口模块
$ go get -u github.com/golang/protobuf/proto
        3.进入protobuf文件所在目录下,执行:
protoc -I . --go_out=plugins=grpc:. *.proto 即可生成相关go 文件(详情查看编译protobuf部分)
注意 :如果出现protoc-gen-go: unable to determine Go import path for "myproto.proto" 解决:在myproto.proto文件中的代码package pb;下面加入option go_package = ".;proto";详情见go_package
python
1.下载依赖
            
            
              arduino
              
              
            
          
          pip install grpcio
pip install grpcio-tools  # python的grpc tools包含了protoc及其插件,用来生成客户端和服务端代码
        2.在protobuf文件所在目录下执行 python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I . *.proto 即可生成相关py文件
注意:会生成两个py文件,如果运行报错,要修改xxx_pb2_grpc.py中的import user_pb2 as user__pb2改成from . import user_pb2 as user__pb2
注意事项
- package主要是用于避免命名冲突的,不同的项目(project)需要指定不同的package。同一package下msg名称必须唯一。
 - import,如果proto文件需要使用在其他proto文件中已经定义的结构,可以使用import引入。
 - option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb"; go_packge有两层意思,一层是表明如果要引用这个proto生成的文件的时候import后面的路径;一层是如果不指定--go_opt(默认值),生成的go文件存放的路径。
 - message成员编号,可以不从1开始,但不能重复,且数字19000-19999不能用,若在 .proto 文件中使用了这些预留的编号 protocol buffer 编译器会发出警告。(最小的字段编号为 1,最大的为 2^29 - 1,或 536,870,911。)
 - 可以使用message嵌套
 - 定义数组、切片用repeated关键字
 - 可以使用枚举enum
 - 可以使用联合体。oneof关键字,成员编号,不能重复。
 
示例:
            
            
              ini
              
              
            
          
          //  指定版本号,默认是proto2
syntax = "proto3";
//  指定所在包包名
package pb;
//定义枚举
enum Week {
  Monday = 0;//枚举值必须从0开始
  Turesday = 1;
}
//  定义消息体
message Student {
  //  =1 =2 是标识该字段在二进制编码中的唯一"标记"
  int32 age = 1;  // 可以不从1开始,但是不能重复,也不能用19000-19999,不同message下的可以重复
  string name = 2;
  People p = 3;
  repeated int32 score = 4;//数组或切片
  //枚举
  Week w = 5;
  //联合体
  oneof data {
    string teacher = 6;
    string class = 7;
  }
}
// 消息体可以嵌套
message People {
  int32 weight = 1;
}
        编译protobuf
Go
go语言中编译命令,进入probuf文件所在目录下,执行: protoc -I (1) (2) --go_out=plugins=grpc:(3) ---> 生成xxx.pb.go 文件
(1)proto文件所在目录,如果是当前目录则为.
(2)编译哪些proto文件,*.proto表示全部proto文件
(3)将生成的文件存放到的位置,.表示当前目录,目录必须存在
注意 :如果出现protoc-gen-go: unable to determine Go import path for "myproto.proto" 解决:在myproto.proto文件中的代码package pb;下面加入option go_package = "../pb";
Python
在protobuf文件所在目录下执行: python -m grpc_tools.protoc -I . *.proto --python_out=. --grpc_python_out=. 即可生成相关py文件
注意:会生成两个py文件,如果运行报错,要修改xxx_pb2_grpc.py中的import user_pb2 as user__pb2改成from . import user_pb2 as user__pb2
protobuf中添加rpc服务
- 
语法:
iniservice 服务名称 { rpc 函数名(参数:消息体) returns (返回值:信息体) } message People { string name = 1; } message Student { int32 age = 2; } 例: service hello { rpc HelloWorld(People) returns (Student); } - 
知识点:
- 
默认,protobuf在编译期间,不编译服务。要想使之编译,需要使用gRPC。
 - 
go使用的编译指令为:
protoc --go_out=plugins=grpc:. *.protoprotoc --go_out=plugins=grpc:生成go文件的位置 proto文件位置
 
 - 
 
protobuf类型
一个标量消息字段可以含有一个如下的类型------该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:
| .proto Type | Notes | Python Type | Go Type | 
|---|---|---|---|
| double | float | float64 | |
| float | float | float32 | |
| int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int | int32 | 
| uint32 | 使用变长编码 | int | uint32 | 
| uint64 | 使用变长编码 | int | uint64 | 
| sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int | int32 | 
| sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int | int64 | 
| fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | int | uint32 | 
| fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | int | uint64 | 
| sfixed32 | 总是4个字节 | int | int32 | 
| sfixed64 | 总是8个字节 | int | int64 | 
| bool | bool | bool | |
| string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | str | string | 
| bytes | 可能包含任意顺序的字节数据。 | str | []byte | 
数组表示:repeated 类型 变量名 = 编号;
你可以在文章Protocol Buffer 编码中,找到更多"序列化消息时各种类型如何编码"的信息
类型默认值
当一个消息被解析的时候,如果被编码的信息不包含一个特定的singular元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:
- 对于string,默认是一个空string
 - 对于bytes,默认是一个空的bytes
 - 对于bool,默认是false
 - 对于数值类型,默认是0
 - 对于枚举,默认是第一个定义的枚举值,必须为0;
 - 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide 对于可重复域的默认值是空(通常情况下是对应语言中空列表)。 注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。 查看generated code guide选择你的语言的默认值的工作细节。
 
go_package
option go_package=".;proto"; //前一个参数用于指定生成文件的位置,后一个参数指定生成的 .go 文件的 package(如果第二个参数不写默认对应位置下的package)
option go_package="../../common/stream/proto/v1"; //指定生成的.go文件位置
使用go_package了就不用package了,并且go_package不会影响到其他语言的生成
java也有java_package
protobuf引用其他protobuf文件
            
            
              ini
              
              
            
          
          //base.proto
syntax = "proto3";
option go_package = ".;proto";
message Pong{
    string id = 1;
}
        
            
            
              ini
              
              
            
          
          //hello.proto 与base同级
syntax = "proto3";
import "base.proto";    //引入base中定义的
import "google/protobuf/empty.proto";//引入公共内置的
option go_package = ".;proto";
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
    rpc Ping(google.protobuf.Empty) returns (Pong);
}
message HelloRequest {
    string url = 1;
    string name = 2;
}
message HelloReply {
    string message = 1;
    message Result {
        string name = 1;
        string url = 2;
    }
    repeated Result data = 2;
}
        message嵌套
            
            
              ini
              
              
            
          
          message Hello {
    string name = 1;
    message Result{
        string r = 1;
    }
}
        枚举类型
            
            
              ini
              
              
            
          
          enum Gender {
    MALE = 0;
    FEMALE = 1;
}
        map类型
            
            
              ini
              
              
            
          
          message H{
    string name = 1;
    string url  = 2;
    Gender g = 3;
    map<string,string> mp = 4;
}
        数组类型
            
            
              ini
              
              
            
          
          // 只需要在相关类型前面加repeat
message H{
    repeat int32 data = 1;
}
        protobuf内置timestamp类型
            
            
              ini
              
              
            
          
          import "google/protobuf/timestamp.proto"
message H{
    string name = 1;
    string url  = 2;
    Gender g = 3;
    map<string,string> mp = 4;
    google.protobuf.Timestamp addTime = 5;
}
        grpc
调试工具
- 
命令行:
shell# 查看所有的服务 $ grpc_cli ls localhost:50051  # 查看 Greeter 服务的详细信息 $ grpc_cli ls localhost:50051 helloworld.Greeter -l  # 查看 Greeter.SayHello 方法的详细信息 $ grpc_cli ls localhost:50051 helloworld.Greeter.SayHello -l  # 远程调用 $ grpc_cli call localhost:50051 SayHello "name: 'gRPC CLI'" - 
用
go install github.com/fullstorydev/grpcui/cmd/grpcui@latest安装好后,调用grpcui -help查看是否安装成功,安装成功后命令行执行grpcui -plaintext grpc服务的地址(ip:port)后,会打开浏览器进入调试页面 - 
go和python使用grpc调试的前置条件
go
0. 执行go install github.com/fullstorydev/grpcui/cmd/grpcui@latest- 会在环境变量
$GOPATH的bin目录下生成一个grpcui.exe,只需要把$GOPATH/bin添加到环境变量PATH中即可。 - 控制台执行
grpcui -help,查看是否安装成功 - 注册反射:在grpc服务器代码中添加
reflection.Register(server),这样就不需要指定proto文件位置了 - 启动grpc服务
 - 控制台执行
grpcui -plaintext 被调试的grpc地址:被调试的grpc端口,会在浏览器打开一个调试页面 
python
- 
需要手动安装grpc reflection:
pip install grpcio-reflection - 
grpc服务端代码引入安装的reflection包,实例:
ini# 生成grpc服务器实例 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) # 注册本地服务到服务器中 goods_pb2_grpc.add_GoodsServicer_to_server(GoodsServicer(), server) # 使用grpc调试只需要添加以下代码 from grpc_reflection.v1alpha import reflection reflection.enable_server_reflection([header.service_name() for header in server._state.generic_handlers], server) # 启动grpc服务器 - 
控制台执行
grpcui -plaintext 被调试的grpc地址:被调试的grpc端口,会在浏览器打开一个调试页面 
 - 会在环境变量
 
安装
python:
pip install grpciopip install grpcio-tools googleapis-common-protos
go:
四种通信模式及其应用场景选型
gRPC有四种通信方式,分别是:简单 RPC(Unary RPC)、服务端流式 RPC (Server streaming RPC)、客户端流式 RPC (Client streaming RPC)、双向流式 RPC(Bi-directional streaming RPC)。它们主要有以下特点:
| 服务类型 | 特点 | 
|---|---|
| 简单 RPC | 一般的rpc调用,传入一个请求对象,返回一个返回对象 | 
| 服务端流式 RPC | 传入一个请求对象,服务端可以返回多个结果对象 | 
| 客户端流式 RPC | 客户端传入多个请求对象,服务端返回一个结果对象 | 
| 双向流式 RPC | 结合客户端流式RPC和服务端流式RPC,可以传入多个请求对象,返回多个结果对象 | 
简单 RPC
一般的rpc调用,传入一个请求对象,返回一个返回对象
proto语法:
            
            
              scss
              
              
            
          
          rpc simpleHello(Person) returns (Result) {}
        客户端发起一次请求,服务端响应一个数据,即标准RPC通信。 这种模式,一个每一次都是发起一个独立的tcp连接,走一次三次握手和四次挥手!
服务端流式 RPC
传入一个请求对象,服务端可以返回多个结果对象
proto语法 :
            
            
              scss
              
              
            
          
          rpc serverStreamHello(Person) returns (stream Result) {}
        服务端流 RPC 下,客户端发出一个请求,但不会立即得到一个响应,而是在服务端与客户端之间建立一个单向的流,服务端可以随时向流中写入多个响应消息,最后主动关闭流,而客户端需要监听这个流,不断获取响应直到流关闭
应用场景举例: 典型的例子是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。
客户端流式 RPC
客户端传入多个请求对象,服务端返回一个结果对象
proto语法 :
            
            
              scss
              
              
            
          
          rpc clientStreamHello(stream Person) returns (Result) {}
        应用场景: 物联网终端向服务器报送数据。
双向流式 RPC
结合客户端流式RPC和服务端流式RPC,可以传入多个请求对象,返回多个结果对象
proto语法 :
            
            
              scss
              
              
            
          
          rpc biStreamHello(stream Person) returns (stream Result) {}
        应用场景:聊天应用。
server
            
            
              go
              
              
            
          
          func (*greeter) SayHelloStream(stream proto.Greeter_SayHelloStreamServer) error {
    for {
        args, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                return nil
            }
            return err
        }
        fmt.Println("Recv: " + args.Name)
        reply := &proto.HelloReply{Message: "hi " + args.Name}
        err = stream.Send(reply)
        if err != nil {
            return err
        }
    }
}
        client
通过一个 goroutine 发送消息,主程序的 for 循环接收消息。
            
            
              go
              
              
            
          
          func main() {
    ....
    client := proto.NewGreeterClient(conn)
    // 流处理
    stream, err := client.SayHelloStream(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    // 发送消息
    go func() {
        for {
            if err := stream.Send(&proto.HelloRequest{Name: "zhangsan"}); err != nil {
                log.Fatal(err)
            }
            time.Sleep(time.Second)
        }
    }()
    // 接收消息
    for {
        reply, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                break
            }
            log.Fatal(err)
        }
        fmt.Println(reply.Message)
    }
}
        metadata机制
类似于http的header
新建metadata
MD 类型实际上是map,key是string,value是string类型的slice。
            
            
              go
              
              
            
          
          type MD map[string][]string
        创建的时候可以像创建普通的map类型一样使用new关键字进行创建:
            
            
              go
              
              
            
          
          // 第一种方式,通过给New方法传入一个map
// 由于map的key不能重复,因为默认key会被转成小写,所以如果要kv一对多需要使用不同大小写的key,比较麻烦,所以这种一般只用于kv一对一的时候
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
// 最终变成:
//  name: []string{"bobo"}
//  password: []string{"123456"}
// 源码:
func New(m map[string]string) MD {
    md := make(MD, len(m))
    for k, val := range m {
        key := strings.ToLower(k)
        md[key] = append(md[key], val)
    }
    return md
}
//第二种方式 key不区分大小写,会被默认统一转成小写。
md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
    "key2", "val2",
)
// 源码
func Pairs(kv ...string) MD {
    if len(kv)%2 == 1 {
        panic(fmt.Sprintf("metadata: Pairs got the odd number of input pairs for metadata: %d", len(kv)))
    }
    md := make(MD, len(kv)/2)
    for i := 0; i < len(kv); i += 2 {
        key := strings.ToLower(kv[i])
        md[key] = append(md[key], kv[i+1])
    }
    return md
}
        发送metadata
- NewOutgoingContext:创建一个附加了传出 md 的新上下文,可供外部的 gRPC 客户端、服务端使用
 
            
            
              go
              
              
            
          
          // 两种构建方式,参考上面
md := metadata.Pairs("key", "val")
// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 需要注意一点,在新增 metadata 信息时,务必使用 Append 类别的方法,否则如果直接 New 一个全新的 md,将会导致原有的 metadata 信息丢失(除非你确定你希望得到这样的结果)。
newCtx := metadata.AppendToOutgoingContext(ctx, "eddycjy", "Go 语言编程之旅")
// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)
        接收metadata
- NewIncomingContext:创建一个附加了所传入的 md 新上下文,仅供自身的 gRPC 服务端内部使用。
 
            
            
              scss
              
              
            
          
          func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    // 从上下文中通过FromIncomingContext接收
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}
        拦截器机制
interceptor
server端(可以实现验证token等)
            
            
              go
              
              
            
          
          // 1.先实现这样一个函数 相当于中间件
interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
        fmt.Println("接收到了一个新的请求")
        res, err := handler(ctx, req) // 继续处理请求
        fmt.Println("请求已经完成")
        return res, err
    }
// 2.使用,
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
proto.RegisterGreeterServer(g, &Server{})
lis, err := net.Listen("tcp", "0.0.0.0:50051")
if err != nil {
    panic("failed to listen:" + err.Error())
}
err = g.Serve(lis)
if err != nil {
    panic("failed to start grpc:" + err.Error())
}
        client端
            
            
              go
              
              
            
          
          interceptor := func(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...)
        fmt.Printf("耗时:%s\n", time.Since(start))
        return err
    }
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))//还有grpc.WithPerRPCCredentials()方法,也是对interceptor的一种封装
conn, err := grpc.Dial("127.0.0.1:50051", opts...)
if err != nil {
    panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "bobby"})
if err != nil {
    panic(err)
}
fmt.Println(r.Message)
        自定义认证
实现Token认证
先改造服务端
有了上文验证器的经验,那么可以采用同样的方式,写一个拦截器,然后在初始化 server 时候注入。(基于metadata+拦截器+token)
- 
实现认证函数:
gofunc Auth(ctx context.Context) error { // metadata.FromIncomingContext 从上下文读取token,然后判断是否通过认证。 md, ok := metadata.FromIncomingContext(ctx) if !ok { return fmt.Errorf("missing credentials") }  var token string if val, ok := md["x-token"]; ok { token = val[0] }  if !ValidateToken(token) { return grpc.Errorf(codes.Unauthenticated, "invalid token") } return nil } - 
构造拦截器:
govar authInterceptor grpc.UnaryServerInterceptor authInterceptor = func( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (resp interface{}, err error) { //拦截普通方法请求,验证 Token err = Auth(ctx) if err != nil { return } // 继续处理请求 return handler(ctx, req) } - 
初始化:
cssserver := grpc.NewServer( grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( authInterceptor, grpc_validator.UnaryServerInterceptor(),//下面的验证器 ), ), grpc.StreamInterceptor( grpc_middleware.ChainStreamServer( grpc_validator.StreamServerInterceptor(), ), ), ) 
最后是客户端改造
- 
客户端需要实现
PerRPCCredentials接口。在 gRPC 中所提供的 PerRPCCredentials,是 gRPC 默认提供用于自定义认证 Token 的接口,它的作用是将所需的安全认证信息添加到每个 RPC 方法的上下文中。其包含两个接口方法,如下:
- GetRequestMetadata:获取当前请求认证所需的元数据(metadata)。
 - RequireTransportSecurity:是否需要基于 TLS 认证进行安全传输。
 
gotype PerRPCCredentials interface { GetRequestMetadata(ctx context.Context, uri ...string) ( map[string]string, error, ) RequireTransportSecurity() bool }GetRequestMetadata方法返回认证需要的必要信息,RequireTransportSecurity方法表示是否启用安全链接,在生产环境中,一般都是启用的,但为了测试方便,暂时这里不启用了。实现接口:
gotype Authentication struct { Token string }  
func (a *Authentication) GetRequestMetadata(context.Context, ...string) ( map[string]string, error, ) { return map[string]string{"x-token": a.Token}, nil } func (a *Authentication) RequireTransportSecurity() bool { return false } ```
- 
连接:
cssconn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))好了,现在我们的服务就有 Token 认证功能了。如果token验证错误,客户端就会收到:
lua2021/10/11 20:39:35 rpc error: code = Unauthenticated desc = invalid token exit status 1如果token正确,则可以正常返回。
 
证书认证
证书认证分两种方式:单向认证,双向认证
单向证书认证
先看一下单向认证方式:
生成证书
首先通过 openssl 工具生成自签名的 SSL 证书。
1、生成私钥:
            
            
              csharp
              
              
            
          
          openssl genrsa -des3 -out server.pass.key 2048
        2、去除私钥中密码:
            
            
              vbnet
              
              
            
          
          openssl rsa -in server.pass.key -out server.key
        3、生成 csr 文件:
            
            
              vbnet
              
              
            
          
          openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=beijing/L=beijing/O=grpcdev/OU=grpcdev/CN=example.grpcdev.cn"
        4、生成证书:
            
            
              vbscript
              
              
            
          
          openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
        gRPC 代码
证书有了之后,剩下的就是改造程序了,首先是服务端代码。
            
            
              go
              
              
            
          
          // 证书认证-单向认证
creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
if err != nil {
    log.Fatal(err)
    return
}
server := grpc.NewServer(grpc.Creds(creds))
        只有几行代码需要修改,很简单,接下来是客户端。
由于是单向认证,不需要为客户端单独生成证书,只需要把服务端的 crt 文件拷贝到客户端对应目录下即可。
            
            
              go
              
              
            
          
          // 证书认证-单向认证
creds, err := credentials.NewClientTLSFromFile("keys/server.crt", "example.grpcdev.cn")
if err != nil {
    log.Fatal(err)
    return
}
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
        好了,现在我们的服务就支持单向证书认证了。
但是还没完,这里可能会遇到一个问题:
            
            
              lua
              
              
            
          
          2021/10/11 21:32:37 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"
exit status 1
        原因是 Go 1.15 开始废弃了 CommonName,推荐使用 SAN 证书。如果想要兼容之前的方式,可以通过设置环境变量的方式支持,如下:
            
            
              ini
              
              
            
          
          export GODEBUG="x509ignoreCN=0"
        但是需要注意,从 Go 1.17 开始,环境变量就不再生效了,必须通过 SAN 方式才行。所以,为了后续的 Go 版本升级,还是早日支持为好。
双向证书认证
最后来看看双向证书认证。
还是先生成证书,但这次有一点不一样,我们需要生成带 SAN 扩展的证书。 什么是 SAN?SAN(Subject Alternative Name)是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。
将默认的 OpenSSL 配置文件拷贝到当前目录。
Linux 系统在:
            
            
              bash
              
              
            
          
          /etc/pki/tls/openssl.cnf
        Mac 系统在:
            
            
              bash
              
              
            
          
          /System/Library/OpenSSL/openssl.cnf
        修改临时配置文件,找到 [ req ] 段落,然后将下面语句的注释去掉。
            
            
              ini
              
              
            
          
          req_extensions = v3_req # The extensions to add to a certificate request
        接着添加以下配置:
            
            
              ini
              
              
            
          
          [ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = www.example.grpcdev.cn
        [ alt_names ] 位置可以配置多个域名,比如:
            
            
              ini
              
              
            
          
          [ alt_names ]
DNS.1 = www.example.grpcdev.cn
DNS.2 = www.test.grpcdev.cn
        为了测试方便,这里只配置一个域名。
1、生成 ca 证书:
            
            
              vbnet
              
              
            
          
          openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=example.grpcdev.com" -days 5000 -out ca.pem
        2、生成服务端证书:
            
            
              bash
              
              
            
          
          # 生成证书
openssl req -new -nodes \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" \
    -config <(cat openssl.cnf \
        <(printf "[SAN]\nsubjectAltName=DNS:www.example.grpcdev.cn")) \
    -keyout server.key \
    -out server.csr
# 签名证书
openssl x509 -req -days 365000 \
    -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") \
    -out server.pem
        3、生成客户端证书:
            
            
              bash
              
              
            
          
          # 生成证书
openssl req -new -nodes \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" \
    -config <(cat openssl.cnf \
        <(printf "[SAN]\nsubjectAltName=DNS:www.example.grpcdev.cn")) \
    -keyout client.key \
    -out client.csr
# 签名证书
openssl x509 -req -days 365000 \
    -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") \
    -out client.pem
        gRPC 代码
接下来开始修改代码,先看服务端:
            
            
              go
              
              
            
          
          // 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
    // 设置证书链,允许包含一个或多个
    Certificates: []tls.Certificate{cert},
    // 要求必须校验客户端的证书。可以根据实际情况选用以下参数
    ClientAuth: tls.RequireAndVerifyClientCert,
    // 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
    ClientCAs: certPool,
})
        再看客户端:
            
            
              go
              
              
            
          
          // 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
    // 设置证书链,允许包含一个或多个
    Certificates: []tls.Certificate{cert},
    // 要求必须校验客户端的证书。可以根据实际情况选用以下参数
    ServerName: "www.example.grpcdev.cn",
    RootCAs:    certPool,
})
        大功告成。
验证器机制
地址:(grpc实战:跨语言的rpc框架到底好不好用,试试就知道 - 知乎 (zhihu.com))
这个需求是很自然会想到的,因为涉及到接口之间的请求,那么对参数进行适当的校验是很有必要的。 如果是内部的一些可以不用验证,毕竟验证也耗费资源
在这里我们使用 protoc-gen-govalidators 和 go-grpc-middleware 来实现。
- 
先安装:
gogo get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators go get github.com/grpc-ecosystem/go-grpc-middleware - 
接下来修改 proto 文件:
proto
iniimport "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";  message HelloRequest { string name = 1 [ (validator.field) = {regex: "^[z]{2,5}$"} ]; }在这里对
name参数进行校验,需要符合正则的要求才可以正常请求。还有其他验证规则,比如对数字大小进行验证等,这里不做过多介绍。
 - 
接下来生成 *.pb.go 文件:
iniprotoc \ --proto_path=${GOPATH}/pkg/mod \ --proto_path=${GOPATH}/pkg/mod/github.com/gogo/protobuf@v1.3.2 \ --proto_path=. \ --govalidators_out=. --go_out=plugins=grpc:.\ *.proto执行成功之后,目录下会多一个 helloworld.validator.pb.go 文件。
这里需要特别注意一下,使用之前的简单命令是不行的,需要使用多个
proto_path参数指定导入 proto 文件的目录。官方给了两种依赖情况,一个是 google protobuf,一个是 gogo protobuf。我这里使用的是第二种。
注意:即使使用上面的命令,也有可能会遇到这个报错:
arduinoImport "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors但不要慌,大概率是引用路径的问题,一定要看好自己的安装版本,以及在
GOPATH中的具体路径。 - 
最后是服务端代码改造:
- 
引入包:
arduinogrpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator" - 
然后在初始化的时候增加验证器功能(即添加到拦截器中):
cssserver := grpc.NewServer( grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( grpc_validator.UnaryServerInterceptor(), ), ), grpc.StreamInterceptor( grpc_middleware.ChainStreamServer( grpc_validator.StreamServerInterceptor(), ), ), ) 
 - 
 - 
启动程序之后,我们再用之前的客户端代码来请求,会收到报错:
lua2021/10/11 18:32:59 rpc error: code = InvalidArgument desc = invalid field Name: value 'zhangsan' must be a string conforming to regex "^[z]{2,5}$" exit status 1因为
name: zhangsan是不符合服务端正则要求的,但是如果传参name: zzz,就可以正常返回了。 
错误处理机制
地址:zhuanlan.zhihu.com/p/435011704
判断Error的错误原理
要了解怎么处理gRPC的error之前,我们首先来看下Go普通的error是怎么处理的。
我们在判断一个error的根因,需要根因error是一个固定地址的指针类型,这样我们才能够使用官方的errors.Is方法判断他是否为根因。
我们先看这个代码errors.Is(wrapNewPointerError(), fmt.Errorf("i am error"))的执行步骤,首先构造了一个error,然后使用官方%w的方式将error进行了包装,我们在使用errors.Is方法判断的时候,底层函数会将error解包来判断两个error的地址是否一致。
gRPC网络传输的Error
grpc网络传输的error不能简单通过官方提供的errors.Is()来进行判断。 我们客户端在获取到gRPC的error的时候,是否可以使用上文说的官方errors.Is进行判断呢。如果我们直接使用该方法,通过判断error地址是否相等,是无法做到的。原因是因为我们在使用gRPC的时候,在远程调用过程中,客户端获取的服务端返回的error,在tcp传递的时候实际上是一串文本。客户端拿到这个文本,是要将其反序列化转换为error,在这个反序列化的过程中,其实是new了一个新的error地址,这样就无法判断error地址是否相等。
grpc与http对比:
- grpc的meta对应http的请求头,code对应http的状态码,error对应http的具体报错信息。
 - grpc客户端远程调用服务端方法时,得到的是(resp,error),我们需要通过status.FromError将error转化为status,继而通过status获取响应的code和error。
 - grpc服务端向客户端返回响应的时候,返回的格式也是(resp, status.Error(code,err.Error()))。
 
为了更好的解释gRPC网络传输的error,以下描述了整个error的处理流程。
- 客户端通过
invoker方法将请求发送到服务端。 - 服务端通过
processUnaryRPC方法,获取到用户代码的error信息。 - 服务端通过
status.FromError方法,将error转化为status.Status。 - 服务端通过
WriteStatus方法将status.Status里的数据,写入到grpc-status、grpc-message、grpc-status-details-bin的header头里。 - 客户端通过网络获取到这些
header头,使用strconv.ParseInt解析到grpc-status信息、decodeGrpcMessage解析到grpc-message信息、decodeGRPCStatusDetails解析为grpc-status-details-bin信息。 - 客户端通过
a.Status().Err()获取到用户代码的错误。 

超时机制
进行超时控制。网络抖动,网络断开
            
            
              css
              
              
            
          
              ctx, _ := context.WithTimeout(context.Background(), time.Second*3)
    _, err = c.SayHello(ctx, &proto.HelloRequest{Name: "bobby"})
        负载均衡
Demo
- 
创建并且编写proto文件
 - 
执行插件生成相关语言类型的文件
 - 
实习服务端和客户端代码逻辑
- 
服务器
goconst ( port = ":50051" )  type server struct{} //服务对象  // SayHello 实现服务的接口 在proto中定义的所有服务都是接口 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{}) // 注册反射服务 这个服务是CLI使用的 跟服务本身没有关系 reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } - 
客户端
goconst ( address = "localhost:50051" defaultName = "world" )  func main() { //建立链接 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] } // 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) } 
 - 
 
连接consul
注册到consul中后,consul会根据注册信息定期向服务器发送健康检查请求。 如果没有设置健康检查,注册后会因为健康检查机制立马下线。
一般用HTTP或者GRPC方式,一般HTTP项目用HTTP方式,GRPC项目用GRPC方式
HTTP方式
            
            
              javascript
              
              
            
          
          // 开放一个 /health 接口,用于服务注册中心向此接口发送健康检查请求
r.GET("/health", func(ctx *gin.Context) {
    ctx.JSON(http.StatusOK, gin.H{
    "code": http.StatusOK,
    "msg":  "",
    })
})
        然后注册到consul的时候配置是HTTP方式,及其健康检查请求url
            
            
              go
              
              
            
          
          type Registry struct {
   Host string
   Port int
}
type RegistryClient interface {
   Register(address string, port int, name string, tags []string, id string) error
   DeRegister(serviceId string) error
}
func NewRegistryClient(host string, port int) RegistryClient {
   return &Registry{Host: host, Port: port}
}
func (r *Registry) Register(address string, port int, name string, tags []string, id string) error {
   cfg := api.DefaultConfig()
   cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)
   client, err := api.NewClient(cfg)
   if err != nil {
      panic(err)
   }
   //生成对应grpc的检查对象
   check := &api.AgentServiceCheck{
      //HTTP方式
      HTTP:                           fmt.Sprintf("http://%s:%d/health", address, port),
      Timeout:                        "5s",
      Interval:                       "5s",
      DeregisterCriticalServiceAfter: "15s",
   }
   //生成注册对象
   registration := new(api.AgentServiceRegistration)
   registration.Name = name
   registration.ID = id
   registration.Port = port
   registration.Tags = tags
   registration.Address = address
   registration.Check = check
   err = client.Agent().ServiceRegister(registration)
   if err != nil {
      panic(err)
   }
   return nil
}
func (r *Registry) DeRegister(serviceId string) error {
   cfg := api.DefaultConfig()
   cfg.Address = fmt.Sprintf("%s:%d", r.Host, r.Port)
   client, err := api.NewClient(cfg)
   if err != nil {
      return err
   }
   err = client.Agent().ServiceDeregister(serviceId)
   return err
}
        GRPC方式
比较简单,官方直接提供了,只需要注册到服务器实例中就行
            
            
              scss
              
              
            
          
          // 6.1 生成grpc服务器实例
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", initialize.ServerConfig.Host, initialize.ServerConfig.Port))
if err != nil {
    zap.S().Panic(err)
}
server := grpc.NewServer()
zap.S().Debugf("启动服务器,端口:%d", initialize.ServerConfig.Port)
// 6.2 本地rpc服务注册到服务器
pb.RegisterInventoryServer(server, &api.InventoryServer{})
// 6.3 健康检查注册注册到服务器
hsrv := health.NewServer()
hsrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(server, hsrv)
// 6.4 启动实例
go func(){
    if err := server.Serve(l); err != nil {
        log.Fatal(err)
    }
}
// 相关信息注册到consul中
registerClient := NewRegistryClient(initialize.ServerConfig.ConsulConfig.Host, initialize.ServerConfig.ConsulConfig.Port)
serviceId := uuid.NewV4().String()
if err := registerClient.Register(initialize.ServerConfig.Host, initialize.ServerConfig.Port, initialize.ServerConfig.Name, initialize.ServerConfig.Tags, serviceId); err != nil {
    zap.S().Panic("服务注册失败:", err.Error())
}
        consul部分只需要把HTTP修改为GRPC相关
            
            
              css
              
              
            
          
          check := &api.AgentServiceCheck{
    //启用GRPC
    GRPC:                           fmt.Sprintf("%s:%d", address, port),
    GRPCUseTLS:                     false,
    Timeout:                        "5s",
    Interval:                       "5s",
    DeregisterCriticalServiceAfter: "15s",
}
        源码分析
gRPC 源码分析(二): gRPC Server 的 RPC 连接阶段
gRPC 源码分析(三): gRPC Server 的 RPC 交互阶段
gRPC 源码分析(四): gRPC server 中 frame 的处理
gRPC 源码分析(五): gRPC server 的流量控制 - 采样流量控制
gRPC 源码分析(六): gRPC server 的流量控制 - connection 和 stream level 的流量控制
custom
客户端流程浅析
DialContext
- 
--> cc.parseTargetAndFindResolver ,
- 
通过Scheme获取对应resolver(是一个resolver.Builder接口)
--> cc.getResolver,先从初始化的配置中找是否存在该resolver(即先从grpc.WithResolvers()方法传递进来的resolvers中找),如果不存在再使用resolver.Get方法去全局中找(即再去通过使用resolver.Register方法注册进来的resolvers中找)。
 - 
都获取不到则使用默认的passthrough方案,不使用该功能。
 
 - 
 - 
调用newCCBalancerWrapper方法去初始化grpc.ClientConn的属性balancerWrapper。该方法先将传入的grpc.ClientConn对象包装成实现了balancer.ClientConn接口的ccBalancerWrapper对象(该结构体对象所在位置:
google.golang.org\grpc@v1.55.0\balancer_conn_wrappers.go),该对象有一个属性updateCh,其本质是一个长度为1的chan interface{},然后会通过goroutine启动其watch方法用来监听updateCh中的数据,通过断言收到的数据类型,从而根据不同的数据类型做出不同的处理,例如收到的数据是ccStateUpdate类型(该类型包含了resolver.State类型,即resolver.ClientConn接口的UpdateState方法的参数),则会调用该对象的handleClientConnStateChange方法,可能过滤数据,该方法最终调用其内部属性balancer的UpdateClientConnState方法(内部调用balToUpdate.UpdateClientConnState --> UpdateSubConnState --> bw.Balancer.UpdateSubConnState --..... / ),并且将结果发送到resultCh中。(此刻可以猜想resolver模块与balancer模块如何联系,但还需找到resolver.ClientConn接口的UpdateState方法最终向updateCh发送ccStateUpdate类型数据的地方,才能确认)。 - 
调用newCCResolverWrapper方法去初始化grpc.ClientConn的属性resolverWrapper。该方法先将传入的grpc.ClientConn对象包装成实现了resolver.ClientConn接口的grpc.ccResolverWrapper对象(该结构体对象所在位置:
google.golang.org\grpc@v1.55.0\resolver_conn_wrapper.go),最后调用前面找到的resolver的Build()方法,将该对象作为参数之一传进去。查看grpc.ccResolverWrapper对象实现的UpdateState方法,可知其调用了grpc.ClientConn的updateResolverState方法,此方法内部最终调用了gprc.ClientConn的属性balancerWrapper的updateClientConnState方法,并且将类型resolver.State包装成了类型balancer.ClientConnState,然后传入进去,而updateClientConnState方法内部又将类型balancer.ClientConnState包装成了类型*ccStateUpdate,然后发送到balancerWrapper的updateCh中(跟2中连起来了,找到并且验证了balancer模块和resolver通信的全过程)。
 - 
*ccStateUpdate类型
 
resover.Builder接口的Build方法有一个参数是resolver.ClientConn接口类型,此接口的UpdateState方法要求传入更新的值,此方法具体实现可在grpc.ccResolverWrapper结构体的UpdateState方法中查看,位置:google.golang.org\grpc@v1.55.0\resolver_conn_wrapper.go,查看后可知内部调用了grpc.ClientConn的updateResolverState方法,此方法内部最终调用了gprc.ClientConn的属性balancerWrapper的updateClientConnState方法