Netpoll導讀

byeane發表於2021-10-29

Netpoll 是由 位元組跳動 開發的高效能 NIO(Non-blocking I/O) 網路庫,專注於 RPC 場景。
詳情:https://github.com/cloudwego/...

  1. 要了解一個像Netty這樣專案的原始碼是一件非常困難的事情。專案本身原始碼量大,涉及的底層知識多,加上利用了各種設計模式,都會對原始碼閱讀增加難度。對於這樣的工程最好是根據作者提供的導圖先了解個大概,然後充分閱讀文件,頭腦中構建一個思維框架。一開始就衝進去直懟原始碼是一件非常勇敢的事情。這樣的專案往往是一個大工程,匯聚了很多傑出開源作者的心血,想一口氣讀完,而且絲毫不差的理解裡面的設計思路是不現實的,閱讀原始碼往往我們只需要閱讀它一個主要流程的過程是怎麼產生構建的,而不需要字字斟酌的去讀,這樣容易陷在碼海里。通過第一遍理清流程之後,可以第二遍系統的閱讀,然後再去反思作者這樣設計的好處。
  2. 原始碼目錄
    netpoll原始碼檔案全在同一個目錄下,一目瞭然,有多少檔案。
  3. 主要架構
  1. 萬事開頭難,從服務啟動開始
func main() {
    //建立 Listener
    listener, err := netpoll.CreateListener(network, address)
    if err != nil {
        panic("create netpoll listener failed")
    }
    //建立 EventLoop
    eventLoop, _ := netpoll.NewEventLoop(
        handler,   //
        netpoll.WithOnPrepare(prepare),
        netpoll.WithReadTimeout(time.Second),
    )
    //執行 Server
    eventLoop.Serve(listener)
}
//擴充套件net.Listener
type Listener interface {
   net.Listener
   Fd() (fd int)
}

這個步驟是開啟MianReactor 作用在於監聽埠,獲取操作符地址

func CreateListener(network, addr string) (l Listener, err error) {
   if network == "udp" {
      // TODO: udp listener.
      return udpListener(network, addr)
   }
   // tcp, tcp4, tcp6, unix
   //呼叫go原生
   ln, err := net.Listen(network, addr)
   if err != nil {
      return nil, err
   }
   return ConvertListener(ln)
}

//轉換成定義的Listener ,向系統設定Nonblock

func ConvertListener(l net.Listener) (nl Listener, err error) {
    if tmp, ok := l.(Listener); ok {
        return tmp, nil
    }
    ln := &listener{}
    ln.ln = l
    ln.addr = l.Addr()
    err = ln.parseFD()
    if err != nil {
        return nil, err
    }
    return ln, syscall.SetNonblock(ln.fd, true)
}

//獲取操作符

func (ln *listener) parseFD() (err error) {
    switch netln := ln.ln.(type) {
    case *net.TCPListener:
        ln.file, err = netln.File()
    case *net.UnixListener:
        ln.file, err = netln.File()
    default:
        return errors.New("listener type can't support")
    }
    if err != nil {
        return err
    }
    ln.fd = int(ln.file.Fd())
    return nil
}

//看一些結構體,介面的具體定義。
//EventLoop介面

type EventLoop interface {
   Serve(ln net.Listener) error            //開啟服務
   Shutdown(ctx context.Context) error     //中斷服務
}

//

type eventLoop struct {
   sync.Mutex                    //
   opt     *options              // 引數配置項
   prepare OnPrepare             // handler封裝
   svr     *server               //抽象的服務實體
   stop    chan error
}

//

type server struct {
   operator    FDOperator            // 檔案操作符
   ln          Listener              // 這就是你建立的那個listener
   prepare     OnPrepare             // 這就是你建立的那個handler
   quit        func(err error)       //
   connections sync.Map              // key=fd, value=connection
}

//建立server後,啟動服務

func (evl *eventLoop) Serve(ln net.Listener) error {
   npln, err := ConvertListener(ln)
   if err != nil {
      return err
   }
   evl.Lock()
   evl.svr = newServer(npln, evl.prepare, evl.quit)
   evl.svr.Run()
   evl.Unlock()
   return evl.waitQuit()
}

//這裡正式讓服務跑起來

func (s *server) Run() (err error) {
   s.operator = FDOperator{
      FD:     s.ln.Fd(),
      OnRead: s.OnRead,
      OnHup:  s.OnHup,
   }
    //通過負載均衡演算法選取一個Poll
   s.operator.poll = pollmanager.Pick()
    //將poll和系統操作符事件關聯,PollReadable 用於監聽連線的開啟或者關閉
   err = s.operator.Control(PollReadable)
   if err != nil {
      s.quit(err)
   }
   return err
}

//eventLoop的個數 通過獲取 runtime.GOMAXPROCS(0)來比較設定
//pollmanager.Pick()
//eventLoop的啟動是通過 poll_manager.go中的init()方法啟動

