比特币源码解读-P2P网络

  ----by 红亚太学链:yjh、bjgpdn

  比特币又被称为分布式账本,具有去中心化、匿名性、鲁棒性等优势,与其采用的P2P网络架构有分不开的联系。可以说,P2P网络是比特币运行的基石,没有P2P,比特币的账本设计则失去了价值。本文着重讲解比特币的P2P网络实现。

1. P2P网络简介

  P2P网络按节点查询的方式分类,主要由四种不同的模型,这四种模型按时间顺序的产生分别是集中式、纯分布式、混合式和结构化模型。

1.1 集中式

  最早出现的P2P模型是集中式,中心节点保存了所有其它节点的索引信息,一般包括节点 IP 地址、端口、节点资源等。这种方式和谷歌的GFS文件系统有异曲同工之妙。当某个节点希望了另一个节点建立连接时,只需向中心节点请求信息即可。集中式路由的优点就是结构简单、实现容易。但缺点也很明显,由于中心节点需要存储所有节点的路由信息,当节点规模扩展时,就很容易出现性能瓶颈;而且也存在单点故障问题。

1.2 全分布式非结构化

  纯分布式的P2P网络不存在中心节点,所有节点之间的链接是随机产生。新节点加入只需要随便找到一其中一个节点连接即可。比特采用的是纯分布式的P2P架构,它使用 DNS 的方式来查询其它节点,DNS 一般是硬编码到代码里的,这些 DNS 服务器就会提供比特币节点的 IP 地址列表,从而新节点就可以找到其它节点建立连接通道。

  最早的纯分布式网络,每个节点会向全网以洪泛的方式广播自己的信息,让全网都知道自己的存在。纯分布式结构不存在集中式结构的单点性能瓶颈问题和单点故障问题,但大量的洪泛如果不做控制将会导致广播风暴以致网络瘫痪。

1.3 半分布式

  半分布式集合全分布式和集中式的优点,将两种方式结合并做了平衡。半分布式P2P网络由少量超级节点和大量普通节点组成。半分布式网络可以看做是由多个以超级节点为中心节点的集中式网络链接而成。新加入的节点可以选择某个超级节点来加入,由于洪泛洪泛只在超级节点之间,减少了广播风暴的风险。多超级节点的备份,也能提高整体网络的健壮性。

1.4 全分布式结构化

  最后一种是结构化的分布式网络,这种网络和非结构化分布式一样不存在中心节点。但不同的是,非结构化分布式网络的网络构成是完全随机的,但结构化网络则将所有节点按照某种结构进行有序组织,比如形成一个环状网络或树状网络。而结构化网络的具体实现上,普遍都是基于 DHT(Distributed Hash Table,分布式哈希表) 算法思想。DHT 只是提出一种网络模型,并不涉及具体实现。

2. 比特币0.01的P2P实现

2.1 节点的定义

  比特币节点是比特币网络通信的基本单位,比特币将每个节点的信息定义在CNode结构体中,CNode结构体的定义如下:

  这里先给出CNode类成员简要释义,后续会详细给出各成员作用。

2.2 IRCSeed

  比特币的一个新节点如果想要加入比特币网络,首先要能够找到一个已经在比特币网络中的节点进行连接。那么要怎样才能找到一个比特币中的节点呢?首先要有一个叫做Seed的东西提供服务,这个Seed往往是一个DNS服务器,通过访问Seed就能够获取到比特币节点地址值列表,从而获得许多比特币节点地址,就像一颗种子能够通过它获取到更多果实。

  然而早期版本的比特币并没有采用SeedDNS服务器的方式来获取比特币网络地址,而是采用的IRC(Internet Relay Chat),IRC的特点是能够在聊天对象之间建立频道,频道内的人都能够收到信息,频道外的人则不能,就相当于一个聊天室。比特币0.01正是利用这一特点,在IRC的服务器上建立比特币的唯一频道,这样,想要加入比特币网络的人可以通过进入这一频道和其它其它在这一频道的比特币使用者进行信息交换,获取到他们的IP,从而得以加入比特币网络。

  下面是比特币0.01源码ThreadIRCSeed函数解读:

