盒子
盒子
文章目录
  1. 1.RPC 入门
    1. 1.1.RPC 框架原理
    2. 1.2.业界主流的 RPC 框架
    3. 1.3.gRPC 简介
    4. 1.3.1.gRPC 概览
    5. 1.3.2.gRPC 特点
  2. 2.gRPC 服务端创建
    1. 2.1.PROTOBUF协议
      1. 2.1.1.什么是PROTOBUF协议
      2. 2.1.2.PROTOBUF数据编码
      3. 2.1.3.PROTOBUF数据协议的优劣势
      4. 2.1.4.参考
    2. 2.2.服务端创建业务代码
    3. 2.3.服务端 service 调用流程
    4. 2.4.服务端注册过程
    5. 2.5.服务端请求处理流程
    6. 2.6.客户端发送请求流程
      1. 2.6.1.ClientConn与ClientConnInterface的关系
  3. 3.从零开始创建Demo程序
    1. 3.1.编写并编译proto文件
    2. 3.2.编写Server程序
    3. 3.3.编写Client程序
    4. 3.4.直接通过Goland运行
      1. 3.4.1.Goland执行go get报SSL certificate problem: unable to get local issuer certificate错误
  4. 4.参考
  5. 5.结语

gRPC源码分析

1.RPC 入门

1.1.RPC 框架原理

  RPC 框架的目标就是让远程服务调用更加简单、透明,RPC 框架负责屏蔽底层的传输方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。

  RPC框架的调用原理图如下所示:
RPC框架的调用原理图

1.2.业界主流的 RPC 框架

  业界主流的 RPC 框架整体上分为三类:

  • 支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift
  • 只支持特定语言的 RPC 框架,例如新浪微博的 Motan
  • 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo

  随着微服务的发展,基于语言中立性原则构建微服务,逐渐成为一种主流模式,例如对于后端并发处理要求高的微服务,比较适合采用 Go 语言构建,而对于前端的 Web 界面,则更适合 Java 和 JavaScript。
因此,基于多语言的 RPC 框架来构建微服务,是一种比较好的技术选择。例如 Netflix,API 服务编排层和后端的微服务之间就采用 gRPC 进行通信。

1.3.gRPC 简介

  gRPC 是一个高性能、开源和通用的 RPC 框架,面向服务端和移动端,基于 HTTP/2 设计。

1.3.1.gRPC 概览

  gRPC 是由 Google 开发并开源的一种语言中立的 RPC 框架,当前支持 C、Java 和 Go 语言,其中 C 版本支持 C、C++、Node.js、C# 等。

  gRPC 的调用示例如下所示:
gRPC的调用示例

1.3.2.gRPC 特点

  • 语言中立,支持多种语言;
  • 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
  • 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
  • 序列化支持 PB(Protocol Buffer)JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。

2.gRPC 服务端创建

  以官方 Go 语言的 helloworld 为例,介绍 gRPC 服务端创建以及 service 调用流程(采用简单 RPC 模式)。流式请求和响应的基本原理类似,下文就不再详细探讨。

2.1.PROTOBUF协议

2.1.1.什么是PROTOBUF协议

  protocolbuffer(以下简称PB)是google的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:JAVA、C++、Python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

2.1.2.PROTOBUF数据编码

proto文件定义了协议数据中的实体结构(message, field)

  • 关键字message: 代表了实体结构,由多个消息字段field组成;
  • 消息字段field: 包括数据类型、字段名、字段规则、字段唯一标识、默认值;
  • 数据类型:如下图所示;
  • 字段规则:
    • required:必须初始化字段,如果没有赋值,在数据序列化时会抛出异常;
    • optional:可选字段,可以不必初始化;
    • repeated:数据可以重复(相当于Java 中的Array或List);
    • 字段唯一标识:序列化和反序列化将会使用到。
      PROTOBUF协议数据类型

2.1.3.PROTOBUF数据协议的优劣势

优点:

  • 二进制消息,性能好/效率高(空间和时间效率都很不错)
  • 平台无关,语言无关,可扩展;
  • 提供了友好的动态库,使用简单;
  • 解析速度快,比对应的XML快约20-100倍;
  • 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10;
  • 支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级;
  • 支持多种语言(可以把 proto 文件看做 IDL 文件);
  • Netty 等一些框架集成。

