上篇我们已经进入了比特币的网络节点部分,并且了解了本机节点是如何获取自己的IP地址与外网地址的,我们现在接着往下看:使用域种子(dnsseed)查找节点。
我们先了解下域种子(dnsseed):。IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成。
这些DNS服务器提供比特币节点的IP地址列表。 其中一些DNS种子提供了稳定的比特币侦听节点的静态IP地址列表。 一些DNS种子是BIND(Berkeley Internet Name Daemon)的自定义实现,它从搜索器或长时间运行的比特币节点收集的比特币节点地址列表中返回一个随机子集。 Bitcoin Core客户端包含五种不同DNS种子的名称。 不同DNS种子的所有权和多样性的多样性为初始引导过程提供了高水平的可靠性。 在Bitcoin Core客户端中,使用DNS种子的选项由选项switch -dnsseed控制(默认设置为1)
然后我们看下 ThreadDNSAddressSeed 这个函数是如何使用域查找IP地址的。
下面,是通过对upnp端口的映射。
UPNP 的英文全称是Universal Plug and Play,即通用即插即用。是各种各样的智能设备、无线设备和个人电脑等实现遍布全球的对等网络连接(P2P)的结构。
比特币客户端是支持UPNP的,所以需要地址映射,我们现在就看下是如何映射的。要映射upnp设备是需要miniupnpc.lib这个库,用于映射端口的API是UPNP_AddPortMapping这个接口。我们现在就看下源码。
这部分我们了解了 UPNP的一些操作API,关于更多这方面的API,大家可以自行搜索。现在我们了解了如何发现其他节点和设备,现在我们就了解下和其他节点的消息通讯都做了哪些事情,这部分就是对各种消息的侦听。我们就先看下ThreadSocketHandler这个接口。我们和其他节点连通后,就需要进行消息交换,发送和接收数据就是通过这个接口来进行的,我们现在就看下这个函数的内部都做了些什么。这部分的源码非常多,我们分几个部分介绍:
1.在节点列表里查找已经失去连接和未用的节点并删除
2.查找哪个socket连接有数据接收
这里的源码使用了文件描述符集fd_set(file descript set),既然其名字是一种集合,其实就是一long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件发生了可读或可写事件。
这里最重要的是select()函数,采用select()的方式原因是 :使用Select就可以完成非阻塞。(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
Select的函数格式(我所说的是Unix系统下的伯克利socket编程 )我们看下其原型和参数。
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
maxfdp:是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。
readfds:指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
writefds:和readfds同理,只不过描述的是 是否可以从这些文件中写数据了。
errorfds:用来监听读写文件或socket的异常。
timeout:用来设置超时时间。它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
函数返回值:
负值:select错误 正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件。
有了上面的认识后,我们再看下源码:
3.接收一个连接
因为我们每个节点既是其他节点的服务端,也是其他节点的客户端,所以首先把我们当服务器的时候,需要接收其他节点的连接,这就是我们要介绍的accept函数。
accept函数指定服务端去接受客户端的连接,接收后,返回了客户端套接字的标识,且获得了客户端套接字的“地方”(包括客户端IP和端口信息等)。
4.从其他节点接收发送的数据
socket接收从其他节点发送的数据使用的是recv()函数。
(1)recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR;
(2)如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的);
recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
有了这个描述,应该就可以了解上面的代码了。有了接收就要有发送,下面就是向其他节点通过socket 发送数据的代码。
这里我们重要看下SocketSendData()函数,看下是如何发送数据的。在看源码前我们也介绍下socket的一个send()函数,此函数就是向Socket另一方发送数据的函数()。函数原型为:
int send( SOCKET s, const char FAR *buf, int len, int flags );
该函数的第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数为标志位,一般情况下设置为MSG_NOSIGNAL(表示发送动作不愿被SIGPIPE信号中断),同时还有其他标志如:MSG_OOB(发送带外数据)MSG_DONTROUTE(告诉IP协议,目的主机在本地网络,没有必要查找路由表)MSG_DONTWAIT(设置为非阻塞操作)
今天的内容应该是比较多了,我们了解了域种子,UPNP设备端口映射,fd_set(文件描述集),Socket的通讯。这也是获取其他节点的IP地址的目的。也是网络中进行通讯的常用方式。这篇就写到这里了。大家可以在网络中搜相关内容进行详细了解。
作者:区块链研习社比特币源码研读班,black