1.send函数
我们在之前的章节中,看到,最终发送网络消息的是这个函数:
int nBytes = send(hSocket, &vSend[0], vSend.size(), 0);
这个函数是windows API的发送网络数据的函数。
其中第一个参数是SOCKET类型,第二个参数是数据起始地址,第三个参数是发送的数据大小。第4个参数默认为0即可。
返回值,是发送结果,成功发送则返回成功发送的数据大小。
我们知道,在网络上发送数据给别人,我们得知道对方的IP地址和端口。
但这个函数都没有显式的指明,那只有一个可能。这些数据都在第一个参数hSocket里(或指明)。
所以我们知道,发送网络数据,要先建立好SOCKET。
我们来看一下怎么建立。
2.WSAStartup
首先使用Winsock网络API,必须选初始化,启动winsock,固定的如下代码:
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
里面的2,2表明希望使用2.2版本的winsock,而wsaData用来接收版本信息及winsock一些其它的信息。返回值可以用来判断启动是否成功,这里我们用的不多。大概了解即可。基本上遵循上面固定的写法即可。
3.服务端
TCP可分为服务端和客户端,区别是服务端是绑定IP等待别人来连接,客户端是发送连接给服务端。建立连接后双方收发数据则没有什么区别。
但其实看你怎么设计,可以是对等的,比如比特币中,可以即是服务端(等待别人连接),又是客户端(连接别人)。
以下为服务端创建流程:
4.创建socket
然后我们就可以来创建套接字socket,如下:
// 1. 创建 socket
SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
tcp就按上面的固定的格式写,其中,AF_INET表示创建一个ipv4的套接字,SOCK_STREAM表示TCP流式,就是可靠的传输,先建立连接再通信,跟打电话打通一样,而UDP(SOCK_DGRAM),直接就是发送了。
IPPROTO_TCP,表示协议,用这个参数则表示使用TCP协议。UDP协议为IPPROTO_UDP。
5.IP和端口sockaddr_in
接下来就是绑定IP和端口相关,那么我们需要传入IP和端口,还得告知一些其它信息,比如是ipv4类型的。
储存这些数据有个专用的类型sockaddr_in,我们只需要给定它三个参数,绑定的IP和端口,还有AF_INET指明给定的IP类型,即IPV4。
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY;
这里调用了htons(8888),将8888端口转换成相应的类型赋值,而后面的INADDR_ANY,即给地址赋值,这个表示绑定本机所有IP地址。
这些函数,以及赋值方法都不是唯一的,还有很多其它的方法。
比如IP赋值,也可这样:
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
inet_pton这个函数,表明AF_INET的IP类型,然后是"127.0.0.1"这个IP地址,把它填进serverAddr.sin_addr。但这个一般是给客户端使用的。
我们只需要知道最关键的点,sin_family,sin_port,sin_addr,这三个参数要赋值,然后创建相应的类型赋值即可。
6.bind
好,套接字建立起来了,IP和端口也有了,接下来就是调用bind绑定IP和端口,代码如下:
// 2. 绑定端口
bind(serverSock, (sockaddr*)&addr, sizeof(addr));
这些参数都好理解吧,第一个参数就是建立好的socket,第二个参数是sockaddr_in指明IP和端口。第三参数指明sockaddr_In的大小,因为第二参数传的是指针类型的。
7.listen
绑定后,我们就可以告诉系统,启动监听:
// 3. 监听
listen(serverSock, 5);
第一个参数是相关socket,第三个参数告诉系统允许排队的客户端,表明5个,如果前面已有5个在排队,第6个尝试连接时则有可能会被拒绝。
8.accept
然后就是等待接受连接了,如下代码:
// 4. 接受连接
sockaddr_in clientAddr{};
int len = sizeof(clientAddr);
SOCKET clientSock = accept(serverSock, (sockaddr*)&clientAddr, &len);
accept的参数,clientAddr用来接收客户端的地址信息。如果连接上了的话。
这个accept是个阻塞函数,一直等待有客户端连接了,才会执行下一步。
9.recv
然后们调用recv函数,看客户端有没有什么数据发送过来,如下:
// 5. 接收数据
char buffer[1024];
int ret = recv(clientSock, buffer, sizeof(buffer) - 1, 0);
if (ret > 0) {
buffer[ret] = '\0';
std::cout << "Received: " << buffer << std::endl;
}
我们选建一个缓冲区buffer用来接收客户端发送来的数据,此函数前三个参数就不解释了,现在应该一看就知道它的意思吧,后面一个参数正常写0就可以了,是标志位,意为正常接收。没有特殊的要求,现在我们也用不上,写0即可。
另外这个函数也是个阻塞函数,没有数据发送过来则会一直卡在这里。
9.send
好,不仅客户端可以发送消息给我,我们当然也是可以给客户端发送消息,如下:
// 6. 发送数据
const char* msg = "Hello from server";
send(clientSock, msg, strlen(msg), 0);
send函数最后一个参数跟recv的一样,默认为0即可。
看到没有,收发消息就是使用recv和send函数,服务端和客户端都是用这两个函数收发的。
10.断开连接
然后就是断开连接的收发工作,代码如下:
// 7. 关闭
closesocket(clientSock);
closesocket(serverSock);
WSACleanup();
return 0;
当然,如果你想一直工作连接收发消息,就把accept写在循环语句里。然后看情况断开连接。
11.客户端
接下来我们来看一下客户端的代码怎么写,开始如下:
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 1. 创建 socket
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
// 服务器 IP(本机测试)
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
WSAStartup启动,然后创建SOCKET,和sockaddr_in,步骤一样,但注意的是,这里的serverAddr填的是服务端的IP和端口,因为客户端是系统自动绑定IP和临时随机分配端口,不需要你指明。
12.connect
然后不一样的是,我们需要调用connect函数连接服务端,如下:
// 2. 连接服务器
connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr));
13.收发数据
连接上后,就是发送接收数据,跟服务端一样,调用收发函数实现即可。
// 3. 发送数据
const char* msg = "Hello from client";
send(sock, msg, strlen(msg), 0);
// 4. 接收数据
char buffer[1024];
int ret = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (ret > 0) {
buffer[ret] = '\0';
std::cout << "Server says: " << buffer << std::endl;
}
// 5. 关闭
closesocket(sock);
WSACleanup();
return 0;
14.区块链中winsock应用
好,明白了tcp网络winsock整个流程后。
我们来看一下比特币源码中是怎么使用的,要了解这个我们就必须得知道CNode这个类,这个是负责维护网络连接的,在系统中会有一个CNode集合,就是存着已连接上的所有节点。负责维护连接中的节点,注意并不是全网节点,这个节点是相关的已知(邻居)节点。而且数量有限制的。事实广播也不是全网节点广播,而是向节点广播,然后收到的节点再向它的邻居节点广播。变相的达到全网传播的目的。
15.CNode和CAddress
CNode是维护已连接上的节点,有着数量上的限制,而CAddress是存储已知的节点,不负责连接。可以看作是一张地址表,节点之间会分享这些数据,然后把它存到数据库里(如果没有这个IP节点)。
我们在了解数据库那一章可以看到,除了CWalletDB,CTxDB,还有这个:
class CAddrDB : public CDB
{
public:
CAddrDB(const char* pszMode="r+", bool fTxn=false) : CDB("addr.dat", pszMode, fTxn) { }
private:
CAddrDB(const CAddrDB&);
void operator=(const CAddrDB&);
public:
bool WriteAddress(const CAddress& addr);
bool LoadAddresses();
};
bool LoadAddresses();
这就是CAddrDB类的作用,存储节点的地址数据,通过CAddrDB类从数据库里加载地址,连接上后存储到CNode里。注意这里并不是把CAddrDB里的节点都连接上,CNode是有数量限制的。达到要求就会停止。
16.vNodes
连接上的节点,都会把它放在vNodes中,该变量定义如下:
vector<CNode*> vNodes;
然后这个节点分为主动连接,就是从CAddrDB加载后连接上,和被动连接,就是别的节点从CAddrDB加载到你的IP,然后连接上你。
然后你都会把这个节点存到vNodes,其实按TCP来说,就是你服务端客户端都写了处理代码。
17.ConnectNode
我们来看一下ConnectNode,这个是主动连接:
CNode* ConnectNode(CAddress addrConnect, int64 nTimeout)
{
if (addrConnect.ip == addrLocalHost.ip)
return NULL;
// Look for an existing connection
CNode* pnode = FindNode(addrConnect.ip);
if (pnode)
{
if (nTimeout != 0)
pnode->AddRef(nTimeout);
else
pnode->AddRef();
return pnode;
}
/// debug print
printf("trying %s\n", addrConnect.ToString().c_str());
// Connect
SOCKET hSocket;
if (ConnectSocket(addrConnect, hSocket))
{
/// debug print
printf("connected %s\n", addrConnect.ToString().c_str());
// Add node
CNode* pnode = new CNode(hSocket, addrConnect, false);
if (nTimeout != 0)
pnode->AddRef(nTimeout);
else
pnode->AddRef();
CRITICAL_BLOCK(cs_vNodes)
vNodes.push_back(pnode);
CRITICAL_BLOCK(cs_mapAddresses)
mapAddresses[addrConnect.GetKey()].nLastFailed = 0;
return pnode;
}
else
{
CRITICAL_BLOCK(cs_mapAddresses)
mapAddresses[addrConnect.GetKey()].nLastFailed = GetTime();
return NULL;
}
}
它的参数是CAddress addrConnect,说明上级是加载CAddress(仅以数据库加载为例),然后调用ConnectNode函数。
这个函数,里有一些其它的代码,是因为连接节点,当然不是没头脑就是直接连了,还要做一些管理,比如这个节点是不是已经连接上了,就是那个FindNode判断。
还得记录连接是否成功,或者失败了记录一下失败时间,并且下次优先连接容易连接成功的节点。
现在我们大概了解即可,我们来看一下关键的代码:
18.ConnectSocket
if (ConnectSocket(addrConnect, hSocket))
这个就是负责连接网络的函数,不涉及到节点维护,在这个函数代码我们看到winsock的应用:
bool ConnectSocket(const CAddress& addrConnect, SOCKET& hSocketRet)
{
hSocketRet = INVALID_SOCKET;
SOCKET hSocket = socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
return false;
bool fRoutable = !(addrConnect.GetByte(3) == 10 || (addrConnect.GetByte(3) == 192 && addrConnect.GetByte(2) == 168));
bool fProxy = (addrProxy.ip && fRoutable);
struct sockaddr_in sockaddr = (fProxy ? addrProxy.GetSockAddr() : addrConnect.GetSockAddr());
if (connect(hSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
{
closesocket(hSocket);
return false;
}
if (fProxy)
{
printf("Proxy connecting to %s\n", addrConnect.ToString().c_str());
char pszSocks4IP[] = "\4\1\0\0\0\0\0\0user";
memcpy(pszSocks4IP + 2, &addrConnect.port, 2);
memcpy(pszSocks4IP + 4, &addrConnect.ip, 4);
char* pszSocks4 = pszSocks4IP;
int nSize = sizeof(pszSocks4IP);
int ret = send(hSocket, pszSocks4, nSize, 0);
if (ret != nSize)
{
closesocket(hSocket);
return error("Error sending to proxy\n");
}
char pchRet[8];
if (recv(hSocket, pchRet, 8, 0) != 8)
{
closesocket(hSocket);
return error("Error reading proxy response\n");
}
if (pchRet[1] != 0x5a)
{
closesocket(hSocket);
return error("Proxy returned error %d\n", pchRet[1]);
}
printf("Proxy connection established %s\n", addrConnect.ToString().c_str());
}
hSocketRet = hSocket;
return true;
}
建立sock在这句:
SOCKET hSocket = socket(AF_INET, SOCK_STREAM, 0);
然后是建立sockaddr_in
bool fRoutable = !(addrConnect.GetByte(3) == 10 || (addrConnect.GetByte(3) == 192 && addrConnect.GetByte(2) == 168));
bool fProxy = (addrProxy.ip && fRoutable);
struct sockaddr_in sockaddr = (fProxy ? addrProxy.GetSockAddr() : addrConnect.GetSockAddr());
因为服务端的地址和端口,以及其它的数据都是在CAddress里面,所以取值的过程跟我们之前不一样。
上面这三句是什么意思呢?
19.代理
第一句,其实就是判断这个IP地址是局域网还是公网地址,比如10开头的,和192.168这样的地址的话,不是公网的话,那么fRoutable就为假,公网(外网)地址的话,fRoutable就为真。
然后是这句:
bool fProxy = (addrProxy.ip && fRoutable);
addrProxy.ip是代理ip,这个参数能知道你本机是否配置的了代理,为0则无代理。
fProxy的意思,为真,即表示,你要连接的节点是外网IP,然后你本机又配置了代理。
此时给的sockaddr,当然得先连接代理再转发,即第三句,赋值addrProxy.GetSockAddr(),否则直接
addrConnect.GetSockAddr就是直连IP就行。
上面就是因为代理的原因,所以这里会这么个判断,而如果是192.168这种地址,不管你用不用代理,都是直连,因为不可能你走一圈代理,然后又连到你的局域网,那是不可能的。
那个局域网代理也找不到。所以要排除局域网。
然后就是连接了,如下:
if (connect(hSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR)
{
closesocket(hSocket);
return false;
}
这个connect函数就不解释了,都是winsock的基本函数,知道是干什么的。
接下来还是代理相关:
if (fProxy)
{
printf("Proxy connecting to %s\n", addrConnect.ToString().c_str());
char pszSocks4IP[] = "\4\1\0\0\0\0\0\0user";
memcpy(pszSocks4IP + 2, &addrConnect.port, 2);
memcpy(pszSocks4IP + 4, &addrConnect.ip, 4);
char* pszSocks4 = pszSocks4IP;
int nSize = sizeof(pszSocks4IP);
int ret = send(hSocket, pszSocks4, nSize, 0);
if (ret != nSize)
{
closesocket(hSocket);
return error("Error sending to proxy\n");
}
char pchRet[8];
if (recv(hSocket, pchRet, 8, 0) != 8)
{
closesocket(hSocket);
return error("Error reading proxy response\n");
}
if (pchRet[1] != 0x5a)
{
closesocket(hSocket);
return error("Proxy returned error %d\n", pchRet[1]);
}
printf("Proxy connection established %s\n", addrConnect.ToString().c_str());
}
就是如果走的的代理模式,就得通知代理,我要真正通信的是谁(通过addrConnect),接下来帮我转发数据。通过socks4协议的方式。pszSocks4发送的数据里包含有说明。
然后代理回复"0x5a"就表示建立转发通道成功。
好了,ConnectoSocket里面其实写的代码,就是winsock通信的标准示例,只是多了个代理的功能。搞清楚后,我们就不难理解了。后续其实也可以简化,我们可以很安全的去掉代理功能。而不会导致程序出编译bug.
20.CNode
我们在上一章中发送tx的时候,发现会先将数据存到vSend中,即每个节点都是发送一下,就是广播。这个vSend变量是属于CNode类的,如下定义:
class CNode
{
public:
// socket
uint64 nServices;
SOCKET hSocket;
CDataStream vSend;
CDataStream vRecv;
CCriticalSection cs_vSend;
CCriticalSection cs_vRecv;
unsigned int nPushPos;
CAddress addr;
int nVersion;
bool fClient;
bool fInbound;
bool fNetworkNode;
bool fDisconnect;
其中第三个就是vSend,这样我们就知道了,给节点要发送什么数据。只要访问CNode中的vSend就行,它就是数据发送缓冲区。那当然还有下面的vRecv,我们也能猜到了,数据接收缓冲区,对应的节点发送给们的数据。
好,关于CNode和CAddress这些类的详细解释后续说明,以及是从哪里加载CAddress并连接的,这个过程我们后面都要搞清楚(以数据库为例)。
未来:当整个网络部分搞定后,整个大框架已经建好了,剩下的就是一些小细节了,比如验证那块(有很多代码都被注释掉),默克尔树验证等。把它们都恢复处理好,我们就可以来完整运行程序了。当然ui部分就去掉了,我们只了解其核心逻辑代码即可,把它们用命令行控制,比如发送按钮绑定的函数这些。

216

被折叠的 条评论
为什么被折叠?



