﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-CPP&amp;&amp;设计模式小屋-随笔分类-P2P DHT</title><link>http://www.cppblog.com/shenhuafeng/category/2577.html</link><description>(STL,Templete,Generric Programming COM,COM+,ActiveX)---Windows &amp;&amp; Linux &amp;&amp;OpenSource
</description><language>zh-cn</language><lastBuildDate>Mon, 19 May 2008 23:01:05 GMT</lastBuildDate><pubDate>Mon, 19 May 2008 23:01:05 GMT</pubDate><ttl>60</ttl><item><title>P2P之UDP穿透NAT的原理与实现（附源代码）（转）</title><link>http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17215.html</link><dc:creator>CPP&amp;&amp;设计模式小屋</dc:creator><author>CPP&amp;&amp;设计模式小屋</author><pubDate>Thu, 04 Jan 2007 05:37:00 GMT</pubDate><guid>http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17215.html</guid><wfw:comment>http://www.cppblog.com/shenhuafeng/comments/17215.html</wfw:comment><comments>http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17215.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/shenhuafeng/comments/commentRss/17215.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shenhuafeng/services/trackbacks/17215.html</trackback:ping><description><![CDATA[
		<div align="center">
				<h1 class="aTitle">P2P之UDP穿透NAT的原理与实现（附源代码）</h1>
		</div>
		<table width="97%" align="center">
				<tbody>
						<tr>
								<td align="middle">作者：<span id="AuthorLabel">shootingstars</span> | 日期：<span id="TimeLabel">2004-05-25</span> | 字体：<a href="javascript:ContentSize(16)">大</a><a href="javascript:ContentSize(14)">中</a><a href="javascript:ContentSize(12)">小</a></td>
						</tr>
				</tbody>
		</table>
		<div class="content" id="BodyLabel" style="PADDING-RIGHT: 10px; DISPLAY: block; PADDING-LEFT: 10px; PADDING-BOTTOM: 0px; PADDING-TOP: 0px">
				<p>P2P 之 UDP穿透NAT的原理与实现（附源代码）<br />原创：shootingstars<br />参考：<a href="http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt">http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt</a><!--IWMS_AD_BEGIN--></p>
				<div style="FLOAT: right; WIDTH: 350px">
						<script src="ads/show350_250.js" type="text/javascript">
						</script>
				</div>
				<!--IWMS_AD_END-->
				<p>
				</p>
				<p>论坛上经常有对P2P原理的讨论，但是讨论归讨论，很少有实质的东西产生（源代码）。呵呵，在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。</p>
				<p>首先先介绍一些基本概念：<br />    NAT(Network Address Translators)，网络地址转换：网络地址转换是在IP地址日益缺乏的情况下产生的，它的主要目的就是为了能够地址重用。NAT分为两大类，基本的NAT和NAPT(Network Address/Port Translator)。<br />    最开始NAT是运行在路由器上的一个功能模块。<br />    <br />    最先提出的是基本的NAT，它的产生基于如下事实：一个私有网络（域）中的节点中只有很少的节点需要与外网连接（呵呵，这是在上世纪90年代中期提出的）。那么这个子网中其实只有少数的节点需要全球唯一的IP地址，其他的节点的IP地址应该是可以重用的。<br />    因此，基本的NAT实现的功能很简单，在子网内使用一个保留的IP子网段，这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络，那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址，但是不会改变IP包中的端口)<br />    关于基本的NAT可以参看RFC 1631<br />    <br />    另外一种NAT叫做NAPT，从名称上我们也可以看得出，NAPT不但会改变经过这个NAT设备的IP数据报的IP地址，还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多（呵呵，我没有见到过），NAPT才是我们真正讨论的主角。看下图：<br />                                Server S1                         <br />                         18.181.0.31:1235                          <br />                                      |<br />          ^  Session 1 (A-S1)  ^      |  <br />          |  18.181.0.31:1235  |      |   <br />          v 155.99.25.11:62000 v      |    <br />                                      |<br />                                     NAT<br />                                 155.99.25.11<br />                                      |<br />          ^  Session 1 (A-S1)  ^      |  <br />          |  18.181.0.31:1235  |      |  <br />          v   10.0.0.1:1234    v      |  <br />                                      |<br />                                   Client A<br />                                10.0.0.1:1234<br />    有一个私有网络10.*.*.*，Client A是其中的一台计算机，这个网络的网关（一个NAT设备）的外网IP是155.99.25.11(应该还有一个内网的IP地址，比如10.0.0.10)。如果Client A中的某个进程（这个进程创建了一个UDP Socket,这个Socket绑定1234端口）想访问外网主机18.181.0.31的1235端口，那么当数据包通过NAT时会发生什么事情呢？<br />    首先NAT会改变这个数据包的原IP地址，改为155.99.25.11。接着NAT会为这个传输创建一个Session（Session是一个抽象的概念，如果是TCP，也许Session是由一个SYN包开始，以一个FIN包结束。而UDP呢，以这个IP的这个端口的第一个UDP开始，结束呢，呵呵，也许是几分钟，也许是几小时，这要看具体的实现了）并且给这个Session分配一个端口，比如62000，然后改变这个数据包的源端口为62000。所以本来是（10.0.0.1:1234-&gt;18.181.0.31:1235）的数据包到了互联网上变为了（155.99.25.11:62000-&gt;18.181.0.31:1235）。<br />    一旦NAT创建了一个Session后，NAT会记住62000端口对应的是10.0.0.1的1234端口，以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。（注意：这里是说18.181.0.31发送到62000端口的数据会被转发，其他的IP发送到这个端口的数据将被NAT抛弃）这样Client A就与Server S1建立以了一个连接。</p>
				<p>    呵呵，上面的基础知识可能很多人都知道了，那么下面是关键的部分了。<br />    看看下面的情况：<br />    Server S1                                     Server S2<br /> 18.181.0.31:1235                              138.76.29.7:1235<br />        |                                             |<br />        |                                             |<br />        +----------------------+----------------------+<br />                               |<br />   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^<br />   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |<br />   v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v<br />                               |<br />                            Cone NAT<br />                          155.99.25.11<br />                               |<br />   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^<br />   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |<br />   v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v<br />                               |<br />                            Client A<br />                         10.0.0.1:1234<br />    接上面的例子，如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包，那么这个UDP包在通过NAT时会怎么样呢？<br />    这时可能会有两种情况发生，一种是NAT再次创建一个Session，并且再次为这个Session分配一个端口号（比如：62001）。另外一种是NAT再次创建一个Session，但是不会新分配一个端口号，而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT，后一种叫做Cone NAT。我们期望我们的NAT是第二种，呵呵，如果你的NAT刚好是第一种，那么很可能会有很多P2P软件失灵。（可以庆幸的是，现在绝大多数的NAT属于后者，即Cone NAT）<br />   <br />    好了，我们看到，通过NAT,子网内的计算机向外连结是很容易的（NAT相当于透明的，子网内的和外网的计算机不用知道NAT的情况）。<br />    但是如果外部的计算机想访问子网内的计算机就比较困难了（而这正是P2P所需要的）。<br />    那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢？首先，我们必须在内网的NAT上打上一个“洞”（也就是前面我们说的在NAT上建立一个Session），这个洞不能由外部来打，只能由内网内的主机来打。而且这个洞是有方向的，比如从内部某台主机（比如：192.168.0.10）向外部的某个IP(比如：219.237.60.1)发送一个UDP包，那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”，（这就是称为UDP Hole Punching的技术）以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。（但是其他的IP不能利用这个洞）。</p>
				<p>呵呵，现在该轮到我们的正题P2P了。有了上面的理论，实现两个内网的主机通讯就差最后一步了：那就是鸡生蛋还是蛋生鸡的问题了，两边都无法主动发出连接请求，谁也不知道谁的公网地址，那我们如何来打这个洞呢？我们需要一个中间人来联系这两个内网主机。<br />    现在我们来看看一个P2P软件的流程，以下图为例：</p>
				<p>                       Server S （219.237.60.1）<br />                          |<br />                          |<br />   +----------------------+----------------------+<br />   |                                             |<br /> NAT A (外网IP:202.187.45.3)                 NAT B (外网IP:187.34.1.56)<br />   |   (内网IP:192.168.0.1)                      | (内网IP:192.168.0.1)<br />   |                                             |<br />Client A  (192.168.0.20:4000)             Client B (192.168.0.10:40000)</p>
				<p>    首先，Client A登录服务器，NAT A为这次的Session分配了一个端口60000，那么Server S收到的Client A的地址是202.187.45.3:60000，这就是Client A的外网地址了。同样，Client B登录Server S，NAT B给此次Session分配的端口是40000，那么Server S收到的B的地址是187.34.1.56:40000。<br />    此时，Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B，那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000，是不是Client A向这个地址发送信息Client B就能收到了呢？答案是不行，因为如果这样发送信息，NAT B会将这个信息丢弃（因为这样的信息是不请自来的，为了安全，大多数NAT都会执行丢弃动作）。现在我们需要的是在NAT B上打一个方向为202.187.45.3（即Client A的外网地址）的洞，那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢，呵呵，当然是Server S。<br />    总结一下这个过程：如果Client A想向Client B发送信息，那么Client A发送命令给Server S，请求Server S命令Client B向Client A方向打洞。呵呵，是不是很绕口，不过没关系，想一想就很清楚了，何况还有源代码呢（侯老师说过：在源代码面前没有秘密 8）），然后Client A就可以通过Client B的外网地址与Client B通信了。<br />    <br />    注意：以上过程只适合于Cone NAT的情况，如果是Symmetric NAT，那么当Client B向Client A打洞的端口已经重新分配了，Client B将无法知道这个端口（如果Symmetric NAT的端口是顺序分配的，那么我们或许可以猜测这个端口号，可是由于可能导致失败的因素太多，我们不推荐这种猜测端口的方法）。<br />    <br />    下面是一个模拟P2P聊天的过程的源代码，过程很简单，P2PServer运行在一个拥有公网IP的计算机上，P2PClient运行在两个不同的NAT后（注意，如果两个客户端运行在一个NAT后，本程序很可能不能运行正常，这取决于你的NAT是否支持loopback translation，详见<a href="http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt">http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt</a>，当然，此问题可以通过双方先尝试连接对方的内网IP来解决，但是这个代码只是为了验证原理，并没有处理这些问题），后登录的计算机可以获得先登录计算机的用户名，后登录的计算机通过send username message的格式来发送消息。如果发送成功，说明你已取得了直接与对方连接的成功。<br />    程序现在支持三个命令：send , getu , exit<br />    <br />    send格式：send username message<br />    功能：发送信息给username<br />    <br />    getu格式：getu<br />    功能：获得当前服务器用户列表<br />    <br />    exit格式：exit<br />    功能：注销与服务器的连接（服务器不会自动监测客户是否吊线）<br />        <br />    代码很短，相信很容易懂，如果有什么问题，可以给我发邮件<a href="mailto:zhouhuis22@sina.com">zhouhuis22@sina.com</a>  或者在CSDN上发送短消息。同时，欢迎转发此文，但希望保留作者版权8-）。<br />    <br />    最后感谢CSDN网友 PiggyXP 和 Seilfer的测试帮助</p>
				<p>P2PServer.c</p>
				<p>/* P2P 程序服务端<br /> * <br /> * 文件名：P2PServer.c<br /> *<br /> * 日期：2004-5-21<br /> *<br /> * 作者：shootingstars(<a href="mailto:zhouhuis22@sina.com">zhouhuis22@sina.com</a>)<br /> *<br /> */<br />#pragma comment(lib, "ws2_32.lib")</p>
				<p>#include "windows.h"<br />#include "..\proto.h"<br />#include "..\Exception.h"</p>
				<p>UserList ClientList;</p>
				<p>void InitWinSock()<br />{<br /> WSADATA wsaData;</p>
				<p> if (WSAStartup(MAKEWORD(2, 2), &amp;wsaData) != 0)<br /> {<br />  printf("Windows sockets 2.2 startup");<br />  throw Exception("");<br /> }<br /> else{<br />  printf("Using %s (Status: %s)\n",<br />   wsaData.szDescription, wsaData.szSystemStatus);<br />  printf("with API versions %d.%d to %d.%d\n\n",<br />   LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),<br />   LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));<br />  <br /> }<br />}</p>
				<p>SOCKET mksock(int type)<br />{<br /> SOCKET sock = socket(AF_INET, type, 0);<br /> if (sock &lt; 0)<br /> {<br />        printf("create socket error");<br />  throw Exception("");<br /> }<br /> return sock;<br />}</p>
				<p>stUserListNode GetUser(char *username)<br />{<br /> for(UserList::iterator UserIterator=ClientList.begin();<br />      UserIterator!=ClientList.end();<br />       ++UserIterator)<br /> {<br />  if( strcmp( ((*UserIterator)-&gt;userName), username) == 0 )<br />   return *(*UserIterator);<br /> }<br /> throw Exception("not find this user");<br />}</p>
				<p>int main(int argc, char* argv[])<br />{<br /> try{<br />  InitWinSock();<br />  <br />  SOCKET PrimaryUDP;<br />  PrimaryUDP = mksock(SOCK_DGRAM);</p>
				<p>  sockaddr_in local;<br />  local.sin_family=AF_INET;<br />  local.sin_port= htons(SERVER_PORT); <br />  local.sin_addr.s_addr = htonl(INADDR_ANY);<br />  int nResult=bind(PrimaryUDP,(sockaddr*)&amp;local,sizeof(sockaddr));<br />  if(nResult==SOCKET_ERROR)<br />   throw Exception("bind error");</p>
				<p>  sockaddr_in sender;<br />  stMessage recvbuf;<br />  memset(&amp;recvbuf,0,sizeof(stMessage));</p>
				<p>  // 开始主循环.<br />  // 主循环负责下面几件事情:<br />  // 一:读取客户端登陆和登出消息,记录客户列表<br />  // 二:转发客户p2p请求<br />  for(;;)<br />  {<br />   int dwSender = sizeof(sender);<br />   int ret = recvfrom(PrimaryUDP, (char *)&amp;recvbuf, sizeof(stMessage), 0, (sockaddr *)&amp;sender, &amp;dwSender);<br />   if(ret &lt;= 0)<br />   {<br />    printf("recv error");<br />    continue;<br />   }<br />   else<br />   {<br />    int messageType = recvbuf.iMessageType;<br />    switch(messageType){<br />    case LOGIN:<br />     {<br />      //  将这个用户的信息记录到用户列表中<br />      printf("has a user login : %s\n", recvbuf.message.loginmember.userName);<br />      stUserListNode *currentuser = new stUserListNode();<br />      strcpy(currentuser-&gt;userName, recvbuf.message.loginmember.userName);<br />      currentuser-&gt;ip = ntohl(sender.sin_addr.S_un.S_addr);<br />      currentuser-&gt;port = ntohs(sender.sin_port);<br />      <br />      ClientList.push_back(currentuser);</p>
				<p>      // 发送已经登陆的客户信息<br />      int nodecount = (int)ClientList.size();<br />      sendto(PrimaryUDP, (const char*)&amp;nodecount, sizeof(int), 0, (const sockaddr*)&amp;sender, sizeof(sender));<br />      for(UserList::iterator UserIterator=ClientList.begin();<br />        UserIterator!=ClientList.end();<br />        ++UserIterator)<br />      {<br />       sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&amp;sender, sizeof(sender)); <br />      }</p>
				<p>      break;<br />     }<br />    case LOGOUT:<br />     {<br />      // 将此客户信息删除<br />      printf("has a user logout : %s\n", recvbuf.message.logoutmember.userName);<br />      UserList::iterator removeiterator = NULL;<br />      for(UserList::iterator UserIterator=ClientList.begin();<br />       UserIterator!=ClientList.end();<br />       ++UserIterator)<br />      {<br />       if( strcmp( ((*UserIterator)-&gt;userName), recvbuf.message.logoutmember.userName) == 0 )<br />       {<br />        removeiterator = UserIterator;<br />        break;<br />       }<br />      }<br />      if(removeiterator != NULL)<br />       ClientList.remove(*removeiterator);<br />      break;<br />     }<br />    case P2PTRANS:<br />     {<br />      // 某个客户希望服务端向另外一个客户发送一个打洞消息<br />      printf("%s wants to p2p %s\n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);<br />      stUserListNode node = GetUser(recvbuf.message.translatemessage.userName);<br />      sockaddr_in remote;<br />      remote.sin_family=AF_INET;<br />      remote.sin_port= htons(node.port); <br />      remote.sin_addr.s_addr = htonl(node.ip);</p>
				<p>      in_addr tmp;<br />      tmp.S_un.S_addr = htonl(node.ip);<br />      printf("the address is %s,and port is %d\n",inet_ntoa(tmp), node.port);</p>
				<p>      stP2PMessage transMessage;<br />      transMessage.iMessageType = P2PSOMEONEWANTTOCALLYOU;<br />      transMessage.iStringLen = ntohl(sender.sin_addr.S_un.S_addr);<br />      transMessage.Port = ntohs(sender.sin_port);<br />                        <br />      sendto(PrimaryUDP,(const char*)&amp;transMessage, sizeof(transMessage), 0, (const sockaddr *)&amp;remote, sizeof(remote));</p>
				<p>      break;<br />     }<br />    <br />    case GETALLUSER:<br />     {<br />      int command = GETALLUSER;<br />      sendto(PrimaryUDP, (const char*)&amp;command, sizeof(int), 0, (const sockaddr*)&amp;sender, sizeof(sender));</p>
				<p>      int nodecount = (int)ClientList.size();<br />      sendto(PrimaryUDP, (const char*)&amp;nodecount, sizeof(int), 0, (const sockaddr*)&amp;sender, sizeof(sender));</p>
				<p>      for(UserList::iterator UserIterator=ClientList.begin();<br />        UserIterator!=ClientList.end();<br />        ++UserIterator)<br />      {<br />       sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&amp;sender, sizeof(sender)); <br />      }<br />      break;<br />     }<br />    }<br />   }<br />  }</p>
				<p> }<br /> catch(Exception &amp;e)<br /> {<br />  printf(e.GetMessage());<br />  return 1;<br /> }</p>
				<p> return 0;<br />}<br /></p>
				<p>/* P2P 程序客户端<br /> * <br /> * 文件名：P2PClient.c<br /> *<br /> * 日期：2004-5-21<br /> *<br /> * 作者：shootingstars(<a href="mailto:zhouhuis22@sina.com">zhouhuis22@sina.com</a>)<br /> *<br /> */</p>
				<p>#pragma comment(lib,"ws2_32.lib")</p>
				<p>#include "windows.h"<br />#include "..\proto.h"<br />#include "..\Exception.h"<br />#include &lt;iostream&gt;<br />using namespace std;</p>
				<p>UserList ClientList;</p>
				<p> </p>
				<p>#define COMMANDMAXC 256<br />#define MAXRETRY    5</p>
				<p>SOCKET PrimaryUDP;<br />char UserName[10];<br />char ServerIP[20];</p>
				<p>bool RecvedACK;</p>
				<p>void InitWinSock()<br />{<br /> WSADATA wsaData;</p>
				<p> if (WSAStartup(MAKEWORD(2, 2), &amp;wsaData) != 0)<br /> {<br />  printf("Windows sockets 2.2 startup");<br />  throw Exception("");<br /> }<br /> else{<br />  printf("Using %s (Status: %s)\n",<br />   wsaData.szDescription, wsaData.szSystemStatus);<br />  printf("with API versions %d.%d to %d.%d\n\n",<br />   LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),<br />   LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));<br /> }<br />}</p>
				<p>SOCKET mksock(int type)<br />{<br /> SOCKET sock = socket(AF_INET, type, 0);<br /> if (sock &lt; 0)<br /> {<br />        printf("create socket error");<br />  throw Exception("");<br /> }<br /> return sock;<br />}</p>
				<p>stUserListNode GetUser(char *username)<br />{<br /> for(UserList::iterator UserIterator=ClientList.begin();<br />      UserIterator!=ClientList.end();<br />       ++UserIterator)<br /> {<br />  if( strcmp( ((*UserIterator)-&gt;userName), username) == 0 )<br />   return *(*UserIterator);<br /> }<br /> throw Exception("not find this user");<br />}</p>
				<p>void BindSock(SOCKET sock)<br />{<br /> sockaddr_in sin;<br /> sin.sin_addr.S_un.S_addr = INADDR_ANY;<br /> sin.sin_family = AF_INET;<br /> sin.sin_port = 0;<br /> <br /> if (bind(sock, (struct sockaddr*)&amp;sin, sizeof(sin)) &lt; 0)<br />  throw Exception("bind error");<br />}</p>
				<p>void ConnectToServer(SOCKET sock,char *username, char *serverip)<br />{<br /> sockaddr_in remote;<br /> remote.sin_addr.S_un.S_addr = inet_addr(serverip);<br /> remote.sin_family = AF_INET;<br /> remote.sin_port = htons(SERVER_PORT);<br /> <br /> stMessage sendbuf;<br /> sendbuf.iMessageType = LOGIN;<br /> strncpy(sendbuf.message.loginmember.userName, username, 10);</p>
				<p> sendto(sock, (const char*)&amp;sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&amp;remote,sizeof(remote));</p>
				<p> int usercount;<br /> int fromlen = sizeof(remote);<br /> int iread = recvfrom(sock, (char *)&amp;usercount, sizeof(int), 0, (sockaddr *)&amp;remote, &amp;fromlen);<br /> if(iread&lt;=0)<br /> {<br />  throw Exception("Login error\n");<br /> }</p>
				<p> // 登录到服务端后，接收服务端发来的已经登录的用户的信息<br /> cout&lt;&lt;"Have "&lt;&lt;usercount&lt;&lt;" users logined server:"&lt;&lt;endl;<br /> for(int i = 0;i&lt;usercount;i++)<br /> {<br />  stUserListNode *node = new stUserListNode;<br />  recvfrom(sock, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&amp;remote, &amp;fromlen);<br />  ClientList.push_back(node);<br />  cout&lt;&lt;"Username:"&lt;&lt;node-&gt;userName&lt;&lt;endl;<br />  in_addr tmp;<br />  tmp.S_un.S_addr = htonl(node-&gt;ip);<br />  cout&lt;&lt;"UserIP:"&lt;&lt;inet_ntoa(tmp)&lt;&lt;endl;<br />  cout&lt;&lt;"UserPort:"&lt;&lt;node-&gt;port&lt;&lt;endl;<br />  cout&lt;&lt;""&lt;&lt;endl;<br /> }<br />}</p>
				<p>void OutputUsage()<br />{<br /> cout&lt;&lt;"You can input you command:\n"<br />  &lt;&lt;"Command Type:\"send\",\"exit\",\"getu\"\n"<br />  &lt;&lt;"Example : send Username Message\n"<br />  &lt;&lt;"          exit\n"<br />  &lt;&lt;"          getu\n"<br />  &lt;&lt;endl;<br />}</p>
				<p>/* 这是主要的函数：发送一个消息给某个用户(C)<br /> *流程：直接向某个用户的外网IP发送消息，如果此前没有联系过<br /> *      那么此消息将无法发送，发送端等待超时。<br /> *      超时后，发送端将发送一个请求信息到服务端，<br /> *      要求服务端发送给客户C一个请求，请求C给本机发送打洞消息<br /> *      以上流程将重复MAXRETRY次<br /> */<br />bool SendMessageTo(char *UserName, char *Message)<br />{<br /> char realmessage[256];<br /> unsigned int UserIP;<br /> unsigned short UserPort;<br /> bool FindUser = false;<br /> for(UserList::iterator UserIterator=ClientList.begin();<br />      UserIterator!=ClientList.end();<br />      ++UserIterator)<br /> {<br />  if( strcmp( ((*UserIterator)-&gt;userName), UserName) == 0 )<br />  {<br />   UserIP = (*UserIterator)-&gt;ip;<br />   UserPort = (*UserIterator)-&gt;port;<br />   FindUser = true;<br />  }<br /> }</p>
				<p> if(!FindUser)<br />  return false;</p>
				<p> strcpy(realmessage, Message);<br /> for(int i=0;i&lt;MAXRETRY;i++)<br /> {<br />  RecvedACK = false;</p>
				<p>  sockaddr_in remote;<br />  remote.sin_addr.S_un.S_addr = htonl(UserIP);<br />  remote.sin_family = AF_INET;<br />  remote.sin_port = htons(UserPort);<br />  stP2PMessage MessageHead;<br />  MessageHead.iMessageType = P2PMESSAGE;<br />  MessageHead.iStringLen = (int)strlen(realmessage)+1;<br />  int isend = sendto(PrimaryUDP, (const char *)&amp;MessageHead, sizeof(MessageHead), 0, (const sockaddr*)&amp;remote, sizeof(remote));<br />  isend = sendto(PrimaryUDP, (const char *)&amp;realmessage, MessageHead.iStringLen, 0, (const sockaddr*)&amp;remote, sizeof(remote));<br />  <br />  // 等待接收线程将此标记修改<br />  for(int j=0;j&lt;10;j++)<br />  {<br />   if(RecvedACK)<br />    return true;<br />   else<br />    Sleep(300);<br />  }</p>
				<p>  // 没有接收到目标主机的回应，认为目标主机的端口映射没有<br />  // 打开，那么发送请求信息给服务器，要服务器告诉目标主机<br />  // 打开映射端口（UDP打洞）<br />  sockaddr_in server;<br />  server.sin_addr.S_un.S_addr = inet_addr(ServerIP);<br />  server.sin_family = AF_INET;<br />  server.sin_port = htons(SERVER_PORT);<br /> <br />  stMessage transMessage;<br />  transMessage.iMessageType = P2PTRANS;<br />  strcpy(transMessage.message.translatemessage.userName, UserName);</p>
				<p>  sendto(PrimaryUDP, (const char*)&amp;transMessage, sizeof(transMessage), 0, (const sockaddr*)&amp;server, sizeof(server));<br />  Sleep(100);// 等待对方先发送信息。<br /> }<br /> return false;<br />}<br /></p>
				<p>// 解析命令，暂时只有exit和send命令<br />// 新增getu命令，获取当前服务器的所有用户<br />void ParseCommand(char * CommandLine)<br />{<br /> if(strlen(CommandLine)&lt;4)<br />  return;<br /> char Command[10];<br /> strncpy(Command, CommandLine, 4);<br /> Command[4]='\0';</p>
				<p> if(strcmp(Command,"exit")==0)<br /> {<br />  stMessage sendbuf;<br />  sendbuf.iMessageType = LOGOUT;<br />  strncpy(sendbuf.message.logoutmember.userName, UserName, 10);<br />  sockaddr_in server;<br />  server.sin_addr.S_un.S_addr = inet_addr(ServerIP);<br />  server.sin_family = AF_INET;<br />  server.sin_port = htons(SERVER_PORT);</p>
				<p>  sendto(PrimaryUDP,(const char*)&amp;sendbuf, sizeof(sendbuf), 0, (const sockaddr *)&amp;server, sizeof(server));<br />  shutdown(PrimaryUDP, 2);<br />  closesocket(PrimaryUDP);<br />  exit(0);<br /> }<br /> else if(strcmp(Command,"send")==0)<br /> {<br />  char sendname[20];<br />  char message[COMMANDMAXC];<br />  int i;<br />  for(i=5;;i++)<br />  {<br />   if(CommandLine[i]!=' ')<br />    sendname[i-5]=CommandLine[i];<br />   else<br />   {<br />    sendname[i-5]='\0';<br />    break;<br />   }<br />  }<br />  strcpy(message, &amp;(CommandLine[i+1]));<br />  if(SendMessageTo(sendname, message))<br />   printf("Send OK!\n");<br />  else <br />   printf("Send Failure!\n");<br /> }<br /> else if(strcmp(Command,"getu")==0)<br /> {<br />  int command = GETALLUSER;<br />  sockaddr_in server;<br />  server.sin_addr.S_un.S_addr = inet_addr(ServerIP);<br />  server.sin_family = AF_INET;<br />  server.sin_port = htons(SERVER_PORT);</p>
				<p>  sendto(PrimaryUDP,(const char*)&amp;command, sizeof(command), 0, (const sockaddr *)&amp;server, sizeof(server));<br /> }<br />}</p>
				<p>// 接受消息线程<br />DWORD WINAPI RecvThreadProc(LPVOID lpParameter)<br />{<br /> sockaddr_in remote;<br /> int sinlen = sizeof(remote);<br /> stP2PMessage recvbuf;<br /> for(;;)<br /> {<br />  int iread = recvfrom(PrimaryUDP, (char *)&amp;recvbuf, sizeof(recvbuf), 0, (sockaddr *)&amp;remote, &amp;sinlen);<br />  if(iread&lt;=0)<br />  {<br />   printf("recv error\n");<br />   continue;<br />  }<br />  switch(recvbuf.iMessageType)<br />  {<br />  case P2PMESSAGE:<br />   {<br />    // 接收到P2P的消息<br />    char *comemessage= new char[recvbuf.iStringLen];<br />    int iread1 = recvfrom(PrimaryUDP, comemessage, 256, 0, (sockaddr *)&amp;remote, &amp;sinlen);<br />    comemessage[iread1-1] = '\0';<br />    if(iread1&lt;=0)<br />     throw Exception("Recv Message Error\n");<br />    else<br />    {<br />     printf("Recv a Message:%s\n",comemessage);<br />     <br />     stP2PMessage sendbuf;<br />     sendbuf.iMessageType = P2PMESSAGEACK;<br />     sendto(PrimaryUDP, (const char*)&amp;sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&amp;remote, sizeof(remote));<br />    }</p>
				<p>    delete []comemessage;<br />    break;</p>
				<p>   }<br />  case P2PSOMEONEWANTTOCALLYOU:<br />   {<br />    // 接收到打洞命令，向指定的IP地址打洞<br />    printf("Recv p2someonewanttocallyou data\n");<br />    sockaddr_in remote;<br />    remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen);<br />    remote.sin_family = AF_INET;<br />    remote.sin_port = htons(recvbuf.Port);</p>
				<p>    // UDP hole punching<br />    stP2PMessage message;<br />    message.iMessageType = P2PTRASH;<br />    sendto(PrimaryUDP, (const char *)&amp;message, sizeof(message), 0, (const sockaddr*)&amp;remote, sizeof(remote));<br />                <br />    break;<br />   }<br />  case P2PMESSAGEACK:<br />   {<br />    // 发送消息的应答<br />    RecvedACK = true;<br />    break;<br />   }<br />  case P2PTRASH:<br />   {<br />    // 对方发送的打洞消息，忽略掉。<br />    //do nothing ...<br />    printf("Recv p2ptrash data\n");<br />    break;<br />   }<br />  case GETALLUSER:<br />   {<br />    int usercount;<br />    int fromlen = sizeof(remote);<br />    int iread = recvfrom(PrimaryUDP, (char *)&amp;usercount, sizeof(int), 0, (sockaddr *)&amp;remote, &amp;fromlen);<br />    if(iread&lt;=0)<br />    {<br />     throw Exception("Login error\n");<br />    }<br />    <br />    ClientList.clear();</p>
				<p>    cout&lt;&lt;"Have "&lt;&lt;usercount&lt;&lt;" users logined server:"&lt;&lt;endl;<br />    for(int i = 0;i&lt;usercount;i++)<br />    {<br />     stUserListNode *node = new stUserListNode;<br />     recvfrom(PrimaryUDP, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&amp;remote, &amp;fromlen);<br />     ClientList.push_back(node);<br />     cout&lt;&lt;"Username:"&lt;&lt;node-&gt;userName&lt;&lt;endl;<br />     in_addr tmp;<br />     tmp.S_un.S_addr = htonl(node-&gt;ip);<br />     cout&lt;&lt;"UserIP:"&lt;&lt;inet_ntoa(tmp)&lt;&lt;endl;<br />     cout&lt;&lt;"UserPort:"&lt;&lt;node-&gt;port&lt;&lt;endl;<br />     cout&lt;&lt;""&lt;&lt;endl;<br />    }<br />    break;<br />   }<br />  }<br /> }<br />}</p>
				<p>
						<br />int main(int argc, char* argv[])<br />{<br /> try<br /> {<br />  InitWinSock();<br />  <br />  PrimaryUDP = mksock(SOCK_DGRAM);<br />  BindSock(PrimaryUDP);</p>
				<p>  cout&lt;&lt;"Please input server ip:";<br />  cin&gt;&gt;ServerIP;</p>
				<p>  cout&lt;&lt;"Please input your name:";<br />  cin&gt;&gt;UserName;</p>
				<p>  ConnectToServer(PrimaryUDP, UserName, ServerIP);</p>
				<p>  HANDLE threadhandle = CreateThread(NULL, 0, RecvThreadProc, NULL, NULL, NULL);<br />  CloseHandle(threadhandle);<br />  OutputUsage();</p>
				<p>  for(;;)<br />  {<br />   char Command[COMMANDMAXC];<br />   gets(Command);<br />   ParseCommand(Command);<br />  }<br /> }<br /> catch(Exception &amp;e)<br /> {<br />  printf(e.GetMessage());<br />  return 1;<br /> }<br /> return 0;<br />}<br /></p>
				<p>/* 异常类<br /> *<br /> * 文件名：Exception.h<br /> *<br /> * 日期：2004.5.5<br /> *<br /> * 作者：shootingstars(<a href="mailto:zhouhuis22@sina.com">zhouhuis22@sina.com</a>)<br /> */</p>
				<p>#ifndef __HZH_Exception__<br />#define __HZH_Exception__</p>
				<p>#define EXCEPTION_MESSAGE_MAXLEN 256<br />#include "string.h"</p>
				<p>class Exception<br />{<br />private:<br /> char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];<br />public:<br /> Exception(char *msg)<br /> {<br />  strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);<br /> }</p>
				<p> char *GetMessage()<br /> {<br />  return m_ExceptionMessage;<br /> }<br />};</p>
				<p>#endif<br /></p>
				<p>/* P2P 程序传输协议<br /> * <br /> * 日期：2004-5-21<br /> *<br /> * 作者：shootingstars(<a href="mailto:zhouhuis22@sina.com">zhouhuis22@sina.com</a>)<br /> *<br /> */</p>
				<p>#pragma once<br />#include &lt;list&gt;</p>
				<p>// 定义iMessageType的值<br />#define LOGIN 1<br />#define LOGOUT 2<br />#define P2PTRANS 3<br />#define GETALLUSER  4</p>
				<p>// 服务器端口<br />#define SERVER_PORT 2280</p>
				<p>// Client登录时向服务器发送的消息<br />struct stLoginMessage<br />{<br /> char userName[10];<br /> char password[10];<br />};</p>
				<p>// Client注销时发送的消息<br />struct stLogoutMessage<br />{<br /> char userName[10];<br />};</p>
				<p>// Client向服务器请求另外一个Client(userName)向自己方向发送UDP打洞消息<br />struct stP2PTranslate<br />{<br /> char userName[10];<br />};</p>
				<p>// Client向服务器发送的消息格式<br />struct stMessage<br />{<br /> int iMessageType;<br /> union _message<br /> {<br />  stLoginMessage loginmember;<br />  stLogoutMessage logoutmember;<br />  stP2PTranslate translatemessage;<br /> }message;<br />};</p>
				<p>// 客户节点信息<br />struct stUserListNode<br />{<br /> char userName[10];<br /> unsigned int ip;<br /> unsigned short port;<br />};</p>
				<p>// Server向Client发送的消息<br />struct stServerToClient<br />{<br /> int iMessageType;<br /> union _message<br /> {<br />  stUserListNode user;<br /> }message;</p>
				<p>};</p>
				<p>//======================================<br />// 下面的协议用于客户端之间的通信<br />//======================================<br />#define P2PMESSAGE 100               // 发送消息<br />#define P2PMESSAGEACK 101            // 收到消息的应答<br />#define P2PSOMEONEWANTTOCALLYOU 102  // 服务器向客户端发送的消息<br />                                     // 希望此客户端发送一个UDP打洞包<br />#define P2PTRASH        103          // 客户端发送的打洞包，接收端应该忽略此消息</p>
				<p>// 客户端之间发送消息格式<br />struct stP2PMessage<br />{<br /> int iMessageType;<br /> int iStringLen;         // or IP address<br /> unsigned short Port; <br />};</p>
				<p>using namespace std;<br />typedef list&lt;stUserListNode *&gt; UserList;</p>
		</div>
