在NSQD的TCPServer,当客户端与服务连接成功之后,会有一个goroutine专门处理这个连接的请求,在上篇中的IOLoop中,可看出服务以\n 为分隔符作为一条命令的结束(协议)读取客户请求,然后在将命令分解成 “命令 参数 参数1 惨2...”,最后交给Exec 函数执行,并返回响应。
详细流程参考 https://blog.csdn.net/H_L_S/article/details/104709619 中的逻辑流程图。
主要代码文件:
1.nsqd/protocol_v2.go
Exce 函数
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]) //TLS 加密
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")): //Ready 确认
return p.RDY(client, params)
case bytes.Equal(params[0], []byte("REQ")): //重新消费
return p.REQ(client, params)
case bytes.Equal(params[0], []byte("PUB")): //单消息发布
return p.PUB(client, params)
case bytes.Equal(params[0], []byte("MPUB")): //多消息发布
return p.MPUB(client, params)
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")): //更新消息的超时时间(msgTimeout )
return p.TOUCH(client, params)
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]))
}
IDENTITY 函数(身份信息交换)
func (p *protocolV2) IDENTIFY(client *clientV2, params [][]byte) ([]byte, error) {
var err error
//读取消息长度
bodyLen, err := readLen(client.Reader, client.lenSlice)
...
//读取消息实体
body := make([]byte, bodyLen)
_, err = io.ReadFull(client.Reader, body)
/*
client_v2 中
type identifyDataV2 struct {
ClientID string `json:"client_id"`
Hostname string `json:"hostname"`
HeartbeatInterval int `json:"heartbeat_interval"`
OutputBufferSize int `json:"output_buffer_size"`
OutputBufferTimeout int `json:"output_buffer_timeout"`
FeatureNegotiation bool `json:"feature_negotiation"`
TLSv1 bool `json:"tls_v1"`
Deflate bool `json:"deflate"`
DeflateLevel int `json:"deflate_level"`
Snappy bool `json:"snappy"`
SampleRate int32 `json:"sample_rate"`
UserAgent string `json:"user_agent"`
MsgTimeout int `json:"msg_timeout"`
}
*/
var identifyData identifyDataV2
err = json.Unmarshal(body, &identifyData)
...
/*
Identify的目的是,对一些参数得出折中方案
比如MsgTimeout,客户端希望的消息超时时间和NSQD配置的消息超时时间进行比较,得出一个折中方案。
主要有:
HeartbeatInterval
OutputBufferSize
SampleRate
MsgTimeout
*/
err = client.Identify(identifyData)
if err != nil {
...
}
/*
Deflate:是否压缩
DeflateLevel:压缩等级
TLSv1:加密
Snappy: 不知道干啥???????????
*/
resp, err := json.Marshal(struct {
MaxRdyCount int64 `json:"max_rdy_count"`
Version string `json:"version"`
MaxMsgTimeout int64 `json:"max_msg_timeout"`
MsgTimeout int64 `json:"msg_timeout"`
TLSv1 bool `json:"tls_v1"`
Deflate bool `json:"deflate"`
DeflateLevel int `json:"deflate_level"`
MaxDeflateLevel int `json:"max_deflate_level"`
Snappy bool `json:"snappy"`
SampleRate int32 `json:"sample_rate"`
AuthRequired bool `json:"auth_required"`
OutputBufferSize int `json:"output_buffer_size"`
OutputBufferTimeout int64 `json:"output_buffer_timeout"`
}{
MaxRdyCount: p.ctx.nsqd.getOpts().MaxRdyCount,
Version: version.Binary,
MaxMsgTimeout: int64(p.ctx.nsqd.getOpts().MaxMsgTimeout / time.Millisecond),
MsgTimeout: int64(client.MsgTimeout / time.Millisecond),
TLSv1: tlsv1,
Deflate: deflate,
DeflateLevel: deflateLevel,
MaxDeflateLevel: p.ctx.nsqd.getOpts().MaxDeflateLevel,
Snappy: snappy,
SampleRate: client.SampleRate,
AuthRequired: p.ctx.nsqd.IsAuthEnabled(),
OutputBufferSize: client.OutputBufferSize,
OutputBufferTimeout: int64(client.OutputBufferTimeout / time.Millisecond),
})
...
//发送最终方案给客户端。
err = p.Send(client, frameTypeResponse, resp)
...
if tlsv1 {
....
}
if snappy {
....
}
if deflate {
...
}
return nil, nil
}
NOP 函数(啥也不干)
func (p *protocolV2) NOP(client *clientV2, params [][]byte) ([]byte, error) {
return nil, nil
}
SUB 函数(订阅消息)
func (p *protocolV2) SUB(client *clientV2, params [][]byte) ([]byte, error) {
...
//参数格式:SUB TopicName ChannelName
if len(params) < 3 {
return nil, protocol.NewFatalClientErr(nil, "E_INVALID", "SUB insufficient number of parameters")
}
topicName := string(params[1])
//验证TopicName 的有效性
if !protocol.IsValidTopicName(topicName) {
...
}
channelName := string(params[2])
//验证ChannelName 的有效性
if !protocol.IsValidChannelName(channelName) {
...
}
...
var channel *Channel
for {
topic := p.ctx.nsqd.GetTopic(topicName) //获取topic实例,如果不存在就创建一个
channel = topic.GetChannel(channelName) //获取channel实例
...
break
}
//更新client实例的订阅状态
atomic.StoreInt32(&client.State, stateSubscribed)
client.Channel = channel
//通知messagePump中select 中的 subEventChan,有客户端订阅,准备消费
client.SubEventChan <- channel
return okBytes, nil
}
PUB 函数(发布消息)
func (p *protocolV2) PUB(client *clientV2, params [][]byte) ([]byte, error) {
var err error
//参数格式: PUB TopicName
if len(params) < 2 {
...
}
topicName := string(params[1])
//验证topicName 的有效性
if !protocol.IsValidTopicName(topicName) {
...
}
//读取消息长度
bodyLen, err := readLen(client.Reader, client.lenSlice)
if err != nil {
...
}
....
//读取消息实体
messageBody := make([]byte, bodyLen)
_, err = io.ReadFull(client.Reader, messageBody)
...
//获取topic实例
topic := p.ctx.nsqd.GetTopic(topicName)
/*
message.go 中定义了消息的结构体。
type Message struct {
ID MessageID
Body []byte
Timestamp int64
Attempts uint16
NSQDAddress string
Delegate MessageDelegate
autoResponseDisabled int32
responded int32
}
*/
msg := NewMessage(topic.GenerateID(), messageBody)
//通过topic将msg发给所有订阅了这个topic的channel
err = topic.PutMessage(msg)
...
//统计发布消息的数量
client.PublishedMessage(topicName, 1)
return okBytes, nil
}
MPUB 函数(批量消息发布) 与PUB基本流程相同,区别在于MPUB通过readMPUB读取多条消息,并且通过topic.PutMessages(messages) 发布多条消息
func readMPUB(r io.Reader, tmp []byte, topic *Topic, maxMessageSize int64, maxBodySize int64) ([]*Message, error) {
//获取消息的总条数
numMessages, err := readLen(r, tmp)
...
// 这个不知道啥意思??
maxMessages := (maxBodySize - 4) / 5
....
messages := make([]*Message, 0, numMessages)
//读取每条消息
for i := int32(0); i < numMessages; i++ {
//读取消息长度
messageSize, err := readLen(r, tmp)
...
//读取消息实体
msgBody := make([]byte, messageSize)
_, err = io.ReadFull(r, msgBody)
...
messages = append(messages, NewMessage(topic.GenerateID(), msgBody))
}
return messages, nil
}
DPUB 函数(发布延时消息),这种消息并不会马上发送给channel,而是先放入一个延时队列中,然后通过nsqd中queueScanLoop这个goroutine 扫描这个延时队列,将满足条件的msg抛入channel。
func (p *protocolV2) DPUB(client *clientV2, params [][]byte) ([]byte, error) {
var err error
//参数格式:DPUB TopicName Timeout(延时时间)
if len(params) < 3 {
...
}
//验证TopicName 的有效性
topicName := string(params[1])
if !protocol.IsValidTopicName(topicName) {
...
}
/* 将 ASCII 转换为10进制
在ASCII中 0-9 的ASCII码是 48-57。
在ByteToBase10中 d-'0' , 用ASCII表示就是 (48-57中任意数) - 48 = (0-9)
我感觉这个挺好玩的。
*/
timeoutMs, err := protocol.ByteToBase10(params[2])
timeoutDuration := time.Duration(timeoutMs) * time.Millisecond
...
//读取消息实体
bodyLen, err := readLen(client.Reader, client.lenSlice)
...
messageBody := make([]byte, bodyLen)
_, err = io.ReadFull(client.Reader, messageBody)
topic := p.ctx.nsqd.GetTopic(topicName)
msg := NewMessage(topic.GenerateID(), messageBody)
//标记msg中的延时标记 deffered, topic在发布消息给channel时,
//如果遇到这个标记,会将消息发给延时队列,而不是channel
msg.deferred = timeoutDuration
err = topic.PutMessage(msg)
...
client.PublishedMessage(topicName, 1)
return okBytes, nil
}
FIN 函数(消费确认)
func (p *protocolV2) FIN(client *clientV2, params [][]byte) ([]byte, error) {
//只有订阅的客户端,才能进行消息确认命令
state := atomic.LoadInt32(&client.State)
if state != stateSubscribed && state != stateClosing {
...
}
//参数格式:FIN 消息ID(具有唯一性)
if len(params) < 2 {
return nil, protocol.NewFatalClientErr(nil, "E_INVALID", "FIN insufficient number of params")
}
id, err := getMessageID(params[1])
/*
channel每次消费一个消息,都会将消息放入
inFlightMessages 和 inFlightPQ,当长时间没有消费确认的时候,会将inFlinghPQ中的消息
重新放入消费队列。
FIN命令的作用就是将 inFlightMessages 和 inFlightPQ 中的消息移除。
*/
err = client.Channel.FinishMessage(client.ID, *id)
//
//消费确认统计
client.FinishedMessage()
return nil, nil
}
总结:
今天主要讲了几个主要的Exec的几个常用命令函数 IDENTITY、SUB、PUB、MPUB、DPUB、FIN、NOP等。这些命令主要是与topic 和 channel 交互,进行消息发布与消费的一些操作。至于其他未提到的,大家有兴趣可以看看。
下次分享:topic 的具体实现