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带来的不同区别)
- HTTP2决定了c/s(client/server):
- 连接时,TCP握手之后,c/s需要统一frame设置——frame大小、滑动窗口大小等。
- 交互时,也是遵从传统的request==response风格。
- protobuf决定了数据传输的不同
- 相比于HTTP,GRPC比较简单——更快,基于二进制流传输
- 相较于json,protobuf转化的二进制只有在特定的消息接口才能被正确放序列化。
关于gRPC的使用,已经有很多教程,文章主要从源码讲解gRPC框架的运行过程。
gRPC的操作教程:https://juejin.cn/post/7025527003667234824
简述gRPC的开发步骤
网上已经有很多很全面的教程,包括gRPC认证访问、注册中心、中间件等。 这里就简述一下gRPC服务的开发步骤:
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()
cc.getResolver(parsedTarget.Scheme)-->
cc.parsedTarget = parsedTarget
newCCBalancerWrapper(.)-->
newCCResolverWrapper(cc, resolverBuilder)
cc.Connect()
交互:
-
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)
s.serveStreams(st)-->
st.HandleStreams --> func (t *http2Server) HandleStreams()
s.handleStream(st, stream, s.traceInfo(st, stream))
s.processStreamingRPC(t, stream, srv, sd, trInfo)
gRPC请求处理
-
server:
- gRPC server 在一个 for 循环中等待来自 client 的访问, 创建一个 golang 原生的 net.Conn , 并创建一个新的 goroutine 来处理这个 net.Conn . 所有来自这个 client 的 request, 不论这个 client 调用哪一个远程方法, 或者调用几次, 都会由着一个 goroutine 处理.
- 对于每一个来自 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) -->
-
首先创建了一个 framer, 用来负责接收和发送 HTTP2 frame, 是 server 和 client 交流的实际接口. -
gRPC server 端首先明确自己的 HTTP2 的初始配置, 比如 InitialWindowSize, MaxHeaderListSize 等等, 并将这些配置信息通过 framer.fr 发送给 client. framer.fr 实际上就是 golang 原生的 http2.Framer . 在底层, 这些配置信息会被包裹在一个 Setting Frame 中以二进制的格式发送给 client. -
controlBuf 是用来缓存 Setting Frame 等和设置相关的 frame 的缓存. 在 flow control 的相关章节会详细分析它的作用. -
在 HTTP2 中, client 和 server 都要求在建立连接之前发送一个 connection preface, 作为对所使用协议的最终确认, 并确定 HTTP2 连接的初始设置. client 发送的 preface 以一个 24 字节的序列开始, 用 16 进制表示为 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a .之后紧跟着一个 setting frame, 用来表示 client 端最终决定的 HTTP2 配置参数. NewServerTransport 中包含了读取并验证 preface, 以及读取并应用 setting frame 的代码. -
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() -->
- 首先就创建stream对象,在cs间交流。
- gRPC server 会遍历 frame 中的 field, 并将 field 中的信息记录在 stream 中. 值得注意的是 :method 和 :path 两个 field, client 端需要填写好这两个 field 来明确地指定要调用 server 端提供的哪一个 remote procedure. 也就是说, 调用哪一个 server 方法的信息是和调用方法的参数分开在不同的 frame 中的.
- 根据 headers frame 中 path 和 method 的信息, gRPC server 找到注册好的 method 并执行
- 从 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)
- 根据 streamId, 从 server 的 activeStreams map 中找到 stream 对象.
- 从 bufferPool 中拿到一块 buffer, 并把 frame 的数据写入到 buffer 中.
- 将这块 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 的对比:
- 结构上
- HTTP / 2并不是对HTTP协议的重写,相对于HTTP / 1,HTTP / 2的侧重点主要在性能。请求方法,状态码和语义和HTTP / 1都是相同的,可以使用与HTTP / 1.x相同的API(可能有一些小的添加)来表示协议。
- HTTP2 使用了超文本传输协议版本2 还有HPACK头部压缩算法
- HTTP2 是属于HTTP1.x的升级,兼容的同时,又使用多种方式优化了性能。
- 过程中
- HTTP2使用流作为传输单元,流是存在于与TCP连接中的一个虚拟通道(双向、能往前流也能往回流)
- HTTP2使用二进制传输数据,而HTTP1使用的是文本格式传输
- 多路复用。HTTP2支持多路复用,也就是提供了单个连接上复用HTTP请求和响应的能力,多个请求和响应可以同时在一个连接上使用流。
- 头部信息建立索引。压缩头部的时候,可以通过头部信息分组。cs之间可以建立索引,相同的表头只需要传输索引就可以了。
- 特性实现原理
多路复用:
- 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
|