上节中通过设置静态节点BootstrapNodes节点来发现更多全网的其他节点,这部分只是发现节点并找出其中可以ping通的节点,但是还没有进行使用,还没建立TCP连接进行数据传输,协议处理等。
这里主要分析P2P系统的TCP连接池的建立,以及是怎么跟其他节点通信的。
一、TCP监听
P2P网络服务启动时调用Server.Start(),其后面部分代码:
p2p/server.go里面的Start()函数后面部分代码
dynPeers := srv.maxDialedConns()
//新建一个dialstate结构返回 , StaticNodes 会直接加到srv.static[]数组里面
//dialer是用来接收到上面的一堆管道事件后,管理nodes列表的, 发送TCP连接的
dialer := newDialState(srv.StaticNodes, srv.BootstrapNodes, srv.ntab, dynPeers, srv.NetRestrict)
// handshake
srv.ourHandshake = &protoHandshake{Version: baseProtocolVersion, Name: srv.Name, ID: discover.PubkeyID(&srv.PrivateKey.PublicKey)}
for _, p := range srv.Protocols {
srv.ourHandshake.Caps = append(srv.ourHandshake.Caps, p.cap())
}
if srv.ListenAddr != "" {
//开始监听TCP请求, TCP端口是需要加密的,传输的信息比较敏感,所以有握手过程
if err := srv.startListening(); err != nil {
return err
}
}
if srv.NoDial && srv.ListenAddr == "" {
srv.log.Warn("P2P server will be useless, neither dialing nor listening")
}
srv.loopWG.Add(1)
//开始监听各个信号量,处理peer的增删改。
go srv.run(dialer)
srv.running = true
return nil
}
这部分代码调用 startListening 开始监听TCP请求, 后面创建server.run协程处理连接池管理任务。
startListening 创建一个TCP监听句柄, 然后创建listenLoop协程进行不断的监听请求,进行Accept,然后握手,建立安全连接. startListening 创建协程后会立即返回。
p2p/server.go中的startListening()函数
func (srv *Server) startListening() error {
// 先创建一个TCP监听句柄, 然后创建listenLoop协程进行不断的监听请求,进行Accept,然后握手,建立安全连接
listener, err := net.Listen("tcp", srv.ListenAddr)
if err != nil {
return err
}
laddr := listener.Addr().(*net.TCPAddr)
srv.ListenAddr = laddr.String()
srv.listener = listener
srv.loopWG.Add(1)
//进入协程监听请求
go srv.listenLoop()
// 如果配置tcp监听端口映射
if !laddr.IP.IsLoopback() && srv.NAT != nil {
srv.loopWG.Add(1)
go func() {
nat.Map(srv.NAT, srv.quit, "tcp", laddr.Port, laddr.Port, "ethereum p2p")
srv.loopWG.Done()
}()
}
return nil
}
listenLoop 协程的工作,分两部分,初始化,接受连接, 以及连接建立。
func (srv *Server) listenLoop() {
defer srv.loopWG.Done()
srv.log.Info("RLPx listener up", "self", srv.makeSelf(srv.listener, srv.ntab))
tokens := defaultMaxPendingPeers
if srv.MaxPendingPeers > 0 {
tokens = srv.MaxPendingPeers
}
slots := make(chan struct{}, tokens)
for i := 0; i < tokens; i++ {
//先直接放入这么多个slot进去,稍后就一个个获取了,用来实现并发控制。
//因为实际上的链家请求简历,是通过协程进行的,并非在本协程处理,所以用了管道的形式来控制并发。
slots <- struct{}{}
}
for {
//并发控制
<-slots
var (
fd net.Conn
err error
)
for {
//接受链接, 检查是否有问题,没问题就break
fd, err = srv.listener.Accept()
if tempErr, ok := err.(tempError); ok && tempErr.Temporary() {
srv.log.Debug("Temporary read error", "err", err)
continue
} else if err != nil {
srv.log.Debug("Read error", "err", err)
return
}
break
}
二、TCP连接建立
srv.listener.Accept() 接受连接之后,便会创建新的协程专门处理这一个peer节点的请求。
if srv.NetRestrict != nil {
if tcp, ok := fd.RemoteAddr().(*net.TCPAddr); ok && !srv.NetRestrict.Contains(tcp.IP) {
srv.log.Debug("Rejected conn (not whitelisted in NetRestrict)", "addr", fd.RemoteAddr())
fd.Close()
slots <- struct{}{}
continue
}
}
fd = newMeteredConn(fd, true)
srv.log.Trace("Accepted connection", "addr", fd.RemoteAddr())
//创建协程去处理这个链接
go func() {
//TCP连接是加密的,所以需要进行两边的握手,互相交换公钥
srv.SetupConn(fd, inboundConn, nil)
slots <- struct{}{}
}()
}
调用srv.SetupConn进行协议握手,TCP传输链接是加密传输的,目前使用的是RLPx加密协议,是在Start函数的srv.newTransport = newRLPX进行的赋值,则处理函数是newRLPX 。
func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error {
self := srv.Self()
if self == nil {
return errors.New("shutdown")
}
//newTransport 目前使用的是RLPx加密协议,处理函数是newRLPX
c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)}
err := srv.setupConn(c, flags, dialDest)
if err != nil {
c.close(err)
srv.log.Trace("Setting up connection failed", "id", c.id, "err", err)
}
return err
}
继续调用setupConn 函数进行一个TCP协议上的握手功能。setupConn 主要是对握手协议的处理,发送数据。
func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) error {
// 进行tcp协议的握手
srv.lock.Lock()
running := srv.running
srv.lock.Unlock()
if !running {
return errServerStopped
}
// Run the encryption handshake.
var err error
//这里实际上执行的是rlpx.go里面的doEncHandshake.因为transport是conn的一个匿名字段。 匿名字段的方法会直接作为conn的一个方法。
if c.id, err = c.doEncHandshake(srv.PrivateKey, dialDest); err != nil {
srv.log.Trace("Failed RLPx handshake", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err)
return err
}
clog := srv.log.New("id", c.id, "addr", c.fd.RemoteAddr(), "conn", c.flags)
// For dialed connections, check that the remote public key matches.
// 如果连接握手的ID和对应的ID不匹配
if dialDest != nil && c.id != dialDest.ID {
clog.Trace("Dialed identity mismatch", "want", c, dialDest.ID)
return DiscUnexpectedIdentity
}
// 这个checkpoint其实就是把第一个参数发送给第二个参数指定的队列。然后从c.cout接收返回信息。 是一个同步的方法。
//至于这里,后续的操作只是检查了一下连接是否合法就返回了。
err = srv.checkpoint(c, srv.posthandshake)
if err != nil {
clog.Trace("Rejected peer before protocol handshake", "err", err)
return err
}
// Run the protocol handshake
phs, err := c.doProtoHandshake(srv.ourHandshake)
if err != nil {
clog.Trace("Failed proto handshake", "err", err)
return err
}
if phs.ID != c.id {
clog.Trace("Wrong devp2p handshake identity", "err", phs.ID)
return DiscUnexpectedIdentity
}
c.caps, c.name = phs.Caps, phs.Name
// 这里两次握手都已经完成了。 把c发送给addpeer队列。 后台处理这个队列的时候,会处理这个连接
err = srv.checkpoint(c, srv.addpeer)
if err != nil {
clog.Trace("Rejected peer", "err", err)
return err
}
// If the checks completed successfully, runPeer has now been
// launched by run.
clog.Trace("connection set up", "inbound", dialDest == nil)
return nil
}
srv.checkpoint(c, srv.addpeer)比较重要,会在addpeer 上面插入一条消息,而消息的另外一端就是 server.run()。
当最后协议握手完成后,会调用到下面的代码:
p2p/server.go中的run()函数
...
select {
...
case c := <-srv.addpeer:
// At this point the connection is past the protocol handshake.
// Its capabilities are known and the remote identity is verified.
err := srv.protoHandshakeChecks(peers, inboundCount, c)
if err == nil {
// The handshakes are done and it passed all checks.
p := newPeer(c, srv.Protocols)
// If message events are enabled, pass the peerFeed
// to the peer
if srv.EnableMsgEvents {
p.events = &srv.peerFeed
}
name := truncateName(c.name)
srv.log.Debug("Adding p2p peer", "name", name, "addr", c.fd.RemoteAddr(), "peers", len(peers)+1)
go srv.runPeer(p)
peers[c.id] = p
if p.Inbound() {
inboundCount++
}
}
// The dialer logic relies on the assumption that
// dial tasks complete after the peer has been added or
// discarded. Unblock the task last.
select {
case c.cont <- err:
case <-srv.quit:
break running
}
case pd := <-srv.delpeer:
// A peer disconnected.
d := common.PrettyDuration(mclock.Now() - pd.created)
pd.log.Debug("Removing p2p peer", "duration", d, "peers", len(peers)-1, "req", pd.requested, "err", pd.err)
delete(peers, pd.ID())
if pd.Inbound() {
inboundCount--
}
}
}
protoHandshakeChecks 进行一些必要检查后如果正确,那么调用newPeer创建一个peer结构,在创建peer结构后,调用matchProtocols()函数先排序,再通过Name和Version区分出子协议,设置好返回子协议的offset等参数,然后创建协程go srv.runPeer§, 并记录这个peer节点:peers[c.id] = p。
runPeer 的主要任务是进行回调,以及创建了peer的时间处理,另外最主要的是调用peer.run启动跟这个peer的协程读写循环。
func (srv *Server) runPeer(p *Peer) {
//跟一个peer连接建立完成后,调用这里来启动一个peer的维护,监听工作
if srv.newPeerHook != nil {
srv.newPeerHook(p)
}
// 广播添加一个peer
srv.peerFeed.Send(&PeerEvent{
Type: PeerEventTypeAdd,
Peer: p.ID(),
})
// 运行协议
remoteRequested, err := p.run()
// 广播peer销毁
srv.peerFeed.Send(&PeerEvent{
Type: PeerEventTypeDrop,
Peer: p.ID(),
Error: err.Error(),
})
srv.delpeer <- peerDrop{p, err, remoteRequested}
}
三、连接池管理
server.run 主要任务是进行 接池管理协程,负责维护TCP连接列表, 协程里面运行, 开始监听各个信号量,处理peer的增删改。 上面说的startListening 主要进行被动的接受链接然后进行握手,最后加入到连接池。
而server.run则是一个事件循环和连接池管理功能。
server.run中startTasks函数 和 scheduleTasks函数 , 其功能如下:
- startTasks 用来跟参数数组代表的节点一个个创建协程建立连接,并且每次活跃的正在建立连接的任务数不超过 maxActiveDialTasks。
- scheduleTasks 如果没有足够的节点,那么调用这里开始去尝试从p2p discover 中找出更多节点来用。scheduleTasks 会调用startTasks进行连接。
//用来跟参数数组代表的节点一个个创建协程建立连接
startTasks := func(ts []task) (rest []task) {
i := 0
//最多可以建立maxActiveDialTasks 个连接
for ; len(runningTasks) < maxActiveDialTasks && i < len(ts); i++ {
t := ts[i]
srv.log.Trace("New dial task", "task", t)
go func() { t.Do(srv); taskdone <- t }()
//当前正在使用的链接
runningTasks = append(runningTasks, t)
}
return ts[i:]
}
//用来扫描服务发现的P2P节点,建立TCP连接
scheduleTasks := func() {
//先尝试用startTasks 调用 queuedTasks 列表,试图一个个建立链接,不过可能由于maxActiveDialTasks 的限制,默认16个
queuedTasks = append(queuedTasks[:0], startTasks(queuedTasks)...)
//如果已经建立的链接还不到16个,那么尝试从p2p discover 中找出更多节点来用。
if len(runningTasks) < maxActiveDialTasks {
nt := dialstate.newTasks(len(runningTasks)+len(queuedTasks), peers, time.Now())
queuedTasks = append(queuedTasks, startTasks(nt)...)
}
}
server.run 一开始在循环开始便调用scheduleTasks()函数来尝试链接对端节点,如果不够就从discover模块申请更多节点。其中比较重要的是调用Do 和 newTasks函数。
dialTask.Do 函数用来跟一个节点建立链接,节点为bootnodes, 以及s.ntab.ReadRandomNodes(s.randomNodes) 返回的节点。
func (t *dialTask) Do(srv *Server) {
if t.dest.Incomplete() {
if !t.resolve(srv) {
return
}
}
//连接某个TCP节点
err := t.dial(srv, t.dest)
if err != nil {
log.Trace("Dial error", "task", t, "err", err)
// Try resolving the ID of static nodes if dialing failed.
if _, ok := err.(*dialError); ok && t.flags&staticDialedConn != 0 {
if t.resolve(srv) {
t.dial(srv, t.dest)
}
}
}
}
func (t *dialTask) dial(srv *Server, dest *discover.Node) error {
//连接某个TCP节点,发送一个TCP连接请求
fd, err := srv.Dialer.Dial(dest)
if err != nil {
return &dialError{err}
}
mfd := newMeteredConn(fd, false)
//进行握手,创建安全连接
return srv.SetupConn(mfd, t.flags, dest)
}
newTasks 按照一定优先级,查找可以连接的节点,首先从bootnode开始,然后从p2p 服务发现的节点中随机取一些出来,返回这些节点给调用方。
func (s *dialstate) newTasks(nRunning int, peers map[discover.NodeID]*Peer, now time.Time) []task {
//按照一定优先级,查找可以连接的节点,首先从bootnode开始,然后从p2p 服务发现的节点中随机取一些出来,返回这些节点给调用方。
if s.start.IsZero() {
s.start = now
}
var newtasks []task
addDial := func(flag connFlag, n *discover.Node) bool {
//检查连接状态 , 如果连接可用,那么标记为正在连接,并且加到待连接任务newtasks 里面
if err := s.checkDial(n, peers); err != nil {
log.Trace("Skipping dial candidate", "id", n.ID, "addr", &net.TCPAddr{IP: n.IP, Port: int(n.TCP)}, "err", err)
return false
}
//标记正在进行
s.dialing[n.ID] = flag
newtasks = append(newtasks, &dialTask{flags: flag, dest: n})
return true
}
// //首先判断已经建立的连接的类型。如果是动态类型。那么需要建立动态链接数量减少。
needDynDials := s.maxDynDials
for _, p := range peers {
if p.rw.is(dynDialedConn) {
needDynDials--
}
}
//然后再判断正在建立的链接。如果是动态类型。那么需要建立动态链接数量减少
for _, flag := range s.dialing {
if flag&dynDialedConn != 0 {
needDynDials--
}
}
s.hist.expire(now)
// //查看所有的静态类型。如果可以那么也创建链接。
for id, t := range s.static {
err := s.checkDial(t.dest, peers)
switch err {
case errNotWhitelisted, errSelf:
log.Warn("Removing static dial candidate", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP, Port: int(t.dest.TCP)}, "err", err)
delete(s.static, t.dest.ID)
case nil:
s.dialing[id] = t.flags
newtasks = append(newtasks, t)
}
}
//如果当前还没有任何链接。 而且20秒(fallbackInterval)内没有创建任何链接。 那么就使用bootnode创建链接。
if len(peers) == 0 && len(s.bootnodes) > 0 && needDynDials > 0 && now.Sub(s.start) > fallbackInterval {
bootnode := s.bootnodes[0]
s.bootnodes = append(s.bootnodes[:0], s.bootnodes[1:]...)
s.bootnodes = append(s.bootnodes, bootnode)
if addDial(dynDialedConn, bootnode) {
needDynDials--
}
}
//否则使用1/2的随机节点创建链接。
randomCandidates := needDynDials / 2
if randomCandidates > 0 {
n := s.ntab.ReadRandomNodes(s.randomNodes)
for i := 0; i < randomCandidates && i < n; i++ {
if addDial(dynDialedConn, s.randomNodes[i]) {
needDynDials--
}
}
}
// Create dynamic dials from random lookup results, removing tried
// items from the result buffer.
i := 0
for ; i < len(s.lookupBuf) && needDynDials > 0; i++ {
if addDial(dynDialedConn, s.lookupBuf[i]) {
needDynDials--
}
}
s.lookupBuf = s.lookupBuf[:copy(s.lookupBuf, s.lookupBuf[i:])]
// 如果就算这样也不能创建足够动态链接。 那么创建一个discoverTask用来再网络上查找其他的节点。放入lookupBuf。
if len(s.lookupBuf) < needDynDials && !s.lookupRunning {
s.lookupRunning = true
newtasks = append(newtasks, &discoverTask{})
}
// 如果当前没有任何任务需要做,那么创建一个睡眠的任务返回。
if nRunning == 0 && len(newtasks) == 0 && s.hist.Len() > 0 {
t := &waitExpireTask{s.hist.min().exp.Sub(now)}
newtasks = append(newtasks, t)
}
return newtasks
}
server.run接下来就是简单的进行各个管道的监听,如果有事件便处理,主要还是涉及到握手协议的流程,以及进行peer的增删改操作。
p2p/server.go中的server.run代码
running:
for {
...
//下面开始监听各个管道的事件,来处理peer的增删改操作
select {
case <-srv.quit:
// The server was stopped. Run the cleanup logic.
break running
case n := <-srv.addstatic:
// This channel is used by AddPeer to add to the
// ephemeral static peer list. Add it to the dialer,
// it will keep the node connected.
srv.log.Trace("Adding static node", "node", n)
dialstate.addStatic(n)
case n := <-srv.removestatic:
// This channel is used by RemovePeer to send a
// disconnect request to a peer and begin the
// stop keeping the node connected.
srv.log.Trace("Removing static node", "node", n)
dialstate.removeStatic(n)
if p, ok := peers[n.ID]; ok {
p.Disconnect(DiscRequested)
}
case n := <-srv.addtrusted:
// This channel is used by AddTrustedPeer to add an enode
// to the trusted node set.
srv.log.Trace("Adding trusted node", "node", n)
trusted[n.ID] = true
// Mark any already-connected peer as trusted
if p, ok := peers[n.ID]; ok {
p.rw.set(trustedConn, true)
}
case n := <-srv.removetrusted:
// This channel is used by RemoveTrustedPeer to remove an enode
// from the trusted node set.
srv.log.Trace("Removing trusted node", "node", n)
if _, ok := trusted[n.ID]; ok {
delete(trusted, n.ID)
}
// Unmark any already-connected peer as trusted
if p, ok := peers[n.ID]; ok {
p.rw.set(trustedConn, false)
}
case op := <-srv.peerOp:
// This channel is used by Peers and PeerCount.
op(peers)
srv.peerOpDone <- struct{}{}
case t := <-taskdone:
// A task got done. Tell dialstate about it so it
// can update its state and remove it from the active
// tasks list.
srv.log.Trace("Dial task done", "task", t)
dialstate.taskDone(t, time.Now())
delTask(t)
case c := <-srv.posthandshake:
// A connection has passed the encryption handshake so
// the remote identity is known (but hasn't been verified yet).
if trusted[c.id] {
// Ensure that the trusted flag is set before checking against MaxPeers.
c.flags |= trustedConn
}
// TODO: track in-progress inbound node IDs (pre-Peer) to avoid dialing them.
select {
case c.cont <- srv.encHandshakeChecks(peers, inboundCount, c):
case <-srv.quit:
break running
}
case c := <-srv.addpeer:
// At this point the connection is past the protocol handshake.
// Its capabilities are known and the remote identity is verified.
err := srv.protoHandshakeChecks(peers, inboundCount, c)
if err == nil {
// The handshakes are done and it passed all checks.
p := newPeer(c, srv.Protocols)
// If message events are enabled, pass the peerFeed
// to the peer
if srv.EnableMsgEvents {
p.events = &srv.peerFeed
}
name := truncateName(c.name)
srv.log.Debug("Adding p2p peer", "name", name, "addr", c.fd.RemoteAddr(), "peers", len(peers)+1)
go srv.runPeer(p)
peers[c.id] = p
if p.Inbound() {
inboundCount++
}
}
// The dialer logic relies on the assumption that
// dial tasks complete after the peer has been added or
// discarded. Unblock the task last.
select {
case c.cont <- err:
case <-srv.quit:
break running
}
case pd := <-srv.delpeer:
// A peer disconnected.
d := common.PrettyDuration(mclock.Now() - pd.created)
pd.log.Debug("Removing p2p peer", "duration", d, "peers", len(peers)-1, "req", pd.requested, "err", pd.err)
delete(peers, pd.ID())
if pd.Inbound() {
inboundCount--
}
}
}
TCP模块靠startListening 来进行被动连接监听, server.run进行主动的连接池管理,以及连接状态跳转,peer的增删改操作。
server.run在如果连接数不够,那么会开始进行不断的尝试,按照一定优先级去查找可以连接的节点,首先从bootnode开始,然后从p2p 服务发现的节点中随机取一些出来。