struct hostent* phostent = gethostbyname("chat.freenode.net");
CAddress addrConnect(*(u_long*)phostent->h_addr_list[0], htons(6667));
SOCKET hSocket;
if (!ConnectSocket(addrConnect, hSocket))
{
     printf("IRC connect failed\n");
     return;
}

  首先是要通过域名http://chat.freenode.net连接到IRC的服务器,端口号是6667

  ConnectSocket函数主要是封装了套接字的connect函数,另外了进行了路由判断和代理判断。

  用户可能连接的是代理,也可能连接的就是http://chat.freenode.net域名的地址。

if (!RecvUntil(hSocket, "Found your hostname", "using your IP address instead", "Couldn't look up your hostname"))
{
   closesocket(hSocket);
   return;
}

  这个函数主要封装了套接字的recv函数,解析IRC服务器发来的消息,若是三个字符串中的其中一个则,要返回错误。

string strMyName = EncodeAddress(addrLocalHost);
if (!addrLocalHost.IsRoutable())
    strMyName = strprintf("x%u", GetRand(1000000000));

Send(hSocket, strprintf("NICK %s\r", strMyName.c_str()).c_str());
Send(hSocket, strprintf("USER %s 8 * : %s\r", strMyName.c_str(), strMyName.c_str()).c_str());

  之后将本地地址(ip+port)通过base58编码得到一个字符串作为名字,编码的名字前面还固定加了一个字符’u’。如果本地址是内网地址,也就是http://192.168.XXX.XXX,则通过一个随机数组成一个名字。用套接字send函数向服务器发送暱称信息和用户名信息。

if (!RecvUtil(hSocket, " 004 "))
{
     closesocket(hSocket);
     return;
}

Send(hSocket, "JOIN #bitcoin\r");
Send(hSocket, "WHO #bitcoin\r");

  然后等待服务器发送来消息,如果是004,应该是就错误号,则关闭连接。这些过程就是遵循IRC协议内容,不作过多深究。之后向服务器再次发送两条消息,分别是加入#bitcoin频道请求和询问#bitcoin频道内用户的请求,#bitcoin就是属于比特币用户们的聊天室,

while (!fRestartIRCSeed)
{
    string strLine;
    if (fShutdown || !RecvLineIRC(hSocket, strLine))
    {
        closesocket(hSocket);
        return;
    }
    if (strLine.empty() || strLine[0] != ':')
        continue;

    vector<string> vWords;
    ParseString(strLine, ' ', vWords);
    if (vWords.size() < 2)
        continue;

    char pszName[10000];
    pszName[0] = '\0';

    if (vWords[1] == "352" && vWords.size() >= 8)
    {
        strcpy(pszName, vWords[7].c_str());
        printf("GOT WHO: [%s]  ", pszName);
    }

    if (vWords[1] == "JOIN")
{
    strcpy(pszName, vWords[0].c_str() + 1);
        if (strchr(pszName, '!'))
            *strchr(pszName, '!') = '\0';
        printf("GOT JOIN: [%s]  ", pszName);
    }

    if (pszName[0] == 'u')
    {
        CAddress addr;
        if (DecodeAddress(pszName, addr))
        {
            CAddrDB addrdb;
            if (AddAddress(addrdb, addr))
                printf("new  ");
            addr.print();
        }
        else
        {
            printf("decode failed\n");
        }
    }
}

  之后进入消息接收的循环,通过RecvLineIRC函数将服务器发送来的消息放入strLine中,使用ParseString函数对该字符串消息进行解析,前两个if应该就是服务器返回的确认信息,第三个if,也解释当收到的字符串的第一个字符是’u’时,代表收到一个#bitcoin内用户的用户名信息,将这个用户名进行base58反编码(因为#bitcoin频道内用户名都是地址编码而来的)便得到了一个比特币用户的地址。然后就可以将这个地址存入到本地的地址数据库中,方便后续进行连接。

  至此通过IRC作为种子获取其它其它比特币节点地址的流程介绍完毕。

