gRPC基础解读与源代码过程分析

gRPC基础解读与源代码过程分析

GRPC安装

首先说一下GRPC的安装,看到有一些文档的安装教程没有更新,还是老的版本。

go get google.golang.org/protobuf/proto
go get google.golang.org/grpc

go get -u google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

什么是RPC以及什么是gRPC

网上针对RPC已经很多概念了,但是这样还是想从浅显一点的角度介绍RPC。

  • 什么是RPC
    按照时间来看,首先是TCP出现,然后RPC,再是HTTP。严格来说,RPC并不是协议,而是一种风格,应该是与REST对应。
    远程过程调用允许运行于一台计算机的程序调用另外一台计算机的子程序,就像本地调用一样,无需为这个交互编程。
  • 什么是GRPC
    GRPC才是一种协议,与HTTP对应。GRPC是RPC的一种实现方式,方便我们编写RPC服务。

一个RPC框架大致需要动态代理、序列化、网络请求、网络请求接受(netty实现)、动态加载、反射这些知识点。现在开源及各公司自己造的RPC框架层出不穷,唯有掌握原理是一劳永逸的.

我们学习gRPC的过程,应该是学会使用,再了解运行机制,最后体会其中的展现的方法论。

上面已经提到,gRPC是RPC的一种实现。

grpc采用多种语言开发,支持多种语言调用,通信协议基于HTTP2,数据传输使用protobuf(一种比json轻量的数据传输格式)。

(HTTP2带来的不同区别)

  1. HTTP2决定了c/s(client/server):
    • 连接时,TCP握手之后,c/s需要统一frame设置——frame大小、滑动窗口大小等。
    • 交互时,也是遵从传统的request==response风格。
  2. protobuf决定了数据传输的不同
    • 相比于HTTP,GRPC比较简单——更快,基于二进制流传输
    • 相较于json,protobuf转化的二进制只有在特定的消息接口才能被正确放序列化。

关于gRPC的使用,已经有很多教程,文章主要从源码讲解gRPC框架的运行过程。

gRPC的操作教程:https://juejin.cn/post/7025527003667234824

简述gRPC的开发步骤

网上已经有很多很全面的教程,包括gRPC认证访问、注册中心、中间件等。
这里就简述一下gRPC服务的开发步骤:

  • 编写protocol文件

    • 确定包名
    • 文件位置
    • 传输的信息(这里面就需要确定好request与response的结构体)
    • 传输方式(用不用流、要不要双向流)
  • 生成pb代码文件
    文件生成后,基本框架就立起来了。

    • 生成的request与response,增加了相关的方法——算是帮忙实现了接口,便于与grpc框架集成。
    • 定义中的service,生成了与之对应的client与server代码,而后面做的就是顺着这个生成的client与server接上逻辑。简单的服务会直接生成实现的函数;而针对流传输,会多生成一个流操作的接口,
    • 针对流传输的,一般不同的流(比如服务端流、客户端流、双向流)都需要生成额外的client与server,负责流的接收、传输、关闭等。这些负责不同流的client与server之间的差别只是个别方法的区别。属于接口嵌套。生成的代码整体上还是调用了grpc框架的方法。
    • 生成的文件的另外一些信息,是针对初始化与信息压缩相关,属于协议那一块内容的。
  • 根据pb文件生成的结构,完成具体服务的方法实现

    • 一般为了管理,会新建imp文件夹负责实现——因为一方面生成的文件只是逻辑,并没有集成;另一方面,我们自己的逻辑还没有加入(要通过pb文件加入到grpc框架中)
    • 这里实现了pb文件中的server接口,这里的东西可以直接注册进去(那么,client不需要实现吗?)
    • 实现pb文件中的server接口,如果只有一个简单的request=response,那么完全可以直接定义结构体实现相应的方法;但是,我们定义了流相关的——服务端流、客户端流、双端流,那么还需要自己实现这些方法以完成接口实现。
  • server的实现(imp)

    • 如果是简单响应,直接接收request参数,然后完成逻辑并返回response
    • 如果是服务端流式应答,则服务端直接在函数中调用send然后返回(因为我们是在实现server的方法)
    • 如果是客户端流式访问,则服务端需要在函数中for不停接收参数并处理,最好接收完统一发送回客户端并关闭。
    • 如果是双端流的情况,则服务端需要在for中不断处理,并不断返回。(没有统一发送与关闭的操作)
  • server端实现完应该是注册与开始服务

    • 开启地址监听
    • 定义服务端并注册方法
    • 开始服务
  • client端的实现

    • 通过Dail获取一个连接,并创建客户端
    • 客户端普通调用,则直接调用得到返回
    • 客户端使用流发送,则需要调用获取客户端流对象,在for中通过流对象Send
    • 客户端接收服务端流,则需要调用获取服务端流对象,在for中调用流对象接收
    • 使用双端流的,直接从pb生成的文件中获取双端流对象,for中发送与for中接收。
  • 开始服务

