文章目录
nsqd之TCP服务详解
代码文件地址./internal/protocol/tcp_server.go
nsqd通过protocol.TCPServer(n.tcpListener, n.tcpServer, n.logf)
方式启动TCP服务,启动完成后协程会调用下面的方法监听tcp客户端的连接并对每个连接开启一个专有的协程Handle方法来处理客户端的连接需求,TCPServer方法详解如下
func TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) error {
logf(lg.INFO, "TCP: listening on %s", listener.Addr())
var wg sync.WaitGroup
for {
clientConn, err := listener.Accept() // 监听tcp客户端连接
if err != nil {
// net.Error.Temporary()已弃用,但对accept有效
// 这是一个避免静态检查错误的破解方法
if te, ok := err.(interface{ Temporary() bool }); ok && te.Temporary() {
logf(lg.WARN, "temporary Accept() failure - %s", err)
runtime.Gosched() // 让出时间片,短暂休息后跳回tcp客户端监听状态
continue
}
// 没有直接的方法来检测这个错误,因为它没有被暴露,这里直接匹配是否没有包含字符串
if !strings.Contains(err.Error(), "use of closed network connection") {
return fmt.Errorf("listener.Accept() error - %s", err)
}
break
}
wg.Add(1)
go func() { // 协程方式开启客户端长连接处理业务任务
handler.Handle(clientConn) // ***重点:此方法下逻辑相对比较复杂,下面会提出来详解
wg.Done()
}()
}
// 等待返回,直到所有处理程序goroutines完成
wg.Wait()
logf(lg.INFO, "TCP: closing %s", listener.Addr())
return nil
}
详解(p *tcpServer) Handle方法
这部分代码是TCP客户端连接请求处理的核心代码,在这里会根据客户端发送的流数据判断使用的协议版本,从而分发到不通的协议版本下完成不同业务任务的调度,核心逻辑在prot.IOLoop(client)
中完成
// Handle TCP客户端连接请求处理
func (p *tcpServer) Handle(conn net.Conn) {
p.nsqd.logf(LOG_INFO, "TCP: new client(%s)", conn.RemoteAddr())
// 客户端通过发送一个4字节的序列来初始化自己,表明它通信使用的协议版本
// 这将使我们能够优雅地升级协议,从面向text/line的协议升级到任何协议。
buf := make([]byte, 4)
_, err := io.ReadFull(conn, buf)
if err != nil {
p.nsqd.logf(LOG_ERROR, "failed to read protocol version - %s", err)
conn.Close()
return
}
protocolMagic := string(buf)
p.nsqd.logf(LOG_INFO, "CLIENT(%s): desired protocol magic '%s'",
conn.RemoteAddr(), protocolMagic) // 打印连接的客户端IP及端口信息和所请求使用的协议版本
var prot protocol.Protocol
switch protocolMagic {
case " V2": // V2协议将初始化protocolV2对象
prot = &protocolV2{nsqd: p.nsqd}
default: // 其他协议未支持,所以返回E_BAD_PROTOCOL信息并主动关闭客户端连接对象
protocol.SendFramedResponse(conn, frameTypeError, []byte("E_BAD_PROTOCOL"))
conn.Close()
p.nsqd.logf(LOG_ERROR, "client(%s) bad protocol magic '%s'",
conn.RemoteAddr(), protocolMagic)
return
}
client := prot.NewClient(conn) // 根据连接对象初始化TCP协议下的处理客户端对象
p.conns.Store(conn.RemoteAddr(), client) // 存储更新到连接对象Map中
err = prot.IOLoop(client) // ***重点:调用此客户端连接处理的核心逻辑,此方法下逻辑相对比较复杂,下面会提出来详解
if err != nil {
p.nsqd.logf(LOG_ERROR, "client(%s) - %s", conn.RemoteAddr(), err)
}
p.conns.Delete(conn.RemoteAddr()) // 客户端连接对象退出则从连接对象Map中清理掉
client.Close() // 关闭客户端连接对象
}
详解(p *protocolV2) IOLoop方法
这是nsqd服务中TCP协议下处理任务的核心逻辑模块,在这里是阻塞上面(p *tcpServer) Handle
方法的,此模块包含客户端的生产者和消费者的需求处理,路由处理核心是p.Exec(client, params)
方法,消息队列处理核心是p.messagePump(client, messagePumpStartedChan)
方法
// IOLoop nsqd TCP处理的核心逻辑
// 需要注意的是每个客户端连接都是单线程调用不能并发调用(客户端请求后必须等到服务端响应后才能做后续请求)
func (p *protocolV2) IOLoop(c protocol.Client) error {
var err error
var line []byte
var zeroTime time.Time
client := c.(*clientV2)
// 同步启动messagePump,以保证它有机会初始化源自客户端属性的goroutine本地状态,
// 并避免与IDENTIFY的潜在竞赛(客户端可能已经改变或禁用上述属性)。
messagePumpStartedChan := make(chan bool)
go p.messagePump(client, messagePumpStartedChan) // ***重点:协程方式启动tcp消息泵方法
<-messagePumpStartedChan // 等待消息泵的启动完成通知
for {
if client.HeartbeatInterval > 0 { // 心跳是客户端可配置的,但默认为30秒
client.SetReadDeadline(time.Now().Add(client.HeartbeatInterval * 2)) // 设置客户端连接读取的超时时间为60秒
} else {
client.SetReadDeadline(zeroTime) // 设置客户端连接读取无超时限制
}
// 每次请求时,仅读取到\n截至,ReadSlice都不会为数据分配新的空间,也就是说,返回的切片line只在下次调用之前有效。
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
}
// trim the '\n' | 删除末尾的\n
line = line[:len(line)-1]
// optionally trim the '\r' | 删除末尾的\r
if len(line) > 0 && line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
params := bytes.Split(line, separatorBytes) // 按separatorBytes切割出每个参数
p.nsqd.logf(LOG_DEBUG, "PROTOCOL(V2): [%s] %s", client, params) // 输出的此客户端连接交互提交的参数内容
var response []byte
response, err = p.Exec(client, params) // ***重点:分发到路由上处理客户端请求内容
if err != nil { // 路由中执行出现异常
ctx := ""
// 首先将err强制类型转换为protocol.ChildErr类型。
// 然后,从这个child error中获取到它的parentErr父错误,判断是否存在。
// 如果存在父错误,则在错误信息前面添加一个"-"符号,将其与父错误的错误信息连接起来,形成一个完整的错误信息ctx。
if parentErr := err.(protocol.ChildErr).Parent(); parentErr != nil {
ctx = " - " + parentErr.Error()
}
p.nsqd.logf(LOG_ERROR, "[%s] - %s%s", client, err, ctx)
sendErr := p.Send(client, frameTypeError, []byte(err.Error()))
if sendErr != nil { // 无法向客户端写入信息则强行关闭连接。
p.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.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting ioloop", client)
close(client.ExitChan) // 通知tcp消息泵messagePump优雅退出服务
if client.Channel != nil { // 如果订阅过频道则从此频道的客户端列表中删除指定客户端消费者对象信息
client.Channel.RemoveClient(client.ID)
}
return err
}
详解(p *protocolV2) messagePump方法
这里是消息队列处理的核心逻辑模块,主要用于给消费者分发从频道channel中拿到的消息数据(内存通道和磁盘队列)
注: 磁盘队列任务取出来后我这边增加了一个逻辑是为了修复nsqd重启丢失延迟信息所补充的,详见PR1454,修复之后构建的镜像(使用方式与nsqio/nsq镜像一致)是registry.cn-hangzhou.aliyuncs.com/houyao/nsq:v1.2.1
// messagePump 客户端连接对象订阅指定topic下的channel后,此方法会周期性的管理是否需要将消息发送到此客户端流中
func (p *protocolV2) messagePump(client *clientV2, startedChan chan bool) {
var err error
var memoryMsgChan chan *Message
var backendMsgChan <-chan []byte // 仅可读权限的通道
var subChannel *Channel
// flusherChan 周期刷新客户端写入流的可读通道(outputBufferTicker定时器提供)
var flusherChan <-chan time.Time // 仅可读权限的通道
var sampleRate int32
subEventChan := client.SubEventChan // 将子事件频道对应的通道对象赋值给subEventChan对象
identifyEventChan := client.IdentifyEventChan // 将鉴权事件通道对象赋值给identifyEventChan对象
outputBufferTicker := time.NewTicker(client.OutputBufferTimeout) // 根据输出刷新频率生成定时器(默认频率250毫秒)
heartbeatTicker := time.NewTicker(client.HeartbeatInterval) // 根据心跳检测频率生成定时器(默认频率客户端连接超时频率60秒/2)
heartbeatChan := heartbeatTicker.C // 赋值心跳定时器的可读通道给heartbeatChan对象
msgTimeout := client.MsgTimeout // 消息被客户端读取后等待的最长时间,过了此时间则自动重新入队,默认1分钟
// v2会周期性地将数据缓冲给客户端,以减少写系统的调用。
// 我们在两种情况下强制冲刷:
// 1.当客户端没有准备好接收信息时
// 2.我们被缓冲了,通道没有什么东西可以发给我们了(也就是说,无论如何我们都会在这个循环中阻塞)。
//
flushed := true
// 向启动messagePump的goroutine发出信号,表示我们已经启动了
close(startedChan)
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 {
// 刷新状态下,重新更新内存通道对象和磁盘队列对象
memoryMsgChan = subChannel.memoryMsgChan
backendMsgChan = subChannel.backend.ReadChan()
flusherChan = nil
} else {
// 客户端已经准备号且非刷新的情况下,重新更新内存通道对象和磁盘队列对象并设置定期刷新客户端流的通道对象
memoryMsgChan = subChannel.memoryMsgChan
backendMsgChan = subChannel.backend.ReadChan()
flusherChan = outputBufferTicker.C
}
select {
case <-flusherChan: // 收到刷新写入流通知 (注意:鉴权数据若未设置则定时器通道将永远关闭)
client.writeLock.Lock()
err = client.Flush()
client.writeLock.Unlock()
if err != nil { // 刷新数据异常走退出流程
goto exit
}
flushed = true
case <-client.ReadyStateChan: // 接收到频道channel的开启或关闭通知,重新跳转到上面的逻辑中检查通道是否暂停分发任务
case subChannel = <-subEventChan: // 获取订阅topic下的channel对象(客户端连接后第二步是订阅)
// 订阅成功后不再重新获取订阅的channel对象
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: // 磁盘队列中获取到数据
// 采样率存在且随机值在采样率范围内则仅处理随机的这部分数据,若为0则处理读取的所有数据
if sampleRate > 0 && rand.Int31n(100) > sampleRate {
continue
}
msg, err := decodeMessage(b) // 解析出消息对象
if err != nil {
p.nsqd.logf(LOG_ERROR, "failed to decode message - %s", err)
continue
}
// 这部分代码是为了修复nsqd重启丢失延迟信息所补充的,详见PR1454[https://github.com/nsqio/nsq/pull/1454]
if msg.deferred != 0 {
subChannel.StartDeferredTimeout(msg, msg.deferred)
continue
}
msg.Attempts++ // 消息对象发送到消费队列中前需要设置此消息超时时间并增加一次分发次数
subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout) // 设置消费截至时间
client.SendingMessage() // 更新消费队列中的计数和总消费量计数
err = p.SendMessage(client, msg) // 发送消息对象到client的写入流中
if err != nil {
goto exit
}
flushed = false
case msg := <-memoryMsgChan: // 内存通道中获取到数据
if sampleRate > 0 && rand.Int31n(100) > sampleRate {
continue
}
msg.Attempts++ // 消息对象发送到消费队列中前需要设置此消息超时时间并增加一次分发次数
subChannel.StartInFlightTimeout(msg, client.ID, msgTimeout) // 设置消费截至时间
client.SendingMessage() // 更新消费队列中的计数和总消费量计数
err = p.SendMessage(client, msg) // 发送消息对象到client的写入流中
if err != nil {
goto exit
}
flushed = false
case <-client.ExitChan:
goto exit
}
}
exit:
p.nsqd.logf(LOG_INFO, "PROTOCOL(V2): [%s] exiting messagePump", client)
heartbeatTicker.Stop() // 关闭心跳定时器
outputBufferTicker.Stop() // 关闭输出流定时器
if err != nil {
p.nsqd.logf(LOG_ERROR, "PROTOCOL(V2): [%s] messagePump error - %s", client, err)
}
}
详解(p *protocolV2) Exec方法
这里是路由处理的核心逻辑,都加了注释,内部方法注释太多也不是什么核心方法也就不提出来逐一详解了
// Exec TCP路由逻辑
func (p *protocolV2) Exec(client *clientV2, params [][]byte) ([]byte, error) {
if bytes.Equal(params[0], []byte("IDENTIFY")) {
return p.IDENTIFY(client, params) // IDENTIFY 鉴权数据更新(消费者方法)
}
err := enforceTLSPolicy(client, p, params[0]) // enforceTLSPolicy 用于检查并强制执行 TLS 策略。
if err != nil {
return nil, err
}
switch {
case bytes.Equal(params[0], []byte("FIN")):
return p.FIN(client, params) // FIN 修改消息状态为完成消费状态(消费者方法)
case bytes.Equal(params[0], []byte("RDY")):
return p.RDY(client, params) // RDY 设置客户端的读取数据量条数(消费者方法)
case bytes.Equal(params[0], []byte("REQ")):
return p.REQ(client, params) // REQ 发送请求将指定消息ID重新加入到消费队列中(消费者方法)
case bytes.Equal(params[0], []byte("PUB")):
return p.PUB(client, params) // PUB 发布单条消息(生产者方法)
case bytes.Equal(params[0], []byte("MPUB")):
return p.MPUB(client, params) // MPUB 发布多条消息(生产者方法)
case bytes.Equal(params[0], []byte("DPUB")):
return p.DPUB(client, params) // DPUB 发布一条延迟消息(生产者方法)
case bytes.Equal(params[0], []byte("NOP")):
return p.NOP(client, params) // NOP 空操作,啥也不做
case bytes.Equal(params[0], []byte("TOUCH")):
return p.TOUCH(client, params) // TOUCH 重置飞行中(消费中)消息的超时设置(消费者方法)
case bytes.Equal(params[0], []byte("SUB")):
return p.SUB(client, params) // SUB 消费者订阅指定topic并创建channel对象(消费者方法)
case bytes.Equal(params[0], []byte("CLS")):
return p.CLS(client, params) // CLS 客户端连接请求关闭(消费者方法)
case bytes.Equal(params[0], []byte("AUTH")):
return p.AUTH(client, params) // AUTH 客户端连接认证方法(消费者方法)
}
return nil, protocol.NewFatalClientErr(nil, "E_INVALID", fmt.Sprintf("invalid command %s", params[0]))
}