缺点:

  • 官方只支持C++JAVAPython语言绑定;
  • 二进制可读性差;
  • 二进制不具有自描述特性;
  • 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持);
  • 只涉及序列化和反序列化技术,不涉及 RPC 功能(类似 XML 或者 JSON 的解析器)。

2.1.4.参考

详解通信数据协议ProtoBuf

2.2.服务端创建业务代码

  用 Protocol Buffer 服务定义如下(helloworld.proto):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";
option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
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;
}

  服务端创建代码如下:

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
// server 继承 /examples/helloworld/helloworld/helloworld_grpc.pb.go 中的 UnimplementedGreeterServer,获得一个默认的 SayHello 方法实现,即报错提醒。
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello 真正的实现
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
// 监听端口
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 启动GRPC服务
s := grpc.NewServer()
// 注册具体服务
pb.RegisterGreeterServer(s, &server{})
// 绑定GPRC服务到端口上
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

  客户端调用服务代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
// 建立连接
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
...
defer conn.Close()
c := pb.NewGreeterClient(conn)
// 向服务器发送请求,并打印返回的Message
name := "world"
...
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
...
log.Printf("Greeting: %s", r.GetMessage())
}

2.3.服务端 service 调用流程

  整个 service 调用可以划分为如下四个过程:

  • gRPC 请求消息接入;
  • gRPC 消息头和消息体处理;
  • 内部的服务路由和调用;
  • 响应消息发送。

2.4.服务端注册过程

  从服务端创建代码中可以看到,其注册服务的核心就是pb.RegisterGreeterServer(s, &server{})函数,该函数位于google.golang.org/grpc/examples/helloworld/helloworld/helloworld_grpc.pb.go文件中,是 protoc-gen-go-grpc 编译生成的代码,源码如下:

1
2
3
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}

  其核心就是调用grpc.ServerRegisterService方法,参数分别是一个服务描述实例和具体服务实例,描述实例是 protoc 自动生成的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
var _Greeter_serviceDesc = grpc.ServiceDesc{
ServiceName: "helloworld.Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello", // 方法名
Handler: _Greeter_SayHello_Handler, // 真正调用SayHello方法的Handler
},
},
Streams: []grpc.StreamDesc{},
Metadata: "examples/helloworld/helloworld/helloworld.proto",
}

  grpc.ServerRegisterService方法调用其私有的register方法,最终将服务注册到一个map中保存,以服务描述实例的ServiceName为key, RegisterService 源码如下:

1
2
3
4
5
6
7
8
func (s *Server) RegisterService(sd *ServiceDesc, ss interface{}) {
ht := reflect.TypeOf(sd.HandlerType).Elem()
st := reflect.TypeOf(ss)
if !st.Implements(ht) {
grpclog.Fatalf("grpc: Server.RegisterService found the handler of type %v that does not satisfy %v", st, ht)
}
s.register(sd, ss)
}

  register 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (s *Server) register(sd *ServiceDesc, ss interface{}) {
s.mu.Lock()
defer s.mu.Unlock()
...
srv := &service{
server: ss,
md: make(map[string]*MethodDesc),
sd: make(map[string]*StreamDesc),
mdata: sd.Metadata,
}
for i := range sd.Methods {
// 将ServiceDesc中的Methods放入一个map中,以MethodName为key,Methods为value
d := &sd.Methods[i]
srv.md[d.MethodName] = d
}
for i := range sd.Streams {
// 将ServiceDesc中的Streams放入一个map中,以StreamName为key,Streams为value
d := &sd.Streams[i]
srv.sd[d.StreamName] = d
}
s.m[sd.ServiceName] = srv // 即以"helloworld.Greeter"为key
}

