在 nsqd 中,每个 nsqd 节点初始化时会默认监听两个端口:
[nsqd] INFO: TCP: listening on [::]:4150 TCP 连接
[nsqd] INFO: HTTP: listening on [::]:4151 HTTP 连接
那么这两个端口有什么作用呢?nsqd 是怎么和 lookupd 以及客户端进行通信的呢?如何处理新到达的链接呢?在 nsqd 的 Main 函数中,启动了三个服务器分别用来监听 TCP,HTTP,HTTPS 的连接,下面依次来看一下
nsqd 中的 TCP 连接
使用如下代码创建一个 TCP 服务器来监听 4150 端口上的新连接:
protocol.TCPServer(n.tcpListener, n.tcpServer, n.logf)
创建一个 TCPServer 需要传入三个参数:TCP 监听地址,tcpServer 结构体,日志打印方法.1,3 不需要解释,来看看什么是 tcpServer,以及为什么要传入一个 tcpServer:
// 管理所有 TCP 连接
// 通过实现 TCPHandler 接口来对新连接进行处理
type tcpServer struct {
ctx *context
conns sync.Map // 这里使用了 syncmap,用 Addr 作为 key,Conn 作为 value
}
// nsqd 中对新 TCP 连接的处理函数
func (p *tcpServer) Handle(clientConn net.Conn) {
// 新的连接打印对方地址端口
p.ctx.nsqd.logf(LOG_INFO, "TCP: new client(%s)", clientConn.RemoteAddr())
// 客户端通过发送四个字节的消息来初始化自己,这里包括它通信的协议版本
buf := make([]byte, 4)
// 获取协议版本,客户端建立连接后立即发送四字节版本初始化自己
_, err := io.ReadFull(clientConn, buf)
if err != nil {
// 初始化失败
p.ctx.nsqd.logf(LOG_ERROR, "failed to read protocol version - %s", err)
clientConn.Close()
return
}
protocolMagic := string(buf)
// 打印协议版本,v1.2.0的协议版本打印出来的是 " V2"
p.ctx.nsqd.logf(LOG_INFO, "CLIENT(%s): desired protocol magic '%s'",
clientConn.RemoteAddr(), protocolMagic)
// 创建对应的协议管理器
var prot protocol.Protocol
switch protocolMagic {
case " V2":
// 使用 V2 版本的协议操作来进行处理,创建对应的版本管理器即可
prot = &protocolV2{ctx: p.ctx}
default:
// 暂时仅支持 V2 一个版本,其余的版本直接断开连接并返回
protocol.SendFramedResponse(clientConn, frameTypeError, []byte("E_BAD_PROTOCOL"))
clientConn.Close()
p.ctx.nsqd.logf(LOG_ERROR, "client(%s) bad protocol magic '%s'",
clientConn.RemoteAddr(), protocolMagic)
return
}
// 将连接加入到 TCP 服务器的 sync.map 中
p.conns.Store(clientConn.RemoteAddr(), clientConn)
// 协议定义的操作,开始运行这个客户端的连接,进行数据的交换
err = prot.IOLoop(clientConn)
if err != nil {
p.ctx.nsqd.logf(LOG_ERROR, "client(%s) - %s", clientConn.RemoteAddr(), err)
}
// 连接断开
p.conns.Delete(clientConn.RemoteAddr())
}
// nsqd 退出时调用
func (p *tcpServer) CloseAll() {
// 遍历 sync.map 进行 close 操作即可
p.conns.Range(func(k, v interface{}) bool {
v.(net.Conn).Close()
return true
})
}
protocolV2 后面再进行讲解,从上面代码可以看出 tcpServer 只有两个方法,用来处理新的连接的 Handle,用来关闭所有连接的 CloseAll,然后在调用 TCPServer 时把 tcpServer 转换成 TCPHandler 接口进行传入即可
在 TCPServer 内部运行了一个 for 循环不断地接收新的连接并将其添加到 nsqd.tcpServer 中即可,代码很简单如下:
func TCPServer(listener net.Listener, handler TCPHandler, logf lg.AppLogFunc) error {
// 开始监听 TCP 端口默认是 4150
logf(lg.INFO, "TCP: listening on %s", listener.Addr())
var wg sync.WaitGroup
for {
// 接收一个新的连接
clientConn, err := listener.Accept()
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
logf(lg.WARN, "temporary Accept() failure - %s", err)
runtime.Gosched()
continue
}
// 这里错误直接使用 fmt 进行打印的,因为没有将错误暴露出去?但是将这个错误传递出去了啊
// 可能是因为,仅仅传递出去了错误,但是外面并没有进行处理,因为这个错误直接导致了系统的关闭
// 传出去的错误是 LOG_FATAL 级别,并不会进行打印
if !strings.Contains(err.Error(), "use of closed network connection") {
return fmt.Errorf("listener.Accept() error - %s", err)
}
break
}
// 这里进行 Add(1) 会一直保持已有连接不会因为错误连接导致 TCPServer 退出而断开,后面有 wg.Wait()
wg.Add(1)
go func() {
// 执行连接处理函数,其实就是把连接放进 map 中进行管理而已
handler.Handle(clientConn)
wg.Done()
}()
}
// 等待所有协程完成
wg.Wait()
logf(lg.INFO, "TCP: closing %s", listener.Addr())
return nil
}
到这里我们就建立了一个 TCP 连接.到这里一个客户端完成的操作为:建立连接,把连接添加到 tcpServer.conns 中,然后创建一个 protocolV2 并执行 IOLoop 来使该客户端和 nsqd 进行 IO 通信.
nsqd 中的 HTTP 连接
同样的,在 NSQD.Main() 中启动了 HTTP 的监听 goroutine.
// 开启对 HTTP 进行监听
httpServer := newHTTPServer(ctx, false, n.getOpts().TLSRequired == TLSRequired)
n.waitGroup.Wrap(func() {
exitFunc(http_api.Serve(n.httpListener, httpServer, "HTTP", n.logf))
})
先创建一个 HTTPServer 并注册好一系列路由操作,然后调用 http_api.Serve 开始启动 HTTPServer 并在 4151 端口进行 HTTP 通信.
// 创建一个新的 HTTP server 并注册路由规则
func newHTTPServer(ctx *context, tlsEnabled bool, tlsRequired bool) *httpServer {
log := http_api.Log(ctx.nsqd.logf)
router := httprouter.New()
router.HandleMethodNotAllowed = true
router.PanicHandler = http_api.LogPanicHandler(ctx.nsqd.logf)
router.NotFound = http_api.LogNotFoundHandler(ctx.nsqd.logf)
router.MethodNotAllowed = http_api.LogMethodNotAllowedHandler(ctx.nsqd.logf)
s := &httpServer{
ctx: ctx,
tlsEnabled: tlsEnabled,
tlsRequired: tlsRequired,
router: router,
}
// 这里是是所有的路由规则
router.Handle("GET", "/ping", http_api.Decorate(s.pingHandler, log, http_api.PlainText))
router.Handle("GET", "/info", http_api.Decorate(s.doInfo, log, http_api.V1))
// v1 negotiate
router.Handle("POST", "/pub", http_api.Decorate(s.doPUB, http_api.V1))
router.Handle("POST", "/mpub", http_api.Decorate(s.doMPUB, http_api.V1))
router.Handle("GET", "/stats", http_api.Decorate(s.doStats, log, http_api.V1))
// only v1
router.Handle("POST", "/topic/create", http_api.Decorate(s.doCreateTopic, log, http_api.V1))
router.Handle("POST", "/topic/delete", http_api.Decorate(s.doDeleteTopic, log, http_api.V1))
router.Handle("POST", "/topic/empty", http_api.Decorate(s.doEmptyTopic, log, http_api.V1))
router.Handle("POST", "/topic/pause", http_api.Decorate(s.doPauseTopic, log, http_api.V1))
router.Handle("POST", "/topic/unpause", http_api.Decorate(s.doPauseTopic, log, http_api.V1))
router.Handle("POST", "/channel/create", http_api.Decorate(s.doCreateChannel, log, http_api.V1))
router.Handle("POST", "/channel/delete", http_api.Decorate(s.doDeleteChannel, log, http_api.V1))
router.Handle("POST", "/channel/empty", http_api.Decorate(s.doEmptyChannel, log, http_api.V1))
router.Handle("POST", "/channel/pause", http_api.Decorate(s.doPauseChannel, log, http_api.V1))
router.Handle("POST", "/channel/unpause", http_api.Decorate(s.doPauseChannel, log, http_api.V1))
router.Handle("GET", "/config/:opt", http_api.Decorate(s.doConfig, log, http_api.V1))
router.Handle("PUT", "/config/:opt", http_api.Decorate(s.doConfig, log, http_api.V1))
// debug
router.HandlerFunc("GET", "/debug/pprof/", pprof.Index)
router.HandlerFunc("GET", "/debug/pprof/cmdline", pprof.Cmdline)
router.HandlerFunc("GET", "/debug/pprof/symbol", pprof.Symbol)
router.HandlerFunc("POST", "/debug/pprof/symbol", pprof.Symbol)
router.HandlerFunc("GET", "/debug/pprof/profile", pprof.Profile)
router.Handler("GET", "/debug/pprof/heap", pprof.Handler("heap"))
router.Handler("GET", "/debug/pprof/goroutine", pprof.Handler("goroutine"))
router.Handler("GET", "/debug/pprof/block", pprof.Handler("block"))
router.Handle("PUT", "/debug/setblockrate", http_api.Decorate(setBlockRateHandler, log, http_api.PlainText))
router.Handler("GET", "/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
return s
}
只需要了解在 HTTPServer 中注册的一系列路由规则的使用即可.注册的命令除了状态查看的规则之外,就是对 topic 和 channel 进行管理的,这些都是由 nsq 的管理界面进行操作的.另外 HTTPS 的启动和 HTTP 的并无太多区别仅仅是开启了 TLS 而已.
所有操作如下:
- GET: ping // 类似于 keep_alive 用来获取 nsqd 的 healthy 状态
- GET: info // 获取 nsqd 的基础信息
- POST: pub // 向 topic 推送一条消息
- POST: mpub // 向 topic 推送多条消息
- GET: stats // 获取 nsqd 的统计信息(版本,开始时间,topics 等等)
- POST: /topic/create // 创建一个 topic
- POST: /topic/delete // 删除一个 topic
- POST: /topic/empty // 清空一个 topic
- POST: /topic/pause // 暂停一个 topic
- POST: /topic/unpause // 恢复一个 topic
- POST: /channel/create // 创建一个 channel
- POST: /channel/delete // 删除一个 channel
- POST: /channel/empty // 清空一个 channel
- POST: /channel/pause // 暂停一个 channel
- POST: /channel/unpause // 恢复一个 channel
- GET: /config/:opt // 获取 nsqd 的 options
- PUT: /config/:opt // 更新 nsqd 的 options
- …一系列统计操作
小结
现在可以知道每个客户端通过 TCP 和服务器进行连接,建立后的连接统一使用 nsqd 中的 tcpServer 字段进行管理.创建 HTTP 和 HTTPS 来从管理界面对 nsqd 进行操作
最后附上 github 的 nsqd 的源码解析地址:https://github.com/nercoeus/nsq_source_note