<img src ="http://www.cppblog.com/shenhuafeng/aggbug/17215.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shenhuafeng/" target="_blank">CPP&&设计模式小屋</a> 2007-01-04 13:37 <a href="http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17215.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Peer-to-Peer (P2P) communication across middleboxes(转)</title><link>http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17214.html</link><dc:creator>CPP&amp;&amp;设计模式小屋</dc:creator><author>CPP&amp;&amp;设计模式小屋</author><pubDate>Thu, 04 Jan 2007 05:35:00 GMT</pubDate><guid>http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17214.html</guid><wfw:comment>http://www.cppblog.com/shenhuafeng/comments/17214.html</wfw:comment><comments>http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17214.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/shenhuafeng/comments/commentRss/17214.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shenhuafeng/services/trackbacks/17214.html</trackback:ping><description><![CDATA[&nbsp;&nbsp;&nbsp;&nbsp; 摘要: Internet Draft                                                   B. FordDocument: draft-ford-midcom-p2p-01.txt                            M.I.T.Expires: April 27, 2004                                 ...&nbsp;&nbsp;<a href='http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17214.html'>阅读全文</a><img src ="http://www.cppblog.com/shenhuafeng/aggbug/17214.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shenhuafeng/" target="_blank">CPP&&设计模式小屋</a> 2007-01-04 13:35 <a href="http://www.cppblog.com/shenhuafeng/archive/2007/01/04/17214.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>P2P直播相关(转)</title><link>http://www.cppblog.com/shenhuafeng/archive/2006/12/19/16617.html</link><dc:creator>CPP&amp;&amp;设计模式小屋</dc:creator><author>CPP&amp;&amp;设计模式小屋</author><pubDate>Tue, 19 Dec 2006 09:17:00 GMT</pubDate><guid>http://www.cppblog.com/shenhuafeng/archive/2006/12/19/16617.html</guid><wfw:comment>http://www.cppblog.com/shenhuafeng/comments/16617.html</wfw:comment><comments>http://www.cppblog.com/shenhuafeng/archive/2006/12/19/16617.html#Feedback</comments><slash:comments>3</slash:comments><wfw:commentRss>http://www.cppblog.com/shenhuafeng/comments/commentRss/16617.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shenhuafeng/services/trackbacks/16617.html</trackback:ping><description><![CDATA[
		<p>
				<font face="宋体">最近对</font>P2P<font face="宋体">直播技术进行了一些研究，谈谈个人对</font>Tvkoo<font face="宋体">软件的优缺点的看法，一方面希望</font>Tvkoo<font face="宋体">能做的更好，另一方面也抛砖引玉一下：<?XML:NAMESPACE PREFIX = O /?><o:p></o:p></font></p>
		<p>P2P<font face="宋体">直播的技术难点有：</font><br /><font face="宋体">一、防火墙的穿透：超过</font>90<font face="宋体">％的电脑都在防火墙后面，如果让</font>2<font face="宋体">台在防火墙后面的电脑能够实现</font>P2P<font face="宋体">互联，这是一个技术的难点。有</font>2<font face="宋体">种方式：<o:p></o:p></font></p>
		<p>1. <font face="宋体">要求用户配置</font>TCP<font face="宋体">端口：</font>BT<font face="宋体">和电驴采用的方式，配置需要网络专业知识。一般做法是通过在防火墙上开启</font>TCP<font face="宋体">端口来实现，如果开启了端口或者本身有</font>Internet IP<font face="宋体">地址的，为高联通性电脑；在防火墙内并且没有开启</font>TCP<font face="宋体">端口的电脑，为低联通性电脑。高联通性电脑可以和其它的高联通性电脑已经低联通性电脑进行</font>P2P<font face="宋体">；而低联通电脑只能和高联通性电脑进行</font>P2P<font face="宋体">。因此在</font>BT<font face="宋体">、电驴中，有</font>Internet IP<font face="宋体">或者已经在防火墙上开启端口的电脑速度很快，而在防火墙后面的电脑</font>(<font face="宋体">一般为局域网上网方式</font>)<font face="宋体">就比较慢了。而通过局域网方式上网的电脑超过</font>70%<font face="宋体">，如果没有网络基础，或者没有网管特殊配置，只能处于低联通性，速度很慢。<o:p></o:p></font></p>
		<p>2. <font face="宋体">防火墙自动穿透。无需用户配置，自动让</font>2<font face="宋体">台在防火墙后面的电脑能</font>P2P<font face="宋体">互联。</font>P2P<font face="宋体">连接可以使用</font>TCP<font face="宋体">和</font>UDP 2<font face="宋体">种方式。由于</font>WinXP SP2<font face="宋体">限制了一个应用程序的</font>TCP<font face="宋体">连接数，因此采用</font>TCP<font face="宋体">方式进行</font>P2P<font face="宋体">的话，要安装</font>TCP<font face="宋体">限制的破解软件。而</font>Tvkoo<font face="宋体">是使用</font>UDP<font face="宋体">进行数据传送的，因此不会有</font>TCP<font face="宋体">限制。这是为什么有些用户说：通过</font>Netstat<font face="宋体">看不到很多的</font>TCP<font face="宋体">连接而怀疑</font>Tvkoo<font face="宋体">是不是</font>P2P<font face="宋体">软件的原因。<o:p></o:p></font></p>
		<p>Tvkoo<font face="宋体">的</font>P2P<font face="宋体">穿透力是我见到最强的软件了，当然由于没有使用</font>TCP<font face="宋体">，使得在仅允许</font>HTTP<font face="宋体">访问的防火墙后面的计算机没有办法访问。</font>Skype<font face="宋体">在</font>P2P<font face="宋体">穿透时有一个技巧，让一些有</font>Internet IP<font face="宋体">的电脑使用</font>TCP<font face="宋体">的</font>80<font face="宋体">端口，在仅允许</font>HTTP<font face="宋体">访问的防火墙后面的电脑通过类似</font>HTTP<font face="宋体">的访问方式连接这些</font>80<font face="宋体">端口的电脑进行</font>P2P<font face="宋体">连接。</font>Tvkoo<font face="宋体">也可以考虑一下这个方式。<o:p></o:p></font></p>
		<p>
				<font face="宋体">二、</font>WMV<font face="宋体">格式的分析<o:p></o:p></font></p>
		<p>
				<font face="宋体">目前</font>P2P<font face="宋体">直播都是使用</font>WMV<font face="宋体">格式，通过模拟</font>HTTP<font face="宋体">服务器的方式，把</font>WMV<font face="宋体">数据流传送给</font>Media Player<font face="宋体">。这是</font>P2P<font face="宋体">直播的关键点。需要将</font>Media Encoder<font face="宋体">发出的</font>HTTP<font face="宋体">数据进行拆包，然后组成</font>30<font face="宋体">秒－</font>1<font face="宋体">分钟的</font>P2P<font face="宋体">数据块，然后通过</font>P2P<font face="宋体">方式将数据块发送给</font>Tvkoo<font face="宋体">客户端，</font>Tvkoo<font face="宋体">模拟成</font>HTTP<font face="宋体">服务器将传输完成的数据块发送给</font>Media Player<font face="宋体">。这部分</font>Tvkoo<font face="宋体">做的也不错。<o:p></o:p></font></p>
		<p>
				<font face="宋体">三、如何有效的选择</font>P2P<font face="宋体">的节点<o:p></o:p></font></p>
		<p>
				<font face="宋体">这是</font>Tvkoo<font face="宋体">的弱项。因为一台电脑在</font>P2P<font face="宋体">传输时，最多连接几十台其它的电脑。当几万台电脑同时传送一个</font>P2P<font face="宋体">数据块时，要有一个优化算法。比如：美国有</font>5000<font face="宋体">个用户，中国电信有</font>5000<font face="宋体">个用户，中国网通有</font>5000<font face="宋体">个用户，而每个用户最多只能连</font>30<font face="宋体">－</font>50<font face="宋体">个节点，如果不凑巧，一个电信的用户连接了</font>20<font face="宋体">个网通的用户和</font>30<font face="宋体">个美国的用户，就不断的出现断断续续的情况了。这就是为什么人一多，</font>Tvkoo<font face="宋体">就卡，并且</font>Tvkoo<font face="宋体">要把国外</font>IP<font face="宋体">封掉的原因了。<o:p></o:p></font></p>
		<p>
				<font face="宋体">有什么好的方式解决这个问题呢？我先抛砖引玉一下：<o:p></o:p></font>
		</p>
		<p>(1)    <font face="宋体">作为</font>P2P<font face="宋体">直播的营运商，可以多设几台</font>P2P<font face="宋体">种子服务器，分布在不同的网段中。比如：北方网通设一台</font>(<font face="宋体">组</font>)<font face="宋体">，南方电信设一台</font>(<font face="宋体">组</font>)<font face="宋体">，种子的内容是一样的。种子服务器多了，可以降低优化算法的难度。<o:p></o:p></font></p>
		<p>(2)    <font face="宋体">种子服务器和普通节点的优先级：种子服务器的优先级总数低于普通节点的，如果普通节点的速度快了，就减少从种子服务器获取的数据量。<o:p></o:p></font></p>
		<p>(3)    <font face="宋体">全球</font>IP<font face="宋体">地址表。</font>P2P<font face="宋体">节点仲裁服务器中，应该有一个全球</font>IP<font face="宋体">地址表，分中国大陆、香港、台湾、北美、欧洲、澳洲、其它。中国大陆先按照营运商分：电信、网通、铁通、联通、教育网等，再按照省份分类。</font>(<font face="宋体">网上有下载，可以整理</font>)<o:p></o:p></p>
		<p>(4)    <font face="宋体">高速网段表。在</font>P2P<font face="宋体">访问中，节点动态地将速度快的其它节点</font>IP<font face="宋体">地址传回服务器，服务器根据全球</font>IP<font face="宋体">地址表算出网段，以网段</font>-<font face="宋体">网段的方式记录在数据库中。<o:p></o:p></font></p>
		<p>(5)    <font face="宋体">当一个新用户连入节点时，在全球</font>IP<font face="宋体">地址表中找到最近的节点，按照比例依次分配最快网段的节点；最近的节点；差一个级别的稍近的节点；随机节点以及种子服务器。<o:p></o:p></font></p>
		<p>(6)    P2P<font face="宋体">在数据传送中，可以将</font>30<font face="宋体">秒视频作为</font>1<font face="宋体">块数据包；数据包中按照每</font>16KB<font face="宋体">作为一个数据块。每个时间段</font>(<font face="宋体">如</font>2<font face="宋体">秒</font>)<font face="宋体">，本节点向其它节点交换一下数据块的传送情况，然后计算一下数据包中每个数据块的拥有率，优先传送拥有率低的数据块。在拥有率相当的情况下，随机选择。<o:p></o:p></font></p>
		<p>(7)    <font face="宋体">在数据交换中，对于传送慢的节点，定期剔除，然后问节点仲裁服务器要新的节点。<o:p></o:p></font></p>
		<p>(8)    <font face="宋体">如果数据包中小于</font>10%<font face="宋体">的数据块没有传送完毕，在时间充足的情况下，对于余下的数据块，可以同一个数据块向多个节点请求。<o:p></o:p></font></p>
		<p>(9)    <font face="宋体">节点仲裁服务器也会将新的</font>P2P<font face="宋体">节点强行加载到另一个节点上，但不能超过节点最大连接数。</font></p>
