grpc-go源码剖析三十九之帧发送器LoopyWriter是如何存储数据帧的?

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

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

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

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

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

前面的章节中我们已经知道通过grpc-go/internal/transport/http2_client.go文件中的http2Client结构体下的Write方法创建数据帧,然后将数据帧存储到帧缓存器controlBuf里的;

此时,帧发送器looyWriter的run方法会从帧缓存器controlBuf里获取数据帧;

那么问题来了,帧发送器loopyWriter是如何来维护获取到的数据帧呢?

可能存在的场景:如果客户端请求多次SayHello方法,会产生多次流,创建多个数据帧,那么,帧发送器发送数据帧时,很有可能不能及时发送出去,这些待发送的数据帧如何管理呢?

本小节,我们就来看看,帧发送器是通过什么来管理这些数据帧的?

1、将数据帧注册(存储)到帧发送器里

在帧发送器器中,提供了数据帧预处理器preprocessData,该方法的主要目就是将数据帧注册到帧发送器中;

在帧发送器LoopyWriter中,存在两个流的缓存,

  • estdStreams,类型是Map[uint32]*outStream
    如果流的头帧,已经通过帧发送器发送出去了的话,就可以将此流存储到estdStreams里
  • activeStreams,类型是outStreamList, 就是一个链表
    • 在帧发送器中将数据帧发送前,需要将数据帧存储outStream里,然后将outStream存储到activeStreams里
    • 数据发送器processData,就是从activeStreams里来获取要发送的流的,然后从流里获取要发送的数据帧

进入grpc-go/internal/transport/controlbuf.go文件中的preprocessData方法里:

1func (l *loopyWriter) preprocessData(df *dataFrame) error {
2.	str, ok := l.estdStreams[df.streamID]
3if !ok {
4return nil
5}

6// If we got data for a stream it means that
7// stream was originated and the headers were sent out.
8.	str.itl.enqueue(df)
9if str.state == empty {
10.		str.state = active
11.		l.activeStreams.enqueue(str)
12}
13return nil
14}
  • 第2-5行:根据streamID,获取outStream
  • 第8行:将数据帧df,存储到outStream流中的itemList类型的itl变量里;ittemList这是链表。
  • 第9-12行:outStream的默认初始化状态是empty,也就是说,将outStream状态由empty更新为active,并且将outStream流注册到activeStreams变量里; activeStreams类型就是一个链表,outStreamList。

通过下面的图,简单说明数据帧在帧发送器里是如何存储的?

数据帧在帧缓存器中是如何存储的

也就是说,帧发送器从帧缓存里获取数据帧后,

  • 将数据帧存储到outStream结构体里itemList类型的str.itl变量里,即添加链表尾部
  • 将类型为outStream的变量str,添加到类型为outStreamList的l.activeStreams变量,同样是添加到链表的尾部。

简单一点,即,

将获取到的数据帧封装到OutStream结构体里,再将OutStream封装到类型为outStreamList的双向链表l.activeStreams变量;

(是不是说明,无论是头帧还是数据帧都不能直接对外,必须存储到流里,如OutStream,外界都是通过OutStream来访问数据帧)

2、帧发送器LoopyWriter是如何具体存储数据帧的?

上一个小节,我们已经知道帧发送器从帧缓存器controlBuffer里获取数据帧后,存储到的了帧发送器LoopyWriter的outStream里,然后将outStream存储到了一个链表里;

本小节,我们重点分析一下,outStream是如何存储到链表里呢;

2.1、链表outStreamList结构以及链表节点outStream结构

首先看一下outStream链表节点结构:
(grpc-go/internal/transport/controlbuf.go)

1type outStream struct {
2.	id uint32
3.	state outStreamState
4.	itl   *itemList
5.	bytesOutStanding int
6.	wq               *writeQuota
7.	next *outStream
8.	prev *outStream
9}

主要代码说明:

  • 第2行:id表示,streamID号;可见,一个流对应一个outStream链表节点;
  • 第3行:state,表示链表节点的状态,帧发送器在发送数据时,会校验outStream的状态,如active状态,可以继续对其进行发送;否则,只能将非active状态的outStream流,移除链表outStreamList,移除后,帧发送器就不能对其进行发送数据了,直到状态变为active
  • 第4行:itl,是用来存储帧的链表;itl, 存储数据帧,或者窗口更新帧,头帧;只能往链表的尾部添加帧数据,取帧时,只能从链表的头部获取
  • 第7行:next,表示此链表节点的下一个链表节点的索引地址;
  • 第8行:prev,表示此链表节点的上一个链表节点的索引地址;

