go-libp2p-host Connect 源碼分析

Connect 過程解析

  • go-libp2p-host 中定義了 Host 接口,它有幾個實現都在 go-libp2p 包中,我們關注 basic 包中的 BasicHost 實現,因爲 IPFS 用了這個實現

  • Connect 主要是 dial peer 並完成握手,再去交換 Identify 信息,Identify 信息後文有提到,文中反覆提到的 ID 是指 Identify 協議的名稱

  • 發起連接一端我們稱爲 from ,被連接一端成爲 to ,則有如下過程在兩端建立連接,這個時序圖只爲理解交互過程,對於閱讀代碼並無實際參考價值;


1、2 是指 from 和 to 要先啓動 libp2p 的 host 再做後續操作,在這一步已經執行了 Swarm.Listen 並啓動了 handlencoming 線程來 accept 連接,並且爲 ID 協議註冊了 StreamHandler ,爲通道上的連接註冊了 ConnHandler;
3 from 端通過 Connect 來 dial to 端;
4、 to 端的 newConnHandler 被觸發,這個方法調用了 IdentifyConn ;
5、於此同時 from 端發起連接請求成功後會去調用 IdentifyConn ;
6~14 雙方行爲一致,握手成功後激活 ID 協議的 StreamHandler 觸發 requestHandler 來向對方發送 Identify 消息,兩端用各自的 responseHandler 來處理 Identify 消息,並將 Identify 信息放入 peerstore 中。

看代碼,簡單看一下 Connect 過程

首先看看接口怎麼定義的,因爲所有的邏輯都要建立在 Connect 的基礎上,所以以 Connect 爲入口來欣賞 BasicHost 的實現過程

// Host is an object participating in a p2p network, which
// implements protocols or provides services. It handles
// requests like a Server, and issues requests like a Client.
// It is called Host because it is both Server and Client (and Peer
// may be confusing).
type Host interface {
    ......
    // Connect ensures there is a connection between this host and the peer with
    // given peer.ID. Connect will absorb the addresses in pi into its internal
    // peerstore. If there is not an active connection, Connect will issue a
    // h.Network.Dial, and block until a connection is open, or an error is
    // returned. // TODO: Relay + NAT.
    Connect(ctx context.Context, pi pstore.PeerInfo) error
    ......
}

Host 是什麼以及 Connect 要做的事情通過注視都能看出來,只是看到 TODO 時感到有些遺憾,這個 Relay 足足耽誤我幾天時間,看來讀代碼應該先讀接口.

From 端 Connect 的實現過程

注視跟接口描述的差不多,如果沒有可用連接就會去嘗試 dial 這個 peer 並且把它加入到 peerstore 中

//https://github.com/libp2p/go-libp2p/blob/master/p2p/host/basic/basic_host.go
// Connect ensures there is a connection between this host and the peer with
// given peer.ID. If there is not an active connection, Connect will issue a
// h.Network.Dial, and block until a connection is open, or an error is returned.
// Connect will absorb the addresses in pi into its internal peerstore.
// It will also resolve any /dns4, /dns6, and /dnsaddr addresses.
func (h *BasicHost) Connect(ctx context.Context, pi pstore.PeerInfo) error {
    // absorb addresses into peerstore
    h.Peerstore().AddAddrs(pi.ID, pi.Addrs, pstore.TempAddrTTL)

    if h.Network().Connectedness(pi.ID) == inet.Connected {
        return nil
    }

    resolved, err := h.resolveAddrs(ctx, h.Peerstore().PeerInfo(pi.ID))
    if err != nil {
        return err
    }
    h.Peerstore().AddAddrs(pi.ID, resolved, pstore.TempAddrTTL)

    return h.dialPeer(ctx, pi.ID)
}

dialPeer 雖然很複雜但最終是調用到 IdentifyConn 方法上,我們直接看重點

//===========================================
// go-libp2p/p2p/protocol/identify/id.go
//===========================================

func (ids *IDService) IdentifyConn(c inet.Conn) {
    ids.currmu.Lock()
    if wait, found := ids.currid[c]; found {
        ids.currmu.Unlock()
        log.Debugf("IdentifyConn called twice on: %s", c)
        <-wait // already identifying it. wait for it.
        return
    }
    ch := make(chan struct{})
    ids.currid[c] = ch
    ids.currmu.Unlock()

    defer func() {
        close(ch)
        ids.currmu.Lock()
        delete(ids.currid, c)
        ids.currmu.Unlock()
    }()

    s, err := c.NewStream()
    if err != nil {
        log.Debugf("error opening initial stream for %s: %s", ID, err)
        log.Event(context.TODO(), "IdentifyOpenFailed", c.RemotePeer())
        c.Close()
        return
    }
    
    // 指定了當前這個 Stream 是一個 ID 協議的流,握手成功後就會收到 ID 消息
    // 指定了當前這個 Stream 是一個 ID 協議的流,握手成功後就會收到 ID 消息
    // 指定了當前這個 Stream 是一個 ID 協議的流,握手成功後就會收到 ID 消息
    s.SetProtocol(ID)
    
    // 在此處完成握手
    // 在此處完成握手
    // 在此處完成握手
    // ok give the response to our handler.
    if err := msmux.SelectProtoOrFail(ID, s); err != nil {
        log.Event(context.TODO(), "IdentifyOpenFailed", c.RemotePeer(), logging.Metadata{"error": err})
        s.Reset()
        return
    }
    // 在此處接收 ID 消息
    ids.responseHandler(s)
}