gRPC代码过程

Client流程

连接:

  • 初始化与启动,接收参数与opts,然后启动resolver。

  • Resolver 根据目标地址获取 server 的地址列表 (比如一个 DNS name 可能会指向多个 server ip, dnsResovler 是 gRPC 内置的 resolver 之一). 启动 balancer.

  • Balancer 根据平衡策略, 从诸多 server 地址中选择一个或多个建立 TCP 连接.

  • client 在 TCP 连接建立完成之后, 等待 server 发来的 HTTP2 Settings frame, 并调整自身的 HTTP2 相关配置. 随后向 server 发送 HTTP2 Settings frame.

    对应的源代码步骤:(建议自己跟着点击grpc代码来理解)

    grpc.Dial(.)
      DialContext()
        cc.parseTargetAndFindResolver()
        newCCBalancerWrapper(.)  // 核心处理函数
          go ccb.watcher()
        newCCResolverWrapper(.)
        cc.Connect()
          cc.balancerWrapper.exitIdle()
            ccb.updateCh.Put(&exitIdleUpdate{}) --> // 借助了watch机制
              func (ccb *ccBalancerWrapper) watcher()
                ccb.handleExitIdle()
                  ccb.balancer.ExitIdle()
                    sc.Connect()
                      go acbw.ac.connect()
                        ac.resetTransport()
                           ac.tryAllAddrs(.)
                             ac.createTransport(addr, copts, connectDeadline)
    

交互:

  • Client 创建一个 stream 对象用来管理整个交互流程.

  • Client 将 service name, method name 等信息放到 header frame 中并发送给 server.

  • Client 将 method 的参数信息放到 data frame 中并发送给 server.

  • Client 等待 server 传回的 header frame 和 data frame. 一次 RPC call 的 result status 会被包含在 header frame 中, 而 method 的返回值被包含在 data frame 中.

    c.xxxClient()-->
    c.cc.Invoket()
       invoke()-->
          newClientStream()-->
          SendMsg()-->
            prepareMsg()
    
Server流程

连接:

  • 完成初始化配置之后,开始监听TCP端口。net.Listen()
    交互:

  • Server 等待 client 发来的 header frame, 从而创建出一个 stream 对象来管理整个交互流程. 根据 header frame 中的信息, server 知道 client 请求的是哪个 service 的哪个 method.

  • Server 接收到 client 发来的 data frame, 并执行 method.

  • Server 将执行是否成功等信息方法放在 header frame 中发送给 client.

  • Server 将 method 执行的结果 (返回值) 放在 data frame 中发送给 client.

    grpc.NewServer()
    --
    pb.RegisterHelloServer(s,x)
    s.RegisterService()-->
      s.register(sd, ss)
        s.services[sd.ServiceName] = info
    --
    s.Serve(listen)
    lis.Accept()
    --
    s.Serve(listen)
    s.handleRawConn(lis.Addr().String(), rawConn)
      s.newHTTP2Transport(rawConn)                                       // cs之间进行HTTP2握手 -- HTTP2 frame的传递
        s.serveStreams(st)-->                                           // 通过新的goroutine处理client的数据,对 frame 类型的分类和处理
          st.HandleStreams --> func (t *http2Server) HandleStreams()   // 会阻塞当前 goroutine, 并等待来自 client 的 frame.
            s.handleStream(st, stream, s.traceInfo(st, stream))       // 当一个新的 stream 被创建之后, 进行一些配置
              s.processStreamingRPC(t, stream, srv, sd, trInfo)       // 根据 headers frame 中 path 和 method 的信息, gRPC server 找到注册好的 method 并执行
    