2.5.服务端请求处理流程

  从服务端创建代码中可以看到,其绑定GPRC服务到端口,并正式提供服务的核心就是s.Serve(lis)函数,该函数位于google.golang.org/grpc/server.go文件中,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (s *Server) Serve(lis net.Listener) error {
... // 预处理操作,比如defer中关闭TCP监听
for {
rawConn, err := lis.Accept()
if err != nil {
... // 一些错误处理等操作
}
s.serveWG.Add(1)
go func() {
s.handleRawConn(rawConn)
s.serveWG.Done()
}()
}
}

  核心是在一个死循环里,不断Accept新的连接请求,并启动一个新的协程,执行func (s *Server) handleRawConn(rawConn net.Conn)方法,其源码如下:

1
2
3
4
5
6
7
8
9
10
func (s *Server) handleRawConn(rawConn net.Conn) {
...
// 完成HTTP2的握手
st := s.newHTTP2Transport(conn, authInfo)
...
go func() {
s.serveStreams(st)
s.removeConn(st)
}()
}

  核心是对新连接进行处理,启动一个新的协程,执行func (s *Server) serveStreams(st transport.ServerTransport)方法,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *Server) serveStreams(st transport.ServerTransport) {
...
if s.opts.numServerWorkers > 0 {
...
go func() {
s.handleStream(st, stream, s.traceInfo(st, stream))
wg.Done()
}()
} else {
go func() {
defer wg.Done()
s.handleStream(st, stream, s.traceInfo(st, stream))
}()
}
...
}

  核心是启动一个新的协程,执行func (s *Server) handleStream(...)方法,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Stream, trInfo *traceInfo) {
sm := stream.Method()
...
service := sm[:pos]
method := sm[pos+1:]
srv, knownService := s.m[service]
if knownService {
if md, ok := srv.md[method]; ok {
s.processUnaryRPC(t, stream, srv, md, trInfo)
return
}
if sd, ok := srv.sd[method]; ok {
s.processStreamingRPC(t, stream, srv, sd, trInfo)
return
}
}
...
}

  核心是通过RPC请求中指定的 servicemethond ,从之前保存被注册服务的map中找到对应的服务的方法描述实例,执行 func (s *Server) processUnaryRPC(...) (err error) 方法,其源码如下:

1
2
3
4
5
6
7
func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.Stream, srv *service, md *MethodDesc, trInfo *traceInfo) (err error) {
// 解压Request流等预处理
...
reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt)
// 发送Response等操作
...
}

  核心是执行 md.Handler(srv.server, ctx, df, s.opts.unaryInt) 方法,这个Handler其实是调用到了 _Greeter_SayHello_Handler ,该函数在一开始的服务描述实例中注册过,最终通过srv.(GreeterServer).SayHello(ctx, in)调用到由我们实现的具体的业务逻辑,其源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
...
if interceptor == nil {
// 最终通过这里调用到由我们实现的具体的业务逻辑
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/helloworld.Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
// 这里应该是一个兜底逻辑
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}

2.6.客户端发送请求流程

  从客户端的 main.go 可以看到发起请求的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
...
}
defer conn.Close()
c := pb.NewSpeakerClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
if err != nil {
...
}
log.Printf("Speaking: %s, %d", r.GetMessage(), r.GetCode())
}

  首先调用 grpc.Dial(...) 创建一个 ClientConn 类型的连接;再将连接封装到 speakerClient 结构体中的 ClientConnInterface 类型的变量 cc;调用 speakerClient 结构体的 SayHello 方法。SayHello 方法代码如下:

1
2
3
4
5
6
7
8
func (c *speakerClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := c.cc.Invoke(ctx, "/helloworld.Speaker/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}

  这里调用到了 ClientConnInvoke 方法,其中调用了私有的 invode 方法。Invoke 方法源码如下:

1
2
3
4
5
6
7
8
9
10
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error {
// allow interceptor to see all applicable call options, which means those
// configured as defaults from dial option as well as per-call options
opts = combine(cc.dopts.callOptions, opts)
if cc.dopts.unaryInt != nil {
return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
}
return invoke(ctx, method, args, reply, cc, opts...)
}

  Invoke 方法中,新建一个Client流,SendMsg 发送请求,将Request进行预处理后,写入Client的流中;RecvMsg 循环尝试接受Response。SendMsgRecvMsg 源码位于 google.golang.org\grpc\stream.go 文件中,有兴趣可以自行查看。 Invoke 方法源码如下:

1
2
3
4
5
6
7
8
9
10
func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {
cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
if err != nil {
return err
}
if err := cs.SendMsg(req); err != nil {
return err
}
return cs.RecvMsg(reply)
}

2.6.1.ClientConn与ClientConnInterface的关系

  google.golang.org\grpc\clientconn.go 定义了一个接口 ClientConnInterface,包含了两个重要的函数,其源码如下:

1
2
3
4
type ClientConnInterface interface {
Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...CallOption) error
NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error)
}

  ClientConn 正是实现了这两个接口,所以在 speakerClient 结构体中,仅包含一个 ClientConnInterface 的变量,上述代码中,传入到 speakerClient 结构体的实际是 ClientConn 类型的变量。

  以上就是官方示例的启动到发送,最后到接受请求的整个流程,学艺不精,如有错误,还请指正。

