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
}
以上列出了一些关键点,应该可以导读代码了。