nsqd解析:nsqd-protocolV2

protocol_v2 协议

  一个客户端只能订阅一个 channel,客户端可以向 nsqd 发送指令完成指定操作并返回结果,nsqd 可以向客户端推送消息.

IOLoop 操作

  从上一节我们了解到,nsqd 每和一个客户端建立 TCP 连接都会创建一个 protocolV2 struct,里面仅仅包含所处语境即 nsqd,然后执行 IOLoop 操作进行 nsqd 和客户端的通信.所以只需要从 IOLoop 入手便可以一览 nsqd 与客户端之间的消息处理:

// 建立连接后的进行 IOLoop 操作,接收消息,每一个客户端 TCP 连接对应一个 protocolV2
func (p *protocolV2) IOLoop(conn net.Conn) error {
	var err error
	var line []byte
	var zeroTime time.Time
	// 获取一个 ClientID
	clientID := atomic.AddInt64(&p.ctx.nsqd.clientIDSequence, 1)
	// 创建一个 client 用来代理这个客户端
	client := newClientV2(clientID, conn, p.ctx)
	// 将这个客户端添加到 nsqd 中进行管理
	p.ctx.nsqd.AddClient(client.ID, client)

	// 首先创建一个 messagePumpStartedChan channel,并把它传入到 messagePump 中
	// 下面阻塞在从 messagePumpStartedChan 中获取消息,但其实这个 channel 在 messagePump 中关闭了
	// 这是为了保证让 IOLoop 中的 for 循环和 messagePump 中的 for 循环同步运行.
	// IOLoop 中的 for 循环时接收客户端的请求并进行解析,处理请求,最后给客户端响应.
	// messagePump 中的 for 循环会从内存和 disk 中获取准备好的 message 返回给客户.
	// 如果不添加 messagePumpStartedChan 一般会导致 IOLoop 中的 for 循环先执行,但是 messagePump 还没有准备好
	messagePumpStartedChan := make(chan bool)
	go p.messagePump(client, messagePumpStartedChan)
	<-messagePumpStartedChan

	for {
		// 根据心跳间隔设置 ReadDeadline 时间
		if client.HeartbeatInterval > 0 {
			client.SetReadDeadline(time.Now().Add(client.HeartbeatInterval * 2))
		} else {
			client.SetReadDeadline(zeroTime)
		}

		// 读取一个指令
		line, err = client.Reader.ReadSlice('\n')
		if err != nil {
			if err == io.EOF {
				err = nil
			} else {
				err = fmt.Errorf("failed to read command - %s", err)
			}
			break
		}

		// 删除指令末尾的 '\n'
		line = line[:len(line)-1]
		// 可能还需要修剪一下 '\r'
		if len(line) > 0 && line[len(line)-1] == '\r' {
			line = line[:len(line)-1]
		}
		// 按照空格进行划分
		params := bytes.Split(line, separatorBytes)

		p.ctx.nsqd.logf(LOG_DEBUG, "PROTOCOL(V2): [%s] %s", client, params)

		var response []byte
		// 执行客户端对应的操作,✨✨✨
		response, err = p.Exec(client, params)
		if err != nil {
			ctx := ""
			if parentErr := err.(protocol.ChildErr).Parent(); parentErr != nil {
				ctx = " - " + parentErr.Error()
			}
			p.ctx.nsqd.logf(LOG_ERROR, "[%s] - %s%s", client, err, ctx)
			// 将错误发送给客户端
			sendErr := p.Send(client, frameTypeError, []byte(err.Error()))
			if sendErr != nil {
				p.ctx.nsqd.logf(LOG_ERROR, "[%s] - %s%s", client, sendErr, ctx)
				break
			}

			// FatalClientErr 类型的错误应关闭连接
			if _, ok := err.(*protocol.FatalClientErr); ok {
				break
			}
			continue
		}
		// 将执行后的结果返回给客户端
		if response != nil {
			err = p.Send(client, frameTypeResponse, response)
			if err != nil {
				err = fmt.Errorf("failed to send response - %s", err)
				break
			}
		}
	}
	// 退出循环
	p.ctx.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting ioloop", client)
	conn.Close()
	// 关闭 client 的所有监听 goroutine
	close(client.ExitChan)
	if client.Channel != nil {
		// 如果客户端监听的 channel 不为空,从 channel 移除这个客户端
		client.Channel.RemoveClient(client.ID)
	}
	// 从 nsqd 移除这个客户端
	p.ctx.nsqd.RemoveClient(client.ID)
	return err
}