<img src ="http://www.cppblog.com/shenhuafeng/aggbug/16617.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shenhuafeng/" target="_blank">CPP&&设计模式小屋</a> 2006-12-19 17:17 <a href="http://www.cppblog.com/shenhuafeng/archive/2006/12/19/16617.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Kademlia: 基于异或运算的P2P信息系统(翻译稿)</title><link>http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12267.html</link><dc:creator>CPP&amp;&amp;设计模式小屋</dc:creator><author>CPP&amp;&amp;设计模式小屋</author><pubDate>Mon, 11 Sep 2006 08:18:00 GMT</pubDate><guid>http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12267.html</guid><wfw:comment>http://www.cppblog.com/shenhuafeng/comments/12267.html</wfw:comment><comments>http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12267.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/shenhuafeng/comments/commentRss/12267.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shenhuafeng/services/trackbacks/12267.html</trackback:ping><description><![CDATA[
		<div align="center" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">Petar Maymounkov and David Mazi`eres</span>
		</div>
		<div align="center" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">fpetar,dmg@cs.nyu.edu</span>
		</div>
		<div align="center" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">
						<a href="http://kademlia.scs.cs.nyu.edu/">
								<span style="COLOR: black" twffan="done">http://kademlia.scs.cs.nyu.edu</span>
						</a>
				</span>
		</div>
		<div align="center" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="COLOR: black" twffan="done">摘要</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="COLOR: black" twffan="done">本文我们将描述一个在容易出错的网络环境中拥有可证实的稳定性和高性能<span style="DISPLAY: none" twffan="done">稳定性</span>的点对点(P2P)系统。我们的系统使用一个很新颖的基于异或运算的拓扑来发送查询并且定位节点, 这简化了算法并且使验证更加容易。 这种拓扑结构具有以下特性，它能通过交换消息传达和加强节点间的有用联系信息。本系统利用这个信息来发送平行的,异步的查询消息来对付节点的失效而不会给用户带来超时时延。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">1</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．介绍</span>
				</strong>
		</div>
		<div align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">    </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">本论文描述Kademlia , 一个点对点（P2P）的&lt;键, 值&gt;元组存储和查询系统。 Kademlia拥有许多的可喜的特点，这些特点是任何以前的P2P系统所无法同时提供的。它减少了节点必须发送的用来相互认识的配置消息的数量。在做键查询的同时, 配置消息将会被自动传播。 节点拥有足够的知识和灵活性来通过低时延路径发送查询请求。 Kademlia使用平行的,异步的查询请求来避免节点失效所带来的超时时延。通过节点记录相互的存在的算法可以抵抗某些基本的拒绝服务（DoS）攻击。 最后, 仅仅使用在分布式运行时间上较弱的假设（通过对现有点对点系统的测量而确认的这些假设），我们可以正式的证实Kademlia的许多重要特性<strong>。</strong></span>
		</div>
		<div align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">    Kademlia </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">使用了许多点对点（P2P）系统的基本方法。 键是一个160-bit的隐式数量(例如, 对一些大型数据进行SHA-1哈希的值)。 每个参与的机器都拥有一个节点ID, 160位的键。 &lt;键, 值&gt;对将存储在那些ID与键很‘接近’的节点上, 这里‘接近’当然是按照一个接近度的概念来计算的。最后, 一个基于节点ID的路由算法使得任何人可以在一个目的键附近定位到一个服务器。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">Kademlia</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">的许多的优点都是得益于它使用了一个很新颖的方法, 那就是用节点间的键作异或运算的结果来作为节点间的距离。异或运算是对称的, 允许Kademlia的参与者接收来自相同分布的并且包含在其路由表中的节点的查找请求。如果没有这个性质，就像Chord一样，系统无法从它们收到的查询请求中学习到有用的路由信息。更糟的是， 由于Chord中的运算是不对称的， Chord的路由表更加严格。 Chord节点的查找表的每一项都必须存储精确的按ID域的间隔递增的节点。在这个间隔内的任何节点都比这个间隔内的某些键大，因此离键很远。相反，Kademlia 可以在一定的间隔内发送请求给任何节点， 允许基于时延来选择路由，甚至发送平行的，异步的查询。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">为了在特定的ID附近定位节点，Kademlia自始至终使用一个单程的路由算法。相反，其它一些系统使用一种算法来接近目标ID,然后在最后的几个跳数使用另外一种算法。在现有系统中，Kademlia与pastry的第一阶段最像，（虽然作者并没有用这种方式来描述），Kademlia 的异或运算可以使当前节点到目标ID的距离粗略的持续减半，以此来寻找节点。在第二阶段，Pastry不再使用距离运算，而是改为比较ID的数字区别。它使用第二种，数字区别运算作为替代。不幸的是，按第二种运算计算的接近比第一种的远得多，这造成特定节点ID值的中断，降低了性能，并且导致在最差行为下的正式分析的尝试失败。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">2</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．系统描述</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">每个Kademlia节点有一个160位的节点ID。在Chord系统中，ID是通过某种规则构造出来的，但在这片文章中，为了简化，我们假设每台机器在加入系统时将选择一个随机的160位值。每条节点发送的消息包含它的节点ID， 同时允许接收者记录下发送者的存在信息，如果有必要的话。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">键，同样也是160位的标识符。为了发布和寻找&lt;键，值&gt;对，Kademlia依赖一个概念，那就是两标识符之间的距离的概念。给定两个标识符， x和y， Kademlia定义两者的位异或（XOR）的结果作为两者的距离，d(x，y)＝x⊕y。我们首先注意到异或运算是一个有意义的运算，虽然不是欧几里得运算。很明显具有下面的性质： d(x，x)＝0；如果x≠y, 则d(x, y)&gt;0；任意的x, y: d(x, y) = d(y, x)。 异或运算还满足三角性质：d(x, y) + d(y, z) ≥ d(x, z)。 这个三角性质之所以成立是基于下面这个事实： d(x, z) = d(x, y) + d(y, z); 并且任意的a&gt;=0, b≥0: a+b≥a⊕b。 </span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">跟Chord的顺时针循环运算一样，异或运算也是单向的。对于给定的一个点x以及距离Δ，仅有一个点y，使得d(x, y) = Δ。 单向性确保所有对于相同的键的查询将汇聚到相同路径中来，而不管是什么起源节点。因此，在查找路径上缓存&lt;键，值&gt;对可以减少‘撞车’的机会。跟Pastry而不是Chord一样， 异或运算也是对称的。（对所有的x以及y， d(x,y) = d(y,x）)</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">2</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．1．节点状态</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">Kademlia</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">节点存储互相的联系信息，以用于路由查询消息。对于任何0 =&lt; i &lt; 160, 每个节点保存那些到本节点的距离为2<sup>i </sup>到2<sup>i</sup><sup>＋1</sup>之间的节点信息列表，包括&lt;IP地址，UDP端口， 节点ID&gt;。我们把这些列表称为K-桶。每个K-桶中的节点按最后联系的时间排序――最久未联系的节点放在头部，最近联系的节点放在尾部。对于比较小的i值，K-桶通常是空的（因为没有合适的节点存在于系统中）。对于比较大的i值，列表节点数可以达到k的大小，k是一个系统级别的冗余参数。k值的选择必须满足一个条件，那就是任意k个节点在一个小时内都失效的可能性很小（例如k =20）。</span>
		</div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">图1：</span>
				</strong>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">以当前已在线时间的函数的形式显示了节点在接下来的一小时后继续在线的比例。X轴代表分钟，y轴代表那些已经在线了x分钟的节点中将继续在线1小时的比例。</span>
		</div>
		<div align="left" twffan="done"> </div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">当一个Kademlia节点收到来自另外一个节点的任何消息（请求的或者回复的），它将更新自己的一个K-桶，即发送节点ID对应的那个桶。如果发送节点已经存在于接收者的K-桶中，接收者会把它移到列表的尾部。如果这个节点还没有存在于对应的K-桶中并且这个桶少于k个节点，则接收者把发送者插入到列表的尾部。如果对应的K-桶已经满了，则发送者将向该K-桶中的最久未联系节点发送ping命令测试是否存在，如果最久未联系节点没有回复，则把它从列表中移除，并把新的发送者插入到列表尾部。如果它回复了，则新的发送者信息会丢弃掉。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">K-</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">桶非常高效的实现了剔除最久未联系节点的策略，存活的节点将永远不会从列表中移除。这种偏向保留旧节点的做法是我们对由Saroiu等人收集的Gnutella协议的跟踪数据进行分析而得出来的。图1以当前已存在时间的函数的形式显示了Gnutella节点在一小时后继续在线的比例。一个节点存活的时间越长，则这个节点继续存活一小时的可能性越大。通过保留存活时间最长的那些节点，K-桶中存储的节点继续在线的概率大大提高了。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">K-</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">桶的第二个优点是它提供了对一定的拒绝服务（DoS）的攻击的抵抗。系统中不断涌入新节点并不会造成节点路由状态的更新过快。Kademlia节点只有在旧节点离开系统时才会向k-桶中插入新节点。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">2</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．2．Kademlia协议</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">Kademlia</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">协议由4个远程过程调用（RPC）组成：PING，STORE，FIND_NODE, FIND_VALUE。 PING RPC 测试节点是否存在。STORE指示一个节点存储一个&lt;键，值&gt;对以用于以后的检索。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">FIND_NODE </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">把160位ID作为变量，RPC的接收者将返回k个它所知道的最接近目标ID的&lt;IP地址，UDP端口，节点ID&gt;元组。这些元组可以来自于一个K-桶，也可以来自于多个K-桶（当最接近的K-桶没有满时）。在任何情况下， RPC接收者都必须返回k项（除非这个节点的所有的K-桶的元组加起来都少于k个，这种情况下RPC接收者返回所有它知道的节点）</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">FIND_VALUE</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">和FIND_NODE行为相似――返回&lt;IP地址，UDP端口，节点ID&gt;元组。仅有一点是不同的，如果RPC接收者已经收到了这个键的STORE RPC,则只需要返回这个已存储的值。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">在所有RPC中，接收者都必须回应一个160位的随机RPC ID,这可以防止地址伪造。PING中则可以为RPC接收者在RPC回复中捎回以对发送者的网络地址获得额外的保证。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">Kademlia</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">参与者必须做的最重要的工作是为一个给定的节点ID定位k个最接近节点。我们称这个过程为节点查询。Kademlia使用一种递归算法来做节点查询。查询的发起者从最接近的非空的K-桶中取出а个节点（或者，如果这个桶没有а项，则只取出它所知道的最接近的几个节点）。发起者然后向选定的а个节点发送平行的，异步的FIND_NODE RPC。а是一个系统级别的并行参数，比如为3。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">在这个递归的步骤中，发起者重新发送FIND_NODE给那些从上次RPC中学习到的节点（这个递归可以在之前的所有的а个RPC返回之前开始）。在这返回的与目标最接近的k个节点中，发起者将选择а个还没有被询问过的节点并且重新发送FIND_NODE RPC给它们。没有立即作出响应的节点将不再予以考虑除非并且直到它们作出响应。如果经过一轮的FIND_NODE都没有返回一个比已知最接近的节点更接近的节点，则发起者将重新向所有k个未曾询问的最接近节点发送FIND_NODE。直到发起者已经询问了k个最接近节点并且得到了响应，这个查询才结束。当а＝1时，查询算法在消息开支和检测失效节点时的时延上与Chord非常相似。 然而，Kademlia可以做到低时延路由因为它有足够的灵活性来选择k个节点中的一个去做查询。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">按照上面的查询过程，大多数的操作都可以实现。要存储一个&lt;键，值&gt;对，参与者定位k个与键最接近的节点然后向这些节点发送STORE RPC。另外，每个节点每个小时都会重新发布它所有的&lt;键，值&gt;对。这可以以高概率的把握确保&lt;键，值&gt;对的持续存在于系统中（我们将会在验证概略一节中看到）。通常来说，我们还要求&lt;键，值&gt;对的原始发布者每隔24小时重新发布一次。否则，所有的&lt;键，值&gt;对在最原始发布的24小时后失效，以尽量减少系统中的陈旧信息。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">最后，为了维持&lt;键，值&gt;对在发布－搜索生命周期中的一致性，我们要求任何时候节点w拥有一个新节点u，u比w更接近w中的一些&lt;键，值&gt;对。w将复制这些&lt;键，值&gt;对给u并且不从自己的数据库中删除。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">为了查找到一个&lt;键，值&gt;对，节点首先查找k个ID与键接近的节点。然而，值查询使用FIND_VALUE而不是FIND_NODE RPC。 而且，只要任何节点返回了值，则这个过程立即结束。为了缓存(caching)的缘故，只要一个查询成功了，这个请求节点将会把这个&lt;键，值&gt;对存储到它拥有的最接近的并且没能返回值的节点上。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">由于这个拓扑的单向性，对相同的键的以后的搜索将很有可能在查询最接近节点前命中已缓存的项。对于一个特定的键，经过多次的查找和传播，系统可能在许多的节点上都缓存了这个键。为了避免“过度缓存”，我们设计了一个&lt;键，值&gt;对在任何节点的数据库中的存活时间与当前节点和与键ID最接近的节点ID之间的节点数成指数级的反比例关系。简单的剔除最久未联系节点会导致相似的生存时间分布，没有很自然的方法来选择缓存大小，因为节点不能提前知道系统将会存储多少个值。</span>
		</div>
		<div align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">    </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">一般来说，由于存在于节点之间的查询的通信，桶会保持不停地刷新。为了避免当没有通信时的病态情况，每个节点对在一个小时内没有做过节点查询的桶进行刷新，刷新意味着在桶的范围内选择一个随机ID然后为这个ID做节点搜索。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">为了加入到这个网络中，节点u必须与一个已经加入到网络中的节点w联系。u把w加入到合适的桶中，然后u为自己的节点ID做一次节点查找。最后，节点u刷新所有比最接近的邻居节点更远的K-桶。在这个刷新过程中，节点u进行了两项必需的工作：既填充了自己的K-桶，又把自己插入到了其它节点的K-桶中。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">3</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．验证概述</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">为了验证我们系统中的特有的函数，我们必须证实绝大多数的操作花费</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">[</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">log </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">n]</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">+ </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">c</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">的时间开销，并且c是一个比较小的常数，并且</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">&lt;</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">键，值&gt;查找将会以很高的概率返回一个存储在系统中的键。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">我们首先做一些定义。对于一个覆盖距离的范围为</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">[</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">2</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">i</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">, </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">2</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">i</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">+1)</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">的</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">K-</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">桶，定义这个桶的索引号为i。定义节点的深度h为160－i，其中i是最小的非空的桶的索引号。定义在节点x中节点y的桶高度为y将插入到x的桶的索引号减去x的最不重要的空桶的索引号。由于节点ID是随机选择的，因此高度的不统一分布是不太可能的。因此，在非常高的概率下，任意一个给定节点的高度在log n之内，其中n是系统中的节点数。而且，对于一个ID，最接近节点在第k接近的节点中的桶高度很有可能是在常数log k之内。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">下一步我们将假设一个不变的条件，那就是每个节点的每个K-桶包含至少一个节点的联系信息，如果这个节点存在于一个合适的范围中。有了这个假设，我们可以发现节点的查找过程是正确的并且时间开销是指数级的。假设与目标ID最接近的节点的深度是h。如果这个节点的h个最有意义的K-桶都是非空的，查询过程在每一步都可以查找到一个到目标节点的距离更接近一半的节点（或者说距离更近了一个bit），因此在</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">h </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">-</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">log </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">k</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">步后目标节点将会出现。</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">如果这个节点的一个K-桶是空的，可能是这样的一种情况，目标节点恰好在空桶对应的距离范围之内。这种情况下，最后的几步并不能使距离减半。然而，搜索还是能正确的继续下去就像键中与空桶相关的那个位已经被置反了。因此，查找算法总是能在</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">h </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">-</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">log </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">k</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">步后</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">返回最接近节点。而且，一旦最接近节点已经找到，并行度会从а扩展到k。寻找到剩下的k-1个最接近节点的步数将不会超过最接近节点在第k接近节点中的桶高度，即不太可能超过log k加上一个常数。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">为了证实前面的不变条件的正确性，首先考虑桶刷新的效果，如果不变条件成立。在被刷新后，一个桶或者包含k个有效节点，或者包含在它范围内的所有节点，如果少于k个节点存在的话（这是从节点的查找过程的正确性而得出来的。）新加入的节点也会被插入到任何没有满的桶中去。因此，唯一违反这个不变条件的方法就是在一个特别的桶的范围内存在k+1个活更多的节点，并且桶中的k个节点在没有查找或刷新的干涉下全部失效。然而，k值被精确的选择以保证使所有节点在一小时内（最大的刷新时间）全都失效的概率足够小。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">实际上，失败的概率比k个节点在1小时内全都离开的概率小得多，因为每个进入或外出的请求消息都会更新节点的桶。这是异或运算的对称性产生的，因为在一次进入或外出的请求中，与一个给定节点通信的对端节点的ID在该节点的桶范围之内的分布是非常均匀的。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">而且，即使这个不变条件在单个节点的单个桶中的确失效了，这也只影响到运行时间（在某些查询中添加一个跳数），并不会影响到节点查找的正确性。只有在查找路径中的k个节点都必须在没有查找或刷新的干涉下在相同的桶中丢失k个节点，才可能造成一次查找失败。如果不同的节点的桶没有重叠，这种情况发生的概率是2<sup>-k*k</sup>。否则，节点出现在多个其它的节点的桶中，这就很可能会有更长的运行时间和更低概率的失败情况。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">现在我们来考虑下&lt;键，值&gt;对的恢复问题。当一个&lt;键，值&gt;对发布时，它将在k个与键接近的节点中存储。同时每隔一小时将重新发布一次。因为即使是新节点（最不可靠的节点）都有1/2的概率持续存活一个小时，一个小时后&lt;键，值&gt;对仍然存在于k个最接近节点中的一个上的概率是1-2<sup>-k</sup></span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">。</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">这个性质并不会由于有接近键的新节点的插入而改变，因为一旦有这样的节点插入，它们为了填充它们的桶将会与他们的最接近的那些节点交互，从而收到附近的它们应该存储的&lt;键，值&gt;对。当然，如果这k个最接近键的节点都失效了，并且这个&lt;键，值&gt;对没有在其它任何地方缓存，Kademlia将会丢失这个&lt;键，值&gt;对。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">4</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．讨论</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">我们使用的基于异或拓扑的路由算法与Pastry [1], Tapestry [2]的路由算法中的第一步和 Plaxton的分布式搜索算法都非常的相似。然而，所有的这三个算法，当他们选择一次接近目标节点b个bit的时候都会产生问题（为了加速的目的）。如果没有异或拓扑，我们还需要一个额外的算法结构来从与目标节点拥有相同的前缀但是接下来的b个bit的数字不同的节点找到目标节点。所有的这三个算法在解决这个问题上采取的方法都是各不相同的，每个都有其不足之处；它们在大小为</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">O</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">(2</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">b </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">log</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">2</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">b </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">n</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">)</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">的主表之外</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">都</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">另外</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">需要</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">一个</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">大小为</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">O</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">(2</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">b</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">)</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">的</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">次要路由表，这增加了自举和维护的开支，使协议变的更加复杂了，而且对于Pastry和Tapestry来说阻止了正确性与一致性的正式分析。Plaxton虽然可以得到证实，但在像点对点（P2P）网络中的极易失效的环境中不太适应。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">相反，Kademlia则非常容易的以不是2的基数被优化。我们可以配置我们的桶表来使每一跳b个bit的速度来接近目标节点。这就要求满足一个条件，那就是任意的</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">0 </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">&lt; j &lt; 2<sup>b</sup></span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">和 </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">0 </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">≤</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">i &lt; </span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">160</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">/b</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">，</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">在与我们的距离为[j2<sup>160-(i+1)b</sup>, (j+1)2<sup>160-(i+1)b</sup>]</span>
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">的范围内就要有一个桶，这个有实际的项的总量预计不会超过个桶。目前的实现中我们令b＝5。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">5</span>
				</strong>
				<strong>
						<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">．总结</span>
				</strong>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done">
				<span style="FONT-SIZE: 12pt; COLOR: black" twffan="done">使用了新颖的基于异或运算的拓扑，Kademlia是第一个结合了可证实的一致性和高性能，最小时延路由，和一个对称，单向的拓扑的点对点(P2P)系统。此外，Kademlia引入了一个并发参数，а，这让人们可以通过调整带宽的一个常数参数来进行异步最低时延的跳选择和不产生时延的失效恢复。最后，Kademlia是第一个利用了节点失效与它的已运行时间成反比这个事实的点对点（P2P）系统。</span>
		</div>
		<div style="TEXT-INDENT: 21pt" align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<strong>
						<span style="COLOR: black" twffan="done">参考文献</span>
				</strong>
		</div>
		<div align="left" twffan="done">
				<span style="COLOR: black" twffan="done">[1] A. Rowstron and P. Druschel. Pastry: Scalable, distributed object location and routing for large-scale peer-to-peer systems. <em>Accepted for Middleware, 2001</em>, 2001. http://research.microsoft.com/˜antr/pastry/.</span>
		</div>
		<div align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<span style="COLOR: black" twffan="done">[2] Ben Y. Zhao, John Kubiatowicz, and Anthony Joseph. Tapestry: an infrastructure for fault-tolerant wide-area location and routing. Technical Report UCB/CSD-01-1141, U.C. Berkeley, April 2001.</span>
		</div>
		<div align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<span style="COLOR: black" twffan="done">[3] Andr´ea W. Richa C. Greg Plaxton, Rajmohan Rajaraman. Accessing nearby copies of replicated objects in a distributed environment. In <em>Proceedings of the ACM SPAA</em>, pages 311–320, June 1997.</span>
		</div>
		<div align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<span style="COLOR: black" twffan="done">[4] Stefan Saroiu, P. Krishna Gummadi and Steven D. Gribble. A Measurement Study of Peer-to-Peer File Sharing Systems. Technical Report UW-CSE-01-06-02, University of Washington, Department of Computer Science and Engineering, July 2001.</span>
		</div>
		<div align="left" twffan="done"> </div>
		<div align="left" twffan="done">
				<span style="COLOR: black" twffan="done">[5] Ion Stoica, Robert Morris, David Karger, M. Frans Kaashoek, and Hari Balakrishnan. Chord: A scalable peer-to-peer lookup service for internet applications. In <em>Proceedings of the ACM SIGCOMM ’01 Conference</em>, San Diego, California, August 2001.</span>
		</div>
