以太坊C++源码解析(三)p2p(3)

我们再来深入了解一下Host类里节点和本节点是怎么交互的,在上一节可以看到节点到了Host类后,会调用Host::connect来连接对方,我们可以看下connect()函数实现代码:

void Host::connect(std::shared_ptr<Peer> const& _p)
{
    // ...
    bi::tcp::endpoint ep(_p->endpoint);
    cnetdetails << "Attempting connection to node " << _p->id << "@" << ep << " from " << id();
    auto socket = make_shared<RLPXSocket>(m_ioService);
    socket->ref().async_connect(ep, [=](boost::system::error_code const& ec)
    {
        // ...
    
        if (ec)
        {
            cnetdetails << "Connection refused to node " << _p->id << "@" << ep << " ("
                    << ec.message() << ")";
            // Manually set error (session not present)
            _p->m_lastDisconnect = TCPError;
        }
        else
        {
            cnetdetails << "Connecting to " << _p->id << "@" << ep;
            auto handshake = make_shared<RLPXHandshake>(this, socket, _p->id);
            {
                Guard l(x_connecting);
                m_connecting.push_back(handshake);
            }

            handshake->start();
        }
    
        m_pendingPeerConns.erase(nptr);
    });
}

可以看到先是创建了一个socket,然后用async_connect()异步去连接这个节点,连接成功后生成了一个RLPXHandshake类,并调用了RLPXHandshake::start()来开启握手流程,这里并没有连接成功后就传输数据,因为对方可能并不是一个ethereum节点,或者是运行协议不匹配的节点,握手流程就用来过滤掉不合格的节点,只有通过了握手流程才能进行数据交互。
注:在cpp-ethereum项目中底层数据传输用的是boost::asio库,作为准标准库中一员,boost::asio广泛应用在c++跨平台网络开发中,不熟悉的读者建议先去网络上阅读相关文档,后续文档假定读者已经了解了boost::asio库。

RLPXHandshake类

RLPXHandshake::start()函数实际调用了RLPXHandshake::transition()函数,这个函数是RLPXHandshake类的核心,从中可以看到握手的流程。

void RLPXHandshake::transition(boost::system::error_code _ech)
{
    // ...
    if (m_nextState == New)
    {
        m_nextState = AckAuth;
        if (m_originated)
            writeAuth();
        else
            readAuth();
    }
    else if (m_nextState == AckAuth)
    {
        m_nextState = WriteHello;
        if (m_originated)
            readAck();
        else
            writeAck();
    }
    else if (m_nextState == AckAuthEIP8)
    {
        m_nextState = WriteHello;
        if (m_originated)
            readAck();
        else
            writeAckEIP8();
    }
    else if (m_nextState == WriteHello)
    {
        m_nextState = ReadHello;
        // ...
    }
    else if (m_nextState == ReadHello)
    {
        // Authenticate and decrypt initial hello frame with initial RLPXFrameCoder
        // and request m_host to start session.
        m_nextState = StartSession;
        // ...
    }
}

精简后的流程还是比较清楚的,初始时候m_nextState值为New,那么正常的握手状态是New -> AckAuth -> WriteHello -> ReadHello -> StartSession。如果这些环节中某一步出错了,那么该节点不会走到最后,否则最后的状态会变成StartSession,那么到了StartSession状态后会发生什么事呢?我们再看看看这部分代码:

    else if (m_nextState == ReadHello)
    {
        // Authenticate and decrypt initial hello frame with initial RLPXFrameCoder
        // and request m_host to start session.
        m_nextState = StartSession;
    
        // read frame header
        unsigned const handshakeSize = 32;
        m_handshakeInBuffer.resize(handshakeSize);
        ba::async_read(m_socket->ref(), boost::asio::buffer(m_handshakeInBuffer, handshakeSize), [this, self](boost::system::error_code ec, std::size_t)
        {
            if (ec)
                transition(ec);
            else
            {
                // ...
            
                /// rlp of header has protocol-type, sequence-id[, total-packet-size]
                bytes headerRLP(header.size() - 3 - h128::size);    // this is always 32 - 3 - 16 = 13. wtf?
                bytesConstRef(&header).cropped(3).copyTo(&headerRLP);
            
                /// read padded frame and mac
                m_handshakeInBuffer.resize(frameSize + ((16 - (frameSize % 16)) % 16) + h128::size);
                ba::async_read(m_socket->ref(), boost::asio::buffer(m_handshakeInBuffer, m_handshakeInBuffer.size()), [this, self, headerRLP](boost::system::error_code ec, std::size_t)
                {
                    // ...
                
                    if (ec)
                        transition(ec);
                    else
                    {
                        // ...
                        try
                        {
                            RLP rlp(frame.cropped(1), RLP::ThrowOnFail | RLP::FailIfTooSmall);
                            m_host->startPeerSession(m_remote, rlp, move(m_io), m_socket);
                        }
                        catch (std::exception const& _e)
                        {
                            cnetlog << "Handshake causing an exception: " << _e.what();
                            m_nextState = Error;
                            transition();
                        }
                    }
                });
            }
        });
    }

