【NSQ源码】Go 实时分布式消息平台 nsqd TCPServer 的启动过程

140 阅读3分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

通过上一篇文章的分析我们知道 NSQ 使用go-svc 库,实现了 InitStartStop 三个步骤。

Init方法

创建 NSQD 对象,代码如下:

// 创建 nsqd
nsqd, err := nsqd.New(opts)

下面是创建 NSQD 对象的核心代码,这里只保留了核心代码

func New(opts *Options) (*NSQD, error) {
   var err error

   // 省略部分代码

   n := &NSQD{
      startTime: time.Now(),
      // topic 存储 map
      topicMap:             make(map[string]*Topic),
      exitChan:             make(chan int),
      notifyChan:           make(chan interface{}),
      optsNotificationChan: make(chan struct{}, 1),
      dl:                   dirlock.New(dataPath),
   }
   // 创建 context
   n.ctx, n.ctxCancel = context.WithCancel(context.Background())
   
   // 省略部分代码

   n.logf(LOG_INFO, version.String("nsqd"))
   n.logf(LOG_INFO, "ID: %d", opts.ID)

   // 创建 tcpServer
   n.tcpServer = &tcpServer{nsqd: n}
   // tcp listener
   n.tcpListener, err = net.Listen("tcp", opts.TCPAddress)
   if err != nil {
      return nil, fmt.Errorf("listen (%s) failed - %s", opts.TCPAddress, err)
   }
   if opts.HTTPAddress != "" {
      // 创建 http listener
      n.httpListener, err = net.Listen("tcp", opts.HTTPAddress)
      if err != nil {
         return nil, fmt.Errorf("listen (%s) failed - %s", opts.HTTPAddress, err)
      }
   }
   if n.tlsConfig != nil && opts.HTTPSAddress != "" {
      n.httpsListener, err = tls.Listen("tcp", opts.HTTPSAddress, n.tlsConfig)
      if err != nil {
         return nil, fmt.Errorf("listen (%s) failed - %s", opts.HTTPSAddress, err)
      }
   }
   
   // 省略部分代码
   
   return n, nil
}

NSQD 中的一些核心字段

  • topicMap map[string]*Topic: 用来存储 Topic 信息
  • tcpServer *tcpServer: tcpServer 实现了 TCPHandler 接口,主要负责出来每个连接的I/O
  • tcpListener net.Listener:监听 TCP 连接,生产者和消费者都可以与nsqd建立 TCP 连接
  • httpListener / httpsListener net.Listener:提供 HTTP 接口

Start 启动 NSQD

func (p *program) Start() error {
   // 省略部分代码
   
   go func() {
      // 启动 nsqd
      err := p.nsqd.Main()
      if err != nil {
         p.Stop()
         os.Exit(1)
      }
   }()

   return nil
}

NSQD 中的 Main 方法,根据 tcpListener、httpListener、httpsListener 分别在每个 goroutine 中创建 TCPServer、httpServer 和 httpsServer。

func (n *NSQD) Main() error {
    exitCh := make(chan error)
   
   // 省略部分代码
   
   // tcp
   n.waitGroup.Wrap(func() {
      // 开启一个 goroutine 创建 tcp server,里面是个无限 for 循环
      exitFunc(protocol.TCPServer(n.tcpListener, n.tcpServer, n.logf))
   })
   // http
   if n.httpListener != nil {
      httpServer := newHTTPServer(n, false, n.getOpts().TLSRequired == TLSRequired)
      n.waitGroup.Wrap(func() {
         exitFunc(http_api.Serve(n.httpListener, httpServer, "HTTP", n.logf))
      })
   }
   // https
   if n.httpsListener != nil {
      httpsServer := newHTTPServer(n, true, true)
      n.waitGroup.Wrap(func() {
         exitFunc(http_api.Serve(n.httpsListener, httpsServer, "HTTPS", n.logf))
      })
   }
   
   // 省略部分代码 
   
   // 会一直阻塞
   err := <-exitCh
   return err
}

创建 TCPServer

protocol.TCPServer(n.tcpListener, n.tcpServer, n.logf)

进入 protocol 包 中的 tcp_server.go

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()
      if err != nil {
         if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
            logf(lg.WARN, "temporary Accept() failure - %s", err)
            runtime.Gosched()
            continue
         }
         // theres no direct way to detect this error because it is not exposed
         if !strings.Contains(err.Error(), "use of closed network connection") {
            return fmt.Errorf("listener.Accept() error - %s", err)
         }
         break
      }

      // 连接已经建立
      wg.Add(1)
      // 开启一个 goroutine 处理这个连接的数据I/O
      go func() {
         handler.Handle(clientConn)
         wg.Done()
      }()
   }

   // wait to return until all handler goroutines complete
   wg.Wait()

   logf(lg.INFO, "TCP: closing %s", listener.Addr())

   return nil
}

在一个无限 for 循环中 tcpListener 调用 Accept 等待客户端的连接,对每个新建立的连接都会创建一个 goroutine 把 conn 交给 TCPHandler 去处理, 这里的 TCPHandler 就是我们前面提到的 NSQD 中的 tcpServer。 使用 sync.WaitGroup 等待所有 TCPHandler 的 goroutine 处理完成。

TCPHandler 处理 I/O

TCPHandler 主要负责

  • 对每个新创建的连接验证协议版本号
  • 根据协议版本号创建对应的 protocol.Protocol
  • 对新建立的连接 conn 创建一个 client
  • prot.IOLoop(client) 处理 I/O

具体代码如下:

func (p *tcpServer) Handle(conn net.Conn) {
   p.nsqd.logf(LOG_INFO, "TCP: new client(%s)", conn.RemoteAddr())

   // The client should initialize itself by sending a 4 byte sequence indicating
   // the version of the protocol that it intends to communicate, this will allow us
   // to gracefully upgrade the protocol away from text/line oriented to whatever...
   buf := make([]byte, 4)
   // 会阻塞,直到读取够4字节(  V2)
   _, 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)

   var prot protocol.Protocol
   switch protocolMagic {
   case "  V2":
      prot = &protocolV2{nsqd: p.nsqd}
   default:
      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
   }
   // 对新建立的连接conn 创建一个 client
   client := prot.NewClient(conn)
   p.conns.Store(conn.RemoteAddr(), client)

   err = prot.IOLoop(client)
   if err != nil {
      p.nsqd.logf(LOG_ERROR, "client(%s) - %s", conn.RemoteAddr(), err)
   }

   p.conns.Delete(conn.RemoteAddr())
   // 下面一行等价 conn.Close()
   client.Close()
}

客户端与 NSQD 建立 TCP 连接之后,客户端需要发送4字节的4字节([space][space][V][2])的协议版本号数据给服务端 NSQD, io.ReadFull(conn, buf) 读取协议版本号,目前 NSQ 支持的协议版本号是 V2(前面有2个空格), 如果协议版本号不对,会通过 protocol.SendFramedResponse(conn, frameTypeError, []byte("E_BAD_PROTOCOL")) 发送错误信息(E_BAD_PROTOCOL) 给客户端。

&protocolV2{nsqd: p.nsqd} 创建 protocolV2,client := prot.NewClient(conn) 对于新建立的连接创建一个对应的 client,这个 client 负责后面客户端发送过来的 Command 解析,处理等。

参考资料