type manager struct {
    NumLoops int                //eventloop個數
    balance  loadbalance        // 負載均衡策略  Random、RoundRobin
    polls    []Poll             // all the polls
}
func init() {
   pollmanager = &manager{}
   pollmanager.SetLoadBalance(RoundRobin)
   pollmanager.SetNumLoops(defaultNumLoops())
}

//開啟SubReactor,處理連線,讀寫,關閉事件
//通過SetNumLoops呼叫Run()

func (m *manager) Run() error {
    for idx := len(m.polls); idx < m.NumLoops; idx++ {
        var poll = openPoll()
        m.polls = append(m.polls, poll)
                //開啟
        go poll.Wait()
    }
    m.balance.Rebalance(m.polls)
    return nil
}

通過openPoll()建立 defaultPoll,defaultPoll實現了Poll介面

type Poll interface {
   Wait() error
   Close() error
   Trigger() error
   Control(operator *FDOperator, event PollEvent) error
}
type defaultPoll struct {
    pollArgs   
    fd      int         // epoll fd
    wop     *FDOperator // eventfd, wake epoll_wait
    buf     []byte      // read wfd trigger msg
    trigger uint32      // trigger flag
    Reset   func(size, caps int)
    Handler func(events []epollevent) (closed bool)
}

//這裡才是真正工作的地方,前面都是準備工作
//進入for迴圈阻塞,等待訊息到來

func (p *defaultPoll) Wait() (err error) {
    // init
    var caps, msec, n = barriercap, -1, 0
    p.Reset(128, caps)
    // wait
    for {
        if n == p.size && p.size < 128*1024 {
            p.Reset(p.size<<1, caps)
        }
                //linux epoll
        n, err = EpollWait(p.fd, p.events, msec)
        if err != nil && err != syscall.EINTR {
            return err
        }
        if n <= 0 {
            msec = -1
            runtime.Gosched()
            continue
        }
        msec = 0
        if p.Handler(p.events[:n]) {
            return nil
        }
    }
}

//處理監聽事件
// 這個可以通過協程池來操作,以減少系統頻繁排程協程的開銷

func (p *defaultPoll) handler(events []epollevent) (closed bool) {
    var hups []*FDOperator // TODO: maybe can use sync.Pool
    for i := range events {
        var operator = *(**FDOperator)(unsafe.Pointer(&events[i].data))
        // trigger or exit gracefully
                //處理trigger事件或者exit
        if operator.FD == p.wop.FD {
            ......
            continue
        }
        .........
        default:
                       //資料進來
            if evt&syscall.EPOLLIN != 0 {
                if operator.OnRead != nil {
                                       //處理連線
                    // for non-connection
                    operator.OnRead(p)
                } else {
                                      //處理已連線
                    // for connection
                    var bs = operator.Inputs(p.barriers[i].bs)
                    if len(bs) > 0 {
                        var n, err = readv(operator.FD, bs, p.barriers[i].ivs)
                        operator.InputAck(n)
                        .......
                    }
                }
            }
                        //資料出去
            if evt&syscall.EPOLLOUT != 0 {
                .......
            }
        }
        operator.done()
    }
    if len(hups) > 0 {
                //從poll輪詢中刪除監聽事件
        p.detaches(hups)
    }
    return false
}

//處理連線事件

func (s *server) OnRead(p Poll) error {
   // accept socket
   conn, err := s.ln.Accept()
   if err != nil {
      // shut down
      if strings.Contains(err.Error(), "closed") {
         s.operator.Control(PollDetach)
         s.quit(err)
         return err
      }
      log.Println("accept conn failed:", err.Error())
      return err
   }
   if conn == nil {
      return nil
   }
   // store & register connection
    //儲存連線,在退出的時候用於關閉連線
   var connection = &connection{}
   connection.init(conn.(Conn), s.prepare)
   if !connection.IsActive() {
      return nil
   }
   var fd = conn.(Conn).Fd()
   connection.AddCloseCallback(func(connection Connection) error {
      s.connections.Delete(fd)
      return nil
   })
   s.connections.Store(fd, connection)
   return nil
}

//處理資料
//返回給註冊onRequest的地方,到這裡一個主要流程就完了

func (c *connection) inputAck(n int) (err error) {
   if n < 0 {
      n = 0
   }
   leftover := atomic.AddInt32(&c.waitReadSize, int32(-n))
   err = c.inputBuffer.BookAck(n, leftover <= 0)
   c.triggerRead()
   c.onRequest()
   return err
}

buffer 處理上 設計了Nocopy Buffer ,Nocopy Buffer基於連結串列陣列實現。
netpoll裡面有許多細節處的設計,這裡只是做一個非常粗淺的導讀。
目前看到的程式碼 只支援了linux,macos。

相關文章