//===========================================
// go-multistream/client.go
//===========================================

// proto = "/ipfs/id/1.0.0"
// rwc = stream
func SelectProtoOrFail(proto string, rwc io.ReadWriteCloser) error {
    err := handshake(rwc)
    if err != nil {
        return err
    }

    return trySelect(proto, rwc)
}

// 握手
// ProtocolID = "/multistream/1.0.0" 
func handshake(rwc io.ReadWriteCloser) error {
    errCh := make(chan error, 1)
    go func() {
        errCh <- delimWriteBuffered(rwc, []byte(ProtocolID))
    }()

    tok, readErr := ReadNextToken(rwc)
    writeErr := <-errCh

    if writeErr != nil {
        return writeErr
    }
    if readErr != nil {
        return readErr
    }

    if tok != ProtocolID {
        return errors.New("received mismatch in protocol id")
    }
    return nil
}

// 握手成功後,去激活對端的 proto 對應的 stream 的 handler 
// 在這裏是爲了激活 ID 協議對應的 requestHandler 
// proto = "/ipfs/id/1.0.0"
func trySelect(proto string, rwc io.ReadWriteCloser) error {
    err := delimWriteBuffered(rwc, []byte(proto))
    if err != nil {
        return err
    }

    tok, err := ReadNextToken(rwc)
    if err != nil {
        return err
    }

    switch tok {
    case proto:
        return nil
    case "na":
        return ErrNotSupported
    default:
        return errors.New("unrecognized response: " + tok)
    }
}
To 端 Listen 的實現過程
  • BasicHost.NewHost
// NewHost constructs a new *BasicHost and activates it by attaching its stream and connection handlers to the given inet.Network.
func NewHost(ctx context.Context, net inet.Network, opts *HostOpts) (*BasicHost, error) {
    ......
    net.SetConnHandler(h.newConnHandler)    
    net.SetStreamHandler(h.newStreamHandler)
    return h, nil
}

func (h *BasicHost) newConnHandler(c inet.Conn) {
    // Clear protocols on connecting to new peer to avoid issues caused
    // by misremembering protocols between reconnects
    h.Peerstore().SetProtocols(c.RemotePeer())
    // 當有一個新的連接到來時,就會去執行 IdentifyConn ,跟發起端行爲一致
    h.ids.IdentifyConn(c)
}
  • Swarm.Listen
    創建 BasicHost 時的 net 參數是 Network 接口的 Swarm 實現,那麼啓動過程中會調用 Listen 方法,下面代碼貼出了關鍵部分,AddListenAddr 方法的 list.Accept() 對上文提到的握手信息進行了處理,然後要 upgrade 這個連接,再去觸發 BasicHost 中指定的 net.SetConnHandler(h.newConnHandler)
func (s *Swarm) Listen(addrs ...ma.Multiaddr) error {
    ......
    for i, a := range addrs {
        if err := s.AddListenAddr(a); err != nil {
            errs[i] = err
        } else {
            succeeded++
        }
    }
    ......
}

func (s *Swarm) AddListenAddr(a ma.Multiaddr) error {
    tpt := s.TransportForListening(a)
    if tpt == nil {
        return ErrNoTransport
    }
    // 在這個方法中調用了一個非常重要的方法,handleIncoming 
    list, err := tpt.Listen(a)
    ......
    go func() {
        ......
        for {
            c, err := list.Accept()
            ......
        }
    }()
    return nil
}

Accept 是 Listener 接口的方法
接口定義在 go-libp2p-transport/transport.go 中
實現定義在 go-libp2p-transport-upgrader/listener.go 中,我們看看如何實現

// Accept accepts a connection.
func (l *listener) Accept() (transport.Conn, error) {
    for c := range l.incoming {
        // Could have been sitting there for a while.
        if !c.IsClosed() {
            return c, nil
        }
    }
    return nil, l.err
}

這個實現太簡單了,只是在讀 incoming channel ,所以線索是誰在往 incoming chainnel 中寫數據,於是找到了 handleIncoming() 方法,以 TcpTransport 實現爲例,可以看到是在 UpgradeListener 時啓動的 handleIncoming

// go-tcp-transport/tcp.go
func (t *TcpTransport) Listen(laddr ma.Multiaddr) (tpt.Listener, error) {
    list, err := t.maListen(laddr)
    if err != nil {
        return nil, err
    }
    return t.Upgrader.UpgradeListener(t, list), nil
}

//go-libp2p-transport-upgrader/upgrader.go
func (u *Upgrader) UpgradeListener(t transport.Transport, list manet.Listener) transport.Listener {
    ctx, cancel := context.WithCancel(context.Background())
    l := &listener{
        Listener:  list,
        upgrader:  u,
        transport: t,
        threshold: newThreshold(AcceptQueueLength),
        incoming:  make(chan transport.Conn),
        cancel:    cancel,
        ctx:       ctx,
    }
    // 就是這裏
    // 就是這裏
    // 就是這裏
    go l.handleIncoming()
    return l
}

以上列出了一些關鍵點,應該可以導讀代碼了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章