1.ThreadMessageHandler2
第三个线程调用结构逻辑,就不说明了,跟前两个一样。我们直接看核心代码,Thread..r2函数的代码:
cpp
void ThreadMessageHandler(void* parg)
{
IMPLEMENT_RANDOMIZE_STACK(ThreadMessageHandler(parg));
loop
{
vfThreadRunning[2] = true;
CheckForShutdown(2);
try
{
ThreadMessageHandler2(parg);
}
CATCH_PRINT_EXCEPTION("ThreadMessageHandler()")
vfThreadRunning[2] = false;
Sleep(5000);
}
}
void ThreadMessageHandler2(void* parg)
{
printf("ThreadMessageHandler started\n");
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
loop
{
// Poll the connected nodes for messages
vector<CNode*> vNodesCopy;
CRITICAL_BLOCK(cs_vNodes)
vNodesCopy = vNodes;
foreach(CNode* pnode, vNodesCopy)
{
pnode->AddRef();
// Receive messages
TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
ProcessMessages(pnode);
// Send messages
TRY_CRITICAL_BLOCK(pnode->cs_vSend)
SendMessages(pnode);
pnode->Release();
}
// Wait and allow messages to bunch up
vfThreadRunning[2] = false;
Sleep(100);
vfThreadRunning[2] = true;
CheckForShutdown(2);
}
}
在这个函数可以看到,他也在收发消息,但是收发消息?我们在之前的ThreadSocketHandler2函数也看到收发,这两个有什么区别吗?其实看命名就可以知道,一个是socket,一个是message。
其实前者是网络层,后者是协议层。
真正网络发送的是ThreadSocketHandler2,这是底层收发,纯字节流。负责这部分工作。
ThhreadMessageHandler2这个函数,只负责解析收来的消息,包括发送也是(打包消息)。
我们看到这个函数的代码很少,其实大部分工作都是ProcessMessages,SendMessages这两个函数再做,我们稍后来重点说一下这两个函数。
我们先看代码是怎么开始的:
cpp
IMPLEMENT_RANDOMIZE_STACK(ThreadMessageHandler(parg));
先将线程优先级设置低一级,能在有大量工作任务时,CPU资源又比较紧时,让别的线程优先工作。
然后拷贝一份vNodes用来遍历。
然后给这个节点添加引用标记,表示这个节点正在被使用,因为是多线程的,所以怕被别的线程影响数据。所以标记一下这节点。执行完,会调用pnode->Release();去掉标记(一次)
然后是对这两个函数调用:
cpp
// Receive messages
TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
ProcessMessages(pnode);
// Send messages
TRY_CRITICAL_BLOCK(pnode->cs_vSend)
SendMessages(pnode);
其它没有了,其实就是一直在循环执行这两个函数。
2.ProcessMessages
我们先来看消息处理函数:
cpp
/ 处理来自节点 pfrom 的所有接收到的消息
bool ProcessMessages(CNode* pfrom)
{
// 引用节点接收缓冲区(CDataStream 是一个可动态扩展的字节流)
CDataStream& vRecv = pfrom->vRecv;
// 如果缓冲区为空,直接返回(没有数据可处理)
if (vRecv.empty())
return true;
// 打印当前待处理的数据长度(调试用)
printf("ProcessMessages(%d bytes)\n", vRecv.size());
//
// Message format(消息格式说明)
// (4) message start → 魔数 pchMessageStart
// (12) command → 命令字符串(如 "version"、"inv" 等)
// (4) size → 消息体长度
// (x) data → 实际消息内容
//
// 进入循环,持续处理缓冲区中的消息(可能一次收到多个消息)
loop
{
// ==================== 1. 查找消息起始标志(魔数) ====================
// 在接收缓冲区中搜索魔数 pchMessageStart 的位置
CDataStream::iterator pstart = search(vRecv.begin(), vRecv.end(),
BEGIN(pchMessageStart), END(pchMessageStart));
// 如果剩余数据不足一个完整消息头的大小
if (vRecv.end() - pstart < sizeof(CMessageHeader))
{
// 如果缓冲区数据已经超过一个消息头长度,但还是找不到魔数
// 说明前面都是无效垃圾数据,清除多余部分,只保留最后不足一个头的数据
if (vRecv.size() > sizeof(CMessageHeader))
{
printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n");
vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader));
}
break; // 退出循环,等待更多数据到来
}
// 如果魔数不在缓冲区最开头,说明前面有无效/错位的数据,打印跳过的字节数
if (pstart - vRecv.begin() > 0)
printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin());
// 删除魔数前面的所有无效数据,使缓冲区从魔数开始
vRecv.erase(vRecv.begin(), pstart);
// ==================== 2. 读取并解析消息头 ====================
// 构造消息头对象,并从缓冲区反序列化读取头部数据
CMessageHeader hdr;
vRecv >> hdr;
// 检查消息头是否合法(魔数是否正确、命令是否合理、长度是否合法等)
if (!hdr.IsValid())
{
printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
continue; // 无效头,跳过,继续找下一个魔数
}
// 获取命令名称(字符串形式)
string strCommand = hdr.GetCommand();
// ==================== 3. 处理消息体长度 ====================
// 获取消息体声明的长度
unsigned int nMessageSize = hdr.nMessageSize;
// 如果当前缓冲区剩余数据不足以包含整个消息体
if (nMessageSize > vRecv.size())
{
// 把刚才读出的消息头重新插回缓冲区开头,等待后续数据到达
//(这是一个"半包"情况,需要继续接收)
printf("MESSAGE-BREAK 2\n");
vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr));
Sleep(100); // 短暂休眠,等待更多数据
break;
}
// ==================== 4. 提取消息体并处理 ====================
// 将完整的一条消息体拷贝到独立的缓冲区 vMsg 中
CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize,
vRecv.nType, vRecv.nVersion);
// 从接收缓冲区中移除已处理的消息体(注意:不包含头部,头部已在前面 >> hdr 时移除)
vRecv.ignore(nMessageSize);
// ==================== 5. 实际处理这条消息 ====================
bool fRet = false;
try
{
CheckForShutdown(2); // 检查是否需要关闭节点
// 加锁处理(cs_main 是 Bitcoin 核心的重要全局锁)
CRITICAL_BLOCK(cs_main)
fRet = ProcessMessage(pfrom, strCommand, vMsg); // 真正处理具体消息逻辑
CheckForShutdown(2);
}
CATCH_PRINT_EXCEPTION("ProcessMessage()") // 捕获并打印处理过程中的异常
// 如果消息处理返回失败,打印日志
if (!fRet)
printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n",
strCommand.c_str(), nMessageSize,
pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str());
}
// 压缩缓冲区,释放已处理数据的内存(CDataStream 内部优化)
vRecv.Compact();
return true;
}
3.pchMessageStart
我们知道,收到的网络数据就在节点的, pfrom->vRecv里面,是CDataStream字节流的。
我要需要是把它还原成对应的数据类型。
但从哪里开始的呢?还记得前面的消息头固定标志吗---pchMessageStart。
这是一个固定的4字节的值:
cpp
static const char pchMessageStart[4] = { 0xf9, 0xbe, 0xb4, 0xd9 };
所以我们只要找到这值,就能定位到一条消息的开头,然后再读取就可以了。
所以处理消息以下的代码开头,不停的循环的找这个标志,然后获取它的位置pstart。
cpp
loop
{
// Scan for message start
CDataStream::iterator pstart = search(vRecv.begin(), vRecv.end(), BEGIN(pchMessageStart), END(pchMessageStart));
然后,这里我们不仅处理消息,还需要维护一下数据缓冲区,就是这段消息,如果这个标志头,不在这段消息的起始处,那是不是前面的数据我们要把它去掉,我们是用不上的。
以及这段数据找不到这个消息标志,则会进入下面的处理逻辑(即pstart==vRecv.end):
cpp
if (vRecv.end() - pstart < sizeof(CMessageHeader))
{
if (vRecv.size() > sizeof(CMessageHeader))
{
printf("\n\nPROCESSMESSAGE MESSAGESTART NOT FOUND\n\n");
vRecv.erase(vRecv.begin(), vRecv.end() - sizeof(CMessageHeader));
}
break;
}
就会删掉数据,但末尾保留一个消息头的数据,这是什么原因呢?这就是因为,怕这段数据的最后一部分只收到了部分消息头的数据,所以还需要后续收到的数据才能凑成一个完整的消息头。
如果有这种情况,虽然这段数据没有消息头,你全删掉了,这条消息就无法同步了。
所以为稳妥,保留一个消息头。然后break跳出循环。
然后是,pstart不在数据的起始点,说明前面有无效的数据,直接删除前面的数据:
cpp
if (pstart - vRecv.begin() > 0)
printf("\n\nPROCESSMESSAGE SKIPPED %d BYTES\n\n", pstart - vRecv.begin());
vRecv.erase(vRecv.begin(), pstart);
好,过了这两个判断和处理后,说明获取到一个消息了。我们就先来把这条消息的消息头读取出来:
cpp
// Read header
CMessageHeader hdr;
vRecv >> hdr;
if (!hdr.IsValid())
{
printf("\n\nPROCESSMESSAGE: ERRORS IN HEADER %s\n\n\n", hdr.GetCommand().c_str());
continue;
}
4.消息格式
从这里就可以看出来,消息的开始是消息头,这里我们之前的代码有展示过,就是这段:
cpp
template<typename T1>
void PushMessage(const char* pszCommand, const T1& a1)
{
try
{
BeginMessage(pszCommand);
vSend << a1;
EndMessage();
}
catch (...)
{
AbortMessage();
throw;
}
三十章有说明过,消息就是这样构造的。那么消息标志又是什么时候添加进去的呢?
cpp
void BeginMessage(const char* pszCommand)
{
EnterCriticalSection(&cs_vSend);
if (nPushPos != -1)
AbortMessage();
nPushPos = vSend.size();
vSend << CMessageHeader(pszCommand, 0);
printf("sending: %-12s ", pszCommand);
}
4.CMessageHeader
我们来看,创建消息头,pszCommand就是消息类型,传入CMessageHeader给构造函数。
cpp
class CMessageHeader
{
public:
enum { COMMAND_SIZE=12 };
char pchMessageStart[sizeof(::pchMessageStart)];
char pchCommand[COMMAND_SIZE];
unsigned int nMessageSize;
CMessageHeader()
{
memcpy(pchMessageStart, ::pchMessageStart, sizeof(pchMessageStart));
memset(pchCommand, 0, sizeof(pchCommand));
pchCommand[1] = 1;
nMessageSize = -1;
}
CMessageHeader(const char* pszCommand, unsigned int nMessageSizeIn)
{
memcpy(pchMessageStart, ::pchMessageStart, sizeof(pchMessageStart));
strncpy(pchCommand, pszCommand, COMMAND_SIZE);
nMessageSize = nMessageSizeIn;
}
构造函数里,创建了消息开头标志,pchMessageStart,即你创建消息头对象时,这个标志就自动加入了。
5.IsValid
读取到消息头后,会调用IsValid函数,对这个消息头的一些数据进行判断,比如消息命令是否对得上,消息数据不能超过最大数。
这是一个初步的判断,防止读取到了错误的消息头。可以防止数据随机正好产生了一个消息标志开头。虽然概率极小。
6.GetCommand
通过了后,调用GetCommand,就是获取消息类型了---"tx","addr"等。
然后是消息大小:nMessageSize
cpp
string strCommand = hdr.GetCommand();
// Message size
unsigned int nMessageSize = hdr.nMessageSize;
if (nMessageSize > vRecv.size())
{
// Rewind and wait for rest of message
///// need a mechanism to give up waiting for overlong message size error
printf("MESSAGE-BREAK 2\n");
vRecv.insert(vRecv.begin(), BEGIN(hdr), END(hdr));
Sleep(100);
break;
}
并且判断一下,如果指示的消息大小,比你现在收到的消息还大,说明消息还没接收完整(tcp分包多次发送).
即跳出当前循环,等待数据接收。
但这里我们发现没有,在跳出之前还有一个操作,即重新插入消息头到缓存区,这是因为,读取消息头时,这句:
cpp
CMessageHeader hdr;
vRecv >> hdr;
这个操作,已经把消息头从缓冲区移出去了。所以需要重新插入消息头到vRecv的最前面。
7.读取消息
好,到了最后,就可以读取具体的消息数据了,如下:
cpp
// Copy message to its own buffer
CDataStream vMsg(vRecv.begin(), vRecv.begin() + nMessageSize, vRecv.nType, vRecv.nVersion);
vRecv.ignore(nMessageSize);
读取到vMsg里面,然后调用ignore删掉vRecv里对应的数据。
7.ProcessMessage
然后是:
cpp
// Process message
bool fRet = false;
try
{
CheckForShutdown(2);
CRITICAL_BLOCK(cs_main)
fRet = ProcessMessage(pfrom, strCommand, vMsg);
CheckForShutdown(2);
}
CATCH_PRINT_EXCEPTION("ProcessMessage()")
if (!fRet)
printf("ProcessMessage(%s, %d bytes) from %s to %s FAILED\n", strCommand.c_str(), nMessageSize, pfrom->addr.ToString().c_str(), addrLocalHost.ToString().c_str());
}
从数据流分解出一条有效的消息,得到strCommand和vMsg后,我们干什么,最终调用另一个函数来处理这条消息--ProcessMessage。
在这个函数里,先打印一下这条消息的类型,和数据大小:
cpp
printf("received: %-12s (%d bytes) ", strCommand.c_str(), vRecv.size());
然后打印最多前25个字节的数据(依实际情况而定,如果不足25,则打印实际大小,不超过25个字节),以便用来观察调试:
cpp
for (int i = 0; i < min(vRecv.size(), (unsigned int)25); i++)
printf("%02x ", vRecv[i] & 0xff);
printf("\n");
- nDropMessagesTest
然后这个nDropMessagesTest的值是测试用的,当这个值大于0时,会根据nDropMessagesTest的值(概率)丢弃一段收到消息,模拟节点在比较差的网络环境中(丢包)的健壮性。能否正常运行和处理。
默认设置0,不测试,下面这段代码简单了解可。实际中不使用:
cpp
if (nDropMessagesTest > 0 && GetRand(nDropMessagesTest) == 0)
{
printf("dropmessages DROPPING RECV MESSAGE\n");
return true;
}
好,以上都是关于测试和调试的代码,接下就是真正的消息处理,我们会看到这样的开头:
cpp
if (strCommand == "version")
{
// Can only do this once
if (pfrom->nVersion != 0)
return false;
int64 nTime;
CAddress addrMe;
就是具体的消息类型处理代码,现在我们可以有选择的了解,因为写法都是一样的。只是每种消息有不同的处理代码,彼此关联性不强。可以单独拿出来看。
在之前,我们了解了如何发送"tx"消息,那么在这里我们就先来了解节点在收到"tx"消息是怎么处理的,相关代码如下:
cpp
// 处理收到的 "tx"(交易)消息
else if (strCommand == "tx")
{
// 用于存放需要递归处理的交易哈希队列(处理孤儿交易时使用)
vector<uint256> vWorkQueue;
// 保存一份消息体的副本(用于后续中继给其他节点)
CDataStream vMsg(vRecv);
// 声明一个交易对象
CTransaction tx;
// 从 vRecv 中反序列化读取交易数据(填充 tx 对象)
vRecv >> tx;
// 创建一个交易库存对象(Inventory),类型为 MSG_TX
CInv inv(MSG_TX, tx.GetHash());
// 标记这个节点已经知道这笔交易,避免重复请求
pfrom->AddInventoryKnown(inv);
bool fMissingInputs = false;
// 尝试接受这笔交易(验证交易合法性)
if (tx.AcceptTransaction(true, &fMissingInputs))
{
// 如果是发给自己的交易(钱包相关的),加入钱包
AddToWalletIfMine(tx, NULL);
// 将这笔交易中继(广播)给其他连接的节点
RelayMessage(inv, vMsg);
// 从"已请求列表"中删除,因为已经收到并处理了
mapAlreadyAskedFor.erase(inv);
// 把当前交易哈希加入工作队列(用于后续处理依赖它的孤儿交易)
vWorkQueue.push_back(inv.hash);
// ==================== 处理孤儿交易(递归) ====================
// 递归处理所有依赖于这笔交易的孤儿交易
for (int i = 0; i < vWorkQueue.size(); i++)
{
uint256 hashPrev = vWorkQueue[i];
// 在孤儿交易索引表中查找所有前置交易是 hashPrev 的孤儿交易
for (multimap<uint256, CDataStream*>::iterator mi =
mapOrphanTransactionsByPrev.lower_bound(hashPrev);
mi != mapOrphanTransactionsByPrev.upper_bound(hashPrev);
++mi)
{
// 获取孤儿交易的消息体
const CDataStream& vMsg = *((*mi).second);
CTransaction tx;
// 从孤儿交易数据中反序列化出一个交易对象
CDataStream(vMsg) >> tx;
CInv inv(MSG_TX, tx.GetHash());
// 再次尝试接受这笔孤儿交易
if (tx.AcceptTransaction(true))
{
printf(" accepted orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
AddToWalletIfMine(tx, NULL); // 加入钱包(如果属于自己)
RelayMessage(inv, vMsg); // 中继给其他节点
mapAlreadyAskedFor.erase(inv); // 清理请求记录
// 把这个刚接受的孤儿交易也加入工作队列,继续处理它依赖的交易
vWorkQueue.push_back(inv.hash);
}
}
}
// 处理完所有相关孤儿交易后,从孤儿交易池中删除它们
foreach(uint256 hash, vWorkQueue)
EraseOrphanTx(hash);
}
// 如果交易未被接受,但缺少输入(即依赖的交易还没到),则作为孤儿交易保存
else if (fMissingInputs)
{
printf("storing orphan tx %s\n", inv.hash.ToString().substr(0,6).c_str());
// 将这条交易存入孤儿交易池,等待它依赖的交易到来
AddOrphanTx(vMsg);
}
// 其他情况(交易无效、双花等)则直接丢弃,不做处理
}
代码就不详细解释了,我们大概过一下流程,收到tx后,就会调用:
8.AcceptTransaction
cpp
if (tx.AcceptTransaction(true, &fMissingInputs))
{
此函数代码:
cpp
bool AcceptTransaction(bool fCheckInputs=true, bool* pfMissingInputs=NULL)
{
CTxDB txdb("r");
return AcceptTransaction(txdb, fCheckInputs, pfMissingInputs);
}
9.AddToMemoryPool
AcceptTransaction这个函数我们之前有说过,在这里面会调用:
cpp
bool CTransaction::AddToMemoryPool()
把这笔tx加入到内存交易池,然后供后续挖矿打包成区块(从内存池挑选交易).
就是这么个过程,另外此函数的注释也做了说明:
cpp
bool CTransaction::AddToMemoryPool()
{
// Add to memory pool without checking anything. Don't call this directly,
// call AcceptTransaction to properly check the transaction first.
CRITICAL_BLOCK(cs_mapTransactions)
{
uint256 hash = GetHash();
mapTransactions[hash] = *this;
for (int i = 0; i < vin.size(); i++)
mapNextTx[vin[i].prevout] = CInPoint(&mapTransactions[hash], i);
nTransactionsUpdated++;
}
return true;
}
意思是,不要直接调用这个函数将tx添加到内存池,因为没有做任何验证,先调用AcceptTransaction进行验证。从这段话我们就能知道,AcceptTranasaction函数的作用,和这两个函数的调用关系。
另:关于ProcessMessage函数,tx消息下的孤儿交易,就是在当前节点,引用的交易还未找到,数据库,包括临时池都没有,它会有着相应的处理方法。注意这里不是孤儿块。但可以根据名字了解,即然是孤儿,那都是暂时缺失相应的父亲。需要等待处理。
10.SendMessages
在前面我们了解怎么处理接收到的消息,还需要处理我们要发送的消息,就是通过SendMessages。但是这个函数,只处理部分消息类型,可自行看代码了解:
cpp
bool SendMessages(CNode* pto)
{
// 检查程序是否正在关闭,如果正在关闭则会触发退出流程
CheckForShutdown(2);
// 锁住全局区块链和交易相关数据(cs_main)
// 后面会访问地址、交易池、数据库等共享资源,需要同步
CRITICAL_BLOCK(cs_main)
{
// ===============================
// 握手检查
// ===============================
// 如果还没有收到对方的 version 消息
// 说明握手尚未完成
// 此时不要发送任何业务消息
if (pto->nVersion == 0)
return true;
//
// Message: addr
//
// 临时保存准备发送的地址列表
vector<CAddress> vAddrToSend;
// 提前分配空间,避免vector不断扩容
vAddrToSend.reserve(pto->vAddrToSend.size());
// 遍历等待发送的所有地址
foreach(const CAddress& addr, pto->vAddrToSend)
// 如果对方还不知道这个地址
// 才加入发送列表
if (!pto->setAddrKnown.count(addr))
vAddrToSend.push_back(addr);
// 无论是否发送成功
// 本次等待发送列表全部清空
pto->vAddrToSend.clear();
// 如果确实有新的地址需要发送
// 构造一个 addr 消息放入发送缓冲区
if (!vAddrToSend.empty())
pto->PushMessage("addr", vAddrToSend);
//
// Message: inventory
//
// 临时保存准备发送的 inv
vector<CInv> vInventoryToSend;
// inventory相关数据有自己的锁
CRITICAL_BLOCK(pto->cs_inventory)
{
// 预分配空间
vInventoryToSend.reserve(pto->vInventoryToSend.size());
// 遍历所有等待广播的 inventory
foreach(const CInv& inv, pto->vInventoryToSend)
{
// insert 返回 pair<iterator,bool>
// second==true
// 表示以前没有发送过
// second==false
// 表示已经告诉过对方
// 因此这里只发送新的 inventory
if (pto->setInventoryKnown.insert(inv).second)
vInventoryToSend.push_back(inv);
}
// 清空等待发送队列
pto->vInventoryToSend.clear();
// 清空辅助集合
// 下次重新统计
pto->setInventoryKnown2.clear();
}
// 如果确实有新的 inventory
// 发送 inv 消息
if (!vInventoryToSend.empty())
pto->PushMessage("inv", vInventoryToSend);
//
// Message: getdata
//
// 保存本次准备请求的数据
vector<CInv> vAskFor;
// 当前时间(微秒)
// mapAskFor 的 key 就是计划发送时间
int64 nNow = GetTime() * 1000000;
// 打开交易数据库(只读)
// 用于判断数据是否已经拥有
CTxDB txdb("r");
// mapAskFor 是一个按时间排序的 map
//
// key:
// 请求发送时间
//
// value:
// 要请求的 inventory
//
// 这里只处理已经到时间的请求
while (!pto->mapAskFor.empty() &&
(*pto->mapAskFor.begin()).first <= nNow)
{
// 当前最早需要请求的 inventory
const CInv& inv = (*pto->mapAskFor.begin()).second;
printf("sending getdata: %s\n",
inv.ToString().c_str());
// 如果本地还没有这个对象
// 才真正发送 getdata 请求
if (!AlreadyHave(txdb, inv))
vAskFor.push_back(inv);
// 不管是否请求
// 都从等待队列中删除
pto->mapAskFor.erase(pto->mapAskFor.begin());
}
// 如果确实需要请求数据
// 发送 getdata 消息
if (!vAskFor.empty())
pto->PushMessage("getdata", vAskFor);
}
// 正常结束
return true;
}
可以看到,只处理了三种消息类型,有些消息,比如"tx",是不经过SendMessages处理的,而是别处代码直接调用PushMessage来发送的。
这是因为有些消息,发送的条件不一样。SendMessages适合延时,批量发送的消息。
关于消息类型的理解,比如"getdata",现在我们都知道收发处的代码,我们如果要了解,现在很方便,比如去ProccessMessage找,关于下面这部分的代码:
cpp
else if (strCommand == "getdata")
我们就能知道其含义和作用。这里就不过多的解释了。