已发表的技术专栏
0 grpc-go、protobuf、multus-cni 技术专栏 总入口
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方法里:
1.func (l *loopyWriter) preprocessData(df *dataFrame) error {
2. str, ok := l.estdStreams[df.streamID]
3. if !ok {
4. return 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)
9. if str.state == empty {
10. str.state = active
11. l.activeStreams.enqueue(str)
12. }
13. return 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)
1.type 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方法里:
1.func (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方法里:
1.func (l *outStreamList) dequeue() *outStream {
2. b := l.head.next
3. if b == l.tail {
4. return nil
5. }
6. b.deleteSelf()
7. return b
8.}
主要流程说明:
- 第2行:从头部获取节点,为b
- 第3-7行:根据b是否尾部节点,来执行不同的分支
- 第3-5行:若b为尾部节点的话:说明此链表为空链表,直接返回nil
- 第6行:若b非尾部节点话的:需要调用deleteSelf方法来更新链表
也就是说,每次从链表outStreamList里获取数据的时候,从头部开始获取的。
这就是指定了一个规则,存储数据的时候存储到尾部,获取数据的时候,从头部获取; |
这样的好处是,保证发送数据帧的先后顺序,就跟平常排队一样,先到先执行;
再看一下deleteSelf方法:
1.func (s *outStream) deleteSelf() {
2. if s.prev != nil {
3. s.prev.next = s.next
4. }
5. if 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,如何将数据帧发送到链路上的?
点击下面的图片,返回到专栏大纲 |