区块链特辑 :https://blog.csdn.net/fusan2004/article/details/80879343,欢迎查阅,原创作品,转载请标明!
这周工作有点小忙,部门区块链基础平台的开发开始进入节奏了,和上一篇间隔间隔有点久了,以后还是要坚持,不能刚开始就犯毛病了。上篇讲的是以太坊p2p网络的一个重点部分——节点发现,在介绍的时候提过,节点发现是通过udp的方式来进行的,这一篇就介绍下udp通信的详细细节,这部分不是很多,算是个过渡吧。
回头再看下NodeTable类,可以看到这个类继承了UDPSocketEvents类,也就是这里和udp通信建立了关联,下面我们先看下这个类都干了些啥。。。
struct UDPSocketEvents
{
virtual ~UDPSocketEvents() = default;
virtual void onDisconnected(UDPSocketFace*) {}
virtual void onReceived(UDPSocketFace*, bi::udp::endpoint const& _from, bytesConstRef _packetData) = 0;
};
可以看到,这个类只提供了两个api接口,分别是onDisconnected和onReceived,其中onDisconnected有一个定义,空的函数体,在看代码的时候也能发现这个函数目前的代码并没有起到什么作用,可暂时忽略;另外的onReceived是一个纯虚函数,也就意味着NodeTable必须要实现这个函数,再回头去看NodeTable类中关于这个函数的详细编码,可以看到这是整个NodeTable更新发现的重要起点,也就是在这里解析了前面所描述的ping\pong\findnode\neighbours这四类消息,具体这四类消息,我们后面再细说,现在继续说说UDPSocketEvents是如何跟udp关联上的,看看NodeTable的构造函数实现。。。
NodeTable::NodeTable(ba::io_service& _io, KeyPair const& _alias, NodeIPEndpoint const& _endpoint, bool _enabled):
m_node(Node(_alias.pub(), _endpoint)),
m_secret(_alias.secret()),
m_socket(make_shared(_io, *reinterpret_cast(this), (bi::udp::endpoint)m_node.endpoint)),
m_socketPointer(m_socket.get()),
m_timers(_io)
{
for (unsigned i = 0; i < s_bins; i++)
m_state[i].distance = i;
if (!_enabled)
return;
try
{
m_socketPointer->connect(); //开启连接,这时候就可以接受外界发来的消息了,m_socketPointer指定了回调句柄就是NodeTable
doDiscovery(); //节点发现
}
catch (std::exception const& _e)
{
cwarn << "Exception connecting NodeTable socket: " << _e.what();
cwarn << "Discovery disabled.";
}
}
可以看到m_socket就是一个NodeSocket的实例,这个NodeSocket的构造函数中需要this指针传递,后面都将通过这个指针完成onReceived的回调,逻辑很清晰啦,继续看下NodeSocket吧。。。
template
class UDPSocket: UDPSocketFace, public std::enable_shared_from_this>
{
public:
enum { maxDatagramSize = MaxDatagramSize };
static_assert((unsigned)maxDatagramSize < 65507u, "UDP datagrams cannot be larger than 65507 bytes");
/// Create socket for specific endpoint.
//为指定的endpoint创建socket
UDPSocket(ba::io_service& _io, UDPSocketEvents& _host, bi::udp::endpoint _endpoint): m_host(_host), m_endpoint(_endpoint), m_socket(_io) { m_started.store(false); m_closed.store(true); };
/// Create socket which listens to all ports.
UDPSocket(ba::io_service& _io, UDPSocketEvents& _host, unsigned _port): m_host(_host), m_endpoint(bi::udp::v4(), _port), m_socket(_io) { m_started.store(false); m_closed.store(true); };
virtual ~UDPSocket() { disconnect(); }
/// Socket will begin listening for and delivering packets
// 开始监听并传送数据包
void connect();
/// Send datagram. 发送数据报
bool send(UDPDatagram const& _datagram);
/// Returns if socket is open.
bool isOpen() { return !m_closed; }
/// Disconnect socket.
void disconnect() { disconnectWithError(boost::asio::error::connection_reset); }
protected:
void doRead(); //进行读操作
void doWrite(); //进行写操作
void disconnectWithError(boost::system::error_code _ec); //断开
std::atomic m_started; ///< Atomically ensure connection is started once. Start cannot occur unless m_started is false. Managed by start and disconnectWithError.
std::atomic m_closed; ///< Connection availability.
UDPSocketEvents& m_host; ///< Interface which owns this socket.也就是NodeTable
bi::udp::endpoint m_endpoint; ///< Endpoint which we listen to. 没有监听一说,其实就是一直从这个endpoint上读数据
Mutex x_sendQ;
std::deque m_sendQ; ///< Queue for egress data. 发送数据的队列
std::array m_recvData; ///< Buffer for ingress data. 接受数据的队列
bi::udp::endpoint m_recvEndpoint; ///< Endpoint data was received from. 接受数据的来源
bi::udp::socket m_socket; ///< Boost asio udp socket.
Mutex x_socketError; ///< Mutex for error which can be set from host or IO thread.
boost::system::error_code m_socketError; ///< Set when shut down due to error.
};
这里面直接看函数其实没啥意思,我们还是从流程上来观摩这些代码,在上面的构造函数中,当socket创建完毕之后,紧接着就调用了connect函数,这个函数里就是定义了可以从这个udp socket进行异步读事件,详细可以看下这个函数的代码。。。
template
void UDPSocket::connect()
{
bool expect = false;
if (!m_started.compare_exchange_strong(expect, true))
return;
m_socket.open(bi::udp::v4());
try
{
m_socket.bind(m_endpoint); //绑定本地地址和端口
}
catch (...)
{
m_socket.bind(bi::udp::endpoint(bi::udp::v4(), m_endpoint.port()));
}
//因为connect只会调用一次,为了不发送旧消息,需要清理写数据的队列
Guard l(x_sendQ);
m_sendQ.clear();
m_closed = false;
doRead(); //上面只是绑定了端点信息,这里面真正定义了异步读事件
}
可以看到,这个函数目前还没有实质性的,继续看下doRead函数。。。
template
void UDPSocket::doRead()
{
if (m_closed)
return;
auto self(UDPSocket::shared_from_this());
//异步读读事件,这个函数是说,如果有数据来了,请把数据放到m_recvData中,m_recvEndpoint会记录数据来源的端点信息,读取完成后回调函数
m_socket.async_receive_from(boost::asio::buffer(m_recvData), m_recvEndpoint, [this, self](boost::system::error_code _ec, size_t _len)
{
if (m_closed)
return disconnectWithError(_ec);
if (_ec != boost::system::errc::success)
cnetlog << "Receiving UDP message failed. " << _ec.value() << " : " << _ec.message();
if (_len) //每次上传给上层,由上层处理响应数据
m_host.onReceived(this, m_recvEndpoint, bytesConstRef(m_recvData.data(), _len));
doRead();
});
}
终于看到了onReceived了,可以看到如果有数据过来了,只要读取正常了,udp是不负责数据完整性的,直接会把数据和读取到的长度信息抛给了上层,也就是NodeTable来处理,这也是为啥NodeTable的onReceived的代码里有很大一部分是来处理数据校验的,上层处理完毕之后,继续doRead来进行下一次的数据请求的监听,这个循环就介绍完了;有读就有写呀,写操作都是上层调用了send函数来触发的,看下send函数。。。
template
bool UDPSocket::send(UDPDatagram const& _datagram)
{
if (m_closed)
return false;
Guard l(x_sendQ);
m_sendQ.push_back(_datagram); //先放到队列中去
if (m_sendQ.size() == 1) //一旦有了,立马进行doWrite
doWrite();
return true;
}
大家可能会奇怪,为啥是队列大小为1的时候才进行doWrite,这里解释下,可以看到在队列进行push的时候是有锁的,也就是说队列push肯定是串行的,但是doWrite立马待会可以看到,是一个努力的小伙子,不把队列立马的全清空不罢休,所以也就是说只有队列里有数据,doWrite是一直被调用的,只有这个socket闲下来了,突然来了一个数据报,才需要重新调用doWrite,好吧,来看看勤奋的小伙子怎么做的。。。
template
void UDPSocket::doWrite()
{
if (m_closed)
return;
const UDPDatagram& datagram = m_sendQ[0]; //取出队列中第一个数据,而且第一个数据肯定存在,由前面保证的
auto self(UDPSocket::shared_from_this());
bi::udp::endpoint endpoint(datagram.endpoint()); //数据报中包含了目标地址
m_socket.async_send_to(boost::asio::buffer(datagram.data), endpoint, [this, self, endpoint](boost::system::error_code _ec, std::size_t)
{
if (m_closed) //关闭了
return disconnectWithError(_ec);
if (_ec != boost::system::errc::success)
cnetlog << "Failed delivering UDP message. " << _ec.value() << " : " << _ec.message();
Guard l(x_sendQ);
m_sendQ.pop_front(); //发送成功,将队列头数据剔除
if (m_sendQ.empty()) //勤奋的小伙子完成了工作,退出
return;
doWrite(); //没有完成,继续写
});
}
好了,这就是udp连接、接受、发送的全部流程啦,其实也就是udp的几个方法的使用,很简单。前面提到了,节点发现包含了4中消息类型,下面介绍下这些消息的格式,首先这4个消息都有一些共同的父类,慢慢来看,先看下最最上层的UDPDatagram类。。。
class UDPDatagram
{
public:
UDPDatagram(bi::udp::endpoint const& _ep): locus(_ep) {}
UDPDatagram(bi::udp::endpoint const& _ep, bytes _data): data(_data), locus(_ep) {}
bi::udp::endpoint const& endpoint() const { return locus; }
bytes data; //携带的消息体
protected:
bi::udp::endpoint locus; //待发送的目的地址
};
很简单的,不需要多说啥,继续。。。
struct RLPXDatagramFace: public UDPDatagram
{
static uint32_t futureFromEpoch(std::chrono::seconds _sec) { return static_cast(std::chrono::duration_cast((std::chrono::system_clock::now() + _sec).time_since_epoch()).count()); }
static uint32_t secondsSinceEpoch() { return static_cast(std::chrono::duration_cast((std::chrono::system_clock::now()).time_since_epoch()).count()); }
static Public authenticate(bytesConstRef _sig, bytesConstRef _rlp);
RLPXDatagramFace(bi::udp::endpoint const& _ep): UDPDatagram(_ep) {}
virtual ~RLPXDatagramFace() = default;
virtual h256 sign(Secret const& _from);
virtual uint8_t packetType() const = 0;
virtual void streamRLP(RLPStream&) const = 0;
virtual void interpretRLP(bytesConstRef _bytes) = 0;
};
这个类是个纯虚类,也就是说没干啥具体的事情,只不过从名字我们可以看出来,udp通信的数据采用了rlp来进行了编解码,streamRLP就是rlp编码,interpretRLP就是rlp解码,继续。。。
struct DiscoveryDatagram: public RLPXDatagramFace
{
/// Constructor used for sending. 这是用来发送数据报时调用的构造函数
DiscoveryDatagram(bi::udp::endpoint const& _to): RLPXDatagramFace(_to), ts(futureFromEpoch(std::chrono::seconds(60))) {}
/// Constructor used for parsing inbound packets. 这是接受到数据报时定义的构造函数
DiscoveryDatagram(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): RLPXDatagramFace(_from), sourceid(_fromid), echo(_echo) {}
// These two are set for inbound packets only. 下面定义的变量只有在接受的数据报中才会用到
NodeID sourceid; // sender public key (from signature) 发送方的公钥
h256 echo; // hash of encoded packet, for reply tracking 编码过的数据报的哈希值,用来回复响应的跟踪
// All discovery packets carry a timestamp, which must be greater 所有用来节点发现的数据报都包含了一个时间戳,
// than the current local time. This prevents replay attacks. 这个时间戳必须大于当前时间,防止重复攻击
uint32_t ts = 0;
bool isExpired() const { return secondsSinceEpoch() > ts; } //是否过期,ts里面定义的是发送时刻+60s
/// Decodes UDP packets. 解码udp数据报
static std::unique_ptr interpretUDP(bi::udp::endpoint const& _from, bytesConstRef _packet);
};
这里面定义了节点发现消息的直接父类,后面的四个消息类型都是基于这个类定义的,从这个类中我们总结几个事情,一个是发送消息和接受消息的定义是不一样,接受消息需要看下节点id和echo,来知道是什么人发的数据,已经发的是不是我要的,二是定义了过期策略,这个也就是在udp实现中需要考虑,因为udp是无连接的,必须要自己控制,接下来正经看看那四个消息吧。。。
/**
* Ping packet: Sent to check if node is alive.
* PingNode is cached and regenerated after ts + t, where t is timeout.
* ping数据报,用于检测node是否是活的,被缓存,过期后重新生成
* Ping is used to implement evict. When a new node is seen for / ping用来完成淘汰,一个给定bucket的新node到来时如果满了的话
* a given bucket which is full, the least-responsive node is pinged. // 最少回复的node会被ping
* If the pinged node doesn't respond, then it is removed and the new
* node is inserted. 如果被ping的node没有被回复,那么将会被移除,新节点会被插入
*/
struct PingNode: DiscoveryDatagram
{
using DiscoveryDatagram::DiscoveryDatagram;
PingNode(NodeIPEndpoint const& _src, NodeIPEndpoint const& _dest): DiscoveryDatagram(_dest), source(_src), destination(_dest) {}
PingNode(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
static const uint8_t type = 1;
uint8_t packetType() const { return type; }
unsigned version = 0; //版本号
NodeIPEndpoint source; //源节点
NodeIPEndpoint destination; //目的节点
void streamRLP(RLPStream& _s) const //生成rlp编码数据
{
_s.appendList(4);
_s << dev::p2p::c_protocolVersion; //协议版本号
source.streamRLP(_s); //写入源节点
destination.streamRLP(_s); //写入目的节点
_s << ts; //超时时间节点
}
void interpretRLP(bytesConstRef _bytes) //rlp解码
{
RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
version = r[0].toInt(); //版本好
source.interpretRLP(r[1]); //源节点
destination.interpretRLP(r[2]); //目的节点
ts = r[3].toInt(); //时间
}
};
从上面这段代码可以看到ping数据组成形式为:版本|源地址|目的地址|超时时间,继续看下响应的pong数据报结构。。。
/**
* Pong packet: Sent in response to ping,ping的回复
*/
struct Pong: DiscoveryDatagram
{
Pong(NodeIPEndpoint const& _dest): DiscoveryDatagram((bi::udp::endpoint)_dest), destination(_dest) {}
Pong(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
static const uint8_t type = 2;
uint8_t packetType() const { return type; }
NodeIPEndpoint destination; //目的地址
void streamRLP(RLPStream& _s) const // rlp编码
{
_s.appendList(3);
destination.streamRLP(_s); //目的地址
_s << echo; //ping包的哈希值,貌似后面也没有用到
_s << ts; //时间
}
void interpretRLP(bytesConstRef _bytes)
{
RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
destination.interpretRLP(r[0]);
echo = (h256)r[1];
ts = r[2].toInt();
}
};
可以看到pong数据报更简单,只包括了目的地址|echo|过期时间,其实代码里只关系了,这个节点所携带的node_id,继续看findnode的数据报结构。。。
struct FindNode: DiscoveryDatagram
{
FindNode(bi::udp::endpoint _to, h512 _target): DiscoveryDatagram(_to), target(_target) {}
FindNode(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
static const uint8_t type = 3;
uint8_t packetType() const { return type; }
h512 target; //待寻找的目标节点
void streamRLP(RLPStream& _s) const
{
_s.appendList(2); _s << target << ts; //更简单了,只有目标节点和超时时间
}
void interpretRLP(bytesConstRef _bytes)
{
RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
target = r[0].toHash();
ts = r[1].toInt();
}
};
FindNode的数据结构也很简单,只包括了目标节点id和超时时间,最后看下Neighbours的数据报。。。
struct Neighbours: DiscoveryDatagram
{ //Neighbours数据报,可能包含0个或多个Neighbour节点
Neighbours(bi::udp::endpoint _to, std::vector> const& _nearest, unsigned _offset = 0, unsigned _limit = 0): DiscoveryDatagram(_to)
{
auto limit = _limit ? std::min(_nearest.size(), (size_t)(_offset + _limit)) : _nearest.size();
for (auto i = _offset; i < limit; i++)
neighbours.push_back(Neighbour(*_nearest[i]));
}
Neighbours(bi::udp::endpoint const& _to): DiscoveryDatagram(_to) {} //发送方
Neighbours(bi::udp::endpoint const& _from, NodeID const& _fromid, h256 const& _echo): DiscoveryDatagram(_from, _fromid, _echo) {}
struct Neighbour //定义一个Neighbour结构
{
Neighbour(Node const& _node): endpoint(_node.endpoint), node(_node.id) {} //包含端点信息,节点id
Neighbour(RLP const& _r): endpoint(_r) { node = h512(_r[3].toBytes()); }
NodeIPEndpoint endpoint;
NodeID node;
void streamRLP(RLPStream& _s) const { _s.appendList(4); endpoint.streamRLP(_s, NodeIPEndpoint::StreamInline); _s << node; }
};
static const uint8_t type = 4;
uint8_t packetType() const { return type; }
std::vector neighbours;
void streamRLP(RLPStream& _s) const
{
_s.appendList(2);
_s.appendList(neighbours.size());
for (auto const& n: neighbours)
n.streamRLP(_s);
_s << ts;
}
void interpretRLP(bytesConstRef _bytes)
{
RLP r(_bytes, RLP::AllowNonCanon|RLP::ThrowOnFail);
for (auto const& n: r[0])
neighbours.emplace_back(n);
ts = r[1].toInt();
}
};
Neighbours数据报主要包括三部分数据,节点数 | 节点信息 | 超期时间。上述就是节点通信过程的四种数据类型,结构比较简单,就是发送方与接收方对节点的初始化是不一样的,这点看的时候可能比较绕,但是理解清楚了之后就明白了。最后还要介绍一点事情,就是这四个数据报的结构是转化成rlp数据再通过udp发送的,在发送前,会对这些数据包进行了签名的操作,这部分代码是在RLPXDatagramFace中sign函数完成的,这个函数的详细定义是在udp.cpp中,最后看下这个函数来结束这篇博客吧。。。
h256 RLPXDatagramFace::sign(Secret const& _k) //签名算法
{
assert(packetType());
RLPStream rlpxstream;
// rlpxstream.appendRaw(toPublic(_k).asBytes()); // for mdc-based signature
rlpxstream.appendRaw(bytes(1, packetType())); // prefix by 1 byte for type,第一个字节是类型
streamRLP(rlpxstream); //继续往里面添加数据,得到rlp数据
bytes rlpxBytes(rlpxstream.out()); //放到rlpxBytes中
bytesConstRef rlpx(&rlpxBytes);
h256 sighash(dev::sha3(rlpx)); // H(type||data),计算hash
Signature sig = dev::sign(_k, sighash); // S(H(type||data)) 对哈希值签名,用_k私钥
data.resize(h256::size + Signature::size + rlpx.size()); //数据报的数据,
bytesRef rlpxHash(&data[0], h256::size);
bytesRef rlpxSig(&data[h256::size], Signature::size);
bytesRef rlpxPayload(&data[h256::size + Signature::size], rlpx.size());
sig.ref().copyTo(rlpxSig);
rlpx.copyTo(rlpxPayload);
bytesConstRef signedRLPx(&data[h256::size], data.size() - h256::size);
dev::sha3(signedRLPx).ref().copyTo(rlpxHash);
// data
// h256:: size Signature::size rlpx.size()
// 0 -> h256::size | h256::size + Signature::size | hash::size + Signature + rlp.size
// 对后面的数据又进行了一次哈希的哈希值 | 对类型+数据的rlp的bytes进行哈希后的签名 | 类型+数据的rlp的bytes
return sighash;
}