这个函数是用来从客户端接收指令并进行处理,主要工作有:

  1. 创建一个 client 用来管理客户端
  2. 启动 goroutine 执行 messagePump,并阻塞等待 messagePump 中的 for 同步运行
  3. 在一个 for 循环中不断处理客户端传过来的指令,并将处理结果进行返回

messagePump 操作

  和 IOLoop 同步执行的还有 messagePump,是用来将内存和 disk 中获取准备好的 message 返回给客户.

func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
	var err error
	// 从内存获取消息的通道
	var memoryMsgChan chan *Message
	// 从磁盘获取消息的通道
	var backendMsgChan chan []byte
	// 订阅的频道
	var subChannel *Channel
	// 解决消息延迟的问题,强制刷新
	var flusherChan <-chan time.Time
	var sampleRate int32

	subEventChan := client.SubEventChan
	identifyEventChan := client.IdentifyEventChan
	outputBufferTicker := time.NewTicker(client.OutputBufferTimeout)
	heartbeatTicker := time.NewTicker(client.HeartbeatInterval)
	heartbeatChan := heartbeatTicker.C
	msgTimeout := client.MsgTimeout

	// 用来强制刷新消息给客户端,两种情况不会进行强制刷新:
	// 1. 客户端没有准备好接收消息
	// 2. buffer 中没有消息,怎么刷都没用
	flushed := true

	// 通知 IOLoop 开始执行
	close(startedChan)

  到这里 messagePump 的准备工作就已经做完了,最后 close startedChan 同时启动 IOLoop 和 messagePump 中的 for 循环

	for {
		if subChannel == nil || !client.IsReadyForMessages() {
			// 客户端暂时还不能接收消息
			memoryMsgChan = nil
			backendMsgChan = nil
			// 不允许强制刷消息给客户端
			flusherChan = nil
			// 强制刷新客户端
			client.writeLock.Lock()
			err = client.Flush()
			client.writeLock.Unlock()
			if err != nil {
				goto exit
			}
		
			flushed = true
		} else if flushed {
			// channel 中不是很堵,暂时关闭强制刷新,仅接收 channel 内存通道和磁盘通道即可
			memoryMsgChan = subChannel.memoryMsgChan
			backendMsgChan = subChannel.backend.ReadChan()
			flusherChan = nil
		} else {
			// channel 中有大消息,打开强制刷新定时器
			memoryMsgChan = subChannel.memoryMsgChan
			backendMsgChan = subChannel.backend.ReadChan()
			flusherChan = outputBufferTicker.C
		}

  在这个 for 循环中,每次开始执行 select 之前需要先对客户端的情况进行判断,有如下三种情况:

  1. 客户端没有订阅 channel 或者客户端还没有准备好接收消息,在 select 中等待 subEventChan 和 ReadyStateChan 的消息,用来加载 客户端订阅的 channel 消息通道和知晓客户端已经准备好接受消息.
  2. 客户端可以接受消息,但是 channel 中已经没有消息了,不需要开启强制刷新,仅从 channel 的两个通道中正常获取消息即可
  3. 客户端可以接收消息,并且 channel 中有很多消息等待投递,可以打开 flusherChan 进行手动刷新

  这里处理初始化时属于第一种情况,订阅 channel 后就一直在 2,3 之间进行切换.是怎么判断 channel 的通道中是否有消息需要进行强制刷新呢?如果从内存通道中获取消息就表示目前 channel 情况还正常,一旦开始从 backend 中获取消息就表示 channel 中已经积压了大量的消息,需要打开强制刷新.默认刷新频率为:250ms,通过强制刷新把 client 写缓冲区中的消息推送给客户端,让 client 可以更快的从 channel 中获取消息

		select {
		case <-flusherChan:
			// 强制刷新 client 的写缓冲区
			client.writeLock.Lock()
			err = client.Flush()
			client.writeLock.Unlock()
			if err != nil {
				goto exit
			}
			flushed = true
		case <-client.ReadyStateChan:
		case subChannel = <-subEventChan:
			// 仅订阅一次 channel 之后即使调用 SUB 也不会切换频道
			subEventChan = nil
		case identifyData := <-identifyEventChan:
			// 验证确认客户端,仅触发一次
			// 就是设置各种触发间隔
			identifyEventChan = nil

			outputBufferTicker.Stop()
			if identifyData.OutputBufferTimeout > 0 {
				outputBufferTicker = time.NewTicker(identifyData.OutputBufferTimeout)
			}

			heartbeatTicker.Stop()
			heartbeatChan = nil
			if identifyData.HeartbeatInterval > 0 {
				heartbeatTicker = time.NewTicker(identifyData.HeartbeatInterval)
				heartbeatChan = heartbeatTicker.C
			}

			if identifyData.SampleRate > 0 {
				sampleRate = identifyData.SampleRate
			}

			msgTimeout = identifyData.MsgTimeout
		case <-heartbeatChan:
			// 发送心跳消息给客户端
			err = p.Send(client, frameTypeResponse, heartbeatBytes)
			if err != nil {
				goto exit
			}
		case b := <-backendMsgChan:  // 从 channel 磁盘消息缓冲区中接收消息
			if sampleRate > 0 && rand.Int31n(100) > sampleRate {
				continue
			}
			// 先 decode 消息
			msg, err := decodeMessage(b)
			if err != nil {
				p.ctx.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err)
				continue
			}
			msg.Attempts++
			// 放进到待确认列表并发送给客户端
			subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout)
			client.SendingMessage()
			err = p.SendMessage(client, msg)
			if err != nil {
				goto exit
			}
			flushed = false
		case msg := <-memoryMsgChan:  // 从 channel 内存消息缓冲区中接收消息
			if sampleRate > 0 && rand.Int31n(100) > sampleRate {
				continue
			}
			msg.Attempts++
			// 添加到待确认列表并发送给客户端
			subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout)
			client.SendingMessage()
			err = p.SendMessage(client, msg)
			if err != nil {
				goto exit
			}
			flushed = false
		case <-client.ExitChan:
			// 退出循环
			goto exit
		}
	}

