1.程序入口
在前面我们看到StartNode函数,建立网络套接字,然后启动三个线程函数。
那StartNode函数又是哪里被调用的呢,我们可以推测出来,应该是程序启动时调用。
我们可以顺着main函数来找到它的调用,但是我们在main.cpp里并没有发现程序入口main函数,这是因为main函数,并不一定要在main.cpp文件里,在其它cpp里也可以,但在其它cpp里也没发现,这是因为使用了ui框架,把main函数给封装了。
我们去到ui.cpp里,可以看到下面的代码:
cpp
//////////////////////////////////////////////////////////////////////////////
//
// CMyApp
//
// Define a new application
class CMyApp: public wxApp
{
public:
CMyApp(){};
~CMyApp(){};
bool OnInit();
bool OnInit2();
int OnExit();
// 2nd-level exception handling: we get all the exceptions occurring in any
// event handler here
virtual bool OnExceptionInMainLoop();
// 3rd, and final, level exception handling: whenever an unhandled
// exception is caught, this function is called
virtual void OnUnhandledException();
// and now for something different: this function is called in case of a
// crash (e.g. dereferencing null pointer, division by 0, ...)
virtual void OnFatalException();
};
IMPLEMENT_APP(CMyApp)
2.IMPLEMENT_APP
看到,最下面的这个定义吗?
cpp
IMPLEMENT_APP(CMyApp)
它会生成main函数,这就是wxWidgets框架程序的入口,由于这个宏定义是属于wxWidgets框架的,所以我们看不到这个定义(无法跳转定义),因为我的编译器并没有引入这个框架。
大概类似于这样:
cpp
// 实际 wxWidgets 中(简化后)大致是这样的:
#define IMPLEMENT_APP(appname) \
wxIMPLEMENT_APP(appname) // 内部又是一个宏
// 核心内容:
wxApp* wxCreateApp() \
{ \
return new appname(); \
}
int main(int argc, char **argv) // Windows 下可能是 WinMain
{
// 设置全局 App 实例
wxApp::SetInstance(wxCreateApp());
// 初始化并进入事件循环
return wxEntry(argc, argv);
}
流程如下:
cpp
main() / WinMain()
↓
wxEntry()
↓
CMyApp::OnInit() // ← 第1阶段初始化
↓
CMyApp::OnInit2() // ← 第2阶段初始化(自定义)
↓
进入消息循环(处理事件)
↓
程序退出时 → CMyApp::OnExit()
过了wxEntry()后,就是调用了OnInit,OnInit2这两个函数。
所以我们可以推测出来,这个宏定义,初始部分,大概就是创建一个CMyApp对象。然后调用其下的:
cpp
bool OnInit();
bool OnInit2();
这两个函数,所以我们可以把这里当作入口。
而在OnInit()中,直接调用了OnInit2函数:
cpp
bool CMyApp::OnInit()
{
try
{
return OnInit2();
}
catch (std::exception& e) {
PrintException(&e, "OnInit()");
} catch (...) {
PrintException(NULL, "OnInit()");
}
return false;
}
3.OnInit2
所以我们只要看OnInit2的函数代码就行,如下:
cpp
// 第二个初始化阶段(在 OnInit 之后执行)
bool CMyApp::OnInit2()
{
#ifdef _MSC_VER
// 在 Windows 下关闭微软 CRT 的堆调试噪音输出(避免控制台刷屏)
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_WARN, CreateFile("NUL", GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, 0));
#endif
//// debug print
printf("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n");
printf("Bitcoin CMyApp::OnInit()\n");
//
// 限制每个用户只能运行一个 Bitcoin 实例
// 防止多个实例同时操作数据库文件导致损坏
//
wxString strMutexName = wxString("Bitcoin.") + getenv("HOMEPATH");
// 把路径中的非法字符替换成 '.',构造互斥体名称
for (int i = 0; i < strMutexName.size(); i++)
if (!isalnum(strMutexName[i]))
strMutexName[i] = '.';
// 创建单实例检查器
wxSingleInstanceChecker* psingleinstancechecker = new wxSingleInstanceChecker(strMutexName);
// 如果已经有一个实例在运行
if (psingleinstancechecker->IsAnotherRunning())
{
printf("Existing instance found\n");
unsigned int nStart = GetTime();
loop
{
// 尝试找到已运行的窗口并激活它,然后退出当前实例
HWND hwndPrev = FindWindow("wxWindowClassNR", "Bitcoin");
if (hwndPrev)
{
if (IsIconic(hwndPrev))
ShowWindow(hwndPrev, SW_RESTORE);
SetForegroundWindow(hwndPrev);
return false;
}
// 等待最多 60 秒
if (GetTime() > nStart + 60)
return false;
// 如果旧实例退出了,则继续启动当前实例
delete psingleinstancechecker;
Sleep(1000);
psingleinstancechecker = new wxSingleInstanceChecker(strMutexName);
if (!psingleinstancechecker->IsAnotherRunning())
break;
}
}
//
// 处理命令行参数
//
wxImage::AddHandler(new wxPNGHandler); // 支持 PNG 图片格式
// 解析命令行参数到 mapArgs
map<string, string> mapArgs = ParseParameters(argc, argv);
if (mapArgs.count("/datadir"))
strSetDataDir = mapArgs["/datadir"]; // 设置数据目录
if (mapArgs.count("/proxy"))
addrProxy = CAddress(mapArgs["/proxy"].c_str()); // 设置代理服务器
if (mapArgs.count("/debug"))
fDebug = true; // 开启调试模式
if (mapArgs.count("/dropmessages"))
{
nDropMessagesTest = atoi(mapArgs["/dropmessages"]);
if (nDropMessagesTest == 0)
nDropMessagesTest = 20; // 默认丢包率测试
}
// 测试用:仅加载区块索引并打印后退出
if (mapArgs.count("/loadblockindextest"))
{
CTxDB txdb("r");
txdb.LoadBlockIndex();
PrintBlockTree();
ExitProcess(0);
}
//
// 加载各种数据文件
//
string strErrors;
int64 nStart, nEnd;
printf("Loading addresses...\n");
QueryPerformanceCounter((LARGE_INTEGER*)&nStart);
if (!LoadAddresses())
strErrors += "Error loading addr.dat \n";
QueryPerformanceCounter((LARGE_INTEGER*)&nEnd);
printf(" addresses %20I64d\n", nEnd - nStart);
printf("Loading block index...\n");
QueryPerformanceCounter((LARGE_INTEGER*)&nStart);
if (!LoadBlockIndex())
strErrors += "Error loading blkindex.dat \n";
QueryPerformanceCounter((LARGE_INTEGER*)&nEnd);
printf(" block index %20I64d\n", nEnd - nStart);
printf("Loading wallet...\n");
QueryPerformanceCounter((LARGE_INTEGER*)&nStart);
if (!LoadWallet())
strErrors += "Error loading wallet.dat \n";
QueryPerformanceCounter((LARGE_INTEGER*)&nEnd);
printf(" wallet %20I64d\n", nEnd - nStart);
printf("Done loading\n");
// 调试信息:打印各种数据结构的大小
printf("mapBlockIndex.size() = %d\n", mapBlockIndex.size());
printf("nBestHeight = %d\n", nBestHeight);
printf("mapKeys.size() = %d\n", mapKeys.size());
printf("mapPubKeys.size() = %d\n", mapPubKeys.size());
printf("mapWallet.size() = %d\n", mapWallet.size());
printf("mapAddressBook.size() = %d\n", mapAddressBook.size());
// 如果加载过程中出现错误,弹出提示并退出
if (!strErrors.empty())
{
wxMessageBox(strErrors);
OnExit();
return false;
}
// 把钱包中未确认的交易重新加入内存交易池
ReacceptWalletTransactions();
//
// 处理其他命令行参数
//
if (mapArgs.count("/printblockindex") || mapArgs.count("/printblocktree"))
{
PrintBlockTree();
OnExit();
return false;
}
// 是否开启挖矿
if (mapArgs.count("/gen"))
{
if (mapArgs["/gen"].empty())
fGenerateBitcoins = true;
else
fGenerateBitcoins = atoi(mapArgs["/gen"].c_str());
}
//
// 创建主窗口并启动节点
//
{
pframeMain = new CMainFrame(NULL); // 创建主界面窗口
pframeMain->Show(); // 显示主窗口
// 启动 P2P 网络节点(最关键的一步)
if (!StartNode(strErrors))
wxMessageBox(strErrors);
// 如果开启了挖矿,启动挖矿线程
if (fGenerateBitcoins)
if (_beginthread(ThreadBitcoinMiner, 0, NULL) == -1)
printf("Error: _beginthread(ThreadBitcoinMiner) failed\n");
//
// 命令行测试功能
//
if (argc >= 2 && stricmp(argv[1], "/send") == 0)
{
int64 nValue = 1;
if (argc >= 3)
ParseMoney(argv[2], nValue);
string strAddress;
if (argc >= 4)
strAddress = argv[3];
CAddress addr(strAddress.c_str());
CWalletTx wtx;
wtx.mapValue["to"] = strAddress;
wtx.mapValue["from"] = addrLocalHost.ToString();
wtx.mapValue["message"] = "command line send";
// 弹出发送对话框
CSendingDialog* pdialog = new CSendingDialog(pframeMain, addr, nValue, wtx);
if (!pdialog->ShowModal())
return false;
}
// 随机发送测试(压力测试用)
if (mapArgs.count("/randsendtest"))
{
if (!mapArgs["/randsendtest"].empty())
_beginthread(ThreadRandSendTest, 0, new string(mapArgs["/randsendtest"]));
else
fRandSendTest = true;
fDebug = true;
}
}
return true; // 初始化成功
}
4.流程启动图
在这个函数里,调用了StartNode函数以及其它的一些初始化函数,流程简化如下:
cpp
CMyApp::OnInit()
↓
CMyApp::OnInit2()
↓
LoadAddresses()
LoadBlockIndex()
LoadWallet()
↓
创建主窗口 CMainFrame
↓
StartNode() ← 启动网络节点
↓
如果开启挖矿 → 启动挖矿线程
5.例子
接下来,我们做几个小例子,启动节点,存储节点地址到数据库,以及发送一笔tx。
我们需要先把net.cpp和net.h迁移到我们的项目中,然后修复一些错误。
由于,net.cpp和net.h这两个文件在之前已经创建过了,但只是写了一些变量。
我们现在不需要重新创建文件,只需要把完整的代码复制过来就行。
接下来,修复net.cpp和net.h中的错误。
一些常见小错误就不说明了,只讲关键的一些错误,不感兴趣可跳过,后期直接使用修改好的文件即可。(或对实际代码运行无需求)。
以下代码默认在net.cpp之中。
6."printf"不明确
这个应该是哪里有一个一样的局部函数跟全局函数,在net.cpp里有冲突了。
我们直接显式指定调用全局函数,在net.cpp顶部加入下面的语句:
cpp
// ==================== 解决 printf 冲突 ====================
#undef printf
#define printf ::printf
// =======================================================
7.deque不是模板
加上对应头文件即可,添加到headers.h里面,位置放在自写头文件之上。
cpp
#include <deque>
8.未定义标识符RecvLine
这个函数是人socket中一行一行读取文本的,在net.cpp中的GetMyExternalIP函数中被用到,获取外网IP,通过访问固定的网站(类似于国内ip138等这样的网站)。
然后读取文本,获取到IP。
就会用到RecvLine函数,但这个函数,定义在IRC里,IRC现在用不到,我们如果复制所有的irc代码,那就会陷入循环修复中,工作量比较大。
由于这个RecvLine是个通用函数,本着能修复尽量修复的原则。
我们新建,irc.cpp和irc.h文件。
在irc.cpp只添加这个函数的代码:
cpp
#include "headers.h"
bool RecvLine(SOCKET hSocket, string& strLine)
{
strLine = "";
loop
{
char c;
int nBytes = recv(hSocket, &c, 1, 0);
if (nBytes > 0)
{
if (c == '\n')
continue;
if (c == '\r')
return true;
strLine += c;
}
else if (nBytes <= 0)
{
if (!strLine.empty())
return true;
// socket closed
printf("IRC socket closed\n");
return false;
}
else
{
// socket error
int nErr = WSAGetLastError();
if (nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS)
{
printf("IRC recv failed: %d\n", nErr);
return false;
}
}
}
}
irc.h对外声明:
cpp
extern bool RecvLine(SOCKET hSocket, string& strLine);
headers.h中包含irc.h,在main.h之上即可:
cpp
//...上面还有头文件
#include "irc.h"
#include "main.h"
9.未定义标识符CAddrDB
这里不识别,是因为我们在修复db.cpp和db.h时,因为用不到CAddrDB这个功能,所以并未将些部分代码开放,那么现在我们需要用到数据库存储节点地址了。
所以我们需要到db.cpp和db.h里面把这部分功能开放。
我们先把CAddrDB相关的注释去掉。
10.hostent
cpp
struct hostent* pHostEnt = gethostbyname(pszHostName);
报hostent *类型的值不能用于初始化honstent。
这是windows.h的winsock.h和winsock2.h里的相同类型冲突了。
我们将net.cpp里的头文件顺序换一下可解决,winsock2.h在 headers.h之上:
cpp
#include <winsock2.h>
#include "headers.h"
11.list不是模板
添加对应头文件到headers.h
12.未定义标识符cs_mapProducts
cpp
CRITICAL_BLOCK(cs_mapProducts)
线程锁相关,并且是market部分相关,这部分属于实验性质,实际未使用。
我们可以先注释相关代码,或者定义和声明一下cs_mapProducts也可。
这里先注释吧,如下:
cpp
//// Clear memory, no longer subscribed
//if (nChannel == MSG_PRODUCT)
// CRITICAL_BLOCK(cs_mapProducts)
// mapProducts.clear();
后续再碰到,依然注释。不再另做说明了。
注:在修改的net.cpp的过程中,util.h又报出了很多未定义类型问题(原先没有)。
说明其它cpp引用都问题,后来的net.cpp引用出了问题,为验证,将net.cpp里的headers.h临时注释。util.h文件不再报错。这是一个找问题的小技巧。
13.vfThreadRunning
vfThreadRunning0没有与这些操作数匹配的\[\]运算符
添加相关头文件<array>到headers.h中。
14.TRY_CRITICAL_BLOCK
宏定义,util.h添加相关定义即可。
然后会显示CTryCriticalBlock 未定义标识符。然后把下面这个定义放在宏定义上面就行(util.h里):
cpp
class CTryCriticalBlock
{
protected:
CRITICAL_SECTION* pcs;
public:
CTryCriticalBlock(CRITICAL_SECTION& csIn) { pcs = (TryEnterCriticalSection(&csIn) ? &csIn : NULL); }
CTryCriticalBlock(CCriticalSection& csIn) { pcs = (TryEnterCriticalSection(&csIn) ? &csIn : NULL); }
~CTryCriticalBlock() { if (pcs) LeaveCriticalSection(pcs); }
bool Entered() { return pcs != NULL; }
};
14.消息处理相关函数
ProcessMessages和SendMessages未定义,这两个函数定义在main.cpp里面。整个messages段都拷贝过来,包括processMessage(注意后缀无s)和 AlreadyHave
然后main.h声明一下:
cpp
bool ProcessMessages(CNode* pfrom);
bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv);
bool SendMessages(CNode* pto);
然后将这些函数自身的小错误处理一下,foreach改为for,未定义的变量定义一下等。
util里增加AddTimeData函数定义和声明。
main.h里增加CBlockLocator类定义。
main.cpp里添加GetOrphanRoot函数定义。
15.实参不匹配
cpp
block.ReadFromDisk((*mi).second, !pfrom->fClient);
"CBlockIndex *" 类型的实参与 "unsigned int" 类型的形参不兼容。
这是因为我们的CBlock的ReadFromDisk有好几个重载,但我们只写了一个。
而写了的这个函数,它的第一个参数是unsigned int类型的。现在我们需要把参数为CBlockIndex *类型ReadFromDisk加入到CBlock里。
16.AcceptTransaction
修复CTransaction下的AcceptTransaction函数:
main.h中定义如下:
cpp
bool AcceptTransaction(CTxDB& txdb, bool fCheckInputs=true, bool* pfMissingInputs=NULL);
bool AcceptTransaction(bool fCheckInputs=true, bool* pfMissingInputs=NULL)
{
CTxDB txdb("r");
return AcceptTransaction(txdb, fCheckInputs, pfMissingInputs);
}
第二个直接调用了第一个,虽然有两个重载,但我们现在只要把第一个AcceptTransaction代码解决即可。
这部分代码在main.cpp,由于涉及到验证tx,关联的代码比较多。我们这里直接注释掉,只留个空壳函数。后续看情况再修改。现在主是是让网络部分运行起来。
将tx消息处理的这句代码暂时去掉:
cpp
//AddToWalletIfMine(tx, NULL); //暂时不添加
即,对这笔tx不进行判断是否为本人的,然后添加进本地钱包(存储跟自己有关的tx)
将消息处理处,review类型相关的代码注释,因为这个功能后来已废弃。现在可以不必修复它,直接注释就行。
然后把这两个消息类型相关代码注释掉。用不上,可以省很多事:
checkorder
submitorder
把block消息类型临时注释掉吧,后续再修复,因为代码实在有点多,先跑起来:
cpp
//block为临时注释
//else if (strCommand == "block")
//{
// auto_ptr<CBlock> pblock(new CBlock);
// vRecv >> *pblock;
// //// debug print
// printf("received block:\n"); pblock->print();
// CInv inv(MSG_BLOCK, pblock->GetHash());
// pfrom->AddInventoryKnown(inv);
// if (ProcessBlock(pfrom, pblock.release()))
// mapAlreadyAskedFor.erase(inv);
//}
然后根据提示把一些没定义的函数,变量弄一下。
ok,这三个函数在main.cpp中的代码总算搞定,不再报错:
cpp
bool ProcessMessages(CNode* pfrom);
bool ProcessMessage(CNode* pfrom, string strCommand, CDataStream& vRecv);
bool SendMessages(CNode* pto);
接着我们回到net.cpp,继续修复错误。
17.BitcoinMiner
cpp
bool fRet = BitcoinMiner();
未定义标识符,在net.cpp中的StartNode,会根据设置决定是否挖矿,涉及到调用BitcoinMiner,这个函数,我们之前在main.cpp中已经写过了,但不是原版。
但这里为什么会显示未定义呢?
是因为没在main.h声明,我们声明一下即可。
17._beginthread
未定义标识符,头文件问题,在headers.h添加如下:
cpp
#include <process.h>
接着再添加一些函数和变量后,net.cpp终于不再报错。
然后就是net.h里面有少量几个报错,解决起来很快。
foreach改为for,然后将ARRAYLEN宏定义添加到util.h里。
cpp
#define ARRAYLEN(array) (sizeof(array)/sizeof((array)[0]))
至此net.h也不再报错,net相关修复完成。
接下来要做的事就是编译调试好,然后再测试几个小例子。
今天就先到这里,后续过程放在下一章中。