比特幣源碼解讀-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)。

 

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