grpc-go源码剖析四十六之服务器端是如何将数据帧缓存到recvBuffer里?(多个数据帧时,在底层是如何来存储的?)

已发表的技术专栏
0  grpc-go、protobuf、multus-cni 技术专栏 总入口

1  grpc-go 源码剖析与实战  文章目录

2  Protobuf介绍与实战 图文专栏  文章目录

3  multus-cni   文章目录(k8s多网络实现方案)

4  grpc、oauth2、openssl、双向认证、单向认证等专栏文章目录)

经过前面几篇文章分析,我们已经服务器端接收到数据帧后,经历抽样流控,链路流控,流级别流控;

那么,接下来我们要考虑的问题是,服务器端如何来维护接收到数据帧呢?

换句话说,服务器端接收到不止一个数据帧时,按照什么原则来存储这些数据帧? 是随意存储,还是存储到单链表里呢?

1、数据帧存储到recvBuffer里的整体流程图?

1.1、存储模型图?

grpc-go源码剖析四十六之服务器端是如何将数据帧缓存到recvBuffer里?(多个数据帧时,在底层是如何来存储的?)_第1张图片

  • 数据帧在grpc框架中的存储是利用channel+bytes.Buffer作为底层存储的(recvBuffer.c + recvBuffer.backlog)。
  • 将获取到的数据帧先存储到channel通道里,channel的类型是recvMsg,而recvMsg是对bytes.Buffer的包装
  • 后续接收到的数据帧,存储到backlog切片里,backlog类型是recvMsg的切片

1.2、存储流程图?

grpc-go源码剖析四十六之服务器端是如何将数据帧缓存到recvBuffer里?(多个数据帧时,在底层是如何来存储的?)_第2张图片

主要流程说明:

  • 先从bytes.Buffer对象池里获取一个bytes.Buffer类型的对象
  • 将数据帧存储到bytes.Buffer对象里
  • 调用recvBuffer的存储器put: 就是判断切片backlog里是否有数据
    • 若没有的话,将接收到是数据帧存储到通道里
    • 若有数据的话,将接收到的数据帧存储到切片backlog的尾部即可。

2、从源码的视角进行分析

2.1、将接收到数据帧封装到类型为bytes.Buffer对象里

分析入口在:

在gRPC-go源码中提供了测试用例,随便找一个服务器端的启动文件main.go ,参考下面的调用链即可找到:

main.go->s.Serve->s.handleRawConn(rawConn)->s.serveStreams(st)->st.HandleStreams(func(stream *transport.Stream)->handleData方法

或者

直接进入grpc-go/internal/transport/http2_server.go文件中的handleData方法里:

1func (t *http2Server )handleData(f *http2.DataFrame) {
2// ---省略掉----流控相关代码-----
3.	s := t.getStream(f)
4if size > 0 {
56if len(f.Data()) > 0 {
7.			buffer := t.bufferPool.get()
8.			buffer.Reset()
9.			buffer.Write(f.Data())
10.			s.write(recvMsg{buffer: buffer})
11}
12}
13// ---省略掉----不相关代码-----
14}

本次分析,只关心第7-10行:
主要流程说明:

  • 第7行:从对象池bufferPool获取一个bytes.Buffer类型的对象buffer
  • 第9行:将数据帧存储到对象buffer里
  • 第10行:向将buffer封装到recvMsg结构体里,然后调用流的write方法

2.2、gRPC-go框架是如何存储bytes.Buffer对象的?也就是,如何来存储接收到的数据帧的

接着上面,直接点击write方法:(进入grpc-go/internal/transport/transport.go文件中Stream结构体的Write方法里:)

func (s *Stream) write(m recvMsg) {
	s.buf.put(m)
}

可见,实际调用的是类型recvBuffer的put方法:
进入grpc-go/internal/transport/transport.go文件中recvBuffer结构体的put方法里:

1func (b *recvBuffer) put(r recvMsg) {
2.	b.mu.Lock()
3if b.err != nil {
4.		b.mu.Unlock()
5// An error had occurred earlier, don't accept more
6// data or errors.
7return
8}
9.	b.err = r.err
10if len(b.backlog) == 0 {
11select {
12case b.c <- r:
13.			b.mu.Unlock()
14return
15default:
16}
17}
18.	b.backlog = append(b.backlog, r)
19.	b.mu.Unlock()
20}

主要流程说明:

  • 第3-8行:校验工作
  • 第10-16行:判断一下切片b.backlog是否为空,
    • 如果没有数据的话,就将接收到的数据r,存储到通道b.c里
    • 如果不为空,即切片b.backlog已经有数据帧了的话,就将新接收到的数据帧存储到切片b.backlog的尾部
假设客户端将一个数据帧分多次发送,服务端第一次接收到数据帧后是存储到通道b.c里了,以后的数据帧呢?
可能场景一:
  • 第一次接收到数据帧后,执行的顺序第10-14行,然后就退出了。此时b.backlog依旧没有数据帧。
  • 第二次接收到数据帧后,执行的是第10行,然后进入第11行,多路复用器,发现第12行的通道b.c中的数据,还没有消费,那么就会阻塞,执行defalut分支,然后执行18行,将第二个数据帧里存储到了切片b.backlog里了
  • 第三次接收到数据帧时,就会直接执行第18行了。
可能场景二:
  • 可能是,当第一个数据帧存储到通道后,就立马消费掉了,第二数据帧还会存储到通道b.c里;
    也就是当服务器接收到的数据帧来不及消费时,就会将待处理的数据帧存储到切片里。

如果前一个数据还没有被消费的话,就存储到切片b.backlog里;

这种方式,

其实是保证了数据是按照顺序存储的

到目前为止,已经将接收到数据帧真正的存储到本地内存了,至于如何读取,再后面的章节,会介绍。

3、总结

其实,本篇文章在解决一种场景,接收端接收到多次请求后,如何保证按顺序存储,为以后按顺序读取数据奠定了基础。

底层采用了通道+切片的方式实现。

在自己的项目中,如果有类似的场景,是不是可以作为一种参考方案呢?

将别人的经验,别人的源码,经过抽象整理后,灵活运用到我们自己的场景中,才是我们的最终目的。

下一篇文章
  服务器端在真正执行客户端的请求方法前,是如何完整的读取到请求参数值的?

点击下面的图片,返回到专栏大纲

gRPC-go源码剖析与实战之专栏大纲

gRPC-go源码剖析与实战感谢

你可能感兴趣的:(grpc,golang,docker,kubernetes,微服务架构)