2.3 节点的连接

  如前文所述,节点就是P2P网络中的一个基本单位。在比特币程序中,每个和本地进行连接的节点信息都被存储在2.1节列出的CNode结构体中。在2.2节中我们已经通过IRCSeed获得了比特币中其它节点的地址,下面将介绍如何使用这些地址进行节点之间的连接。

  下面是比特币0.01源码ThreadOpenConnections函数解读:

const int nMaxConnections = 15;

  首先设置最大连接数为15。

  以下内容为loop循环内容:

vfThreadRunning[1] = false;
Sleep(500);
while (vNodes.size() >= nMaxConnections || vNodes.size() >= mapAddresses.size())
{
    CheckForShutdown(1);
    Sleep(2000);
}

vfThreadRunning[1] = true;
CheckForShutdown(1);

  vfThreadRunning[1]是节点链接线程标记,CheckForShutdown(1)则是检查全局变量fShutdown是否是true,如果是则要关闭线程。后续不再解释。vNodes是全局变量,即与本地节点相连的节点的列表类型为vector<CNode*>。这里就是如果实际节点连接个数大于最大连接数,或者地址表个数则要sleep等待1000毫秒也就是1秒。

unsigned char pchIPCMask[4] = { 0xff, 0xff, 0xff, 0x00 };
unsigned int nIPCMask = *(unsigned int*)pchIPCMask;
vector<unsigned int> vIPC;
CRITICAL_BLOCK(cs_mapAddresses)
{
 vIPC.reserve(mapAddresses.size());
 unsigned int nPrev = 0;
 
 foreach(const PAIRTYPE(vector<unsigned char>, CAddress)& item, mapAddresses)
 {
 const CAddress& addr = item.second;
 if (!addr.IsIPv4())
 continue;
 unsigned int ipC = addr.ip & nIPCMask;
 if (ipC != nPrev)
 vIPC.push_back(nPrev = ipC);
 }
}

  这段代码中CRITICAL_BLOCK包含的部分就是互斥访问的字段,用来使互斥数据不被多线程同时访问。

  nIPCMask 很明显是掩码255.255.255.0。这段代码就是将地址表中地址按C类网络号进行分类,如202.204.2.1与202.204.2.1网络号都是http://202.204.2.xxx分为一类。将这些不同的网络号放入到栈vIPC中。值得一提的是mapAddresses是map表,其中元素不会随意摆放,而是进行过排序,所以相同地址一定是紧挨着的。

bool fSuccess = false;
int nLimit = vIPC.size();

  定义两个变量用于while判断,以下代码又在while(!fSuccess && nLimit-- > 0)循环中,也就是loop循环中的一个while循环。因为代码比较长,不便解释,分段给出。

unsigned int ipC = vIPC[GetRand(vIPC.size())];

  从上面分类好的C类网络中随机选取出一个网络号