3.从零开始创建Demo程序

  不仅要学会看源码是怎么运行的,更要会写自己的代码。所以这里从 .proto 文件开始,创建一个简单的Demo程序。
  参考各路大神的文章搭建Goland开发环境,毕竟代码提示和转跳还是比VS CODE好太多了。

3.1.编写并编译proto文件

  在官方实例的基础上,进行改造,添加了两个字段,并新增了一个RPC方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";
option go_package = "google.golang.org/grpc/examples/test/model";
option java_multiple_files = true;
option java_package = "io.grpc.examples.test";
option java_outer_classname = "HelloWorldProto";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHi (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
int64 number = 2;
}
message HelloReply {
string message = 1;
int64 code = 2;
}

  在 /examples/test 目录下,运行下面的指令,生成 helloworld.pb.go 文件。

1
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative model/helloworld.proto

3.2.编写Server程序

  总的来说,还是参考了官方Demo,主要是新增了一个 SayHi 方法,想看看多个方法时,代码的写法是否有所不同。

1
2
3
4
5
6
7
8
9
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("SayHello Func Received: %v, number: %v\n", in.GetName(), in.GetNumber())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func (s *server) SayHi(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("SayHi Func Received: %v, number: %v\n", in.GetName(), in.GetNumber())
return &pb.HelloReply{Message: "Hi " + in.GetName(), Code: 200}, nil
}

3.3.编写Client程序

  同样,Client的代码也参考了官方Demo,主要是新增了一个 SayHi 方法的调用。

1
2
3
4
5
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name, Number: number})
log.Printf("Speaking: %s, %d", r.GetMessage(), r.GetCode())
r, err = c.SayHi(ctx, &pb.HelloRequest{Name: name, Number: number})
log.Printf("Speaking: %s, %d", r.GetMessage(), r.GetCode())

3.4.直接通过Goland运行

  先启动Server,再启动Client,可以看到如下输出:

1
2
3
4
5
6
7
# Server输出
2020/06/22 11:44:26 SayHello Func Received: world, number: 1000
2020/06/22 11:44:26 SayHi Func Received: world, number: 1000
# Client输出
2020/06/22 11:44:26 Speaking: Hello world, 0
2020/06/22 11:44:26 Speaking: Hi world, 200

  其实整个过程跟官方Demo区别不大。最大的不同是官方Demo中的 /examples/helloword/helloword 目录下生成了2个 .go 文件;而我自己测试过程中,编译 .proto 文件后只生成了1个 .go 文件,但其中包含了官方Demo中的所有数据结构、函数,并无本质不同。

3.4.1.Goland执行go get报SSL certificate problem: unable to get local issuer certificate错误

  由于这里使用了Goland,和VS Code连接到开发机直接写代码不同,所以需要在Windows上下载项目依赖,执行 go get 报错。原因是证书过期/或者换证书了,解决方案如下:

1
2
# 取消对证书的合法性验证
git config --global http.sslverify false

4.参考

gRPC 源码阅读系列 01:gRPC 服务端创建和调用(这是Java版本的gRPC源码解读,有一定的共通性)

5.结语

  这是在实习过程中学习到的新知识,且全部是开源数据或者资料,因此分享出来,希望我和大家都有所收获。

参考资料已经在文中列出,这里不再一一列举。

转载说明

转载请注明出处,无偿提供。

支持一下
感谢大佬们的支持