﻿<?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++博客-loop_in_codes-随笔分类-erlang</title><link>http://www.cppblog.com/kevinlynx/category/20475.html</link><description>低调做技术__欢迎移步我的独立博客 &lt;a href="http://codemacro.com"&gt;codemaro.com&lt;/a&gt;</description><language>zh-cn</language><lastBuildDate>Thu, 08 Aug 2013 15:21:54 GMT</lastBuildDate><pubDate>Thu, 08 Aug 2013 15:21:54 GMT</pubDate><ttl>60</ttl><item><title>Dhtcrawler2换用sphinx搜索</title><link>http://www.cppblog.com/kevinlynx/archive/2013/08/08/202417.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Thu, 08 Aug 2013 15:04:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2013/08/08/202417.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/202417.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2013/08/08/202417.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/202417.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/202417.html</trackback:ping><description><![CDATA[<div class="entry-content">
<p>dhtcrawler2最开始使用mongodb自带的全文搜索引擎搜索资源。搜索一些短关键字时很容易导致erlang进程call timeout，也就是查询时间太长。对于像<code>avi</code>这种关键字，搜索时间长达十几秒。搜索的资源数量200万左右。这其中大部分资源只是对root文件名进行了索引，即对于多文件资源而言没有索引单个文件名。索引方式有部分资源是按照字符串子串的形式，没有拆词，非常占用存储空间；有部分是使用了rmmseg（我编译了rmmseg-cpp作为erlang nif库调用 <a href="https://github.com/kevinlynx/erl-rmmseg">erl-rmmseg</a>）进行了拆词，占用空间小了很多，但由于词库问题很多片里的词汇没拆出来。</p>

<p>很早以前我以为搜索耗时的原因是因为数据库太忙，想部署个mongodb集群出来。后来发现数据库没有任何读写的状态下，查询依然慢。终于只好放弃mongodb自带的文本搜索。于是我改用sphinx。简单起见，我直接下载了<a href="http://www.coreseek.cn/">coreseek4.1</a>（sphinx的一个支持中文拆词的包装）。</p>

<p>现在，已经导入了200多万的资源进sphinx，并且索引了所有文件名，索引文件达800M。对于<code>avi</code>关键字的搜索大概消耗0.2秒的时间。<a href="http://bt.cm/e/http_handler:search?q=avi">搜索试试</a>。</p>

<p>以下记录下sphinx在dhtcrawler的应用</p>

<h3>sphinx简介</h3>

<p>sphinx包含两个主要的程序：indexer和searchd。indexer用于建立文本内容的索引，然后searchd基于这些索引提供文本搜索功能，而要使用该功能，可以遵循searchd的网络协议连接searchd这个服务来使用。</p>

<p>indexer可以通过多种方式来获取这些文本内容，文本内容的来源称为数据源。sphinx内置mysql这种数据源，意思是可以直接从mysql数据库中取得数据。sphinx还支持xmlpipe2这种数据源，其数据以xml格式提供给indexer。要导入mongodb数据库里的内容，可以选择使用xmlpipe2这种方式。</p>

<!-- more -->


<h3>sphinx document</h3>

<p>xmlpipe2数据源需要按照以下格式提交：</p>

<pre><code>&lt;sphinx:docset&gt;
    &lt;sphinx:schema&gt;
        &lt;sphinx:field name="subject"/&gt;
        &lt;sphinx:field name="files"/&gt;
        &lt;sphinx:attr name="hash1" type="int" bits="32"/&gt;
        &lt;sphinx:attr name="hash2" type="int" bits="32"/&gt;
    &lt;/sphinx:schema&gt;
    &lt;sphinx:document id="1"&gt;
        &lt;subject&gt;this is the subject&lt;/subject&gt;
        &lt;files&gt;file content&lt;/files&gt;
        &lt;hash1&gt;111&lt;/hash1&gt;
    &lt;/sphinx:document&gt;
&lt;/sphinx:docset&gt;
</code></pre>

<p>该文件包含两大部分：<code>schema</code>和<code>documents</code>，其中<code>schema</code>又包含两部分：<code>field</code>和<code>attr</code>，其中由<code>field</code>标识的字段就会被indexer读取并全部作为输入文本建立索引，而<code>attr</code>则标识查询结果需要附带的信息；<code>documents</code>则是由一个个<code>sphinx:document</code>组成，即indexer真正要处理的数据。注意其中被<code>schema</code>引用的属性名。</p>

<p>document一个很重要的属性就是它的id。这个id对应于sphinx需要唯一，查询结果也会包含此id。一般情况下，此id可以直接是数据库主键，可用于查询到详细信息。searchd搜索关键字，其实可以看作为搜索这些document，搜索出来的结果也是这些document，搜索结果中主要包含schema中指定的attr。</p>

<h3>增量索引</h3>

<p>数据源的数据一般是变化的，新增的数据要加入到sphinx索引文件中，才能使得searchd搜索到新录入的数据。要不断地加入新数据，可以使用增量索引机制。增量索引机制中，需要一个主索引和一个次索引(delta index)。每次新增的数据都建立为次索引，然后一段时间后再合并进主索引。这个过程主要还是使用indexer和searchd程序。实际上，searchd是一个需要一直运行的服务，而indexer则是一个建立完索引就退出的工具程序。所以，这里的增量索引机制，其中涉及到的&#8220;每隔一定时间就合并&#8221;这种工作，需要自己写程序来协调（或通过其他工具）</p>

<h3>sphinx与mongodb</h3>

<p>上面提到，一般sphinx document的id都是使用的数据库主键，以方便查询。但mongodb中默认情况不使用数字作为主键。dhtcrawler的资源数据库使用的是资源info-hash作为主键，这无法作为sphinx document的id。一种解决办法是，将该hash按位拆分，拆分成若干个sphinx document attr支持位数的整数。例如，info-hash是一个160位的id，如果使用32位的attr（高版本的sphinx支持64位的整数），那么可以把该info-hash按位拆分成5个attr。而sphinx document id则可以使用任意数字，只要保证不冲突就行。当获得查询结果时，取得对应的attr，组合为info-hash即可。</p>

<p>mongodb默认的Object id也可以按这种方式拆分。</p>

<h3>dhtcrawler2与sphinx</h3>

<p>dhtcrawler2中我自己写了一个导入程序。该程序从mongodb中读出数据，数据到一定量时，就输出为xmlpipe2格式的xml文件，然后建立为次索引，最后合并进主索引。过程很简单，包含两次启动外部进程的工作，这个可以通过erlang中os:cmd完成。</p>

<p>值得注意的是，在从mongodb中读数据时，使用skip基本是不靠谱的，skip 100万个数据需要好几分钟，为了不增加额外的索引字段，我只好在<code>created_at</code>字段上加索引，然后按时间段来读取资源，这一切都是为了支持程序关闭重启后，可以继续上次工作，而不是重头再来。200万的数据，已经处理了好几天了。</p>

<p>后头数据建立好了，需要在前台展示出来。erlang中似乎只有一个sphinx客户端库：<a href="https://github.com/kevsmith/giza">giza</a>。这个库有点老，写成的时候貌似还在使用sphinx0.9版本。其中查询代码包含了版本判定，已经无法在我使用的sphinx2.x版本中使用。无奈之下我只好修改了这个库的源码，幸运的是查询功能居然是正常的，意味着sphinx若干个版本了也没改动通信协议？后来，我为了取得查询的统计信息，例如消耗时间以及总结果，我再一次修改了giza的源码。新的版本可以在我的github上找到：<a href="https://github.com/kevinlynx/giza">my giza</a>，看起来我没侵犯版本协议吧？</p>

<p>目前dhtcrawler的搜索，先是基于sphinx搜索出hash列表，然后再去mongodb中搜索hash对应的资源。事实上，可以为sphinx的document直接附加这些资源的描述信息，就可以避免去数据库查询。但我想，这样会增加sphinx索引文件的大小，担心会影响搜索速度。实际测试时，发现数据库查询有时候还真的很消耗时间，尽管我做了分页，以使得单页仅对数据库进行少量查询。</p>

<h3>xml unicode</h3>

<p>在导入xml到sphinx的索引过程中，本身我输出的内容都是unicode的，但有很多资源会导致indexer解析xml出错。出错后indexer直接停止对当前xml的处理。后来查阅资料发现是因为这些无法被indexer处理的xml内容包含unicode里的控制字符，例如 &#228; (U+00E4)。我的解决办法是直接过滤掉这些控制字符。unicode的控制字符参看<a href="http://www.utf8-chartable.de/">UTF-8 encoding table and Unicode characters</a>。在erlang中干这个事居然不复杂：</p>

