三十五.区块链网络(6)--程序入口与修复net

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相关修复完成。

接下来要做的事就是编译调试好,然后再测试几个小例子。

今天就先到这里,后续过程放在下一章中。