<img src ="http://www.cppblog.com/shenhuafeng/aggbug/12267.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shenhuafeng/" target="_blank">CPP&&设计模式小屋</a> 2006-09-11 16:18 <a href="http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12267.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Kademlia详解 (转贴NeoRagex2002)</title><link>http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12255.html</link><dc:creator>CPP&amp;&amp;设计模式小屋</dc:creator><author>CPP&amp;&amp;设计模式小屋</author><pubDate>Mon, 11 Sep 2006 06:09:00 GMT</pubDate><guid>http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12255.html</guid><wfw:comment>http://www.cppblog.com/shenhuafeng/comments/12255.html</wfw:comment><comments>http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12255.html#Feedback</comments><slash:comments>2</slash:comments><wfw:commentRss>http://www.cppblog.com/shenhuafeng/comments/commentRss/12255.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/shenhuafeng/services/trackbacks/12255.html</trackback:ping><description><![CDATA[
		<h1 class="block_title">
				<a id="viewpost1_TitleUrl" href="/shenhuafeng/articles/12254.html">
						<font color="#ff6600">Kademlia详解 (转贴NeoRagex2002)</font>
				</a>
		</h1>
		<div class="post">
				<div class="postcontent">前两天在网上看到世界知名的电骡服务器Razorback 2被查封、4人被拘禁的消息，深感当前做eMule / BitTorrent等P2P文件交换软件的不易。以分布式哈希表方式(DHT，Distributed Hash Table)来代替集中索引服务器可以说是目前可以预见到的为数不多的P2P软件发展趋势之一，比较典型的方案主要包括：CAN、CHORD、Tapestry、Pastry、Kademlia和Viceroy等，而Kademlia协议则是其中应用最为广泛、原理和实现最为实用、简洁的一种，当前主流的P2P软件无一例外地采用了它作为自己的辅助检索协议，如eMule、Bitcomet、Bitspirit和Azureus等。鉴于Kademlia日益增长的强大影响力，今天特地在blog里写下这篇小文，算是对其相关知识系统的总结。 