当状态从ReadHelloStartSession转变时,连续收了两个包,然后调用了Host::startPeerSession(),节点在RLPXHandshake类转了一圈以后,如果合格的话又回到了Host类中,从此开始新的征程。

Host类

我们之前看到Host类通过requirePeer()函数推动了P2P发现模块的运转,但同时它又是整个P2P传输模块中的发动机,因此要研究ethereum网络部分需要从这里开始。
我们在libp2p\Host.h文件中找到Host类定义,其中有两个成员变量,熟悉boost::asio库的读者一定不陌生:

ba::io_service m_ioService;
bi::tcp::acceptor m_tcp4Acceptor;

其中m_ioService就是Host类的核心了,它负责处理异步任务,当异步任务完成后调用完成句柄。
m_tcp4Acceptor是负责接收连接的对象,它内部封装了一个socket对象。我们都知道服务端的socket需要经过创建,绑定IP端口,侦听,Accept这几个阶段,对于m_tcp4Acceptor而言也是这样:

  • 创建

直接在Host类初始化列表中进行创建

  • 绑定IP端口和侦听

这部分是在Network::tcp4Listen()函数中完成的:

  for (unsigned i = 0; i < 2; ++i)
  {
      bi::tcp::endpoint endpoint(listenIP, requirePort ? _netPrefs.listenPort : (i ? 0 : c_defaultListenPort));
      try
      {
          /// ...
          _acceptor.open(endpoint.protocol());
          _acceptor.set_option(ba::socket_base::reuse_address(reuse));
          _acceptor.bind(endpoint);
          _acceptor.listen();
          return _acceptor.local_endpoint().port();
      }
      catch (...)
      {
          // bail if this is first attempt && port was specificed, or second attempt failed (random port)
          if (i || requirePort)
          {
              // both attempts failed
              cwarn << "Couldn't start accepting connections on host. Failed to accept socket on " << listenIP << ":" << _netPrefs.listenPort << ".\n" << boost::current_exception_diagnostic_information();
              _acceptor.close();
              return -1;
          }
        
          _acceptor.close();
          continue;
      }
   }

注意到这里有一个循环,是用来防止端口被占用的。如果第一次端口被占用,则第二次使用0端口,也就是随机端口。
在这个函数里,_acceptor依次完成了设置协议,设置端口重用,绑定端口和侦听。

  • Accept

又回到了Host类,在Host::runAcceptor()函数中,我们能找到以下代码:

auto socket = make_shared<RLPXSocket>(m_ioService); 
m_tcp4Acceptor.async_accept(socket->ref(), [=](boost::system::error_code ec)
{
    // ...
    try
    {
        // incoming connection; we don't yet know nodeid
        auto handshake = make_shared<RLPXHandshake>(this, socket);
        m_connecting.push_back(handshake);
        handshake->start();
        success = true;
    }
    catch (Exception const& _e)
    {
        cwarn << "ERROR: " << diagnostic_information(_e);
    }
    catch (std::exception const& _e)
    {
        cwarn << "ERROR: " << _e.what();
    }

    if (!success)
        socket->ref().close();
    runAcceptor();
});

m_tcp4Acceptor通过async_accept()异步接收连接,当一个连接到来的时候发生了什么?我们又看到了熟悉的代码,是的!创建了一个RLPXHandshake类,又开始了握手流程。ethereum对于接收到的连接也是谨慎的,同样需要先进行校验,这里的握手流程与前面connect时的流程稍有不同,区别就在RLPXHandshake::m_originated上,connect时的m_originated值为true,也就是先向对方发送自己的Auth包,而被动接收时m_originated为false,会等待对方发过来Auth包。
最后别忘了启动Host::m_ioService,这部分被放在doWork()函数里,还记得doWork()函数吗?因为Host类是从Worker类继承而来,doWork()会在一个循环中被调用。

void Host::doWork()
{
    try
    {
        if (m_run)
            m_ioService.run();
    }
    catch (std::exception const& _e)
    {
        // ...
    }
}

但是doWork()不是会被循环调用的吗?难道m_ioService.run()也会重复调用吗?答案是不会,因为m_ioService.run()会阻塞在这里,所以只会执行一次。
至此m_tcp4Acceptor能够愉快地接收到TCP连接,并把连接交给RLPXHandshake类去处理了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,647评论 18 139
  • K桶的实现 这里借用一下别人的图: 这个定义等同于 其中s_bins值为255,根据上面的图,s_bins应该是2...
    sky2016阅读 1,638评论 0 0
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,920评论 2 89
  • 春天来了,我猜你一定想美美的装扮起来了。来条连衣裙美美的和春天来场约会吧! 时尚潮流是我们的追求,却不是谁都能够欣...
    东紫形象阅读 271评论 0 0
  • 在深夜跟你讲一个离你很近的故事,并说一声“晚安”。 晓雯子的晚安故事003—今天选择你,余生都是你。 誓言,有时候...
    晓雯子子阅读 190评论 0 0