整个阻塞在 select 有 8 种情况:

  1. 强制刷新消息给客户端
  2. 客户端准备就绪等待接收消息
  3. 客户端订阅频道,加载 channel 消息接收列表(仅能订阅一次 channel)
  4. 验证链接时执行的操作,设置消息循环的各种定时器间隔(只可以确认一次)
  5. 给客户端发送心跳消息
  6. 从磁盘队列接收消息
  7. 从内存队列接收消息
  8. 退出 messagePump
exit:
	p.ctx.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting messagePump", client)
	heartbeatTicker.Stop()
	outputBufferTicker.Stop()
	if err != nil {
		p.ctx.nsqd.logf(LOG_ERROR, "PROTOCOL(V2): [%s] messagePump error - %s", client, err)
	}
}

  收尾工作就是停止几个定时器并退出即可.在 messagePump 中获取了消息之后会调用 SendMessage 来吧消息发送给客户端:

func (p *protocolV2) SendMessage(client *clientV2, msg *Message) error {
	var buf = &bytes.Buffer{}
	// 消息放到 buf 中
	_, err := msg.WriteTo(buf)
	if err != nil {
		return err
	}
	// 调用 Send 发送
	err = p.Send(client, frameTypeMessage, buf.Bytes())
	if err != nil {
		return err
	}

	return nil
}
// 向 client 发送消息
func (p *protocolV2) Send(client *clientV2, frameType int32, data []byte) error {
	client.writeLock.Lock()

	var zeroTime time.Time
	// 写超时设置
	if client.HeartbeatInterval > 0 {
		client.SetWriteDeadline(time.Now().Add(client.HeartbeatInterval))
	} else {
		client.SetWriteDeadline(zeroTime)
	}

	_, err := protocol.SendFramedResponse(client.Writer, frameType, data)
	if err != nil {
		client.writeLock.Unlock()
		return err
	}
	// frameTypeMessage : 发送给客户端的消息立即刷新
	// frameTypeResponse : 客户端指令返回不会立即刷新
	if frameType != frameTypeMessage {
		err = client.Flush()
	}

	client.writeLock.Unlock()

	return err
}

  nsqd 对每个客户端使用两个 gououtine 分别执行 IOLoop 和 messagePump 就完成了二者之间的指令和消息传递.代码很容易理清楚.

