☰
三十.区块链网络(1)--网络发送tx
2026/10/21 1:59:39 网站建设 项目流程
1.网络协议

比特币是采用的P2P网络,因为去中心化,节点对等,这个网络是非常适合比特币的。而这个P2P的实现,在比特币中是采用TCP协议的。

所以我们如果要入手,得了解C++的网络部分,如何构建TCP协议通信。这里调用的是windows API来实现,如果你之前了解,学习起来事半功倍,不了解也没头系,我们会在讲源码的时候,附带说明TCP部分。

我们可以从net.cpp里看到这句:

#include <winsock2.h>

这就是windows 网络API所在头文件。说明是用windows的winsock实现网络通信的。

2.开始

我们从发送比特币交易这里入手,这里肯定是要调用网络广播的。我们循着它找到构建网络的源头。

从最顶层的函数开始(ui.cpp):

void CSendDialog::OnButtonSend(wxCommandEvent& event)

在里面调用了SendMoney函数:

if (!SendMoney(scriptPubKey, nValue, wtx)) return;

第一个参数是收款人脚本(含有收款人账号),nValue是收款人金额,wtx是当前要构建的tx。

3.RelayWalletTransaction

在SendMoney里当wtx构建好了,就会调用如下函数进行广播:

wtxNew.RelayWalletTransaction();

然后会传入本地存储有交易索引的数据库对象,供后续相关查找使用:

void RelayWalletTransaction(CTxDB& txdb); void RelayWalletTransaction() { CTxDB txdb("r"); RelayWalletTransaction(txdb); }

看一下真正干活的这个重载广播函数代码:

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的问题。

在这个函数前部分代码,即:

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是从哪里构建的呢?

在如下函数构建:

wtxNew.AddSupportingTransactions(txdb);

这个函数,在CreateTransaction创建交易里被调用,也即SendMoney里的CreateTransaction。

我们来看一下这个函数的代码:

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挑选规则

我们可以看到关键代码:

const int COPY_DEPTH = 3; if (SetMerkleBranch() < COPY_DEPTH)

COPY_DEPTH=3,限制了引用的深度,3个深度,也就是说引用的tx超过了3个深度,它就不会把这个tx加入到vtxPrev里面,注意这个3个深度,并不是前面三个区块的意思,关于是什么,因为涉及到默克尔树,而我们现在重点是网络部分,所以这里的算法现在不分析了。

我们现在只需要大概知道其做什么就行了,在最后面可以看到添加代码:

int nDepth = tx.SetMerkleBranch(); vtxPrev.push_back(tx);

这个函数的分析将会放在后续章节,写完默克尔树的时候再说明,因为之前的默克尔树只写了一部分,它不仅仅是对数据的摘要,只有这一个功能,生成默克尔树,还有相应的查找和验证功能。

好,当广播完引用的tx后,才是到我们真正要广播的tx了,即广播我们新建的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); } }

但是我们发现,即使是按照SendMoney逻辑,新建的tx,这里还是判断一下是否存在于数据库里再广播,这句:

if (!txdb.ContainsTx(hash))

这其实也是为了写入成功,避免重复广播。因为RelayWalletTransaction函数可能被多次调用(包括其它地方),所以会加入这个判断。

6.RelayMessage

接下来我们来看广播的核心代码,也是我们越来越接近网络的代码,已经带有消息标志头了--MSG_TX:

RelayMessage(CInv(MSG_TX, hash), (CTransaction)*this);

我们来看一下这个函数的代码:

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,然后调用重载:

RelayMessage(inv, ss);

RelayMessage"重载"函数代码:

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分钟,我们可以看到代码设定:

mapRelay[inv] = ss; vRelayExpiration.push_back(make_pair(GetTime() + 15 * 60, inv));

其中GetTime()+15*60,就是预设的过期时间。

然后这句判断:

while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime())

当消息缓存里不为空并且,有过期的消息,当前时间已经大于预设的过期时间,那么就进行删除。

这里的front()返回的是最早添加的元素,所以它这里循环判断依次删除。这样里面剩下的都是没过期的元素了。

7. RelayInventory

删除完过期的tx后,接着会广播这条tx了:

RelayInventory(inv);

但是你发现没,它并没有广播具体的数据ss,只是广播了一个inv,里面标识了这是条tx消息,以及这条tx的哈希值。

其实这是在告诉别的节点,我这里有条这样的tx,你们要不要。然后别的节点确定后(确认没有),就会回复(通过getdata方式),接着才会发送具体tx数据过去。

这样设计,可以节省网络资源。

我们来看一下函数的具体代码:

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保存了已知的连接上的邻居节点。

这里的关键在这个函数:

pnode->PushInventory(inv);

代码如下:

void PushInventory(const CInv& inv) { CRITICAL_BLOCK(cs_inventory) if (!setInventoryKnown.count(inv)) vInventoryToSend.push_back(inv); }

先是setInventoryKnown判断这条消息是否已经发送过,如果没发送,则添加到消息队列vInventoryToSend中。我们可以看到代码执行流程到这里就结束了,并没有网络发送相关。

那这里只是添加消息队例操作。发送方面是在另一个函数中。

8.SendMessages

我们看到接下来的处理是 SendMessages函数中,我们来看这个函数里关于Inventory这一段代码:

// // 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,并去重,我们可以看到这句:

if (pto->setInventoryKnown.insert(inv).second) vInventoryToSend.push_back(inv);

将这个消息添加到setInventoryKnown里,如果已存在,则失败,就不会执行vInventoryToSend。这是去重式添加到vInventoryToSend。所以从这里可以看出,这什么之前这段代码:

void PushInventory(const CInv& inv) { CRITICAL_BLOCK(cs_inventory) if (!setInventoryKnown.count(inv)) vInventoryToSend.push_back(inv); }

通过setInventoryKnown判断消息是否发送过,没发送,但只执行了一句代码,并没有把这个未发送的消息添加到setInventoryKnown,是因为这个逻辑在SendMessage函数里处理添加。

9.PushMessage

这样去重生成的vInventoryToSend后,才是要发送的消息(没发送过的),代码如下:

if (!vInventoryToSend.empty()) pto->PushMessage("inv", vInventoryToSend);

我们来看一下这个函数的代码:

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这个函数:

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这个函数:

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里计算,然后再补上。

这句就是实际写入:

memcpy((char*)&vSend[nPushPos] + offsetof(CMessageHeader, nMessageSize), &nSize, sizeof(nSize));

计算vSend中的偏移位置再写入。这里的nPushPos是消息头的起始位置。

现在大概理解即可,在解析数据的时候,再详细说明其结构。

我们看到,到现在,还没涉及到网络发头,只是在打包消息。而真正的发送数据的地方是在ThreadSocketHandler2函数中。

如下代码:

// // 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 部分里,上面是接收部分。

我们在这里可以看到遍历节点的行为,然后一个个检查,维护,发送,具体发送代码是这句:

int nBytes = send(hSocket, &vSend[0], vSend.size(), 0);

好,在这里已经看到hSocket这种名称,就是API套接字相关部分,而这个send,正是套接字相关API函数,用来网络发送数据的。

关于这些相关函数,以及整个流程,还有ThreadSocketHandler2这个线程函数,何时被调用。

还有net里关键的CNode节点类,怎么启动,获取,维护,以及他的功能将在后续章节中介绍。

然后会总结整个网络流程。

今天就到这里。谢谢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询