stream作为网络传输的通道之一,通过建立HTTP长连接,主要传输数据量小、发送频率较高的数据,例如追加日志、心跳数据。
工作原理
结合上一节的pipeline通道实现的工作原理,我们来看下stream的工作原理:
我们知道stream是HTTP长连接,所以stream需要专门负责写入数据的streamWriter和专门读取数据的streamReader。
streamReader工作原理:
在rafthttp.peer.startPeer()中通过初始化并调用streamReader.start()启动streamReader实例,其中还启动了一个后台goroutine来执行streamReader.run()方法。在streamReader.run()方法中,主要完成下面两件事情:
- 通过调用streamReader.dial()主动与对端建立连接
- 接收对端发送的数据,写入streamReader.recvc或streamReader.propc,这两个通道其实对应的是上层peer.recvc和peer.propc通道,最后消息在rafthttp.peer.startPeer()
中被接收并提交到etcd-raft模块被处理。
streamWriter工作原理:
在rafthttp.peer.startPeer()中通过调用startStreamWriter()初始化并启动streamWriter实例,其中还启动了一个后台goroutine来执行streamWriter.run()方法。在streamWriter.run()方法中,主要完成下面三件事情:
- 在peer章节中提到过当其他节点主动与当前节点创建连接(streamReader.dial())时,该连接实例会写入对应streamWriter.connc通道,在streamWriter.run方法中通过该通道获取该连接实例并进行绑定,之后才能开始后续的消息发送和心跳检测。
- 获取连接之后,定时将心跳消息写入缓冲区,通过HTTP处理器将缓冲区数据刷到对端,用于定期检测网络状况
- 获取连接之后,从streamWriter.msgc通道接收到要发送的消息,将消息写入缓冲区,通过HTTP处理器将缓冲区数据刷到对端。
streamWriter
结构体
type streamWriter struct {
localID types.ID//本地节点ID
peerID types.ID//对端节点ID
status *peerStatus//网络连接状态
r Raft //底层的Raft状态
mu sync.Mutex // guard field working and closer
closer io.Closer//负责关闭底层的长连接
working bool //负责标识当前的streamWriter是否可用
msgc chan raftpb.Message//通过前面对Peer分析可知,Peer会将待发送的消息写入该通道,streamWriter则从该通道中读取消息并发送出去
connc chan *outgoingConn//通过该通道获取当前streamWriter实例关联的底层网络连接, outgoingConn其实是对网络连接的一层封装,其中记录了当前连接使用的协议版本,以及用于关闭连接的Flusher和Closer等信息。
stopc chan struct{
}
done chan struct{
}
}
streamWriter启动
func (cw *streamWriter) run() {
var (
msgc chan raftpb.Message //指向当前streamWriter.msgc字段
/**
定时器会定时向该通道发送信号,触发心跳消息的发送,该心跳消息与后面介绍的Raft的心跳消息有所不同
该心跳消息的主要目的是为了防止连接长时间不用断开的
**/
heartbeatc <-chan time.Time
t streamType//区分不同版本的stream消息通道
enc encoder//编码器,负责将消息序列化并写入连接的缓冲区
flusher http.Flusher//负责刷新底层连接,将数据真正发送出去
batched int //记录当前未Flush的消息个数
)
tickc := time.NewTicker(ConnReadTimeout / 3)//发送心跳消息的定时器
defer tickc.Stop()
unflushed := 0//未Flush的字节数
for {
select {
case <-heartbeatc:
//将心跳消息编码到缓冲区
err := enc.encode(&linkHeartbeatMessage)
unflushed += linkHeartbeatMessage.Size()//统计未发送的字节数
if err == nil {
//若没有异常,则使用flusher将缓冲区的消息全部发送出去,并重置batched和unflushed
flusher.Flush()
batched = 0
sentBytes.WithLabelValues(cw.peerID.String()).Add(float64(unflushed))
unflushed = 0
continue //不阻塞,直接continue
}
//到这里说明有错误,将网络连接状态置为不活跃
cw.status