三十二.区块链网络(3)--启动节点

接下来,我们来了解节点启动的过程。

1.LoadAddresses全局函数

这个就是加载节点ip地址的全局函数:

cpp 复制代码
bool LoadAddresses()
{
    return CAddrDB("cr+").LoadAddresses();
}

供程序启动时调用,然后会加载地址数据库,其实就是调用对应的数据库类,新建CAddrDB对象,然后调用该类的成员函数LoadAddresses。

cpp 复制代码
class CAddrDB : public CDB
{
public:
    CAddrDB(const char* pszMode="r+", bool fTxn=false) : CDB("addr.dat", pszMode, fTxn) { }

我们可以看到,对应的数据库文件为addr.dat

2.LoadAddresses成员函数

然后我们来看一下成员函数的代码:

cpp 复制代码
bool CAddrDB::LoadAddresses()
{
    CRITICAL_BLOCK(cs_mapAddresses)
    {
        // Load user provided addresses
        CAutoFile filein = fopen("addr.txt", "rt");
        if (filein)
        {
            try
            {
                char psz[1000];
                while (fgets(psz, sizeof(psz), filein))
                {
                    CAddress addr(psz, NODE_NETWORK);
                    if (addr.ip != 0)
                        AddAddress(*this, addr);
                }
            }
            catch (...) { }
        }

        // Get cursor
        Dbc* pcursor = GetCursor();
        if (!pcursor)
            return false;

        loop
        {
            // Read next record
            CDataStream ssKey;
            CDataStream ssValue;
            int ret = ReadAtCursor(pcursor, ssKey, ssValue);
            if (ret == DB_NOTFOUND)
                break;
            else if (ret != 0)
                return false;

            // Unserialize
            string strType;
            ssKey >> strType;
            if (strType == "addr")
            {
                CAddress addr;
                ssValue >> addr;
                mapAddresses.insert(make_pair(addr.GetKey(), addr));
            }
        }

        //// debug print
        printf("mapAddresses:\n");
        foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
            item.second.print();
        printf("-----\n");
    }

    return true;
}

3.addr.txt

我们来看该函数开头部分代码:

cpp 复制代码
    // Load user provided addresses
    CAutoFile filein = fopen("addr.txt", "rt");
    if (filein)
    {
        try
        {
            char psz[1000];
            while (fgets(psz, sizeof(psz), filein))
            {
                CAddress addr(psz, NODE_NETWORK);
                if (addr.ip != 0)
                    AddAddress(*this, addr);
            }
        }
        catch (...) { }
    }

明明是读数据库文件,为什么这里也会去读addr.txt呢?这里是用户手动配置的IP。

也就是说用户如果知道一个节点的IP,可以写在addr.txt,程序启动的时候会加载。

fgets一行一行读取,然后转换成CAddress类型,接着调用AddAddres把这个节点添加到数据库和缓存到mapAddresses中。

4.mapAddresses

在这里我们可以知道,加载的IP都存到了mapAddresses中。

我们来看一下它的定义:

cpp 复制代码
map<vector<unsigned char>, CAddress> mapAddresses;

后续我们要连接节点就从这里获取。

然后来看一下后面的代码:

cpp 复制代码
        loop
        {
            // Read next record
            CDataStream ssKey;
            CDataStream ssValue;
            int ret = ReadAtCursor(pcursor, ssKey, ssValue);
            if (ret == DB_NOTFOUND)
                break;
            else if (ret != 0)
                return false;

            // Unserialize
            string strType;
            ssKey >> strType;
            if (strType == "addr")
            {
                CAddress addr;
                ssValue >> addr;
                mapAddresses.insert(make_pair(addr.GetKey(), addr));
            }
        }

