1.网络协议
比特币是采用的P2P网络,因为去中心化,节点对等,这个网络是非常适合比特币的。而这个P2P的实现,在比特币中是采用TCP协议的。
所以我们如果要入手,得了解C++的网络部分,如何构建TCP协议通信。这里调用的是windows API来实现,如果你之前了解,学习起来事半功倍,不了解也没头系,我们会在讲源码的时候,附带说明TCP部分。
我们可以从net.cpp里看到这句:
cpp
#include <winsock2.h>
这就是windows 网络API所在头文件。说明是用windows的winsock实现网络通信的。
2.开始
我们从发送比特币交易这里入手,这里肯定是要调用网络广播的。我们循着它找到构建网络的源头。
从最顶层的函数开始:
cpp
void CSendDialog::OnButtonSend(wxCommandEvent& event)
在里面调用了SendMoney函数:
cpp
if (!SendMoney(scriptPubKey, nValue, wtx))
return;
第一个参数是收款人脚本(含有收款人账号),nValue是收款人金额,wtx是当前要构建的tx。
3.RelayWalletTransaction
在SendMoney里当wtx构建好了,就会调用如下函数进行广播:
cpp
wtxNew.RelayWalletTransaction();
然后会传入本地存储有交易索引的数据库对象,供后续相关查找使用:
cpp
void RelayWalletTransaction(CTxDB& txdb);
void RelayWalletTransaction() { CTxDB txdb("r"); RelayWalletTransaction(txdb); }
看一下真正干活的这个重载广播函数代码:
cpp
void CWalletTx::RelayWalletTransaction(CTxDB& txdb)
{
foreach(const CMerkleTx& tx, vtxPrev)
{
if (!tx.IsCoinBase())
{
uint256 hash = tx.GetHash();
if (!txdb.ContainsTx(hash))
RelayMessage(CInv(MSG_TX, hash), (CTransaction)tx);
}
}
if (!IsCoinBase())
{
uint256 hash = GetHash();
if (!txdb.ContainsTx(hash))
{
printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str());
RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);
}
}
}
我们可以看到,新建一笔tx,应该只是广播一笔tx,但是在里面,它调用了好几次RelayMessage来广播tx。这是为什么呢?
是因为,在你广播这笔tx的时候,由于担心你引用的tx,比如可能就是在上一个区块,那么这时候可能别的节点有可能没接收到,没上链。所以它会一同广播,当然,它会有限制的,引用的tx如果离现在的区块不是很近,那么它不会去判断有没有上链,从而广播,而是默认都在链上。
这个设计,主要是为了解决因为网络延迟导致当前tx先于引用tx的问题。
在这个函数前部分代码,即:
cpp
foreach(const CMerkleTx& tx, vtxPrev)
{
if (!tx.IsCoinBase())
{
uint256 hash = tx.GetHash();
if (!txdb.ContainsTx(hash))
RelayMessage(CInv(MSG_TX, hash), (CTransaction)tx);
}
}
它就会遍历vtxPrev,这些都是引用的tx,然后判断数据库里有没有这个tx,如果没有,那说明可能没上链,所以它会再广播一次。(当然你的数据库有了,别人的数据库也不一定有,这里的设计只是一道保险,并且能省掉一些事,当然别人如果真的找不到引用的交易,也会有相关的处理方法)
4.AddSupportingTransactions
我们可以看到,是从vtxPrev查找引用的tx,那这个vtxPrev是从哪里构建的呢?
在如下函数构建:
cpp
wtxNew.AddSupportingTransactions(txdb);
这个函数,在CreateTransaction创建交易里被调用,也即SendMoney里的CreateTransaction。
我们来看一下这个函数的代码:
cpp
void CWalletTx::AddSupportingTransactions(CTxDB& txdb)
{
vtxPrev.clear();
const int COPY_DEPTH = 3;
if (SetMerkleBranch() < COPY_DEPTH)
{
vector<uint256> vWorkQueue;
foreach(const CTxIn& txin, vin)
vWorkQueue.push_back(txin.prevout.hash);
// This critsect is OK because txdb is already open
CRITICAL_BLOCK(cs_mapWallet)
{
map<uint256, const CMerkleTx*> mapWalletPrev;
set<uint256> setAlreadyDone;
for (int i = 0; i < vWorkQueue.size(); i++)
{
uint256 hash = vWorkQueue[i];
if (setAlreadyDone.count(hash))
continue;
setAlreadyDone.insert(hash);
CMerkleTx tx;
if (mapWallet.count(hash))
{
tx = mapWallet[hash];
foreach(const CMerkleTx& txWalletPrev, mapWallet[hash].vtxPrev)
mapWalletPrev[txWalletPrev.GetHash()] = &txWalletPrev;
}
else if (mapWalletPrev.count(hash))
{
tx = *mapWalletPrev[hash];
}
else if (!fClient && txdb.ReadDiskTx(hash, tx))
{
;
}
else
{
printf("ERROR: AddSupportingTransactions() : unsupported transaction\n");
continue;
}
int nDepth = tx.SetMerkleBranch();
vtxPrev.push_back(tx);
if (nDepth < COPY_DEPTH)
foreach(const CTxIn& txin, tx.vin)
vWorkQueue.push_back(txin.prevout.hash);
}
}
}
reverse(vtxPrev.begin(), vtxPrev.end());
}
5.vtxPrev挑选规则
我们可以看到关键代码:
cpp
const int COPY_DEPTH = 3;
if (SetMerkleBranch() < COPY_DEPTH)
COPY_DEPTH=3,限制了引用的深度,3个深度,也就是说引用的tx超过了3个深度,它就不会把这个tx加入到vtxPrev里面,注意这个3个深度,并不是前面三个区块的意思,关于是什么,因为涉及到默克尔树,而我们现在重点是网络部分,所以这里的算法现在不分析了。
我们现在只需要大概知道其做什么就行了,在最后面可以看到添加代码:
cpp
int nDepth = tx.SetMerkleBranch();
vtxPrev.push_back(tx);
这个函数的分析将会放在后续章节,写完默克尔树的时候奉上,因为之前的默克尔树只写了一部分,它不仅仅是对数据的摘要,只有这一个功能,生成默克尔树,还有相应的查找和验证功能。
好,当广播完引用的tx后,才是到我们真正要广播的tx了,即广播我们新建的tx:
cpp
if (!IsCoinBase())
{
uint256 hash = GetHash();
if (!txdb.ContainsTx(hash))
{
printf("Relaying wtx %s\n", hash.ToString().substr(0,6).c_str());
RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);
}
}
但是我们发现,即使是按照SendMoney逻辑,新建的tx,这里还是判断一下是否存在于数据库里再广播,这句:
cpp
if (!txdb.ContainsTx(hash))
这其实也是为了写入成功,避免重复广播。因为RelayWalletTransaction函数可能被多次调用(包括其它地方),所以会加入这个判断。
6.RelayMessage
接下来我们来看广播的核心代码,也是我们越来越接近网络的代码,已经带有消息标志头了--MSG_TX:
cpp
RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);
我们来看一下这个函数的代码:
cpp
template<typename T>
void RelayMessage(const CInv& inv, const T& a)
{
CDataStream ss(SER_NETWORK);
ss.reserve(10000);
ss << a;
RelayMessage(inv, ss);
}
这是一个模板函数,将tx序列化ss,然后调用重载:
cpp
RelayMessage(inv, ss);
RelayMessage"重载"函数代码:
cpp
template<>
inline void RelayMessage<>(const CInv& inv, const CDataStream& ss)
{
CRITICAL_BLOCK(cs_mapRelay)
{
// Expire old relay messages
while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime())
{
mapRelay.erase(vRelayExpiration.front().second);
vRelayExpiration.pop_front();
}
// Save original serialized message so newer versions are preserved
mapRelay[inv] = ss;
vRelayExpiration.push_back(make_pair(GetTime() + 15 * 60, inv));
}
RelayInventory(inv);
}
注意到这个函数上的template<>吗,这其实是一个显式模板特化函数,为CDataStream类型单独写一个执行函数,否则会执行最上面的那个模板函数,毕竟const T& a当然也包括CDataStream类型。
这个函数干什么呢?他会把消息存起来,除了广播行为,但是他只缓存15分钟,我们可以看到代码设定:
cpp
mapRelay[inv] = ss;
vRelayExpiration.push_back(make_pair(GetTime() + 15 * 60, inv));
其中GetTime()+15*60,就是预设的过期时间。
然后这句判断:
cpp
while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime())
当消息缓存里不为空并且,有过期的消息,当前时间已经大于预设的过期时间,那么就进行删除。
这里的front()返回的是最早添加的元素,所以它这里循环判断依次删除。这样里面剩下的都是没过期的元素了。
7. RelayInventory
删除完过期的tx后,接着会广播这条tx了:
cpp
RelayInventory(inv);
但是你发现没,它并没有广播具体的数据ss,只是广播了一个inv,里面标识了这是条tx消息,以及这条tx的哈希值。
其实这是在告诉别的节点,我这里有条这样的tx,你们要不要。然后别的节点确定后(确认没有),就会回复(通过getdata方式),接着才会发送具体tx数据过去。
这样设计,可以节省网络资源。
我们来看一下函数的具体代码:
cpp
inline void RelayInventory(const CInv& inv)
{
// Put on lists to offer to the other nodes
CRITICAL_BLOCK(cs_vNodes)
foreach(CNode* pnode, vNodes)
pnode->PushInventory(inv);
}
这里的意思,给邻居节点都发送一下,vNodes保存了已知的连接上的邻居节点。
这里的关键在这个函数:
cpp
pnode->PushInventory(inv);
代码如下:
cpp
void PushInventory(const CInv& inv)
{
CRITICAL_BLOCK(cs_inventory)
if (!setInventoryKnown.count(inv))
vInventoryToSend.push_back(inv);
}
先是setInventoryKnown判断这条消息是否已经发送过,如果没发送,则添加到消息队列vInventoryToSend中。我们可以看到代码执行流程到这里就结束了,并没有网络发送相关。
那这里只是添加消息队例操作。发送方面是在另一个函数中。
8.SendMessages
我们看到接下来的处理是 SendMessages函数中,我们来看这个函数里关于Inventory这一段代码:
cpp
//
// Message: inventory
//
vector<CInv> vInventoryToSend;
CRITICAL_BLOCK(pto->cs_inventory)
{
vInventoryToSend.reserve(pto->vInventoryToSend.size());
foreach(const CInv& inv, pto->vInventoryToSend)
{
// returns true if wasn't already contained in the set
if (pto->setInventoryKnown.insert(inv).second)
vInventoryToSend.push_back(inv);
}
pto->vInventoryToSend.clear();
pto->setInventoryKnown2.clear();
}
if (!vInventoryToSend.empty())
pto->PushMessage("inv", vInventoryToSend);
其中pto就是节点,而pto->vInventoryToSend就是之前节点的添加的消息队例,这段代码的作用是,将pto里的vInventoryToSend复制到局部定义的VInventoryToSend,并去重,我们可以看到这句:
cpp
if (pto->setInventoryKnown.insert(inv).second)
vInventoryToSend.push_back(inv);
将这个消息添加到setInventoryKnown里,如果已存在,则失败,就不会执行vInventoryToSend。这是去重式添加到vInventoryToSend。所以从这里可以看出,这什么之前这段代码:
cpp
void PushInventory(const CInv& inv)
{
CRITICAL_BLOCK(cs_inventory)
if (!setInventoryKnown.count(inv))
vInventoryToSend.push_back(inv);
}
通过setInventoryKnown判断消息是否发送过,没发送,但只执行了一句代码,并没有把这个未发送的消息添加到setInventoryKnown,是因为这个逻辑在SendMessage函数里处理添加。
9.PushMessage
这样去重生成的vInventoryToSend后,才是要发送的消息(没发送过的),代码如下:
cpp
if (!vInventoryToSend.empty())
pto->PushMessage("inv", vInventoryToSend);
我们来看一下这个函数的代码:
cpp
template<typename T1>
void PushMessage(const char* pszCommand, const T1& a1)
{
try
{
BeginMessage(pszCommand);
vSend << a1;
EndMessage();
}
catch (...)
{
AbortMessage();
throw;
}
}
这里会封装消息,BeginMessage开始,这是封装消息头,pszCommand表示消息命令,也就是消息类型吧,这个我们知道,现在是"inv"。对应vInventoryToSend。
然后是vSend<<a1,中间是具体消息数据,序列化vInventoryTosend用于发送。vSend是CDataStream类型。
EndMessage表示结束收尾,写入消息长度之类的,就是这样一种格式,具体怎么实现,我们需要了解BeginMessage和EndMessage函数以及对vSend进行了什么操作。
这个函数的作用就是序列化打包消息,变成字节流。
10.BeginMessage
我们先来看BeginMessage这个函数:
cpp
void BeginMessage(const char* pszCommand)
{
EnterCriticalSection(&cs_vSend);
if (nPushPos != -1)
AbortMessage();
nPushPos = vSend.size();
vSend << CMessageHeader(pszCommand, 0);
printf("sending: %-12s ", pszCommand);
}
主要就是将信息头以一定的格式写入到vSend中。
11.EndMessage
再来看一下EndMessage这个函数:
cpp
void EndMessage()
{
extern int nDropMessagesTest;
if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0)
{
printf("dropmessages DROPPING SEND MESSAGE\n");
AbortMessage();
return;
}
if (nPushPos == -1)
return;
// Patch in the size
unsigned int nSize = vSend.size() - nPushPos - sizeof(CMessageHeader);
memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize));
printf("(%d bytes) ", nSize);
//for (int i = nPushPos+sizeof(CMessageHeader); i < min(vSend.size(), nPushPos+sizeof(CMessageHeader)+20U); i++)
// printf("%02x ", vSend[i] & 0xff);
printf("\n");
nPushPos = -1;
LeaveCriticalSection(&cs_vSend);
}
这个函数就是计算消息的长度,然后将长度写入进去,注意这里写入的长度并不是在末尾,而是在消息头里,因为构建消息头时,还没有数据,所以无法计算总长度,所以放在EndMessage里计算,然后再补上。
这句就是实际写入:
cpp
memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize));
计算vSend中的偏移位置再写入。这里的nPushPos是消息头的起始位置。
现在大概理解即可,在解析数据的时候,再详细说明其结构。
我们看到,到现在,还没涉及到网络发头,只是在打包消息。而真正的发送数据的地方是在ThreadSocketHandler2函数中。
如下代码:
cpp
//
// Service each socket
//
vector<CNode*> vNodesCopy;
CRITICAL_BLOCK(cs_vNodes)
vNodesCopy = vNodes;
foreach(CNode* pnode, vNodesCopy)
{
CheckForShutdown(0);
SOCKET hSocket = pnode->hSocket;
//
// Receive
//
if (FD_ISSET(hSocket, &fdsetRecv))
{
TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
{
CDataStream& vRecv = pnode->vRecv;
unsigned int nPos = vRecv.size();
// typical socket buffer is 8K-64K
const unsigned int nBufSize = 0x10000;
vRecv.resize(nPos + nBufSize);
int nBytes = recv(hSocket, &vRecv[nPos], nBufSize, 0);
vRecv.resize(nPos + max(nBytes, 0));
if (nBytes == 0)
{
// socket closed gracefully
if (!pnode->fDisconnect)
printf("recv: socket closed\n");
pnode->fDisconnect = true;
}
else if (nBytes < 0)
{
// socket error
int nErr = WSAGetLastError();
if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS)
{
if (!pnode->fDisconnect)
printf("recv failed: %d\n", nErr);
pnode->fDisconnect = true;
}
}
}
}
//
// Send
//
if (FD_ISSET(hSocket, &fdsetSend))
{
TRY_CRITICAL_BLOCK(pnode->cs_vSend)
{
CDataStream& vSend = pnode->vSend;
if (!vSend.empty())
{
int nBytes = send(hSocket, &vSend[0], vSend.size(), 0);
if (nBytes > 0)
{
vSend.erase(vSend.begin(), vSend.begin() + nBytes);
}
else if (nBytes == 0)
{
if (pnode->ReadyToDisconnect())
pnode->vSend.clear();
}
else
{
printf("send error %d\n", nBytes);
if (pnode->ReadyToDisconnect())
pnode->vSend.clear();
}
}
}
}
}
//send 部分里,上面是接收部分。
我们在这里可以看到遍历节点的行为,然后一个个检查,维护,发送,具体发送代码是这句:
cpp
int nBytes = send(hSocket, &vSend[0], vSend.size(), 0);
好,在这里已经看到hSocket这种名称,就是API套接字相关部分,而这个send,正是套接字相关API函数,用来网络发送数据的。
关于这些相关函数,以及整个流程,还有ThreadSocketHandler2这个线程函数,何时被调用。
还有net里关键的CNode节点类,怎么启动,获取,维护,以及他的功能将在后续章节中介绍。
然后会总结整个网络流程。
今天就到这里。谢谢。