在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 的具體實現