protocol_v2 中客户端的指令

Exec

  在 IOLoop 中,没收到客户端传来的一条指令,都会解析完成后,通过 Exec 进行执行.判断指令的类型然后调用对应的函数执行即可.

// 执行客户端传递过来的指令
func (p *protocolV2) Exec(client *clientV2, params [][]byte) ([]byte, error) {
	// 认证一个客户端
	if bytes.Equal(params[0], []byte("IDENTIFY")) {
		return p.IDENTIFY(client, params)
	}
	err := enforceTLSPolicy(client, p, params[0])
	if err != nil {
		return nil, err
	}
	switch {
	// 确认收到一条消息
	case bytes.Equal(params[0], []byte("FIN")):
		return p.FIN(client, params)
	// 指定可同时处理的消息数量
	case bytes.Equal(params[0], []byte("RDY")):
		return p.RDY(client, params)
	// 消息重新加入 channel 发送
	case bytes.Equal(params[0], []byte("REQ")):
		return p.REQ(client, params)
	// 向 topic 推送一条消息
	case bytes.Equal(params[0], []byte("PUB")):
		return p.PUB(client, params)
	// 向 topic 推送多条消息
	case bytes.Equal(params[0], []byte("MPUB")):
		return p.MPUB(client, params)
	// 向 topic 推送一条延迟消息
	case bytes.Equal(params[0], []byte("DPUB")):
		return p.DPUB(client, params)
	// 不作处理
	case bytes.Equal(params[0], []byte("NOP")):
		return p.NOP(client, params)
	// 重置一个待确认消息的超时时间
	case bytes.Equal(params[0], []byte("TOUCH")):
		return p.TOUCH(client, params)
	// 订阅一个 channel
	case bytes.Equal(params[0], []byte("SUB")):
		return p.SUB(client, params)
	// 关闭客户端
	case bytes.Equal(params[0], []byte("CLS")):
		return p.CLS(client, params)
	// 授权一个客户端
	case bytes.Equal(params[0], []byte("AUTH")):
		return p.AUTH(client, params)
	}
	return nil, protocol.NewFatalClientErr(nil, "E_INVALID", fmt.Sprintf("invalid command %s", params[0]))
}

  这里的各个指令,基本都是先确认参数,然后调用对应的方法即可,不过多介绍,简单了解一下对应的含义及可:

指令例子操作执行的操作
FINFIN MessageID确认收到一条消息topic.FinishMessage
RDYRDY count指定可同时处理的消息数量client.SetReadyCount
REQREQ messageID timeoutMs消息重新加入 channel 发送client.Channel.RequeueMessage
PUBPUB topicName message向 topic 推送一条消息topic.PutMessage
MPUBMPUB topicName messages向 topic 推送多条消息topic.PutMessages
DPUBMPUB topicName timeoutMs message向 topic 推送一条延迟消息topic.PutMessage
NOPNOP无效指令不做处理
TOUCHTOUCH messageID重置一个待确认消息的超时时间client.Channel.TouchMessage
SUBSUB topicName channelName订阅一个 channelclient.SubEventChan <- channel 触发消息循环
CLSCLS关闭客户端client.StartClose
AUTHAUTH body授权一个客户端client.Auth
IDENTIFYIDENTIFY body认证一个客户端client.Identify

这里面比较特别的是 IDENTIFY 和 AUTH 两个操作.
  client 和 nsqd 建立连接后,client 立即通过命令 IDENTIFY 将认证信息发给 nsqd,如果 nsqd 在启动的时候指定了授权地址,nsqd 就会告诉 client 你需要认证,client 就会通过命令 AUTH 将秘钥发给 nsqd,nsqd 去授权地址进行验证,验证通过后,就可以进行正常的消息发布和订阅了

小结

  整个协议的流程基本看完了,其实说起来就是通过 IOLoop 和 messagePump 进行 nsqd 和 client 的通信:

  • IOLoop 负责让 nsqd 执行 client 传过来的指令
  • messagePump 给客户端推送消息

最后附上 github 的 nsqd 的源码解析地址:https://github.com/nercoeus/nsq_source_note

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值