        //// debug print
        printf("mapAddresses:\n");
        foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
            item.second.print();
        printf("-----\n");

就是不停的从数据库读取键值数据,然后如果是addr类型,就将它转换添加到mapAddresses里。

在最后还给出打印地址信息的代码,我们测试时可以使用该代码。但现在我们没有节点数据。

等做例子的时候再来使用,现在留个记号。

然后们可以看到:

cpp 复制代码
 if (strType == "addr")

还要判断一下类型,也就是说addr.dat还有其它类型的数据。我们读取的时候要注意了。

5.StartNode

在net.cpp里的StartNode函数负责启动节点,此函数前部分代码负责把自身节点启动,然后就开始做节点相关的工作,连接其它节点等。

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

cpp 复制代码
bool StartNode(string& strError)
{
    strError = "";

    // Sockets startup
    WSADATA wsadata;
    int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
    if (ret != NO_ERROR)
    {
        strError = strprintf("Error: TCP/IP socket library failed to start (WSAStartup returned error %d)", ret);
        printf("%s\n", strError.c_str());
        return false;
    }

    // Get local host ip
    char pszHostName[255];
    if (gethostname(pszHostName, 255) == SOCKET_ERROR)
    {
        strError = strprintf("Error: Unable to get IP address of this computer (gethostname returned error %d)", WSAGetLastError());
        printf("%s\n", strError.c_str());
        return false;
    }
    struct hostent* pHostEnt = gethostbyname(pszHostName);
    if (!pHostEnt)
    {
        strError = strprintf("Error: Unable to get IP address of this computer (gethostbyname returned error %d)", WSAGetLastError());
        printf("%s\n", strError.c_str());
        return false;
    }
    addrLocalHost = CAddress(*(long*)(pHostEnt->h_addr_list[0]),
                             DEFAULT_PORT,
                             nLocalServices);
    printf("addrLocalHost = %s\n", addrLocalHost.ToString().c_str());

    // Create socket for listening for incoming connections
    SOCKET hListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (hListenSocket == INVALID_SOCKET)
    {
        strError = strprintf("Error: Couldn't open socket for incoming connections (socket returned error %d)", WSAGetLastError());
        printf("%s\n", strError.c_str());
        return false;
    }

    // Set to nonblocking, incoming connections will also inherit this
    u_long nOne = 1;
    if (ioctlsocket(hListenSocket, FIONBIO, &nOne) == SOCKET_ERROR)
    {
        strError = strprintf("Error: Couldn't set properties on socket for incoming connections (ioctlsocket returned error %d)", WSAGetLastError());
        printf("%s\n", strError.c_str());
        return false;
    }

    // The sockaddr_in structure specifies the address family,
    // IP address, and port for the socket that is being bound
    int nRetryLimit = 15;
    struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr();
    if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
    {
        int nErr = WSAGetLastError();
        if (nErr == WSAEADDRINUSE)
            strError = strprintf("Error: Unable to bind to port %s on this computer. The program is probably already running.", addrLocalHost.ToString().c_str());
        else
            strError = strprintf("Error: Unable to bind to port %s on this computer (bind returned error %d)", addrLocalHost.ToString().c_str(), nErr);
        printf("%s\n", strError.c_str());
        return false;
    }
    printf("bound to addrLocalHost = %s\n\n", addrLocalHost.ToString().c_str());

    // Listen for incoming connections
    if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        strError = strprintf("Error: Listening for incoming connections failed (listen returned error %d)", WSAGetLastError());
        printf("%s\n", strError.c_str());
        return false;
    }

    // Get our external IP address for incoming connections
    if (addrIncoming.ip)
        addrLocalHost.ip = addrIncoming.ip;

    if (GetMyExternalIP(addrLocalHost.ip))
    {
        addrIncoming = addrLocalHost;
        CWalletDB().WriteSetting("addrIncoming", addrIncoming);
    }

    // Get addresses from IRC and advertise ours
    if (_beginthread(ThreadIRCSeed, 0, NULL) == -1)
        printf("Error: _beginthread(ThreadIRCSeed) failed\n");