map<unsigned int, vector<CAddress> > mapIP;
CRITICAL_BLOCK(cs_mapAddresses)
{
 /*延时计算省略*/
 for (map<vector<unsigned char>, CAddress>::iterator mi = mapAddresses.lower_bound(CAddress(ipC, 0).GetKey());
 mi != mapAddresses.upper_bound(CAddress(ipC | ~nIPCMask, 0xffff).GetKey());
 ++mi)
 {
 const CAddress& addr = (*mi).second;
 unsigned int nRandomizer = (addr.nLastFailed * addr.ip * 7777U) % 20000;
 // 当前时间 - 地址连接最新失败的时间 要大于对应节点重连的间隔时间
 if (GetTime() - addr.nLastFailed > nDelay * nRandomizer / 10000)
 mapIP[addr.ip].push_back(addr); //同一个地址区段不同地址; 同一个地址的不同端口
}
if (mapIP.empty())
 break;
map::lower_bound(key):返回map中第一个大于或等于key的迭代器指针
map::upper_bound(key):返回map中第一个大于key的迭代器指针

  这段的意思就是从刚刚选出的网络号假设是http://202.204.2.xxx,则根据地址号由小到大遍历map地址表中网络是http://202.204.2.xxx的地址,这里的地址不仅仅是ip还有port。将这个些相同网络号的地址的地址根据ip放入mapIP中。

  这里可能有些绕,总之mapIP中存储的是同一网络区段的不同地址的信息。每个不同地址由于端口号的不同也会有多个。

map<unsigned int, vector<CAddress> >::iterator mi = mapIP.begin();
advance(mi, GetRand(mapIP.size())); // 将指针定位到随机位置

  将迭代器指针随机定位到mapIP表的一个位置。

foreach(const CAddress& addrConnect, (*mi).second)
{
 // ip不能是本地ip,且不能是非ipV4地址,对应的ip地址不在本地的节点列表中
 if (addrConnect.ip == addrLocalHost.ip || !addrConnect.IsIPv4() || FindNode(addrConnect.ip))
    continue;
 
// 链接对应地址信息的节点
 CNode* pnode = ConnectNode(addrConnect);
 if (!pnode)
    continue;
 
pnode->fNetworkNode = true; //设置对应的节点为网络节点,是因为从对应的本地节点列表中没有查询到
 
 // 如果本地主机地址能够进行路由,则需要广播我们的地址
 if (addrLocalHost.IsRoutable())
 {
     // Advertise our address
     vector<CAddress> vAddrToSend;
     vAddrToSend.push_back(addrLocalHost);
     pnode->PushMessage("addr", vAddrToSend); // 将消息推送出去放入vsend中,在消息处理线程中进行处理
 }

 // 从创建的节点获得尽可能多的地址信息,发送消息,在消息处理线程中进行处理
 pnode->PushMessage("getaddr");
 // 新建的节点要订阅我们本地主机订阅的对应通断
 const unsigned int nHops = 0;
 
for (unsigned int nChannel = 0; nChannel < pnodeLocalHost->vfSubscribe.size(); nChannel++)
 if (pnodeLocalHost->vfSubscribe[nChannel])
     pnode->PushMessage("subscribe", nChannel, nHops);
 
 fSuccess = true;
 break;
}

  后面的逻辑就比较清楚了,先判断这个地址的ip是不是已经存在了,不存在则对该节点进行链接,所以同一ip不同端口的地址只允许连接一个。ConnectNode函数主要封装了connect函数,并会返回一个填写好的节点信息。将这些节点设置为网络节点。然后向该节点推送包含本地地址的消息,来将我们的地址向其它节点广播。再向该节点推送地址请求消息,来获取该节点拥有的其它节点的地址信息。

  这里只是消息推送到其它节点映射的节点对象里,并没有真正的和其它节点进行信息交互,真正的信息交互在消息处理的线程中。

  节点连接建立的介绍到此为止。

2.4 节点连接的处理

  2.3节讲述了本地如何去和其它节点建立连接,但本地节点还要监听其它节点发来的连接请求,并且连接可能会失效,节点发来的消息要进行解析,这些都要处理,本节讲述这些处理的过程

  下面是比特币0.01源码ThreadMessageHandler2函数解读:

printf("ThreadSocketHandler   started\n");
SOCKET hListenSocket = *(SOCKET*)parg; // 获得监听socket
list<CNode*> vNodesDisconnected;
int nPrevNodeCount = 0;

  ThreadMessageHandler2函数的参数parg传过来的是在主线程创建的监听线程,我们知道在socket编程中,服务器端常有一个listen套接字专门用来处理连接请求。Parg就是经过listen函数处理过的被动套接字。

  定义一个断连节点列表,定义一个前置节点计数。

  以下代码内容皆在loop循环中:

map<unsigned int, CNode*> mapFirst;
foreach(CNode* pnode, vNodes)
{
 if (pnode->fDisconnect)
     continue;
 
unsigned int ip = pnode->addr.ip;
 // 本地主机ip地址对应的是0,所以所有的ip地址都应该大于这个ip
 if (mapFirst.count(ip) && addrLocalHost.ip < ip)
 {
     CNode* pnodeExtra = mapFirst[ip];
     if (pnodeExtra->GetRefCount() > (pnodeExtra->fNetworkNode ? 1 : 0))
         swap(pnodeExtra, pnode);
 
 if (pnodeExtra->GetRefCount() <= (pnodeExtra->fNetworkNode ? 1 : 0))
 {
     printf("(%d nodes) disconnecting   duplicate: %s\n", vNodes.size(), pnodeExtra->addr.ToString().c_str());
     if (pnodeExtra->fNetworkNode && !pnode->fNetworkNode)
     {
         pnode->AddRef();
         swap(pnodeExtra->fNetworkNode, pnode->fNetworkNode);
         pnodeExtra->Release();
     }
     pnodeExtra->fDisconnect = true;
   }
 }

 mapFirst[ip] = pnode;
}

  上述代码简要来说就是在相同ip的连接节点中,释放掉较小的引用(因为vNode进行了默认排序,后遍历到的较大),而为较大的添加引用,从而保证连接相同ip,地址更大的节点。

vector<CNode*> vNodesCopy = vNodes;
foreach(CNode* pnode, vNodesCopy)
{
 // 节点准备释放链接,并且对应的接收和发送缓存区都是空
 if (pnode->ReadyToDisconnect() && pnode->vRecv.empty() && pnode->vSend.empty())
 {
 // 从节点列表中移除
 vNodes.erase(remove(vNodes.begin(), vNodes.end(), pnode), vNodes.end());
 pnode->Disconnect();
 // 将对应准备释放的节点放在对应的节点释放链接池中,等待对应节点的所有引用释放
 pnode->nReleaseTime = max(pnode->nReleaseTime, GetTime() + 5 * 60); // 向后推迟5分钟
 if (pnode->fNetworkNode)
 pnode->Release();
 vNodesDisconnected.push_back(pnode);
 }
}

  如果节点已经处于准备释放状态(引用为0,fDisconnect为true),并且发送缓冲区和接收缓冲区为空,则将该节点信息从节点链表中移除,并断开连接。然后将该节点放入节点断开池中。

foreach(CNode* pnode, vNodesDisconnectedCopy)
{
 if (pnode->GetRefCount() <= 0){
 bool fDelete = false;
 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
 TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
 TRY_CRITICAL_BLOCK(pnode->cs_mapRequests)
 TRY_CRITICAL_BLOCK(pnode->cs_inventory)
 fDelete = true;
 if (fDelete){
 vNodesDisconnected.remove(pnode);
 delete pnode;}}
}

  对节点断开池中的节点进行删除。

struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 50000; // frequency to poll pnode->vSend 咨询节点是否有数据要发送的频率
 
struct fd_set fdsetRecv; // 记录所有节点对应的socket句柄和监听socket句柄
struct fd_set fdsetSend; // 记录所有有待发送消息的节点对应的socket句柄
 
SOCKET hSocketMax = 0;
FD_SET(hListenSocket, &fdsetRecv); // FD_SET将hListenSocket 放入fdsetRecv对应的数组的最后
hSocketMax = max(hSocketMax, hListenSocket);
CRITICAL_BLOCK(cs_vNodes)
{
 foreach(CNode* pnode, vNodes)
 {
 FD_SET(pnode->hSocket, &fdsetRecv);
 hSocketMax = max(hSocketMax, pnode->hSocket); // 找出所有节点对应的socket的最大值,包括监听socket
 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
 if (!pnode->vSend.empty())
 FD_SET(pnode->hSocket, &fdsetSend); // FD_SET 字段设置
 }
}

  设置两个select监听池fdsetRecv,fdsetSend,分别监听套接字接收消息和发送消息。将节点表内节点以及监听节点全部放入接收监听池fdsetRecv中,并将有发送缓冲区有消息的节点放入发送监听池fdsetSend。

int nSelect = select(hSocketMax + 1, &fdsetRecv, &fdsetSend, NULL, &timeout);

  Seletct函数对两个监听池进行监听。

if (FD_ISSET(hListenSocket, &fdsetRecv))
{
 struct sockaddr_in sockaddr;
 int len = sizeof(sockaddr);
 SOCKET hSocket = accept(hListenSocket, (struct sockaddr*)&sockaddr, &len); // 接收socket链接
 CAddress addr(sockaddr);
 if (hSocket == INVALID_SOCKET)
 {
 if (WSAGetLastError() != WSAEWOULDBLOCK)
 printf("ERROR ThreadSocketHandler   accept failed: %d\n", WSAGetLastError());
 }
 else
 {
 printf("accepted connection from   %s\n", addr.ToString().c_str());
 CNode* pnode = new CNode(hSocket, addr, true); // 有新的socket链接,则新建对应的节点,并将节点在加入本地节点列表中
 pnode->AddRef();
 CRITICAL_BLOCK(cs_vNodes)
 vNodes.push_back(pnode);
 }
}

  首先使用FD_ISSET(hListenSocket, &fdsetRecv)函数对,查看hListenSocket套接字也就是接收连接消息的套接字是否被置位,被置位则代表有其它节点的连接请求。使用accept函数进行连接,并添加新的节点对象。

if (FD_ISSET(hSocket, &fdsetRecv))
{
 TRY_CRITICAL_BLOCK(pnode->cs_vRecv)
 {
 CDataStream& vRecv = pnode->vRecv;
 unsigned int nPos = vRecv.size();
 // typical socket buffer is 8K-64K
 const unsigned int nBufSize = 0x10000;
 vRecv.resize(nPos + nBufSize);// 调整接收缓冲区的大小
 int nBytes = recv(hSocket, &vRecv[nPos], nBufSize, 0);// 从socket中接收对应的数据
 vRecv.resize(nPos + max(nBytes, 0));
 ......
 ......
 }
}

  遍历每个节点socket,首先使用(FD_ISSET(hSocket, &fdsetRecv),查看该节点是否有接收消息,将消息放入节点的接收缓冲区vRecv中等待处理。

if (FD_ISSET(hSocket, &fdsetSend))
{
 TRY_CRITICAL_BLOCK(pnode->cs_vSend)
 {
 CDataStream& vSend = pnode->vSend;
 if (!vSend.empty())
 {
 int nBytes = send(hSocket, &vSend[0], vSend.size(), 0); // 从节点对应的发送缓冲区中发送数据
 ......
  }
 ......
}

  在查看完接收监听池后,再用FD_ISSET(hSocket, &fdsetSend)查看发送监听池,使用send函数,将信息发送出去。

  至此监听处理进程介绍完毕。

3. 总结

  比特币0.01源码,使用IRC作为发现比特币网络的seed,通过加入#bitcoin频道,获取到比特币中其它节点的地址信息,再通过节点链接线程,建立和其它比特币节点的连接。

  比特币网络中的节点可以通过监听处理线程监听其它节点的连接请求,并接收连接。

  这样一来每个比特币节点,即使主动连接他人的客户端,又是监听连接的服务器端。同时每个节点可以向其它节点请求地址列表,或者广播自己的地址,从而建立起多个连接,实现了P2P的网络架构。

 

  完整版PDF下载:比特币源码解读-P2P网络

 

附录

互联网中继聊天协议(IRC)

  IRC是Internet Relay Chat的英文缩写,中文一般称为互联网中继聊天。它是由芬兰人Jarkko Oikarinen于1988年首创的一种网络聊天协议。经过十年的发展,目前世界上有超过60个国家提供了IRC的服务。在人气最旺的EFnet上,您可以看到上万的使用者在同一时间使用IRC。很多人称其为继bbs后的一种即时闲聊方式,相比于bbs来说,它有着更直观,友好的界面,在这里你可以畅所欲言、而且可以表现动作化,是故使众多的网虫们留连忘返。

  相比于ICQ来说,它更具人性化,而且是即时式的聊天,更接近真实的聊天情景。下面看IRC的工作原理。 IRC的工作原理非常简单,您只要在自己的PC上运行客户端软件,然后通过因特网以IRC协议连接到一台IRC服务器上即可。它的特点是速度非常之快,聊天时几乎没有延迟的现象,并且只占用很小的带宽资源。所有用户可以在一个被称为“Channel(频道)”的地方就某一话题进行交谈或密谈。每个IRC的使用者都有一个Nickname(暱称),所有的沟通就在他们所在的Channel内以不同的Nickname进行交谈。

1. 中转

  理解IRC原理的关键就是理解其"中转"功能。什么是中转呢?我们来做一个比较说明。假设,A与B要交谈。如果不采用中转,那么A直接建立一条到达B的通信隧道,二者通过这条通信隧道进行信息交流,信息流的方向为:A-B和B-A;如果采用中转,则需要有一个第三方来担任中转角色,设为C,A建立一条到达C的通信隧道,B也建立一条到达C的通信隧道,然后A与B通过C来间接进行通信,信息流的方向为:A-C-B和B-C-A。C就起着A与B间的中转站的作用。中转有什么优点呢?中转的最大优点是使“群聊”能够方便地进行。恰当地说,中转模式为信息广播提供了方便。我们来举例子。假设A,B和D三者要一起聊天。如果没有C的中转,那么A要将所说的每句话分别发给B和D;如果有C做中转,那么A将所说的话发给C,然后C将A的话分别发给B和D。可见,当没有中转时,每个参与聊天的计算机都要执行信息广播的任务,当存在中转时,信息广播的任务全由中转者来执行。中转站C的存在使得信息交流过程中的工作任务发生分离,可以把网络环境好、机器配置高的计算机作为中转站来提供服务功能。这就形成了IRC的服务器-客户端模型,聊天者作为客户端,连接到中转站服务器上。

2. 服务器网络

  在上面的例子里,只有一个中转者C来承担服务。当聊天者数量很多时,会使C不堪重负。解决的办法是,使用多个服务器,服务器之间互相连接成网络,把聊天者分散到各个服务器上。服务器网络以树型结构互相连通。聊天者可以任选一个服务器连接。举例来说,在北京建立一个IRC服务器,称为BJ,在上海建立一个IRC服务器,称为SH,然后将BJ和SH连接起来,组成一个只有两个服务器的IRC网络。北京的用户连接到BJ上,上海的用户连接到SH上,这样北京的用户就可以与上海的用户聊天了。其它地区的用户可以根据地理位置的远近选择使用BJ或SH服务器。概括地说,聊天网络上的每个服务器都是一个中转站,当它从一个服务器或客户收到一条消息时,就将该消息转发给其它服务器,同时也根据具体情况,决定是否将消息转发给连接到自己的用户。

3. 频道

  频道的本质是广播组。用户可以进入一个频道,也可以离开一个频道。当一个用户朝频道说话时,频道里的其它用户都能收到他的话(由服务器中转)。当第一个用户进入频道时,频道被创建,当最后一个用户离开频道时,频道被取消。因此,从用户的角度看,频道就是聊天室。下面说说频道之所以被称为“频道”的原因。如果一个聊天网络有多个服务器,频道要由服务器共同维护。举一个例子。有三个服务器,连接方式为A-B-C。在服务器A上,有第一个用户进入#IRC频道,这时,服务器A上即创建频道"#IRC",A将频道"IRC"的创建消息发给B和C。由于B和C上都没有用户位于#IRC频道,因此不执行任何操作。在这以后,服务器C上有一个用户进入#IRC频道,此时服务器C上也创建频道"#IRC",C将"#IRC"的创建消息发给A和B。之后,需要执行以下操作:B上建立频道"#IRC"并将A与C的"#IRC"频道连接起来,组成一个统一的#IRC。现在,虽然B上没有用户位于#IRC频道内,但是B上也开通了#IRC频道。可见,频道好像一条通信管道,将所有开通此频道的服务器贯穿起来,信息流在这个管道中流通。

4. 请求与应答

  IRC上的信息交流采用请求与应答的模式。请求是由服务器或客户端发出的,其目的是请求(另)一个服务器执行某个操作或提供某些信息;应答是服务器对一个请求的回应信息。请求通常被称为命令;由于对每种应答都规定了一个三位数字做标识,应答也称为数字应答(numeric reply)。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章