gRPC请求处理

  • server:

    1. gRPC server 在一个 for 循环中等待来自 client 的访问, 创建一个 golang 原生的 net.Conn , 并创建一个新的 goroutine 来处理这个 net.Conn . 所有来自这个 client 的 request, 不论这个 client 调用哪一个远程方法, 或者调用几次, 都会由着一个 goroutine 处理.
    2. 对于每一个来自 client 的新的连接, gRPC server 都会经历 RPC 连接阶段和 RPC 交互阶段.
  • HTTP2握手阶段

    s.newHTTP2Transport(rawConn)
      transport.NewServerTransport(c, config)
        newFramer(conn, writeBufSize, readBufSize, maxHeaderListSize)
        framer.fr.WriteSettings()             // 配置的frame发送给client
        framer.fr.WriteWindowUpdate()
        newControlBuffer(t.done)-->
        t.framer.fr.ReadFrame()
        t.handleSettings(sf)
        newLoopyWriter(serverSide, t.framer, t.controlBuf, t.bdpEst) -->
    
    1. 首先创建了一个 framer, 用来负责接收和发送 HTTP2 frame, 是 server 和 client 交流的实际接口.

    2. gRPC server 端首先明确自己的 HTTP2 的初始配置, 比如 InitialWindowSize, MaxHeaderListSize 等等, 并将这些配置信息通过 framer.fr 发送给 client. framer.fr 实际上就是 golang 原生的 http2.Framer . 在底层, 这些配置信息会被包裹在一个 Setting Frame 中以二进制的格式发送给 client.

    3. controlBuf 是用来缓存 Setting Frame 等和设置相关的 frame 的缓存. 在 flow control 的相关章节会详细分析它的作用.

    4. 在 HTTP2 中, client 和 server 都要求在建立连接之前发送一个 connection preface, 作为对所使用协议的最终确认, 并确定 HTTP2 连接的初始设置. client 发送的 preface 以一个 24 字节的序列开始, 用 16 进制表示为 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a .之后紧跟着一个 setting frame, 用来表示 client 端最终决定的 HTTP2 配置参数. NewServerTransport 中包含了读取并验证 preface, 以及读取并应用 setting frame 的代码.

    5. RPC 连接阶段完毕. 在 NetServerTransport 的最后启动了 loopyWriter , 开始了 RPC 交互阶段. loopyWriter 不断地从 controlBuf 中读取 control frames (包括 setting frame), 并将缓存中的 frame 发送给 client. 可以说 loopyWriter 就是 gRPC server 控制流量以及发送数据的地方

    gRPC server 端首先明确自己的 HTTP2 的初始配置, 比如 InitialWindowSize, MaxHeaderListSize 等等, 并将这些配置信息通过 framer.fr 发送给 client. framer.fr 实际上就是 golang 原生的 http2.Framer . 在底层, 这些配置信息会被包裹在一个 Setting Frame 中以二进制的格式发送给 client.

  • server交互阶段
    st.HandleStreams 在一个 for 循环中等待并读取来自 client 的 frame, 并采取不同的处理方式. 本篇中将以 headers, data 和 settings frame 为例, 简要描述 gRPC server 的处理方法.

    • headers : 在 gRPC server 和 client 端, 存在着一个 stream 的概念, 用来表征一次 gRPC call. 一个 gRPC call 总是以一个来自 client 的 headers frame 开始.这个部分内容,应该仔细看看func (t *http2Server) operateHeaders()函数。

      t.operateHeaders(frame, handle, traceCtx)-->
        streamID := frame.Header().StreamID
        buf := newRecvBuffer()
        t.activeStreams[streamID] = s
        handle(s)
          s.handleStream() -->
      
      1. 首先就创建stream对象,在cs间交流。
      2. gRPC server 会遍历 frame 中的 field, 并将 field 中的信息记录在 stream 中. 值得注意的是 :method 和 :path 两个 field, client 端需要填写好这两个 field 来明确地指定要调用 server 端提供的哪一个 remote procedure. 也就是说, 调用哪一个 server 方法的信息是和调用方法的参数分开在不同的 frame 中的.
      3. 根据 headers frame 中 path 和 method 的信息, gRPC server 找到注册好的 method 并执行
      4. 从 stream 中读取 data frame, 即 RPC 方法中的参数信息. 随后调用 md.Handler 执行已经注册好的方法, 并将 reply 发送给 client (并不会直接翻送给 client, 而是将数据存储在 buffer 中, 由 loopyWriter 发送. 最后将 status statusOK 发送给 client. WriteStatus 在一个 stream 的结尾处执行, 即标志着这个 stream 的结束.
    • Data Frame :

      t.handleData(frame)
        t.getStream(f)
      
      1. 根据 streamId, 从 server 的 activeStreams map 中找到 stream 对象.
      2. 从 bufferPool 中拿到一块 buffer, 并把 frame 的数据写入到 buffer 中.
      3. 将这块 buffer 保存到 stream 的 recvBuffer 中.
    • Setting Frame : server收到来自 client 的 setting frame, 来更新 HTTP2 的一些参数
      handleSettings 并没有直接将 settings frame 的参数应用在 server 上, 而是将其放到了 controlBuf 中, controlBuf 的相关内容会在后续的篇章中涉及到.

通过源代码里的client/server链接与交互过程可以看到 gRPC server 在整个处理流程中, 除了执行注册好的方法以外, 基本上都是异步的. 各个操作之间通过 buffer 连接在一起, 最大限度地避免 goroutine 阻塞.

补充HTTP 2 与 gRPC 的知识

gRPC的特点就是基于HTTP2完成连接与数据传输,那么使用了HTTP2带来了哪些优点呢?

HTTP1 与 HTTP2 的对比:

  1. 结构上
  • HTTP / 2并不是对HTTP协议的重写,相对于HTTP / 1,HTTP / 2的侧重点主要在性能。请求方法,状态码和语义和HTTP / 1都是相同的,可以使用与HTTP / 1.x相同的API(可能有一些小的添加)来表示协议。
  • HTTP2 使用了超文本传输协议版本2 还有HPACK头部压缩算法
  • HTTP2 是属于HTTP1.x的升级,兼容的同时,又使用多种方式优化了性能。
  1. 过程中
  • HTTP2使用流作为传输单元,流是存在于与TCP连接中的一个虚拟通道(双向、能往前流也能往回流)
  • HTTP2使用二进制传输数据,而HTTP1使用的是文本格式传输
  • 多路复用。HTTP2支持多路复用,也就是提供了单个连接上复用HTTP请求和响应的能力,多个请求和响应可以同时在一个连接上使用流。
  • 头部信息建立索引。压缩头部的时候,可以通过头部信息分组。cs之间可以建立索引,相同的表头只需要传输索引就可以了。
  1. 特性实现原理
    多路复用:
  • HTTP/2中,在一个浏览器同域名下的所有请求都是在单个连接中完成,这个连接可以承载任意数量的双向数据流,每个数据流都以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,根据帧首部的流标识可以将多个帧重新组装成一个流。并且,每一个流都有自己的ID,并有自己的优先级。
  • 这样相比较于HTTP1,发送多个请求就必须要有多个TCP链接的情况,无疑更加高效。
    服务器推送:
  • 服务端主动推送也会遵守同源策略,不会随便推送第三方的资源到客户端
  • 如果服务端推送资源是呗客户端缓存过的,客户端是有权力拒绝服务端的推送的,浏览器可以通过发送RST_STREAM帧来拒收。
  • 每一个服务端推送的资源都是一个流

GRPC与HTTP2

gRPC设计时的初衷:
gRPC的设计目标是在任何环境下运行,支持可插拔的负载均衡,跟踪,运行状况检查和身份验证。它不仅支持数据中心内部和跨数据中心的服务调用,它也适用于分布式计算的最后一公里,将设备,移动应用程序和浏览器连接到后端服务,同时,它也是高性能的,而HTTP /2恰好支持这些。

  • HTTP /2天然的通用性满足各种设备,场景
  • HTTP /2的性能相对来说也是很好的,除非你需要极致的性能
  • HTTP /2的安全性非常好,天然支持SSL
  • HTTP /2的鉴权也非常成熟
  • gRPC基于HTTP /2多语言实现也更容易

总结:
文章主要从源代码的过程分析了gRPC中:

  • 服务端的创建与连接过程
  • 客户端连接与交互过程
  • 连接的建立、请求的传递、请求处理等过程在源代码中的体现

另外,为了便于理解,前置讲解了RPC与gRPC的区别,gRPC服务的开发流程,并进一步阐述了使用HTTP2带来的益处。


参考资料

https://juejin.cn/post/7092738387471237127
https://cloud.tencent.com/developer/article/1189548
https://cloud.tencent.com/developer/article/1525185

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值