    //
    // Start threads
    //
    if (_beginthread(ThreadSocketHandler, 0, new SOCKET(hListenSocket)) == -1)
    {
        strError = "Error: _beginthread(ThreadSocketHandler) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

    if (_beginthread(ThreadOpenConnections, 0, NULL) == -1)
    {
        strError = "Error: _beginthread(ThreadOpenConnections) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

    if (_beginthread(ThreadMessageHandler, 0, NULL) == -1)
    {
        strError = "Error: _beginthread(ThreadMessageHandler) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

    return true;
}

开始会启动一下套接字,就是在这里启动的,后续不用再启动,这是针对整个进程的:

cpp 复制代码
    // Sockets startup
    WSADATA wsadata;
    int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
    if (ret != NO_ERROR)
    {
        strError = strprintf("Error: TCP/IP socket library failed to start (WSAStartup returned error %d)", ret);
        printf("%s\n", strError.c_str());
        return false;
    }

然后干什么呢?获取本机地址,然后转换成CAddress类型--addrLocalHost,是为了后续绑定监听使用的:

cpp 复制代码
    struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr();
    if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)

然后启动监听:

cpp 复制代码
    if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        strError = strprintf("Error: Listening for incoming connections failed (listen returned error %d)", WSAGetLastError());
        printf("%s\n", strError.c_str());
        return false;
    }

以上部分,就是节点做为一个服务端创建流程,跟winsock的服务端创建过程其实区别不大。这里应该不难理解。

好,但是没见到accept函数,因为这不是一次连接或发送数据。所以这个函数是在后面相关的线程函数执行任务的。

6.GetMyExternalIP

我们来看StartNode接下来干的事:

cpp 复制代码
    // Get our external IP address for incoming connections
    if (addrIncoming.ip)
        addrLocalHost.ip = addrIncoming.ip;

    if (GetMyExternalIP(addrLocalHost.ip))
    {
        addrIncoming = addrLocalHost;
        CWalletDB().WriteSetting("addrIncoming", addrIncoming);
    }

addrIncoming.ip这个是公网IP,从数据库类的读取出来的,如果存在的话,则把它赋给addrLocalHost,就是上次使用的公网IP(就是我们用某些网站可以获取我们的IP地址的那个IP,非局域网IP)。

而GetMyExternalIP就是获取外网IP的函数,如果成功获取则返回ture,并重新设置addrLocalHost.ip。

然后把它写进数据库里。

好,上面就是外网相关部分。

7.IRC

接下是这个代码:

cpp 复制代码
    // Get addresses from IRC and advertise ours
    if (_beginthread(ThreadIRCSeed, 0, NULL) == -1)
        printf("Error: _beginthread(ThreadIRCSeed) failed\n");

启动了线程去干一件事,执行ThreadIRCSeed函数。

这个是干什么呢?就是如果当一个全新节点启动,之前如果不知道任何节点信息。你怎么获得节点IP呢?虽然节点之间可以互相分享其它节点信息,但第一个节点怎么发现呢?

就是通过这个IRC的机制,IRC是一种聊天协议,有着固定的服务器,这样我们就知道它的IP地址。然后连接上,加入特定的频道,比如bitcoin频道。

这个节点加入后,频道是可以取名字的,就把自己的名字改为ip加端口。

这样就构成了一个分享节点的平台,你可以从这里获取 ,上面的函数就是做这个事。

具体代码就不看了,因为现在已经不采用了。不是很安全,现在已经改成别的方法了。

8.线程函数

StartNode函数的最后,启动了三个线程函数,如下:

cpp 复制代码
    //
    // Start threads
    //
    if (_beginthread(ThreadSocketHandler, 0, new SOCKET(hListenSocket)) == -1)
    {
        strError = "Error: _beginthread(ThreadSocketHandler) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

    if (_beginthread(ThreadOpenConnections, 0, NULL) == -1)
    {
        strError = "Error: _beginthread(ThreadOpenConnections) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

    if (_beginthread(ThreadMessageHandler, 0, NULL) == -1)
    {
        strError = "Error: _beginthread(ThreadMessageHandler) failed";
        printf("%s\n", strError.c_str());
        return false;
    }

这里是比较关键的地方,理解清楚了, 我们就会对节点执行流程有个清晰的掌握。

由于篇幅过长,我们将在后面章节介绍。