go-libp2p-host Connect 源码分析

Connect 过程解析

  • go-libp2p-host 中定义了 Host 接口,它有几个实现都在 go-libp2p 包中,我们关注 basic 包中的 BasicHost 实现,因为 IPFS 用了这个实现

  • Connect 主要是 dial peer 并完成握手,再去交换 Identify 信息,Identify 信息后文有提到,文中反复提到的 ID 是指 Identify 协议的名称

  • 发起连接一端我们称为 from ,被连接一端成为 to ,则有如下过程在两端建立连接,这个时序图只为理解交互过程,对于阅读代码并无实际参考价值;


    go-libp2p-host Connect 源码分析_第1张图片
    from.connect(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
}

以上列出了一些关键点,应该可以导读代码了。

你可能感兴趣的:(go-libp2p-host Connect 源码分析)