可见,这是一个双向链表;
继续看,链表结构outStreamList,如下:(grpc-go/internal/transport/controlbuf.go)

type outStreamList struct {
	head *outStream
	tail *outStream
}

head,表示链表的头部节点
tail,表示链表的尾部节点

到目前为止,我们已经知道了,帧发送器中使用outStreamList双向链表来存储不同流的帧数据。

outStreamList双向链表里存储的是不同streamID号的outStream,也就是不同rpc请求的数据;

2.2、如何往outStreamList双向链表里添加outStream呢?

进入grpc-go/internal/transport/controlbuf.go文件中的enqueue方法里:

1func (l *outStreamList) enqueue(s *outStream) {
2.	e := l.tail.prev
3.	e.next = s

4.	s.prev = e
5.	s.next = l.tail

6.	l.tail.prev = s
7}

主要代码说明:

  • 第2行:通过l.tail.prev来获取尾部节点的前一个链表节点索引e
  • 第3行:将尾部节点的前一个链表节点e的下一个节点索引指向新增加的链表节点s
  • 第4行:将新增加节点s的前一个节点索引指向e
  • 第5行:将新增加节点s的下一个节点索引指向尾部节点
  • 第6行:更新尾部节点l.tail的前一个节点索引为s

可见,每次增加新的链表节点,都是往链表的尾部增加,注意不是替换尾部节点哦。
再通过,下图来形象的描述整个过程:
帧发送器里如何添加元素

2.3、如何从outStreamList双向链表里获取待发送的数据呢?

进入grpc-go/internal/transport/controlbuf.go文件中的dequeue方法里:

1func (l *outStreamList) dequeue() *outStream {
2.	b := l.head.next
3if b == l.tail {
4return nil
5}
6.	b.deleteSelf()
7return b
8}

主要流程说明:

  • 第2行:从头部获取节点,为b
  • 第3-7行:根据b是否尾部节点,来执行不同的分支
    • 第3-5行:若b为尾部节点的话:说明此链表为空链表,直接返回nil
    • 第6行:若b非尾部节点话的:需要调用deleteSelf方法来更新链表

也就是说,每次从链表outStreamList里获取数据的时候,从头部开始获取的。

这就是指定了一个规则,存储数据的时候存储到尾部,获取数据的时候,从头部获取;

这样的好处是,保证发送数据帧的先后顺序,就跟平常排队一样,先到先执行;

再看一下deleteSelf方法:

1func (s *outStream) deleteSelf() {
2if s.prev != nil {
3.		s.prev.next = s.next
4}
5if s.next != nil {
6.		s.next.prev = s.prev
7}
8.	s.next, s.prev = nil, nil
9}

主要代码说明:

  • 第2-4行:将s链表节点的前一个节点next指向s链表节点下一个节点
  • 第5-7行:将s链表节点的下一个节点prev指向s链表节点前一个节点
  • 第8行:将s链表节点的前后索引重置为nil

通过下图,来描述一下获取数据时的整体流程:
帧发送器中是如何删除数据的

获取数据时,是从头部元素获取数据的,也就是,获取的是s1节点(将s1节点从上面的链表里移除);

初始状态是,当前链表里有两个节点,s1, s2;或者说,两个数据帧;

代码中s.prev里的s指的就是s1节点; (是从dequeue方法里的第2,6行看出来的)

s.prev存储的是前一个节点的索引,也就是头节点head索引;

因此,s.prev.next就是指头节点head中的next值,而next值存储的是s1节点索引。

接下来,就可以根据上面的图分析出如何具体的删除了第一个元素了。

3、总结

本篇文章我们主要分析了一下,帧发送器looyWriter是如何来存储获取到的数据帧的; 这里包括如何存储数据帧,如何获取数据帧;

我们可以再多思考一下,本小节要解决的场景,本质上是当客户端请求的数量超过服务提供者的处理能力时,服务提供者如何来处理这一场景? 当然,不同的场景,有不同的解决方案。

到目前为止,我们已经知道帧发送器looyWriter是如何来管理获取到的数据帧了;那么,在下面的章节中,我们要分析,帧发送器是如何将数据帧发送出去的?

下一篇文章
  客户端数据发送器processData,如何将数据帧发送到链路上的?

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

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码二哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值