<div class="highlight">
<pre><code class="erlang"><span class="nf">strip_invalid_unicode</span><span class="p">(</span><span class="o">&lt;&lt;&gt;&gt;</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="o">&lt;&lt;&gt;&gt;</span><span class="p">;</span>
<span class="nf">strip_invalid_unicode</span><span class="p">(</span><span class="o">&lt;&lt;</span><span class="nv">C</span><span class="o">/</span><span class="n">utf8</span><span class="p">,</span> <span class="nv">R</span><span class="o">/</span><span class="n">binary</span><span class="o">&gt;&gt;</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="k">case</span> <span class="n">is_valid_unicode</span><span class="p">(</span><span class="nv">C</span><span class="p">)</span> <span class="k">of</span>
        <span class="n">true</span> <span class="o">-&gt;</span>
            <span class="nv">RR</span> <span class="o">=</span> <span class="n">strip_invalid_unicode</span><span class="p">(</span><span class="nv">R</span><span class="p">),</span>
            <span class="o">&lt;&lt;</span><span class="nv">C</span><span class="o">/</span><span class="n">utf8</span><span class="p">,</span> <span class="nv">RR</span><span class="o">/</span><span class="n">binary</span><span class="o">&gt;&gt;</span><span class="p">;</span>
        <span class="n">false</span> <span class="o">-&gt;</span>
            <span class="n">strip_invalid_unicode</span><span class="p">(</span><span class="nv">R</span><span class="p">)</span>
    <span class="k">end</span><span class="p">;</span>
<span class="nf">strip_invalid_unicode</span><span class="p">(</span><span class="o">&lt;&lt;</span><span class="p">_,</span> <span class="nv">R</span><span class="o">/</span><span class="n">binary</span><span class="o">&gt;&gt;</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="n">strip_invalid_unicode</span><span class="p">(</span><span class="nv">R</span><span class="p">).</span>
    
<span class="nf">is_valid_unicode</span><span class="p">(</span><span class="nv">C</span><span class="p">)</span> <span class="k">when</span> <span class="nv">C</span> <span class="o">&lt;</span> <span class="mi">16#20</span> <span class="o">-&gt;</span>
    <span class="n">false</span><span class="p">;</span>
<span class="nf">is_valid_unicode</span><span class="p">(</span><span class="nv">C</span><span class="p">)</span> <span class="k">when</span> <span class="nv">C</span> <span class="o">&gt;=</span> <span class="mi">16#7f</span><span class="p">,</span> <span class="nv">C</span> <span class="o">=&lt;</span> <span class="mi">16#ff</span> <span class="o">-&gt;</span>
    <span class="n">false</span><span class="p">;</span>
<span class="nf">is_valid_unicode</span><span class="p">(_)</span> <span class="o">-&gt;</span>
    <span class="n">true</span><span class="p">.</span>
</code></pre>
</div>




<p class="post-footer">
            原文地址：
            <a href="http://codemacro.com/2013/08/08/sphinx-dhtcrawler/">http://codemacro.com/2013/08/08/sphinx-dhtcrawler/</a><br />
            written by <a href="http://codemacro.com">Kevin Lynx</a>
            &nbsp;posted at <a href="http://codemacro.com">http://codemacro.com</a>
            </p>

</div><img src ="http://www.cppblog.com/kevinlynx/aggbug/202417.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2013-08-08 23:04 <a href="http://www.cppblog.com/kevinlynx/archive/2013/08/08/202417.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>磁力搜索第二版-dhtcrawler2</title><link>http://www.cppblog.com/kevinlynx/archive/2013/07/20/201994.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Sat, 20 Jul 2013 08:37:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2013/07/20/201994.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/201994.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2013/07/20/201994.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/201994.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/201994.html</trackback:ping><description><![CDATA[<div class="entry-content">
<p>接<a href="http://codemacro.com/2013/06/21/magnet-search-impl/">上篇</a>。</p>

<h2>下载使用</h2>

<p>目前为止dhtcrawler2相对dhtcrawler而言，数据库部分调整很大，DHT部分基本沿用之前。但单纯作为一个爬资源的程序而言，DHT部分可以进行大幅削减，这个以后再说。这个版本更快、更稳定。为了方便，我将编译好的erlang二进制文件作为git的主分支，我还添加了一些Windows下的批处理脚本，总之基本上下载源码以后即可运行。</p>

<p>项目地址：<a href="https://github.com/kevinlynx/dhtcrawler2">https://github.com/kevinlynx/dhtcrawler2</a></p>

<h3>使用方法</h3>

<ul>
<li>下载erlang，我测试的是R16B版本，确保erl等程序被加入<code>Path</code>环境变量</li>
<li>
<p>下载mongodb，解压即用：</p>

<pre><code>  mongod --dbpath xxx --setParameter textSearchEnabled=true
</code></pre>
</li>
<li>
<p>下载dhtcrawler2</p>

<pre><code>  git clone https://github.com/kevinlynx/dhtcrawler2.git
</code></pre>
</li>
<li><p>运行<code>win_start_crawler.bat</code></p></li>
<li>运行<code>win_start_hash.bat</code>
</li>
<li>运行<code>win_start_http.bat</code>
</li>
<li>打开<code>localhost:8000</code>查看<code>stats</code>
</li>
</ul>
<p>爬虫每次运行都会保存DHT节点状态，早期运行的时候收集速度会不够。dhtcrawler2将程序分为3部分：</p>

<ul>
<li>crawler，即DHT爬虫部分，仅负责收集hash</li>
<li>hash，准确来讲叫<code>hash reader</code>，处理爬虫收集的hash，处理过程主要涉及到下载种子文件</li>
<li>http，使用hash处理出来的数据库，以作为Web端接口</li>
</ul>
<p>我没有服务器，但程序有被部署在别人的服务器上：<a href="http://bt.cm">bt.cm</a>，<a href="http://222.175.114.126:8000/">http://222.175.114.126:8000/</a>。</p>

<!-- more -->


<h3>其他工具</h3>

<p>为了提高资源索引速度，我陆续写了一些工具，包括：</p>

<ul>
<li>import_tors，用于导入本地种子文件到数据库</li>
<li>tor_cache，用于下载种子到本地，仅仅提供下载的功能，hash_reader在需要种子文件时，可以先从本地取</li>
<li>cache_indexer，目前hash_reader取种子都是从torrage.com之类的种子缓存站点取，这些站点提供了种子列表，cache_indexer将这些列表导入数据库，hash_reader在请求种子文件前可以通过该数据库检查torrage.com上有无此种子，从而减少多余的http请求</li>
</ul>
<p>这些工具的代码都被放在dhtcrawler2中，可以查看对应的启动脚本来查看具体如何启动。</p>

<h3>OS/Database</h3>

<p>根据实际的测试效果来看，当收集的资源量过百万时（目前bt.cm录入近160万资源），4G内存的Windows平台，mongodb很容易就会挂掉。挂掉的原因全是1455，页面文件太小。有人建议不要在Windows下使用mongodb，Linux下我自己没做过测试。</p>

<p>mongodb可以部署为集群形式(replica-set)，当初我想把http部分的查询放在一个只读的mongodb实例上，但因为建立集群时，要同步已有的10G数据库，而每次同步都以mongodb挂掉结束，遂放弃。在目前bt.cm的配置中，数据库torrent的锁比例（db lock）很容易上50%，这也让http在搜索时，经常出现搜索超时的情况。</p>

<h2>技术信息</h2>

<p>dhtcrawler最早的版本有很多问题，修复过的最大的一个问题是关于erlang定时器的，在DHT实现中，需要对每个节点每个peer做超时处理，在erlang中的做法直接是针对每个节点注册了一个定时器。这不是问题，问题在于定时器资源就像没有GC的内存资源一样，是会由于程序员的代码问题而出现资源泄漏。所以，dhtcrawler第一个版本在节点数配置在100以上的情况下，用不了多久就会内存耗尽，最终导致erlang虚拟机core dump。</p>

<p>除了这个问题以外，dhtcrawler的资源收录速度也不是很快。这当然跟数据库和获取种子的速度有直接关系。尤其是获取种子，使用的是一些提供info-hash到种子映射的网站，通过HTTP请求来下载种子文件。我以为通过BT协议直接下载种子会快些，并且实时性也要高很多，因为这个种子可能未被这些缓存网站收录，但却可以直接向对方请求得到。为此，我还特地翻阅了相关<a href="http://www.bittorrent.org/beps/bep_0009.html">协议</a>，并且用erlang实现了（以后的文章我会讲到具体实现这个协议）。</p>

<p>后来我怀疑get_peers的数量会不会比announce_peer多，但是理论上一般的客户端在get_peers之后都是announce_peer，但是如果get_peers查询的peers恰好不在线呢？这意味着很多资源虽然已经存在，只不过你恰好暂时请求不到。实际测试时，发现get_peers基本是announce_peer数量的10倍。</p>

<p>将hash的获取方式做了调整后，dhtcrawler在几分钟以内以几乎每秒上百个新增种子的速度工作。然后，程序挂掉。</p>

<p>从dhtcrawler到今天为止的dhtcrawler2，中间间隔了刚好1个月。我的所有业余时间全部扑在这个项目上，面临的问题一直都是程序的内存泄漏、资源收录的速度不够快，到后来又变为数据库压力过大。每一天我都以为我将会完成一个稳定版本，然后终于可以去干点别的事情，但总是干不完，目前完没完都还在观察。我始终明白在做优化前需要进行详尽的数据收集和分析，从而真正地优化到正确的点上，但也总是凭直觉和少量数据分析就开始尝试。</p>

<p>这里谈谈遇到的一些问题。</p>

<h3>erlang call timeout</h3>

<p>最开始遇到erlang中<code>gen_server:call</code>出现<code>timeout</code>错误时，我还一直以为是进程死锁了。相关代码读来读去，实在觉得不可能发生死锁。后来发现，当erlang虚拟机压力上去后，例如内存太大，但没大到耗尽系统所有内存（耗进所有内存基本就core dump了），进程间的调用就会出现timeout。</p>

<p>当然，内存占用过大可能只是表象。其进程过多，进程消息队列太长，也许才是导致出现timeout的根本原因。消息队列过长，也可能是由于发生了<em>消息泄漏</em>的缘故。消息泄漏我指的是这样一种情况，进程自己给自己发消息（当然是cast或info），这个消息被处理时又会发送相同的消息，正常情况下，gen_server处理了一个该消息，就会从消息队列里移除它，然后再发送相同的消息，这不会出问题。但是当程序逻辑出问题，每次处理该消息时，都会发生多余一个的同类消息，那消息队列自然就会一直增长。</p>

<p>保持进程逻辑简单，以避免这种逻辑错误。</p>

<h3>erlang gb_trees</h3>

<p>我在不少的地方使用了gb_trees，dht_crawler里就可能出现<code>gb_trees:get(xxx, nil)</code>这种错误。乍一看，我以为我真的传入了一个<code>nil</code>值进去。然后我苦看代码，以为在某个地方我会把这个gb_trees对象改成了nil。但事情不是这样的，gb_tress使用一个tuple作为tree的节点，当某个节点没有子节点时，就会以nil表示。</p>

<p><code>gb_trees:get(xxx, nil)</code>类似的错误，实际指的是<code>xxx</code>没有在这个gb_trees中找到。</p>

<h3>erlang httpc</h3>

<p>dht_crawler通过http协议从torrage.com之类的缓存网站下载种子。最开始我为了尽量少依赖第三方库，使用的是erlang自带的httpc。后来发现程序有内存泄漏，google发现erlang自带的httpc早为人诟病，当然也有大神说在某个版本之后这个httpc已经很不错。为了省事，我直接换了ibrowse，替换之后正常很多。但是由于没有具体分析测试过，加之时间有点远了，我也记不太清细节。因为早期的http请求部分，没有做数量限制，也可能是由于我的使用导致的问题。</p>

<p>某个版本后，我才将http部分严格地与hash处理部分区分开来。相较数据库操作而言，http请求部分慢了若干数量级。在hash_reader中将这两块分开，严格限制了提交给httpc的请求数，以获得稳定性。</p>

<p>对于一个复杂的网络系统而言，分清哪些是耗时的哪些是不大耗时的，才可能获得性能的提升。对于hash_reader而言，处理一个hash的速度，虽然很大程度取决于数据库，但相较http请求，已经快很多。它在处理这些hash时，会将数据库已收录的资源和待下载的资源分离开，以尽快的速度处理已存在的，而将待下载的处理速度交给httpc的响应速度。</p>

<h3>erlang httpc ssl</h3>

<p>ibrowse处理https请求时，默认和erlang自带的httpc使用相同的SSL实现。这经常导致出现<code>tls_connection</code>进程挂掉的错误，具体原因不明。</p>

<h3>erlang调试</h3>

<p>首先合理的日志是任何系统调试的必备。</p>

<p>我面临的大部分问题都是内存泄漏相关，所以依赖的erlang工具也是和内存相关的：</p>

<ul>
<li>
<p>使用<code>etop</code>，可以检查内存占用多的进程、消息队列大的进程、CPU消耗多的进程等等：</p>

<pre><code>  spawn(fun() -&gt; etop:start([{output, text}, {interval, 10}, {lines, 20}, {sort, msg_q }]) end).
</code></pre>
</li>
<li><p>使用<code>erlang:system_info(allocated_areas).</code>检查内存使用情况，其中会输出系统<code>timer</code>数量</p></li>
<li>使用<code>erlang:process_info</code>查看某个具体的进程，这个甚至会输出消息队列里的消息</li>
</ul>
<h3>hash_writer/crawler</h3>

<p>crawler本身仅收集hash，然后写入数据库，所以可以称crawler为hash_writer。这些hash里存在大量的重复。hash_reader从数据库里取出这些hash然后做处理。处理过程会首先判定该hash对应的资源是否被收录，没有收录就先通过http获取种子。</p>

<p>在某个版本之后，crawler会简单地预先处理这些hash。它缓存一定数量的hash，接收到新hash时，就合并到hash缓存里，以保证缓存里没有重复的hash。这个重复率经过实际数据分析，大概是50%左右，即收到的100个请求里，有50个是重复的。这样的优化，不仅会降低hash数据库的压力，hash_reader处理的hash数量少了，也会对torrent数据库有很大提升。</p>

<p>当然进一步的方案可以将crawler和hash_reader之间交互的这些hash直接放在内存中处理，省去中间数据库。但是由于mongodb大量使用虚拟内存的缘故（内存映射文件），经常导致服务器内存不够（4G），内存也就成了珍稀资源。当然这个方案还有个弊端是难以权衡hash缓存的管理。crawler收到hash是一个不稳定的过程，在某些时间点这些hash可能爆多，而hash_reader处理hash的速度也会不太稳定，受限于收到的hash类别（是新增资源还是已存在资源）、种子请求速度、是否有效等。</p>

<p>当然，也可以限制缓存大小，以及对hash_reader/crawler处理速度建立关系来解决这些问题。但另一方面，这里的优化是否对目前的系统有提升，是否是目前系统面临的最大问题，却是需要考究的事情。</p>

<h3>cache indexer</h3>

<p>dht_crawler是从torrage.com等网站获取种子文件，这些网站看起来都是使用了相同的接口，其都有一个sync目录，里面存放了每天每个月索引的种子hash，例如 http://torrage.com/sync/。这个网站上是否有某个hash对应的种子，就可以从这些索引中检查。</p>

<p>hash_reader在处理新资源时，请求种子的过程中发现大部分在这些服务器上都没有找到，也就是发起的很多http请求都是404回应，这不但降低了系统的处理能力、带宽，也降低了索引速度。所以我写了一个工具，先手工将sync目录下的所有文件下载到本地，然后通过这个工具 (cache indexer) 将这些索引文件里的hash全部导入数据库。在以后的运行过程中，该工具仅下载当天的索引文件，以更新数据库。 hash_reader 根据配置，会首先检查某个hash是否存在该数据库中，存在的hash才可能在torrage.com上下载得到。</p>

<h3>种子缓存</h3>

<p>hash_reader可以通过配置，将下载得到的种子保存在本地文件系统或数据库中。这可以建立自己的种子缓存，但保存在数据库中会对数据库造成压力，尤其在当前测试服务器硬件环境下；而保存为本地文件，又特别占用硬盘空间。</p>

<h3>基于BT协议的种子下载</h3>

<p>通过http从种子缓存里取种子文件，可能会没有直接从P2P网络里取更实时。目前还没来得及查看这些种子缓存网站的实现原理。但是通过BT协议获取种子会有点麻烦，因为dht_crawler是根据<code>get_peer</code>请求索引资源的，所以如果要通过BT协议取种子，那么这里还得去DHT网络里查询该种子，这个查询过程可能会较长，相比之下会没有http下载快。而如果通过<code>announce_peer</code>来索引新资源的话，其索引速度会大大降低，因为<code>announce_peer</code>请求比<code>get_peer</code>请求少很多，几乎10倍。</p>

<p>所以，这里的方案可能会结合两者，新开一个服务，建立自己的种子缓存。</p>

<h3>中文分词</h3>

<p>mongodb的全文索引是不支持中文的。我在之前提到，为了支持搜索中文，我将字符串拆成了若干子串。这样的后果就是字符串索引会稍稍偏大，而且目前这一块的代码还特别简单，会将很多非文字字符也算在内。后来我加了个中文分词库，使用的是rmmseg-cpp。我将其C++部分抽离出来编译成erlang nif，这可以在我的github上找到。</p>

<p>但是这个库拆分中文句子依赖于词库，而这个词库不太新，dhtcrawler爬到的大部分资源类型你们也懂，那些词汇拆出来的比率不太高，这会导致搜索出来的结果没你想的那么直白。当然更新词库应该是可以解决这个问题的，目前还没有时间顾这一块。</p>

<h2>总结</h2>

<p>一个老外对我说过，&#8221;i have 2 children to feed, so i will not do this only for fun&#8221;。</p>

<p>你的大部分编程知识来源于网络，所以稍稍回馈一下不会让你丢了饭碗。</p>

<p>我很穷，如果你能让我收获金钱和编程成就，还不会嫌我穿得太邋遢，that&#8217;s really kind of you。</p>

<p class="post-footer">
            原文地址：
            <a href="http://codemacro.com/2013/07/02/dhtcrawler2/">http://codemacro.com/2013/07/02/dhtcrawler2/</a><br />
            written by <a href="http://codemacro.com">Kevin Lynx</a>
            &nbsp;posted at <a href="http://codemacro.com">http://codemacro.com</a>
            </p>

</div><img src ="http://www.cppblog.com/kevinlynx/aggbug/201994.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2013-07-20 16:37 <a href="http://www.cppblog.com/kevinlynx/archive/2013/07/20/201994.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>使用erlang实现P2P磁力搜索-实现</title><link>http://www.cppblog.com/kevinlynx/archive/2013/06/20/201179.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Thu, 20 Jun 2013 12:40:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2013/06/20/201179.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/201179.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2013/06/20/201179.html#Feedback</comments><slash:comments>1</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/201179.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/201179.html</trackback:ping><description><![CDATA[<div class="entry-content">
<p>接<a href="http://codemacro.com/2013/06/20/magnet-search/">上篇</a>，本篇谈谈一些实现细节。</p>

<p>这个爬虫程序主要的问题在于如何获取P2P网络中分享的资源，获取到资源后索引到数据库中，搜索就是自然而然的事情。</p>

<h2>DHT</h2>

<p>DHT网络本质上是一个用于查询的网络，其用于查询一个资源有哪些计算机正在下载。每个资源都有一个20字节长度的ID用于标示，称为infohash。当一个程序作为DHT节点加入这个网络时，就会有其他节点来向你查询，当你做出回应后，对方就会记录下你。对方还会询问其他节点，当对方开始下载这个infohash对应的资源时，他就会告诉所有曾经询问过的节点，包括你。这个时候就可以确定，这个infohash对应的资源在这个网络中是有效的。</p>

<p>关于这个网络的工作原理，参看：<a href="http://codemacro.com/2013/05/19/crawl-dht/">P2P中DHT网络爬虫</a>以及<a href="http://xiaoxia.org/2013/05/11/magnet-search-engine/">写了个磁力搜索的网页</a>。</p>

<p>获取到infohash后能做什么？关键点在于，我们现在使用的磁力链接(magnet url)，是和infohash对应起来的。也就是拿到infohash，就等于拿到一个磁力链接。但是这个爬虫还需要建立资源的信息，这些信息来源于种子文件。种子文件其实也是对应到一个资源，种子文件包含资源名、描述、文件列表、文件大小等信息。获取到infohash时，其实也获取到了对应的计算机地址，我们可以在这些计算机上下载到对应的种子文件。</p>

<!-- more -->


<p>但是我为了简单，在获取到infohash后，从一些提供映射磁力链到种子文件服务的网站上直接下载了对应的种子。dhtcrawler里使用了以下网站：</p>

<pre><code>http://torrage.com
https://zoink.it
http://bt.box.n0808.com
</code></pre>

<p>使用这些网站时，需提供磁力哈希（infohash可直接转换），构建特定的URL，发出HTTP请求即可。</p>

<div class="highlight">
<pre><code class="erlang">   <span class="nv">U1</span> <span class="o">=</span> <span class="s">"http://torrage.com/torrent/"</span> <span class="o">++</span> <span class="nv">MagHash</span> <span class="o">++</span> <span class="s">".torrent"</span><span class="p">,</span>
    <span class="nv">U2</span> <span class="o">=</span> <span class="s">"https://zoink.it/torrent/"</span> <span class="o">++</span> <span class="nv">MagHash</span> <span class="o">++</span> <span class="s">".torrent"</span><span class="p">,</span>
    <span class="nv">U3</span> <span class="o">=</span> <span class="n">format_btbox_url</span><span class="p">(</span><span class="nv">MagHash</span><span class="p">),</span>

<span class="nf">format_btbox_url</span><span class="p">(</span><span class="nv">MagHash</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">H</span> <span class="o">=</span> <span class="nn">lists</span><span class="p">:</span><span class="nf">sublist</span><span class="p">(</span><span class="nv">MagHash</span><span class="p">,</span> <span class="mi">2</span><span class="p">),</span>
    <span class="nv">T</span> <span class="o">=</span> <span class="nn">lists</span><span class="p">:</span><span class="nf">nthtail</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span> <span class="nv">MagHash</span><span class="p">),</span>
    <span class="s">"http://bt.box.n0808.com/"</span> <span class="o">++</span> <span class="nv">H</span> <span class="o">++</span> <span class="s">"/"</span> <span class="o">++</span> <span class="nv">T</span> <span class="o">++</span> <span class="s">"/"</span> <span class="o">++</span> <span class="nv">MagHash</span> <span class="o">++</span> <span class="s">".torrent"</span><span class="p">.</span>
</code></pre>
</div>


<p>但是，以一个节点的身份加入DHT网络，是无法获取大量查询的。在DHT网络中，每个节点都有一个ID。每个节点在查询信息时，仅询问离信息较近的节点。这里的信息除了infohash外还包含节点，即节点询问一个节点，这个节点在哪里。DHT的典型实现中（Kademlia），使用两个ID的xor操作来确定距离。既然距离的计算是基于ID的，为了尽可能获取整个DHT网络交换的信息，爬虫程序就可以建立尽可能多的DHT节点，让这些节点的ID均匀地分布在ID取值区间内，以这样的方式加入网络。</p>

<p>在dhtcrawler中，我使用以下方式产生了N个大致均匀分布的ID：</p>

<div class="highlight">
<pre><code class="erlang"><span class="nf">create_discrete_ids</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="p">[</span><span class="nn">dht_id</span><span class="p">:</span><span class="nf">random</span><span class="p">()];</span>
<span class="nf">create_discrete_ids</span><span class="p">(</span><span class="nv">Count</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">Max</span> <span class="o">=</span> <span class="nn">dht_id</span><span class="p">:</span><span class="nf">max</span><span class="p">(),</span>
    <span class="nv">Piece</span> <span class="o">=</span> <span class="nv">Max</span> <span class="ow">div</span> <span class="nv">Count</span><span class="p">,</span>
    <span class="p">[</span><span class="nn">random</span><span class="p">:</span><span class="nf">uniform</span><span class="p">(</span><span class="nv">Piece</span><span class="p">)</span> <span class="o">+</span> <span class="nv">Index</span> <span class="o">*</span> <span class="nv">Piece</span> <span class="p">||</span> <span class="nv">Index</span> <span class="o">&lt;-</span> <span class="nn">lists</span><span class="p">:</span><span class="nf">seq</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nv">Count</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)].</span>
</code></pre>
</div>


<p>除了尽可能多地往DHT网络里部署节点之外，对单个节点而言，也有些注意事项。例如应尽可能快地将自己告诉尽可能多的节点，这可以在启动时进行大量的随机infohash的查询。随着查询过程的深入，该节点会与更多的节点打交道。因为DHT网络里的节点实际上是不稳定的，它今天在线，明天后天可能不在线，所以计算你的ID固定，哪些节点与你较近，本身就是个相对概念。节点在程序退出时，也最好将自己的路由信息（与自己交互的节点列表）保存起来，这样下次启动时就可以更快地加入网络。</p>

<p>在dhtcrawler的实现中，每个节点每个一定时间，都会向网络中随机查询一个infohash，这个infohash是随机产生的。其查询目的不在于infohash，而在于告诉更多的节点，以及在其他节点上保持自己的活跃。</p>

<div class="highlight">
<pre><code class="erlang"><span class="nf">handle_event</span><span class="p">(</span><span class="n">startup</span><span class="p">,</span> <span class="p">{</span><span class="nv">MyID</span><span class="p">})</span> <span class="o">-&gt;</span>
    <span class="nn">timer</span><span class="p">:</span><span class="nf">apply_interval</span><span class="p">(</span><span class="o">?</span><span class="nv">QUERY_INTERVAL</span><span class="p">,</span> <span class="o">?</span><span class="nv">MODULE</span><span class="p">,</span> <span class="n">start_tell_more_nodes</span><span class="p">,</span> <span class="p">[</span><span class="nv">MyID</span><span class="p">]).</span>

<span class="nf">start_tell_more_nodes</span><span class="p">(</span><span class="nv">MyID</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nb">spawn</span><span class="p">(</span><span class="o">?</span><span class="nv">MODULE</span><span class="p">,</span> <span class="n">tell_more_nodes</span><span class="p">,</span> <span class="p">[</span><span class="nv">MyID</span><span class="p">]).</span>

<span class="nf">tell_more_nodes</span><span class="p">(</span><span class="nv">MyID</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="p">[</span><span class="nn">search</span><span class="p">:</span><span class="nf">get_peers</span><span class="p">(</span><span class="nv">MyID</span><span class="p">,</span> <span class="nn">dht_id</span><span class="p">:</span><span class="nf">random</span><span class="p">())</span> <span class="p">||</span> <span class="p">_</span> <span class="o">&lt;-</span> <span class="nn">lists</span><span class="p">:</span><span class="nf">seq</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">)].</span>
</code></pre>
</div>


<p>DHT节点的完整实现是比较繁琐的，涉及到查询以及繁杂的各种对象的超时（节点、桶、infohash），而超时的处理并不是粗暴地做删除操作。因为本身是基于UDP协议，你得对这些超时对象做进一步的查询才能正确地进一步做其他事情。而搜索也是个繁杂的事情，递归地查询节点，感觉上，你不一定离目标越来越近，由于被查询节点的不确定性（无法确定对方是否在玩弄你，或者本身对方就是个傻逼），你很可能接下来要查询的节点反而离目标变远了。</p>

<p>在我第一次的DHT实现中，我使用了类似transmission里DHT实现的方法，不断无脑递归，当搜索有太久时间没得到响应后终止搜索。第二次实现时，我就使用了etorrent里的实现。这个搜索更聪明，它记录搜索过的节点，并且检查是否离目标越来越远。当远离目标时，就认为搜索是不太有效的，不太有效的搜索尝试几次就可以放弃。</p>

<p>实际上，爬虫的实现并不需要完整地实现DHT节点的正常功能。<strong>爬虫作为一个DHT节点的唯一动机仅是获取网络里其他节点的查询</strong>。而要完成这个功能，你只需要装得像个正常人就行。这里不需要保存infohash对应的peer列表，面临每一次查询，你随便回复几个节点地址就可以。但是这里有个责任问题，如果整个DHT网络有2000个节点，而你这个爬虫就有1000个节点，那么你的随意回复，就可能导致对方根本找不到正确的信息，这样你依然得不到有效的资源。（可以利用这一点破坏DHT网络）</p>

<p>DHT的实现没有使用第三方库。</p>

<h2>种子</h2>

<p>种子文件的格式同DHT网络消息格式一样，使用一种称为bencode的文本格式来编码。种子文件分为两类：单个文件和多个文件。</p>

<p>文件的信息无非就是文件名、大小。文件名可能包含utf8编码的名字，为了后面处理的方便，dhtcrawler都会优先使用utf8编码。</p>

<div class="highlight">
<pre><code class="erlang">   <span class="p">{</span><span class="n">ok</span><span class="p">,</span> <span class="p">{</span><span class="n">dict</span><span class="p">,</span> <span class="nv">Info</span><span class="p">}}</span> <span class="o">=</span> <span class="nn">dict</span><span class="p">:</span><span class="nf">find</span><span class="p">(</span><span class="o">&lt;&lt;</span><span class="s">"info"</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="nv">TD</span><span class="p">),</span>
    <span class="k">case</span> <span class="n">type</span><span class="p">(</span><span class="nv">Info</span><span class="p">)</span> <span class="k">of</span>
        <span class="n">single</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="n">single</span><span class="p">,</span> <span class="n">parse_single</span><span class="p">(</span><span class="nv">Info</span><span class="p">)};</span>
        <span class="n">multi</span> <span class="o">-&gt;</span> <span class="p">{</span><span class="n">multi</span><span class="p">,</span> <span class="n">parse_multi</span><span class="p">(</span><span class="nv">Info</span><span class="p">)}</span>
    <span class="k">end</span><span class="p">.</span>
<span class="nf">parse_single</span><span class="p">(</span><span class="nv">Info</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">Name</span> <span class="o">=</span> <span class="n">read_string</span><span class="p">(</span><span class="s">"name"</span><span class="p">,</span> <span class="nv">Info</span><span class="p">),</span>
    <span class="p">{</span><span class="n">ok</span><span class="p">,</span> <span class="nv">Length</span><span class="p">}</span> <span class="o">=</span> <span class="nn">dict</span><span class="p">:</span><span class="nf">find</span><span class="p">(</span><span class="o">&lt;&lt;</span><span class="s">"length"</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="nv">Info</span><span class="p">),</span>
    <span class="p">{</span><span class="nv">Name</span><span class="p">,</span> <span class="nv">Length</span><span class="p">}.</span>

<span class="nf">parse_multi</span><span class="p">(</span><span class="nv">Info</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">Root</span> <span class="o">=</span> <span class="n">read_string</span><span class="p">(</span><span class="s">"name"</span><span class="p">,</span> <span class="nv">Info</span><span class="p">),</span>
    <span class="p">{</span><span class="n">ok</span><span class="p">,</span> <span class="p">{</span><span class="n">list</span><span class="p">,</span> <span class="nv">Files</span><span class="p">}}</span> <span class="o">=</span> <span class="nn">dict</span><span class="p">:</span><span class="nf">find</span><span class="p">(</span><span class="o">&lt;&lt;</span><span class="s">"files"</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="nv">Info</span><span class="p">),</span>
    <span class="nv">FileInfo</span> <span class="o">=</span> <span class="p">[</span><span class="n">parse_file_item</span><span class="p">(</span><span class="nv">Item</span><span class="p">)</span> <span class="p">||</span> <span class="p">{</span><span class="n">dict</span><span class="p">,</span> <span class="nv">Item</span><span class="p">}</span> <span class="o">&lt;-</span> <span class="nv">Files</span><span class="p">],</span>
    <span class="p">{</span><span class="nv">Root</span><span class="p">,</span> <span class="nv">FileInfo</span><span class="p">}.</span>
</code></pre>
</div>


<h2>数据库</h2>

<p>我最开始在选用数据库时，为了不使用第三方库，打算使用erlang自带的mnesia。但是因为涉及到字符串匹配搜索，mnesia的查询语句在我看来太不友好，在经过一些资料查阅后就直接放弃了。</p>

<p>然后我打算使用couchdb，因为它是erlang写的，而我正在用erlang写程序。第一次接触非关系型数据库，发现NoSQL数据库使用起来比SQL类的简单多了。但是在erlang里要使用couchdb实在太折腾了。我使用的客户端库是couchbeam。</p>

<p>因为couchdb暴露的API都是基于HTTP协议的，其数据格式使用了json，所以couchbeam实际上就是对各种HTTP请求、回应和json的包装。但是它竟然使用了ibrowse这个第三方HTTP客户端库，而不是erlang自带的。ibrowse又使用了jiffy这个解析json的库。这个库更惨烈的是它的解析工作都是交给C语言写的动态库来完成，我还得编译那个C库。</p>

<p>couchdb看起来不支持字符串查询，我得自己创建一个view，这个view里我通过翻阅了一些资料写了一个将每个doc的name拆分成若干次查询结果的map。这个map在处理每一次查询时，我都得动态更新之。couchdb是不支持局部更新的，这还不算大问题。然后很高兴，终于支持字符串查询了。这里的字符串查询都是基于字符串的子串查询。但是问题在于，太慢了。每一次在WEB端的查询，都直接导致erlang进程的call超时。</p>

<p>要让couchdb支持字符串查询，要快速，当然是有解决方案的。但是这个时候我已经没有心思继续折腾，任何一个库、程序如果接口设计得如此不方便，那就可以考虑换一个其他的。</p>

<p>我选择了mongodb。同样的基于文档的数据库。2.4版本还支持全文搜索。什么是全文搜索呢，这是一种基于单词的全文搜索方式。<code>hello world</code>我可以搜索<code>hello</code>，基于单词。mongodb会自动拆词。更关键更让人爽的是，要开启这个功能非常简单：设置启动参数、建立索引。没了。mongodb的erlang客户端库mongodb-erlang也只依赖一个bson-erlang库。然后我又埋头苦干，几个小时候我的这个爬虫程序就可以在浏览器端搜索关键字了。</p>

<p>后来我发现，mongodb的全文搜索是不支持中文的。因为它还不知道中文该怎么拆词。恰好我有个同事做过中文拆词的研究，看起来涉及到很复杂的算法。直到这个时候，我他妈才醒悟，我为什么需要基于单词的搜索。我们大部分的搜索其实都是基于子字符串的搜索。</p>

<p>于是，我将种子文件的名字拆分成了若干个子字符串，将这些子字符串以数组的形式作为种子文档的一个键值存储，而我依然还可以使用全文索引，因为全文索引会将整个字符串作为单词比较。实际上，基于一般的查询方式也是可以的。当然，索引还是得建立。</p>

<p>使用mongodb时唯一让我很不爽的是mongodb-erlang这个客户端库的文档太欠缺。这还不算大问题，因为看看源码参数还是可以大概猜到用法。真正悲剧的是mongodb的有些查询功能它是不支持的。例如通过cursor来排序来限制数量。在cursor模块并没有对应的mongodb接口。最终我只好通过以下方式查询，我不明白batchsize，但它可以工作：</p>

<div class="highlight">
<pre><code class="erlang"><span class="nf">search_announce_top</span><span class="p">(</span><span class="nv">Conn</span><span class="p">,</span> <span class="nv">Count</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">Sel</span> <span class="o">=</span> <span class="p">{</span><span class="n">'$query'</span><span class="p">,</span> <span class="p">{},</span> <span class="n">'$orderby'</span><span class="p">,</span> <span class="p">{</span><span class="n">announce</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">}},</span>
    <span class="nv">List</span> <span class="o">=</span> <span class="n">mongo_do</span><span class="p">(</span><span class="nv">Conn</span><span class="p">,</span> <span class="k">fun</span><span class="p">()</span> <span class="o">-&gt;</span>
        <span class="nv">Cursor</span> <span class="o">=</span> <span class="nn">mongo</span><span class="p">:</span><span class="nf">find</span><span class="p">(</span><span class="o">?</span><span class="nv">COLLNAME</span><span class="p">,</span> <span class="nv">Sel</span><span class="p">,</span> <span class="p">[],</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">Count</span><span class="p">),</span> 
        <span class="nn">mongo_cursor</span><span class="p">:</span><span class="nf">rest</span><span class="p">(</span><span class="nv">Cursor</span><span class="p">)</span>
    <span class="k">end</span><span class="p">),</span>
    <span class="p">[</span><span class="n">decode_torrent_item</span><span class="p">(</span><span class="nv">Item</span><span class="p">)</span> <span class="p">||</span> <span class="nv">Item</span> <span class="o">&lt;-</span> <span class="nv">List</span><span class="p">].</span>
</code></pre>
</div>


<p>另一个悲剧的是，mongodb-erlang还不支持文档的局部更新，它的update接口直接要求传入整个文档。几经折腾，我可以通过runCommand来完成：</p>

<div class="highlight">
<pre><code class="erlang"><span class="nf">inc_announce</span><span class="p">(</span><span class="nv">Conn</span><span class="p">,</span> <span class="nv">Hash</span><span class="p">)</span> <span class="k">when</span> <span class="nb">is_list</span><span class="p">(</span><span class="nv">Hash</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">Cmd</span> <span class="o">=</span> <span class="p">{</span><span class="n">findAndModify</span><span class="p">,</span> <span class="o">?</span><span class="nv">COLLNAME</span><span class="p">,</span> <span class="k">query</span><span class="p">,</span> <span class="p">{</span><span class="n">'_id'</span><span class="p">,</span> <span class="nb">list_to_binary</span><span class="p">(</span><span class="nv">Hash</span><span class="p">)},</span> 
        <span class="n">update</span><span class="p">,</span> <span class="p">{</span><span class="n">'$inc'</span><span class="p">,</span> <span class="p">{</span><span class="n">announce</span><span class="p">,</span> <span class="mi">1</span><span class="p">}},</span>
        <span class="n">new</span><span class="p">,</span> <span class="n">true</span><span class="p">},</span>
    <span class="nv">Ret</span> <span class="o">=</span> <span class="n">mongo_do</span><span class="p">(</span><span class="nv">Conn</span><span class="p">,</span> <span class="k">fun</span><span class="p">()</span> <span class="o">-&gt;</span>
        <span class="nn">mongo</span><span class="p">:</span><span class="nf">command</span><span class="p">(</span><span class="nv">Cmd</span><span class="p">)</span>
    <span class="k">end</span><span class="p">).</span>
</code></pre>
</div>


<h2>Unicode</h2>

<p>不知道在哪里我看到过erlang说自己其实是不需要支持unicode的，因为这门语言本身是通过list来模拟字符串。对于unicode而言，对应的list保存的本身就是整数值。但是为了方便处理，erlang还是提供了一些unicode操作的接口。</p>

<p>因为我需要将种子的名字按字拆分，对于<code>a中文</code>这样的字符串而言，我需要拆分成以下结果：</p>

<pre><code>a
a中
a中文
中
中文
文
</code></pre>

<p>那么，在erlang中当我获取到一个字符串list时，我就需要知道哪几个整数合起来实际上对应着一个汉字。erlang里unicode模块里有几个函数可以将unicode字符串list对应的整数合起来，例如：<code>[111, 222, 333]</code>可能表示的是一个汉字，将其转换以下可得到<code>[111222333]</code>这样的形式。</p>

<div class="highlight">
<pre><code class="erlang"><span class="nf">split</span><span class="p">(</span><span class="nv">Str</span><span class="p">)</span> <span class="k">when</span> <span class="nb">is_list</span><span class="p">(</span><span class="nv">Str</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="nv">B</span> <span class="o">=</span> <span class="nb">list_to_binary</span><span class="p">(</span><span class="nv">Str</span><span class="p">),</span> <span class="c">% 必须转换为binary</span>
    <span class="k">case</span> <span class="nn">unicode</span><span class="p">:</span><span class="nf">characters_to_list</span><span class="p">(</span><span class="nv">B</span><span class="p">)</span> <span class="k">of</span>
        <span class="p">{</span><span class="n">error</span><span class="p">,</span> <span class="nv">L</span><span class="p">,</span> <span class="nv">D</span><span class="p">}</span> <span class="o">-&gt;</span>
            <span class="p">{</span><span class="n">error</span><span class="p">,</span> <span class="nv">L</span><span class="p">,</span> <span class="nv">D</span><span class="p">};</span>
        <span class="p">{</span><span class="n">incomplete</span><span class="p">,</span> <span class="nv">L</span><span class="p">,</span> <span class="nv">D</span><span class="p">}</span> <span class="o">-&gt;</span>
            <span class="p">{</span><span class="n">incomplete</span><span class="p">,</span> <span class="nv">L</span><span class="p">,</span> <span class="nv">D</span><span class="p">};</span>
        <span class="nv">UL</span> <span class="o">-&gt;</span>
        <span class="p">{</span><span class="n">ok</span><span class="p">,</span> <span class="n">subsplit</span><span class="p">(</span><span class="nv">UL</span><span class="p">)}</span>
    <span class="k">end</span><span class="p">.</span>

<span class="nf">subsplit</span><span class="p">([])</span> <span class="o">-&gt;</span>
    <span class="p">[];</span>

<span class="nf">subsplit</span><span class="p">(</span><span class="nv">L</span><span class="p">)</span> <span class="o">-&gt;</span>
    <span class="p">[_|</span><span class="nv">R</span><span class="p">]</span> <span class="o">=</span> <span class="nv">L</span><span class="p">,</span>
    <span class="p">{</span><span class="nv">PreL</span><span class="p">,</span> <span class="p">_}</span> <span class="o">=</span> <span class="nn">lists</span><span class="p">:</span><span class="nf">splitwith</span><span class="p">(</span><span class="k">fun</span><span class="p">(</span><span class="nv">Ch</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="ow">not</span> <span class="n">is_spliter</span><span class="p">(</span><span class="nv">Ch</span><span class="p">)</span> <span class="k">end</span><span class="p">,</span> <span class="nv">L</span><span class="p">),</span>
    <span class="p">[</span><span class="nn">unicode</span><span class="p">:</span><span class="nf">characters_to_binary</span><span class="p">(</span><span class="nn">lists</span><span class="p">:</span><span class="nf">sublist</span><span class="p">(</span><span class="nv">PreL</span><span class="p">,</span> <span class="nv">Len</span><span class="p">))</span> 
        <span class="p">||</span> <span class="nv">Len</span> <span class="o">&lt;-</span> <span class="nn">lists</span><span class="p">:</span><span class="nf">seq</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="nb">length</span><span class="p">(</span><span class="nv">PreL</span><span class="p">))]</span> <span class="o">++</span> <span class="n">subsplit</span><span class="p">(</span><span class="nv">R</span><span class="p">).</span>
</code></pre>
</div>


<p>除了这里的拆字之外，URL的编码、数据库的存储都还好，没遇到问题。</p>

<p><strong>注意</strong>，以上针对数据库本身的吐槽，完全基于我不熟悉该数据库的情况下，不建议作为你工具选择的参考。</p>

<h2>erlang的稳定性</h2>

<p>都说可以用erlang来编写高容错的服务器程序。看看它的supervisor，监视子进程，自动重启子进程。天生的容错功能，就算你宕个几次，单个进程自动重启，整个程序看起来还稳健地在运行，多牛逼啊。再看看erlang的进程，轻量级的语言特性，就像OOP语言里的一个对象一样轻量。如果说使用OOP语言写程序得think in object，那用erlang你就得think in process，多牛逼多骇人啊。</p>

<p>实际上，以我的经验来看，你还得以传统的思维去看待erlang的进程。一些多线程程序里的问题，在erlang的进程环境中依然存在，例如死锁。</p>

<p>在erlang中，对于一些异步操作，你可以通过进程间的交互将这个操作包装成同步接口，例如ping的实现，可以等到对方回应之后再返回。被阻塞的进程反正很轻量，其包含的逻辑很单一。这不但是一种良好的包装，甚至可以说是一种erlang-style。但这很容易带来死锁。在最开始的时候我没有注意这个问题，当爬虫节点数上升的时候，网络数据复杂的时候，似乎就出现了死锁型宕机（进程互相等待太久，直接timeout）。</p>

<p>另一个容易在多进程环境下出现的问题就是消息依赖的上下文改变问题。当投递一个消息到某个进程，到这个消息被处理之前，这段时间这个消息关联的逻辑运算所依赖的上下文环境改变了，例如某个ets元素不见了，在处理这个消息时，你还得以多线程编程的思维来编写代码。</p>

<p>至于supervisor，这玩意你得端正态度。它不是用来包容你的傻逼错误的。当你写下傻逼代码导致进程频繁崩溃的时候，supervisor屁用没有。supervisor的唯一作用，仅仅是在一个确实本身可靠的系统，确实人品问题万分之一崩溃了，重启它。毕竟，一个重启频率的推荐值，是一个小时4次。</p>

<p class="post-footer">
            原文地址：
            <a href="http://codemacro.com/2013/06/21/magnet-search-impl/">http://codemacro.com/2013/06/21/magnet-search-impl/</a><br />
            written by <a href="http://codemacro.com">Kevin Lynx</a>
            &nbsp;posted at <a href="http://codemacro.com">http://codemacro.com</a>
            </p>

</div><img src ="http://www.cppblog.com/kevinlynx/aggbug/201179.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2013-06-20 20:40 <a href="http://www.cppblog.com/kevinlynx/archive/2013/06/20/201179.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>使用erlang实现P2P磁力搜索(开源)</title><link>http://www.cppblog.com/kevinlynx/archive/2013/06/20/201175.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Thu, 20 Jun 2013 06:44:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2013/06/20/201175.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/201175.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2013/06/20/201175.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/201175.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/201175.html</trackback:ping><description><![CDATA[<div class="entry-content">
<p>接上回对<a href="http://codemacro.com/2013/05/19/crawl-dht/">DHT网络的研究</a>，我用erlang克隆了一个<a href="http://bt.shousibaocai.com/">磁力搜索引擎</a>。我这个实现包含了完整的功能，DHT网络的加入、infohash的接收、种子的获取、资源信息的索引、搜索。</p>

<p>如下图：</p>

<p><img src="https://raw.github.com/kevinlynx/dhtcrawler/master/screenshot.png" alt="screenshot" /></p>

<!-- more -->


<p>在我的笔记本上，我开启了100个DHT节点，大致均匀地分布在DHT网络里，资源索引速度大概在1小时一万个左右（包含重复资源）。</p>

<p>这个程序包含三大部分：</p>

<ul>
<li>DHT实现，kdht，<a href="https://github.com/kevinlynx/kdht">https://github.com/kevinlynx/kdht</a>
</li>
<li>基于该DHT实现的搜索引擎，dhtcrawler，<a href="https://github.com/kevinlynx/dhtcrawler">https://github.com/kevinlynx/dhtcrawler</a>，该项目包含爬虫部分和一个简单的WEB端</li>
</ul>
<p>这两个项目总共包含大概2500行的erlang代码。其中，DHT实现部分将DHT网络的加入包装成一个库，爬虫部分在搜索种子时，暂时没有使用P2P里的种子下载方式，而是使用现成的磁力链转种子的网站服务，这样我只需要使用erlang自带的HTTP客户端就可以获取种子信息。爬虫在获取到种子信息后，将数据存储到mongodb里。WEB端我为了尽量少用第三方库，我只好使用erlang自带的HTTP服务器，因此网页内容的创建没有模板系统可用，只好通过字符串构建，编写起来不太方便。</p>

<h2>使用</h2>

<p>整个程序依赖了两个库：bson-erlang和mongodb-erlang，但下载依赖库的事都可以通过rebar解决，项目文件里我已经包含了rebar的执行程序。我仅在Windows7上测试过，但理论上在所有erlang支持的系统上都可以。</p>

<ul>
<li>下载安装<a href="http://www.mongodb.org/downloads">mongodb</a>
</li>
<li>
<p>进入mongodb bin目录启动mongodb，数据库目录保存在db下，需手动建立该目录</p>

<pre><code>  mongod --dbpath db --setParameter textSearchEnabled=true
</code></pre>
</li>
<li><p>下载<a href="http://www.erlang.org/download.html">erlang</a>，我使用的是R16B版本</p></li>
<li>
<p>下载dhtcrawler，不需要单独下载kdht，待会下载依赖项的时候会自动下载</p>

<pre><code>  git clone git@github.com:kevinlynx/dhtcrawler.git
</code></pre>
</li>
<li>
<p>cmd进入dhtcrawler目录，下载依赖项前需保证环境变量里有git，例如<code>D:\Program Files (x86)\Git\cmd</code>，需注意不要将bash的目录加入进来，使用以下命令下载依赖项</p>

<pre><code>  rebar get-deps
</code></pre>
</li>
<li>
<p>编译</p>

<pre><code>  rebar compile
</code></pre>
</li>
<li>
<p>在dhtcrawler目录下，启动erlang</p>

<pre><code>  erl -pa ebin
</code></pre>
</li>
<li>
<p>在erlang shell里运行爬虫，<strong>erlang语句以点号(.)作为结束</strong></p>

<pre><code>  crawler_app:start().
</code></pre>
</li>
<li>
<p>erlang shell里运行HTTP服务器</p>

<pre><code>  crawler_http:start().
</code></pre>
</li>
<li><p>浏览器里输入<code>localhost:8000/index.html</code>，这个时候还没有索引到资源，建议监视网络流量以观察爬虫程序是否正确工作</p></li>
</ul>
<p>爬虫程序启动时会读取<code>priv/dhtcrawler.config</code>配置文件，该文件里配置了DHT节点的UDP监听端口、节点数量、数据库地址等，可自行配置。</p>

<p>接下来我会谈谈各部分的实现方法。</p>

<p class="post-footer">
            原文地址：
            <a href="http://codemacro.com/2013/06/20/magnet-search/">http://codemacro.com/2013/06/20/magnet-search/</a><br />
            written by <a href="http://codemacro.com">Kevin Lynx</a>
            &nbsp;posted at <a href="http://codemacro.com">http://codemacro.com</a>
            </p>

</div><img src ="http://www.cppblog.com/kevinlynx/aggbug/201175.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2013-06-20 14:44 <a href="http://www.cppblog.com/kevinlynx/archive/2013/06/20/201175.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>Erlang使用感受</title><link>http://www.cppblog.com/kevinlynx/archive/2013/05/09/200138.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Thu, 09 May 2013 13:24:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2013/05/09/200138.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/200138.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2013/05/09/200138.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/200138.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/200138.html</trackback:ping><description><![CDATA[<div class="entry-content">
<p>用erlang也算写了些代码了，主要包括<a href="http://codemacro.com/2013/04/11/rabbitmq-erlang/">使用RabbitMQ的练习</a>，以及最近写的<a href="https://github.com/kevinlynx/erlang-tcpserver">kl_tserver</a>和<a href="https://github.com/kevinlynx/icerl">icerl</a>。其中icerl是一个实现了<a href="http://www.zeroc.com/">Ice</a>的erlang库。</p>

<p>erlang的书较少，我主要读过&lt;Programming Erlang&gt;和&lt;Erlang/OTP in Action&gt;。其实erlang本身就语言来说的话比较简单，同ruby一样，类似这种本身目标是应用于实际软件项目的语言都比较简单，对应的语法书很快可以翻完。</p>

<p>这里我仅谈谈自己在编写erlang代码过程中的一些感受。</p>

<h2>语法</h2>

<p>erlang语法很简单，接触过函数式语言的程序员上手会很快。它没有类似common lisp里宏这种较复杂的语言特性。其语法元素很紧凑，不存在一些用处不大的特性。在这之前，我学习过ruby和common lisp。ruby代码写的比common lisp多。但是在学习erlang的过程中我的脑海里却不断出现common lisp里的语法特性。这大概是因为common lisp的语法相对ruby来说，更接近erlang。</p>

<h3>编程模式</h3>

<p>erlang不是一个面向对象的语言，它也不同common lisp提供多种编程模式。它的代码就是靠一个个函数组织出来的。面向对象语言在语法上有一点让我很爽的是，其函数调用更自然。erlang的接口调用就像C语言里接口的调用一样：</p>

<pre><code>func(Obj, args)
Obj-&gt;func(args)
</code></pre>

<p>即需要在函数第一个参数传递操作对象。但是面向对象语言也会带来一些语法的复杂性。如果一门语言可以用很少的语法元素表达很多信息，那么我觉得这门语言就是门优秀的语言。</p>

<h3>表达式/语句</h3>

<p>erlang里没有语句，全部是表达式，意思是所有语法元素都是有返回值的。这实在太好了，全世界都有返回值可以让代码写起来简单多了：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">Flag</span> <span class="o">=</span> <span class="k">case</span> <span class="n">func</span><span class="p">()</span> <span class="k">of</span> <span class="mi">1</span> <span class="o">-&gt;</span> <span class="n">true</span><span class="p">;</span> <span class="mi">0</span> <span class="o">-&gt;</span> <span class="n">false</span> <span class="k">end</span><span class="p">,</span> 
</code></pre>
</div>




<!-- more -->


<h3>命名</h3>

<p>我之所以不想写一行python代码的很大一部分原因在于这门语言居然要求我必须使用代码缩进来编程，真是不敢相信。erlang里虽然没有此规定，却也有不同的语法元素有大小写的限定。变量首字母必须大写，atom必须以小写字母开头，更霸气的是模块命名必须和文件名相同。</p>

<h3>变量</h3>

<p>erlang里的变量是不可更改的。实际上给一个变量赋值，严格来说应该叫<code>bound</code>，即绑定。这个特性完全就是函数式语言里的特性。其带来的好处就像函数式语言宣扬的一样，这会使得代码没有副作用(side effect)。因为程序里的所有函数不论怎样调用，其程序状态都不会改变，因为变量无法被改变。</p>

<p>变量不可更改，直接意味着全局变量没有存在的意义，也就意味着不论你的系统是多么复杂地被构建出来，当系统崩溃时，其崩溃所在位置的上下文就足够找到问题。</p>

<p>但是变量不可改变也会带来一些代码编写上的不便。我想这大概是编程思维的转变问题。erlang的语法特性会强迫人编写非常短小的函数，你大概不愿意看到你的函数实现里出现Var1/Var2/Var3这样的变量，而实际上这样的命名在命令式语言里其实指的是同一个变量，只不过其值不同而已。</p>

<p>但是我们的程序总是应该有状态的。在erlang里我们通过不断创建新的变量来存储这个状态。我们需要通过将这个状态随着我们的程序流程不断地通过函数参数和返回值传递下去。</p>

<h3>atom</h3>

<p>atom这个语法特性本身没问题，它就同lisp里的atom一样，没什么意义，就是一个名字。它主要用在增加代码的可读性上。但是这个atom带来的好处，直接导致erlang不去内置诸如true/false这种关键字。erlang使用true/false这两个atom来作为boolean operator的返回值。但erlang里严格来说是没有布尔类型的。这其实没什么，糟糕的是，对于一些较常见的函数返回值，例如true/false，erlang程序员之间就得做约定。要表示一个函数执行失败了，我可以返回false、null、failed、error、nil，甚至what_the_fuck，这一度让我迷惘。</p>

<h3>list/tuple</h3>

<p>erlang里的list当然没有lisp里的list牛逼，别人整个世界就是由list构成的。在一段时间里，我一直以为list里只能保存相同类型的元素，而tuple才是用于保存不同类型元素的容器。直到有一天我发现tuple的操作不能满足我的需求了，我才发现list居然是可以保存不同类型的。</p>

<p>list相对于tuple而言，更厉害的地方就在于头匹配，意思是可以通过匹配来拆分list的头和剩余部分。</p>

<h3>匹配(match)</h3>

<p>erlang的匹配机制是个好东西。这个东西贯穿了整个语言。在我理解看来，匹配机制减少了很多判断代码。它试图用一个期望的类型去匹配另一个东西，如果这个东西出了错，它就无法完成这个匹配。无法完成匹配就导致程序断掉。</p>

<p>匹配还有个方便的地方在于可以很方便地取出record里的成员，或者tuple和list的某个部分，这其实增强了其他语法元素的能力。</p>

<h3>循环</h3>

<p>erlang里没有循环语法元素，这真是太好了。函数式语言里为什么要有循环语法呢？common lisp干毛要加上那些复杂的循环（宏），每次我遇到需要写循环的场景时，我都诚惶诚恐，最后还是用递归来解决。</p>

<p>同样，在erlang里我们也是用函数递归来解决循环问题。甚至，我们还有list comprehension。当我写C++代码时，我很不情愿用循环去写那些容器遍历代码，幸运的是在C++11里通过lambda和STL里那些算法我终于不用再写这样的循环代码了。</p>

<h3>if/case/guard</h3>

<p>erlang里有条件判定语法if，甚至还有类似C语言里的switch…case。这个我一时半会还不敢评价，好像haskell里也保留了if。erlang里同haskell一样有guard的概念，这其实是一种变相的条件判断，只不过其使用场景不一样。</p>

<h3>进程</h3>

<p>并发性支持属于erlang的最大亮点。erlang里的进程概念非常简单，基于消息机制，程序员从来不需要担心同步问题。每个进程都有一个mailbox，用于缓存发送到此进程的消息。erlang提供内置的语法元素来发送和接收消息。</p>

<p>erlang甚至提供分布式支持，更酷的是你往网络上的其他进程发送消息，其语法和往本地进程发送是一样的。</p>

<h3>模块加载</h3>

<p>如果我写了一个erlang库，该如何在另一个erlang程序里加载这个库？这个问题一度让我迷惘。erlang里貌似有对库打包的功能(.ez?)，按理说应该提供一种整个库加载的方式，然后可以通过手动调用函数或者指定代码依赖项来加载。结果不是这样。</p>

<p>erlang不是按整个库来加载的，因为也没有方式去描述一个库（应该有第三方的）。当我们调用某个模块里的函数时，erlang会自动从某个目录列表里去搜索对应的beam文件。所以，可以通过在启动erlang添加这个模块文件所在目录来实现加载，这还是自动的。当然，也可以在erlang shell里通过函数添加这个目录。</p>

<h2>OTP</h2>

<p>使用erlang来编写程序，最大的优势可能就是其OTP了。OTP基本上就是一些随erlang一起发布的库。这些库中最重要的一个概念是behaviour。behaviour其实就是提供了一种编程框架，应用层提供各种回调函数给这个框架，从而获得一个健壮的并发程序。</p>

<h3>application behaviour</h3>

<p>application behaviour用于组织一个erlang程序，通过一个配置文件，和提供若干回调，就可以让我们编写的erlang程序以一种统一的方式启动。我之前写的都是erlang库，并不需要启动，而是提供给应用层使用，所以也没使用该behaviour。</p>

<h3>gen_server behaviour</h3>

<p>这个behaviour应该是使用频率很高的。它封装了进程使用的细节，本质上也就是将主动收取消息改成了自动收取，收取后再回调给你的模块。</p>

<h3>supervisor behaviour</h3>

<p>这个behaviour看起来很厉害，通过对它进行一些配置，你可以把你的并发程序里的所有进程建立成树状结构。这个结构的牛逼之处在于，当某个进程挂掉之后，通过supervisor可以自动重新启动这个挂掉的进程，当然重启没这么简单，它提供多种重启规则，以让整个系统确实通过重启变成正常状态。这实在太牛逼了，这意味着你的服务器可以7x24小时地运行了，就算有问题你也可以立刻获得一个重写工作的系统。</p>

<h3>热更新</h3>

<p>代码热更新对于一个动态语言而言其实根本算不上什么优点，基本上动态语言都能做到这一点。但是把热更新这个功能加到一个用于开发并发程序的语言里，那就很牛逼了。你再一次可以确保你的服务器7x24小时不停机维护。</p>

<h3>gen_tcp</h3>

<p>最开始我以为erlang将网络部分封装得已经认不出有socket这个概念了。至少，你也得有一个牛逼的网络库吧。结果发现依然还是socket那一套。然后我很失望。直到后来，发现使用一些behaviour，加上调整gen_tcp的一些option，居然可以以很少的代码写出一个维护大量连接的TCP服务器。是啊，erlang天生就是并发的，在传统的网络模型中，我们会觉得使用one-thread-per-connection虽然简单却不是可行的，因为thread是OS资源，太昂贵。但是在erlang里，one-process-per-connection却是再自然不过的事情。你要是写个erlang程序里面却只有一个process你都不好意思告诉别人你写的是erlang。process是高效的（对我们这种二流程序员而言），它就像C++里一个很普通的对象一样。</p>

<p>在使用gen_tcp的过程中我发现一个问题，不管我使用哪一种模型，我竟然找不到一种温柔的关闭方式。我查看了几个tutorial，这些混蛋竟然没有一个人提到如何去正常关闭一个erlang TCP服务器。后来，我没有办法，只好使用API强制关闭服务器进程。</p>

<h2>Story</h2>

<p>其实，我和erlang之间是有故事的。我并不是这个月开始才接触erlang。早在2009年夏天的时候我就学习过这门语言。那时候我还没接触过任何函数式语言，那时候lua里的闭包都让我觉得新奇。然后无意间，我莫名其妙地接触了haskell（&lt;Real World Haskell&gt;），在我决定开始写点什么haskell练习时，我发现我无从下手，最后，Monads把我吓哭了。haskell实在太可怕了。</p>

<p>紧接着我怀揣着对函数式语言的浓烈好奇心看到了erlang。当我看到了concurrent programming的章节时，在一个燥热难耐的下午我的领导找到了我，同我探讨起erlang对我们的网游服务器有什么好处。然后，我结束我了的erlang之旅。</p>

<p>时隔四年，这种小众语言，居然进入了中国程序员的视野，并被用于开发网页游戏服务器。时代在进步，我们总是被甩在后面。</p>

<p class="post-footer">
            原文地址：
            <a href="http://codemacro.com/2013/05/08/thought-about-erlang/">http://codemacro.com/2013/05/08/thought-about-erlang/</a><br>
            written by <a href="http://codemacro.com">Kevin Lynx</a>
             posted at <a href="http://codemacro.com">http://codemacro.com</a>
            </p>

</div><img src ="http://www.cppblog.com/kevinlynx/aggbug/200138.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2013-05-09 21:24 <a href="http://www.cppblog.com/kevinlynx/archive/2013/05/09/200138.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>erlang和RabbitMQ学习总结</title><link>http://www.cppblog.com/kevinlynx/archive/2013/04/12/199393.html</link><dc:creator>Kevin Lynx</dc:creator><author>Kevin Lynx</author><pubDate>Fri, 12 Apr 2013 13:27:00 GMT</pubDate><guid>http://www.cppblog.com/kevinlynx/archive/2013/04/12/199393.html</guid><wfw:comment>http://www.cppblog.com/kevinlynx/comments/199393.html</wfw:comment><comments>http://www.cppblog.com/kevinlynx/archive/2013/04/12/199393.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/kevinlynx/comments/commentRss/199393.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/kevinlynx/services/trackbacks/199393.html</trackback:ping><description><![CDATA[<div class="entry-content">
<h2>AMQP和RabbitMQ概述</h2>

<p><a href="http://www.amqp.org/">AMQP</a>(Advanced Message Queue Protocol)定义了一种消息系统规范。这个规范描述了在一个分布式的系统中各个子系统如何通过消息交互。而<a href="http://www.rabbitmq.com/">RabbitMQ</a>则是AMQP的一种基于erlang的实现。</p>

<p>AMQP将分布式系统中各个子系统隔离开来，子系统之间不再有依赖。子系统仅依赖于消息。子系统不关心消息的发送者，也不关心消息的接受者。</p>

<p>AMQP中有一些概念，用于定义与应用层的交互。这些概念包括：message、queue、exchange、channel, connection, broker、vhost。</p>

<p><em>注：到目前为止我并没有打算使用AMQP，所以没有做更深入的学习，仅为了找个机会写写erlang代码，以下信息仅供参考。</em></p>

<ul>
<li>message，即消息，简单来说就是应用层需要发送的数据</li>
<li>queue，即队列，用于存储消息</li>
<li>exchange，有翻译为&#8220;路由&#8221;，它用于投递消息，<strong>应用程序在发送消息时并不是指定消息被发送到哪个队列，而是将消息投递给路由，由路由投递到队列</strong>
</li>
<li>channel，几乎所有操作都在channel中进行，有点类似一个沟通通道</li>
<li>connection，应用程序与broker的网络连接</li>
<li>broker，可简单理解为实现AMQP的服务，例如RabbitMQ服务</li>
</ul>
<p>关于AMQP可以通过一篇很有名的文章了解更多：<a href="http://blog.ftofficer.com/2010/03/translation-rabbitmq-python-rabbits-and-warrens/">RabbitMQ+Python入门经典 兔子和兔子窝</a></p>

<p>RabbitMQ的运行需要erlang的支持，erlang和RabbitMQ在windows下都可以直接使用安装程序，非常简单。RabbitMQ还支持网页端的管理，这需要开启一些RabbitMQ的插件，可以参考<a href="http://www.rabbitmq.com/management.html">官方文档</a>。</p>

<p>RabbitMQ本质上其实是一个服务器，与这个服务器做交互则是通过AMQP定义的协议，应用可以使用一个实现了AMQP协议的库来与服务器交互。这里我使用erlang的一个客户端，对应着RabbitMQ的tutorial，使用erlang实现了一遍。基于这个过程我将一些关键实现罗列出来以供记忆：</p>

<!-- more -->


<h2>主要功能使用</h2>

<p>关于RabbitMQ erlang client的使用说明可以参考<a href="http://www.rabbitmq.com/erlang-client-user-guide.html">官方文档</a>。这个client library下载下来后是两个ez文件，其实就是zip文件，本身是erlang支持的库打包格式，但据说这个feature还不成熟。总之我是直接解压，然后在环境变量中指定<code>ERL_LIBS</code>到解压目录。使用时使用<code>include_lib</code>包含库文件（类似C语言里的头文件）：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="o">-</span><span class="n">include_lib</span><span class="p">(</span><span class="s">"amqp_client/include/amqp_client.hrl"</span><span class="p">).</span>
</code></pre>
</div>


<h3>Connection/Channel</h3>

<p>对于连接到本地的RabbitMQ服务：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="p">{</span><span class="n">ok</span><span class="p">,</span> <span class="nv">Connection</span><span class="p">}</span> <span class="o">=</span> <span class="nn">amqp_connection</span><span class="p">:</span><span class="nf">start</span><span class="p">(</span><span class="nl">#amqp_params_network</span><span class="p">{}),</span>
    <span class="p">{</span><span class="n">ok</span><span class="p">,</span> <span class="nv">Channel</span><span class="p">}</span> <span class="o">=</span> <span class="nn">amqp_connection</span><span class="p">:</span><span class="nf">open_channel</span><span class="p">(</span><span class="nv">Connection</span><span class="p">),</span>
</code></pre>
</div>


<h3>创建Queue</h3>

<p>每个Queue都有名字，这个名字可以人为指定，也可以由系统分配。Queue创建后如果不显示删除，断开网络连接是不会自动删除这个Queue的，这个可以在RabbitMQ的web管理端看到。</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nl">#'queue.declare_ok'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="nv">Q</span><span class="p">}</span>
        <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nl">#'queue.declare'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"rpc_queue"</span><span class="o">&gt;&gt;</span><span class="p">}),</span>
</code></pre>
</div>


<p>但也可以指定Queue会在程序退出后被自动删除，需要指定<code>exclusive</code>参数：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">QDecl</span> <span class="o">=</span> <span class="nl">#'queue.declare'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="o">&lt;&lt;&gt;&gt;</span><span class="p">,</span> <span class="n">exclusive</span> <span class="o">=</span> <span class="n">true</span><span class="p">},</span>
    <span class="nl">#'queue.declare_ok'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="nv">Q</span><span class="p">}</span> <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">QDecl</span><span class="p">),</span>
</code></pre>
</div>


<p>上例中queue的名字未指定，由系统分配。</p>

<h3>发送消息</h3>

<p>一般情况下，消息其实是发送给exchange的：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">Payload</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"hello"</span><span class="o">&gt;&gt;</span>
    <span class="nv">Publish</span> <span class="o">=</span> <span class="nl">#'basic.publish'</span><span class="p">{</span><span class="n">exchange</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"log_exchange"</span><span class="o">&gt;&gt;</span><span class="p">},</span>
    <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">cast</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">Publish</span><span class="p">,</span> <span class="nl">#amqp_msg</span><span class="p">{</span><span class="n">payload</span> <span class="o">=</span> <span class="nv">Payload</span><span class="p">}),</span>
</code></pre>
</div>


<p>exchange有一系列规则，决定某个消息将被投递到哪个队列。</p>

<p>发送消息时也可以不指定exchange，这个时候消息的投递将依赖于<code>routing_key</code>，<code>routing_key</code>在这种场景下就对应着目标queue的名字：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nl">#'queue.declare_ok'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="nv">Q</span><span class="p">}</span>
        <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nl">#'queue.declare'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"rpc_queue"</span><span class="o">&gt;&gt;</span><span class="p">}),</span>
    <span class="nv">Payload</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"hello"</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="nv">Publish</span> <span class="o">=</span> <span class="nl">#'basic.publish'</span><span class="p">{</span><span class="n">exchange</span> <span class="o">=</span> <span class="o">&lt;&lt;&gt;&gt;</span><span class="p">,</span> <span class="n">routing_key</span> <span class="o">=</span> <span class="nv">Q</span><span class="p">},</span>
    <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">cast</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">Publish</span><span class="p">,</span> <span class="nl">#amqp_msg</span><span class="p">{</span><span class="n">payload</span> <span class="o">=</span> <span class="nv">Payload</span><span class="p">}),</span>
</code></pre>
</div>


<h3>接收消息</h3>

<p>可以通过注册一个消息consumer来完成消息的异步接收：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">Sub</span> <span class="o">=</span> <span class="nl">#'basic.consume'</span> <span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="nv">Q</span><span class="p">},</span>
    <span class="nl">#'basic.consume_ok'</span><span class="p">{</span><span class="n">consumer_tag</span> <span class="o">=</span> <span class="nv">Tag</span><span class="p">}</span> <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">subscribe</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">Sub</span><span class="p">,</span> <span class="n">self</span><span class="p">()),</span>
</code></pre>
</div>


<p>以上注册了了一个consumer，监听变量<code>Q</code>指定的队列。当有消息到达该队列时，系统就会向consumer进程对应的mailbox投递一个通知，我们可以使用<code>receive</code>来接收该通知：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="n">loop</span><span class="p">(</span><span class="nv">Channel</span><span class="p">)</span> <span class="o">-&gt;</span>
        <span class="k">receive</span> 
            <span class="c">% This is the first message received (from RabbitMQ)</span>
            <span class="nl">#'basic.consume_ok'</span><span class="p">{}</span> <span class="o">-&gt;</span> 
                <span class="n">loop</span><span class="p">(</span><span class="nv">Channel</span><span class="p">);</span>
            <span class="c">% a delivery</span>
            <span class="p">{</span><span class="nl">#'basic.deliver'</span><span class="p">{</span><span class="n">delivery_tag</span> <span class="o">=</span> <span class="nv">Tag</span><span class="p">},</span> <span class="nl">#amqp_msg</span><span class="p">{</span><span class="n">payload</span> <span class="o">=</span> <span class="nv">Payload</span><span class="p">}}</span> <span class="o">-&gt;</span>
                <span class="n">echo</span><span class="p">(</span><span class="nv">Payload</span><span class="p">),</span>
                <span class="c">% ack the message</span>
                <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">cast</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nl">#'basic.ack'</span><span class="p">{</span><span class="n">delivery_tag</span> <span class="o">=</span> <span class="nv">Tag</span><span class="p">}),</span>
                <span class="n">loop</span><span class="p">(</span><span class="nv">Channel</span><span class="p">);</span>
        <span class="p">...</span>
</code></pre>
</div>


<h3>绑定exchange和queue</h3>

<p>绑定(binding)其实也算AMQP里的一个关键概念，它用于建立exchange和queue之间的联系，以方便exchange在收到消息后将消息投递到队列。我们不一定需要将队列和exchange绑定起来。</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">Binding</span> <span class="o">=</span> <span class="nl">#'queue.bind'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="nv">Queue</span><span class="p">,</span> <span class="n">exchange</span> <span class="o">=</span> <span class="nv">Exchange</span><span class="p">,</span> <span class="n">routing_key</span> <span class="o">=</span> <span class="nv">RoutingKey</span><span class="p">},</span>
    <span class="nl">#'queue.bind_ok'</span><span class="p">{}</span> <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">Binding</span><span class="p">)</span>
</code></pre>
</div>


<p>在绑定的时候需要填入一个<code>routing_key</code>的参数，不同类型的exchange对该值的处理方式不一样，例如后面提到<code>fanout</code>类型的exchange时，就不需要该值。</p>

<h2>更多细节</h2>

<p>通过阅读<a href="http://www.rabbitmq.com/getstarted.html">RabbitMQ tutorial</a>，我们还会获得很多细节信息。例如exchange的种类、binding等。</p>

<h3>exchange分类</h3>

<p>exchange有四种类型，不同类型决定了其在收到消息后，该如何处理这条消息（投递规则），这四种类型为：</p>

<ul>
<li>fanout</li>
<li>direct</li>
<li>topic</li>
<li>headers</li>
</ul>
<p><strong>fanout</strong>类型的exchange是一个广播exchange，它在收到消息后会将消息广播给所有绑定到它上面的队列。绑定(binding)用于将队列和exchange关联起来。我们可以在创建exchange的时候指定exchange的类型：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">Declare</span> <span class="o">=</span> <span class="nl">#'exchange.declare'</span><span class="p">{</span><span class="n">exchange</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"my_exchange"</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="n">type</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"fanout"</span><span class="o">&gt;&gt;</span><span class="p">}</span>
    <span class="nl">#'exchange.declare_ok'</span><span class="p">{}</span> <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">Declare</span><span class="p">)</span>
</code></pre>
</div>


<p><strong>direct</strong>类型的exchange在收到消息后，会将此消息投递到发送消息时指定的<code>routing_key</code>和绑定队列到exchange上时的<code>routing_key</code>相同的队列里。可以多次绑定一个队列到一个exchange上，每次指定不同的<code>routing_key</code>，就可以接收多种<code>routing_key</code>类型的消息。<strong>注意，绑定队列时我们可以填入一个<code>routing_key</code>，发送消息时也可以指定一个<code>routing_key</code>。</strong></p>

<p><strong>topic</strong>类型的exchange相当于是direct exchange的扩展，direct exchange在投递消息到队列时，是单纯的对<code>routing_key</code>做相等判定，而topic exchange则是一个<code>routing_key</code>的字符串匹配，就像正则表达式一样。在<code>routing_key</code>中可以填入一种字符串匹配符号：</p>

<pre><code>* (star) can substitute for exactly one word.
# (hash) can substitute for zero or more words.
</code></pre>

<p><em>header exchange tutorial中未提到，我也不深究</em></p>

<h3>消息投递及回应</h3>

<p>每个消息都可以提供回应，以使RabbitMQ确定该消息确实被收到。RabbitMQ重新投递消息仅依靠与consumer的网络连接情况，所以只要网络连接正常，consumer卡死也不会导致RabbitMQ重投消息。如下回应消息：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">cast</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nl">#'basic.ack'</span><span class="p">{</span><span class="n">delivery_tag</span> <span class="o">=</span> <span class="nv">Tag</span><span class="p">}),</span>
</code></pre>
</div>


<p>其中<code>Tag</code>来源于接收到消息时里的<code>Tag</code>。</p>

<p>如果有多个consumer监听了一个队列，RabbitMQ会依次把消息投递到这些consumer上。这里的投递原则使用了<code>round robin</code>方法，也就是轮流方式。如前所述，如果某个consumer的处理逻辑耗时严重，则将导致多个consumer出现负载不均衡的情况，而RabbitMQ并不关心consumer的负载。可以通过消息回应机制来避免RabbitMQ使用这种消息数平均的投递原则：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nv">Prefetch</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span>
    <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nl">#'basic.qos'</span><span class="p">{</span><span class="n">prefetch_count</span> <span class="o">=</span> <span class="nv">Prefetch</span><span class="p">})</span>
</code></pre>
</div>


<h3>消息可靠性</h3>

<p>RabbitMQ可以保证消息的可靠性，这需要设置消息和队列都为durable的：</p>

<div class="highlight">
<pre><code class="erlang">    <span class="nl">#'queue.declare_ok'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="nv">Q</span><span class="p">}</span> <span class="o">=</span> <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">call</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nl">#'queue.declare'</span><span class="p">{</span><span class="n">queue</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"hello_queue"</span><span class="o">&gt;&gt;</span><span class="p">,</span> <span class="n">durable</span> <span class="o">=</span> <span class="n">true</span><span class="p">}),</span>

    <span class="nv">Payload</span> <span class="o">=</span> <span class="o">&lt;&lt;</span><span class="s">"foobar"</span><span class="o">&gt;&gt;</span><span class="p">,</span>
    <span class="nv">Publish</span> <span class="o">=</span> <span class="nl">#'basic.publish'</span><span class="p">{</span><span class="n">exchange</span> <span class="o">=</span> <span class="s">""</span><span class="p">,</span> <span class="n">routing_key</span> <span class="o">=</span> <span class="nv">Queue</span><span class="p">},</span>
    <span class="nv">Props</span> <span class="o">=</span> <span class="nl">#'P_basic'</span><span class="p">{</span><span class="n">delivery_mode</span> <span class="o">=</span> <span class="mi">2</span><span class="p">},</span> <span class="c">%% persistent message</span>
    <span class="nv">Msg</span> <span class="o">=</span> <span class="nl">#amqp_msg</span><span class="p">{</span><span class="n">props</span> <span class="o">=</span> <span class="nv">Props</span><span class="p">,</span> <span class="n">payload</span> <span class="o">=</span> <span class="nv">Payload</span><span class="p">},</span>
    <span class="nn">amqp_channel</span><span class="p">:</span><span class="nf">cast</span><span class="p">(</span><span class="nv">Channel</span><span class="p">,</span> <span class="nv">Publish</span><span class="p">,</span> <span class="nv">Msg</span><span class="p">)</span>
</code></pre>
</div>


<h2>参考</h2>

<p>除了参考RabbitMQ tutorial外，还可以看看别人使用erlang是如何实现这些tutorial的，github上有一个这样的项目：<a href="https://github.com/rabbitmq/rabbitmq-tutorials/tree/master/erlang">rabbitmq-tutorials</a>。我自己也实现了一份，包括rabbitmq-tutorials中没实现的RPC。后来我发现原来<a href="https://github.com/kevinlynx/rabbitmq-erlang-client">rabbitmq erlang client</a>的实现里已经包含了一个RPC模块。</p>

<ul>
<li><a href="http://blog.chinaunix.net/uid-22312037-id-3458208.html">RabbitMQ源码解析前奏&#8211;AMQP协议</a></li>
<li><a href="http://blog.ftofficer.com/2010/03/translation-rabbitmq-python-rabbits-and-warrens/">RabbitMQ+Python入门经典 兔子和兔子窝</a></li>
<li><a href="http://www.rabbitmq.com/erlang-client-user-guide.html">Erlang AMQP Client library</a></li>
<li><a href="http://www.rabbitmq.com/management.html">Manage RabbitMQ by WebUI</a></li>
</ul>
<p class="post-footer">
            原文地址：
            <a href="http://codemacro.com/2013/04/11/rabbitmq-erlang/">http://codemacro.com/2013/04/11/rabbitmq-erlang/</a><br />
            written by <a href="http://codemacro.com">Kevin Lynx</a>
            &nbsp;posted at <a href="http://codemacro.com">http://codemacro.com</a>
            </p>

</div><img src ="http://www.cppblog.com/kevinlynx/aggbug/199393.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/kevinlynx/" target="_blank">Kevin Lynx</a> 2013-04-12 21:27 <a href="http://www.cppblog.com/kevinlynx/archive/2013/04/12/199393.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>