<div class="postText"><p><b><br />1. Kademlia简述</b></p><p>Kademlia(简称Kad)属于一种典型的结构化P2P覆盖网络(Structured P2P Overlay Network)，以分布式的应用层全网方式来进行信息的存储和检索是其尝试解决的主要问题。在Kademlia网络中，所有信息均以<key, value="" />的哈希表条目形式加以存储，这些条目被分散地存储在各个节点上，从而以全网方式构成一张巨大的分布式哈希表。我们可以形象地把这张哈希大表看成是一本字典：只要知道了信息索引的key，我们便可以通过Kademlia协议来查询其所对应的value信息，而不管这个value信息究竟是存储在哪一个节点之上。在eMule、BitTorrent等P2P文件交换系统中，Kademlia主要充当了文件信息检索协议这一关键角色，但Kad网络的应用并不仅限于文件交换。下文的描述将主要围绕eMule中Kad网络的设计与实现展开。</p><p><b><br />2. eMule的Kad网络中究竟存储了哪些信息?</b></p><p>只要是能够表述成为<key, value="" />字典条目形式的信息Kad网络均能存储，一个Kad网络能够同时存储多张分布式哈希表。以eMule为例，在任一时刻，其Kad网络均存储并维护着两张分布式哈希表，一张我们可以将其命名为关键词字典，而另一张则可以称之为文件索引字典。</p><p>a. <b>关键词字典</b>：主要用于根据给出的关键词查询其所对应的文件名称及相关文件信息，其中key的值等于所给出的关键词字符串的160比特SHA1散列，而其对应的value则为一个列表，在这个列表当中，给出了所有的文件名称当中拥有对应关键词的文件信息，这些信息我们可以简单地用一个3元组条目表示：(文件名，文件长度，文件的SHA1校验值)，举个例子，假定存在着一个文件“warcraft_frozen_throne.iso”，当我们分别以“warcraft”、“frozen”、“throne”这三个关键词来查询Kad时，Kad将有可能分别返回三个不同的文件列表，这三个列表的共同之处则在于它们均包含着一个文件名为“warcraft_frozen_throne.iso”的信息条目，通过该条目，我们可以获得对应iso文件的名称、长度及其160比特的SHA1校验值。</p><p>b. <b>文件索引字典</b>：用于根据给出的文件信息来查询文件的拥有者(即该文件的下载服务提供者)，其中key的值等于所需下载文件的SHA1校验值(这主要是因为，从统计学角度而言，160比特的SHA1文件校验值可以唯一地确定一份特定数据内容的文件)；而对应的value也是一个列表，它给出了当前所有拥有该文件的节点的网络信息，其中的列表条目我们也可以用一个3元组表示：(拥有者IP，下载侦听端口，拥有者节点ID)，根据这些信息，eMule便知道该到哪里去下载具备同一SHA1校验值的同一份文件了。</p><p><b><br />3. 利用Kad网络搜索并下载文件的基本流程是怎样的?</b></p><p>基于我们对eMule的Kad网络中两本字典的理解，利用Kad网络搜索并下载某一特定文件的基本过程便很明白了，仍以“warcraft_frozen_throne.iso”为例，首先我们可以通过warcraft、frozen、throne等任一关键词查询关键词字典，得到该iso的SHA1校验值，然后再通过该校验值查询Kad文件索引字典，从而获得所有提供“warcraft_frozen_throne.iso”下载的网络节点，继而以分段下载方式去这些节点下载整个iso文件。</p><p>在上述过程中，Kad网络实际上所起的作用就相当于两本字典，但值得再次指出的是，Kad并不是以集中的索引服务器(如华语P2P源动力、Razorback 2、DonkeyServer 等，骡友们应该很熟悉吧)方式来实现这两本字典的存储和搜索的，因为这两本字典的所有<key, value="" />条目均分布式地存储在参与Kad网络的各节点中，相关文件信息、下载位置信息的存储和交换均无需集中索引服务器的参与，这不仅提高了查询效率，而且还提高了整个P2P文件交换系统的可靠性，同时具备相当的反拒绝服务攻击能力；更有意思的是，它能帮助我们有效地抵制FBI的追捕，因为俗话说得好：法不治众…看到这里，相信大家都能理解“分布式信息检索”所带来的好处了吧。但是，这些条目究竟是怎样存储的呢?我们又该如何通过Kad网络来找到它们?不着急，慢慢来。</p><p><b><br />4. 什么叫做节点的ID和节点之间的距离?</b></p><p>Kad网络中的每一个节点均拥有一个专属ID，该ID的具体形式与SHA1散列值类似，为一个长达160bit的整数，它是由节点自己随机生成的，两个节点拥有同一ID的可能性非常之小，因此可以认为这几乎是不可能的。在Kad网络中，两个节点之间距离并不是依靠物理距离、路由器跳数来衡量的，事实上，Kad网络将任意两个节点之间的距离d定义为其二者ID值的逐比特二进制和数，即，假定两个节点的ID分别为a与b，则有：d=a XOR b。在Kad中，每一个节点都可以根据这一距离概念来判断其他节点距离自己的“远近”，当d值大时，节点间距离较远，而当d值小时，则两个节点相距很近。这里的“远近”和“距离”都只是一种逻辑上的度量描述而已；在Kad中，距离这一度量是无方向性的，也就是说a到b的距离恒等于b到a的距离，因为a XOR b==b XOR a</p><p><b><br />5. <key, value="" />条目是如何存储在Kad网络中的?</b></p><p>从上文中我们可以发现节点ID与<key, value="" />条目中key值的相似性：无论是关键词字典的key，还是文件索引字典的key，都是160bit，而节点ID恰恰也是160bit。这显然是有目的的。事实上，节点的ID值也就决定了哪些<key, value="" />条目可以存储在该节点之中，因为我们完全可以把某一个<key, value="" />条目简单地存放在节点ID值恰好等于条目中key值的那个节点处，我们可以将满足(ID==key)这一条件的节点命名为目标节点N。这样的话，一个查找<key, value="" />条目的问题便被简单地转化成为了一个查找ID等于Key值的节点的问题。</p><p>由于在实际的Kad网络当中，并不能保证在任一时刻目标节点N均一定存在或者在线，因此Kad网络规定：任一<key, value="" />条目，依据其key的具体取值，该条目将被复制并存放在节点ID距离key值最近(即当前距离目标节点N最近)的k个节点当中；之所以要将<key, value="" />重复保存k份，这完全是考虑到整个Kad系统稳定性而引入的冗余；这个k的取值也有讲究，它是一个带有启发性质的估计值，挑选其取值的准则为：“在当前规模的Kad网络中任意选择至少k个节点，令它们在任意时刻同时不在线的几率几乎为0”；目前，k的典型取值为20，即，为保证在任何时刻我们均能找到至少一份某<key, value="" />条目的拷贝，我们必须事先在Kad网络中将该条目复制至少20份。</p><p>由上述可知，对于某一<key, value="" />条目，在Kad网络中ID越靠近key的节点区域，该条目保存的份数就越多，存储得也越集中；事实上，为了实现较短的查询响应延迟，在条目查询的过程中，任一条目可被cache到任意节点之上；同时为了防止过度cache、保证信息足够新鲜，必须考虑<key, value="" />条目在节点上存储的时效性：越接近目标结点N，该条目保存的时间将越长，反之，其超时时间就越短；保存在目标节点之上的条目最多能够被保留24小时，如果在此期间该条目被其发布源重新发布的话，其保存时间还可以进一步延长。</p><p><b><br />6. Kad网络节点需要维护哪些状态信息?</b></p><p>在Kad网络中，每一个节点均维护了160个list，其中的每个list均被称之为一个k-桶(k-bucket)，如下图所示。在第i个list中，记录了当前节点已知的与自身距离为2^i~2^(i+1)的一些其他对端节点的网络信息(Node ID，IP地址，UDP端口)，每一个list(k-桶)中最多存放k个对端节点信息，注意，此处的k与上文所提到的复制系数k含义是一致的；每一个list中的对端节点信息均按访问时间排序，最早访问的在list头部，而最近新访问的则放在list的尾部。</p><p><img src="http://www.cppblog.com/images/cppblog_com/shenhuafeng/2600/r_k_bucket.png" /></p><p>k-桶中节点信息的更新基本遵循Least-recently Seen Eviction原则：当list容量未满(k-桶中节点个数未满k个)，且最新访问的对端节点信息不在当前list中时，其信息将直接添入list队尾，如果其信息已经在当前list中，则其将被移动至队尾；在k-桶容量已满的情况下，添加新节点的情况有点特殊，它将首先检查最早访问的队首节点是否仍有响应，如果有，则队首节点被移至队尾，新访问节点信息被抛弃，如果没有，这才抛弃队首节点，将最新访问的节点信息插入队尾。可以看出，尽可能重用已有节点信息、并且按时间排序是k-桶节点更新方式的主要特点。从启发性的角度而言，这种方式具有一定的依据：在线时间长一点的节点更值得我们信任，因为它已经在线了若干小时，因此，它在下一个小时以内保持在线的可能性将比我们最新访问的节点更大，或者更直观点，我这里再给出一个更加人性化的解释：MP3文件交换本身是一种触犯版权法律的行为，某一个节点反正已经犯了若干个小时的法了，因此，它将比其他新加入的节点更不在乎再多犯一个小时的罪……-_-b</p><p>由上可见，设计采用这种多k-bucket数据结构的初衷主要有二：a. 维护最近-最新见到的节点信息更新；b. 实现快速的节点信息筛选操作，也就是说，只要知道某个需要查找的特定目标节点N的ID，我们便可以从当前节点的k-buckets结构中迅速地查出距离N最近的若干已知节点。</p><p><b><br />7. 在Kad网络中如何寻找某特定的节点?</b></p><p>已知某节点ID，查找获得当前Kad网络中与之距离最短的k个节点所对应的网络信息(Node ID，IP地址，UDP端口)的过程，即为Kad网络中的一次节点查询过程(Node Lookup)。注意，Kad之所以没有把节点查询过程严格地定义成为仅仅只查询单个目标节点的过程，这主要是因为Kad网络并没有对节点的上线时间作出任何前提假设，因此在多数情况下我们并不能肯定需要查找的目标节点一定在线或存在。</p><p>整个节点查询过程非常直接，其方式类似于DNS的迭代查询：<br />a. 由查询发起者从自己的k-桶中筛选出若干距离目标ID最近的节点，并向这些节点同时发送异步查询请求；<br />b .被查询节点收到请求之后，将从自己的k-桶中找出自己所知道的距离查询目标ID最近的若干个节点，并返回给发起者；<br />c. 发起者在收到这些返回信息之后，再次从自己目前所有已知的距离目标较近的节点中挑选出若干没有请求过的，并重复步骤1；<br />d. 上述步骤不断重复，直至无法获得比查询者当前已知的k个节点更接近目标的活动节点为止。<br />e. 在查询过程中，没有及时响应的节点将立即被排除；查询者必须保证最终获得的k个最近节点都是活动的。</p><p>简单总结一下上述过程，实际上它跟我们日常生活中去找某一个人打听某件事是非常相似的，比方说你是个Agent Smith，想找小李(key)问问他的手机号码(value)，但你事先并不认识他，你首先肯定会去找你所认识的和小李在同一个公司工作的人，比方说小赵，然后小赵又会告诉你去找与和小李在同一部门的小刘，然后小刘又会进一步告诉你去找和小李在同一个项目组的小张，最后，你找到了小张，哟，正好小李出差去了(节点下线了)，但小张恰好知道小李的号码，这样你总算找到了所需的信息。在节点查找的过程中，“节点距离的远近”实际上与上面例子中“人际关系的密切程度”所代表的含义是一样的。<br /></p><p>最后说说上述查询过程的局限性：Kad网络并不适合应用于模糊搜索，如通配符支持、部分查找等场合，但对于文件共享场合来说，基于关键词的精确查找功能已经基本足够了(值得注意的是，实际上我们只要对上述查找过程稍加改进，并可以令其支持基于关键词匹配的布尔条件查询，但仍不够优化)。这个问题反映到eMule的应用层面来，它直接说明了文件共享时其命名的重要性所在，即，文件名中的关键词定义得越明显，则该文件越容易被找到，从而越有利于其在P2P网络中的传播；而另一方面，在eMule中，每一个共享文件均可以拥有自己的相关注释，而Comment的重要性还没有被大家认识到：实际上，这个文件注释中的关键词也可以直接被利用来替代文件名关键词，从而指导和方便用户搜索，尤其是当文件名本身并没有体现出关键词的时候。</p><p><b><br />8. 在Kad网络中如何存储和搜索某特定的<key, value="" />条目?</b></p><p>从本质上而言，存储、搜索某特定<key, value="" />条目的问题实际上就是节点查找的问题。当需要在Kad网络中存储一个条目时，可以首先通过节点查找算法找到距离key最近的k个节点，然后再通知它们保存<key, value="" />条目即可。而搜索条目的过程则与节点查询过程也是基本类似，由搜索发起方以迭代方式不断查询距离key较近的节点，一旦查询路径中的任一节点返回了所需查找的value，整个搜索的过程就结束。为提高效率，当搜索成功之后，发起方可以选择将搜索到的条目存储到查询路径的多个节点中，作为方便后继查询的cache；条目cache的超时时间与节点-key之间的距离呈指数反比关系。</p><p><b><br />9. 一个新节点如何首次加入Kad网络?</b></p><p>当一个新节点首次试图加入Kad网络时，它必须做三件事，其一，不管通过何种途径，获知一个已经加入Kad网络的节点信息(我们可以称之为节点I)，并将其加入自己的k-buckets；其二，向该节点发起一次针对自己ID的节点查询请求，从而通过节点I获取一系列与自己距离邻近的其他节点的信息；最后，刷新所有的k-bucket，保证自己所获得的节点信息全部都是新鲜的。</p></div></div>
		</div>
<img src ="http://www.cppblog.com/shenhuafeng/aggbug/12255.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/shenhuafeng/" target="_blank">CPP&&设计模式小屋</a> 2006-09-11 14:09 <a href="http://www.cppblog.com/shenhuafeng/archive/2006/09/11/12255.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>