??xml version="1.0" encoding="utf-8" standalone="yes"?>国产成人精品久久亚洲,久久WWW免费人成一看片,中文字幕亚洲综合久久2http://www.shnenglu.com/kevinlynx/category/20475.html低调做技术__Ƣ迎UL我的独立博客 <a >codemaro.com</a>zh-cnThu, 08 Aug 2013 15:21:54 GMTThu, 08 Aug 2013 15:21:54 GMT60Dhtcrawler2换用sphinx搜烦http://www.shnenglu.com/kevinlynx/archive/2013/08/08/202417.htmlKevin LynxKevin LynxThu, 08 Aug 2013 15:04:00 GMThttp://www.shnenglu.com/kevinlynx/archive/2013/08/08/202417.htmlhttp://www.shnenglu.com/kevinlynx/comments/202417.htmlhttp://www.shnenglu.com/kevinlynx/archive/2013/08/08/202417.html#Feedback0http://www.shnenglu.com/kevinlynx/comments/commentRss/202417.htmlhttp://www.shnenglu.com/kevinlynx/services/trackbacks/202417.html

dhtcrawler2最开始用mongodb自带的全文搜索引擎搜索资源。搜索一些短关键字时很容易导致erlangq程call timeoutQ也是查询旉太长。对于像aviq种关键字,搜烦旉长达十几U。搜索的资源数量200万左叟뀂这其中大部分资源只是对root文g名进行了索引Q即对于多文件资源而言没有索引单个文g名。烦引方式有部分资源是按照字W串子串的Ş式,没有拆词Q非常占用存储空_有部分是使用了rmmsegQ我~译了rmmseg-cpp作ؓerlang nif库调?erl-rmmsegQ进行了拆词Q占用空间小了很多,但由于词库问题很多片里的词汇没拆出来?/p>

很早以前我以为搜索耗时的原因是因ؓ数据库太忙,想部|个mongodb集群出来。后来发现数据库没有Md的状态下Q查询依然慢。终于只好放弃mongodb自带的文本搜索。于是我改用sphinx。简单v见,我直接下载了coreseek4.1Qsphinx的一个支持中文拆词的包装Q?/p>

现在Q已l导入了200多万的资源进sphinxQƈ且烦引了所有文件名Q烦引文件达800M。对?code>avi关键字的搜烦大概消?.2U的旉?a >搜烦试试?/p>

以下记录下sphinx在dhtcrawler的应?/p>

sphinx?/h3>

sphinx包含两个主要的程序:indexer和searchd。indexer用于建立文本内容的烦引,然后searchdZq些索引提供文本搜烦功能Q而要使用该功能,可以遵@searchd的网l协议连接searchdq个服务来用?/p>

indexer可以通过多种方式来获取这些文本内容,文本内容的来源称为数据源。sphinx内置mysqlq种数据源,意思是可以直接从mysql数据库中取得数据。sphinxq支持xmlpipe2q种数据源,其数据以xml格式提供lindexer。要导入mongodb数据库里的内容,可以选择使用xmlpipe2q种方式?/p>

sphinx document

xmlpipe2数据源需要按照以下格式提交:

<sphinx:docset>
    <sphinx:schema>
        <sphinx:field name="subject"/>
        <sphinx:field name="files"/>
        <sphinx:attr name="hash1" type="int" bits="32"/>
        <sphinx:attr name="hash2" type="int" bits="32"/>
    </sphinx:schema>
    <sphinx:document id="1">
        <subject>this is the subject</subject>
        <files>file content</files>
        <hash1>111</hash1>
    </sphinx:document>
</sphinx:docset>

该文件包含两大部分:schema?code>documentsQ其?code>schema又包含两部分Q?code>field?code>attrQ其中由field标识的字D就会被indexerdq全部作入文本徏立烦引,?code>attr则标识查询结果需要附带的信息Q?code>documents则是׃个个sphinx:documentl成Q即indexer真正要处理的数据。注意其中被schema引用的属性名?/p>

document一个很重要的属性就是它的id。这个id对应于sphinx需要唯一Q查询结果也会包含此id。一般情况下Q此id可以直接是数据库主键Q可用于查询到详l信息。searchd搜烦关键字,其实可以看作为搜索这些documentQ搜索出来的l果也是q些documentQ搜索结果中主要包含schema中指定的attr?/p>

增量索引

数据源的数据一般是变化的,新增的数据要加入到sphinx索引文g中,才能使得searchd搜烦到新录入的数据。要不断地加入新数据Q可以用增量烦引机制。增量烦引机制中Q需要一个主索引和一个次索引(delta index)。每ơ新增的数据都徏立ؓơ烦引,然后一D|间后再合q进ȝ引。这个过E主要还是用indexer和searchdE序。实际上Qsearchd是一个需要一直运行的服务Q而indexer则是一个徏立完索引退出的工具E序。所以,q里的增量烦引机Ӟ其中涉及到的“每隔一定时间就合ƈ”q种工作Q需要自己写E序来协调(或通过其他工具Q?/p>

sphinx与mongodb

上面提到Q一般sphinx document的id都是使用的数据库主键Q以方便查询。但mongodb中默认情况不使用数字作ؓ主键。dhtcrawler的资源数据库使用的是资源info-hash作ؓ主键Q这无法作ؓsphinx document的id。一U解军_法是Q将该hash按位拆分Q拆分成若干个sphinx document attr支持位数的整数。例如,info-hash是一?60位的idQ如果?2位的attrQ高版本的sphinx支持64位的整数Q,那么可以把该info-hash按位拆分?个attr。而sphinx document id则可以用Q意数字,只要保证不冲H就行。当获得查询l果Ӟ取得对应的attrQ组合ؓinfo-hash卛_?/p>

mongodb默认的Object id也可以按q种方式拆分?/p>

dhtcrawler2与sphinx

dhtcrawler2中我自己写了一个导入程序。该E序从mongodb中读出数据,数据C定量ӞpZؓxmlpipe2格式的xml文gQ然后徏立ؓơ烦引,最后合q进ȝ引。过E很单,包含两次启动外部q程的工作,q个可以通过erlang中os:cmd完成?/p>

值得注意的是Q在从mongodb中读数据Ӟ使用skip基本是不靠谱的,skip 100万个数据需要好几分钟,Z不增加额外的索引字段Q我只好?code>created_at字段上加索引Q然后按旉D|d资源Q这一切都是ؓ了支持程序关闭重启后Q可以l上ơ工作,而不是重头再来?00万的数据Q已l处理了好几天了?/p>

后头数据建立好了Q需要在前台展示出来。erlang中似乎只有一个sphinx客户端库Q?a >giza。这个库有点老,写成的时候貌D在用sphinx0.9版本。其中查询代码包含了版本判定Q已l无法在我用的sphinx2.x版本中用。无奈之下我只好修改了这个库的源码,q运的是查询功能居然是正常的Q意味着sphinx若干个版本了也没改动通信协议Q后来,我ؓ了取得查询的l计信息Q例如消耗时间以及ȝ果,我再一ơ修改了giza的源码。新的版本可以在我的github上找刎ͼmy gizaQ看h我没늊版本协议吧?

目前dhtcrawler的搜索,先是Zsphinx搜烦出hash列表Q然后再去mongodb中搜索hash对应的资源。事实上Q可以ؓsphinx的document直接附加q些资源的描qC息,可以避免去数据库查询。但我想Q这样会增加sphinx索引文g的大,担心会媄响搜索速度。实际测试时Q发现数据库查询有时候还真的很消耗时_管我做了分,以得单仅Ҏ据库q行量查询?/p>

xml unicode

在导入xml到sphinx的烦引过E中Q本w我输出的内定w是unicode的,但有很多资源会导致indexer解析xml出错。出错后indexer直接停止对当前xml的处理。后来查阅资料发现是因ؓq些无法被indexer处理的xml内容包含unicode里的控制字符Q例?ä (U+00E4)。我的解军_法是直接qo掉这些控制字W。unicode的控制字W参?a >UTF-8 encoding table and Unicode characters。在erlang中干q个事居然不复杂Q?/p>

strip_invalid_unicode(<<>>) ->
    <<>>;
strip_invalid_unicode(<<C/utf8, R/binary>>) ->
    case is_valid_unicode(C) of
        true ->
            RR = strip_invalid_unicode(R),
            <<C/utf8, RR/binary>>;
        false ->
            strip_invalid_unicode(R)
    end;
strip_invalid_unicode(<<_, R/binary>>) ->
    strip_invalid_unicode(R).
    
is_valid_unicode(C) when C < 16#20 ->
    false;
is_valid_unicode(C) when C >= 16#7f, C =< 16#ff ->
    false;
is_valid_unicode(_) ->
    true.


Kevin Lynx 2013-08-08 23:04 发表评论
]]>
力搜烦W二?dhtcrawler2http://www.shnenglu.com/kevinlynx/archive/2013/07/20/201994.htmlKevin LynxKevin LynxSat, 20 Jul 2013 08:37:00 GMThttp://www.shnenglu.com/kevinlynx/archive/2013/07/20/201994.htmlhttp://www.shnenglu.com/kevinlynx/comments/201994.htmlhttp://www.shnenglu.com/kevinlynx/archive/2013/07/20/201994.html#Feedback0http://www.shnenglu.com/kevinlynx/comments/commentRss/201994.htmlhttp://www.shnenglu.com/kevinlynx/services/trackbacks/201994.html

?a >上篇?/p>

下蝲使用

目前为止dhtcrawler2相对dhtcrawler而言Q数据库部分调整很大QDHT部分基本沿用之前。但单纯作ؓ一个爬资源的程序而言QDHT部分可以q行大幅削减Q这个以后再说。这个版本更快、更E_。ؓ了方便,我将~译好的erlang二进制文件作为git的主分支Q我q添加了一些Windows下的批处理脚本,M基本上下载源码以后即可运行?/p>

目地址Q?a >https://github.com/kevinlynx/dhtcrawler2

使用Ҏ

  • 下蝲erlangQ我试的是R16B版本Q确保erl{程序被加入Path环境变量
  • 下蝲mongodbQ解压即用:

      mongod --dbpath xxx --setParameter textSearchEnabled=true
    
  • 下蝲dhtcrawler2

      git clone https://github.com/kevinlynx/dhtcrawler2.git
    
  • q行win_start_crawler.bat

  • q行win_start_hash.bat
  • q行win_start_http.bat
  • 打开localhost:8000查看stats

爬虫每次q行都会保存DHT节点状态,早期q行的时候收集速度会不够。dhtcrawler2程序分?部分Q?/p>

  • crawlerQ即DHT爬虫部分Q仅负责攉hash
  • hashQ准来讲叫hash readerQ处理爬虫收集的hashQ处理过E主要涉及到下蝲U子文g
  • httpQ用hash处理出来的数据库Q以作ؓWeb端接?/li>

我没有服务器Q但E序有被部v在别人的服务器上Q?a >bt.cmQ?a >http://222.175.114.126:8000/?/p>

其他工具

Z提高资源索引速度Q我陆箋写了一些工P包括Q?/p>

  • import_torsQ用于导入本地种子文件到数据?/li>
  • tor_cacheQ用于下载种子到本地Q仅仅提供下载的功能Qhash_reader在需要种子文件时Q可以先从本地取
  • cache_indexerQ目前hash_reader取种子都是从torrage.com之类的种子缓存站点取Q这些站Ҏ供了U子列表Qcache_indexer这些列表导入数据库Qhash_reader在请求种子文件前可以通过该数据库查torrage.com上有无此U子Q从而减多余的httph

q些工具的代码都被放在dhtcrawler2中,可以查看对应的启动脚本来查看具体如何启动?/p>

OS/Database

Ҏ实际的测试效果来看,当收集的资源量过百万Ӟ目前bt.cm录入q?60万资源)Q?G内存的Windowsq_Qmongodb很容易就会挂掉。挂掉的原因全是1455Q页面文件太。有人徏议不要在Windows下用mongodbQLinux下我自己没做q测试?/p>

mongodb可以部v为集Ş?replica-set)Q当初我xhttp部分的查询放在一个只ȝmongodb实例上,但因为徏立集时Q要同步已有?0G数据库,而每ơ同步都以mongodb挂掉l束Q遂攑ּ。在目前bt.cm的配|中Q数据库torrent的锁比例Qdb lockQ很Ҏ?0%Q这也让http在搜索时Q经常出现搜索超时的情况?/p>

技术信?/h2>

dhtcrawler最早的版本有很多问题,修复q的最大的一个问题是关于erlang定时器的Q在DHT实现中,需要对每个节点每个peer做超时处理,在erlang中的做法直接是针Ҏ个节Ҏ册了一个定时器。这不是问题Q问题在于定时器资源像没有GC的内存资源一P是会׃E序员的代码问题而出现资源泄漏。所以,dhtcrawlerW一个版本在节点数配|在100以上的情况下Q用不了多久׃内存耗尽Q最l导致erlang虚拟机core dump?/p>

除了q个问题以外Qdhtcrawler的资源收录速度也不是很快。这当然跟数据库和获取种子的速度有直接关pR尤其是获取U子Q用的是一些提供info-hash到种子映的|站Q通过HTTPh来下载种子文件。我以ؓ通过BT协议直接下蝲U子会快些,q且实时性也要高很多Q因个种子可能未被这些缓存网站收录,但却可以直接向对方请求得到。ؓ此,我还特地阅了相?a >协议Qƈ且用erlang实现了(以后的文章我会讲到具体实现这个协议)?/p>

后来我怀疑get_peers的数量会不会比announce_peer多,但是理论上一般的客户端在get_peers之后都是announce_peerQ但是如果get_peers查询的peers恰好不在U呢Q这意味着很多资源虽然已经存在Q只不过你恰好暂时请求不到。实际测试时Q发现get_peers基本是announce_peer数量?0倍?/p>

hash的获取方式做了调整后Qdhtcrawler在几分钟以内以几乎每U上百个新增U子的速度工作。然后,E序挂掉?/p>

从dhtcrawlerC天ؓ止的dhtcrawler2Q中间间隔了刚好1个月。我的所有业余时间全部扑在这个项目上Q面临的问题一直都是程序的内存泄漏、资源收录的速度不够快,到后来又变ؓ数据库压力过大。每一天我都以为我会完成一个稳定版本,然后l于可以d点别的事情,但Lq不完,目前完没完都q在观察。我始终明白在做优化前需要进行详的数据攉和分析,从而真正地优化到正的点上Q但也L凭直觉和量数据分析开始尝试?/p>

q里谈谈遇到的一些问题?/p>

erlang call timeout

最开始遇到erlang?code>gen_server:call出现timeout错误Ӟ我还一直以为是q程死锁了。相关代码读来读去,实在觉得不可能发生死锁。后来发玎ͼ当erlang虚拟机压力上dQ例如内存太大,但没大到耗尽pȝ所有内存(耗进所有内存基本就core dump了)Q进E间的调用就会出现timeout?/p>

当然Q内存占用过大可能只是表象。其q程q多Q进E消息队列太长,也许才是D出现timeout的根本原因。消息队列过长,也可能是׃发生?em>消息泄漏的缘故。消息泄漏我指的是这样一U情况,q程自己l自己发消息Q当然是cast或infoQ,q个消息被处理时又会发送相同的消息Q正常情况下Qgen_server处理了一个该消息Q就会从消息队列里移除它Q然后再发送相同的消息Q这不会出问题。但是当E序逻辑出问题,每次处理该消息时Q都会发生多余一个的同类消息Q那消息队列自然׃一直增ѝ?/p>

保持q程逻辑单,以避免这U逻辑错误?/p>

erlang gb_trees

我在不少的地方用了gb_treesQdht_crawler里就可能出现gb_trees:get(xxx, nil)q种错误。乍一看,我以为我真的传入了一?code>nilD厅R然后我苦看代码Q以为在某个地方我会把这个gb_trees对象Ҏ了nil。但事情不是q样的,gb_tress使用一个tuple作ؓtree的节点,当某个节Ҏ有子节点Ӟ׃以nil表示?/p>

gb_trees:get(xxx, nil)cM的错误,实际指的?code>xxx没有在这个gb_trees中找到?/p>

erlang httpc

dht_crawler通过http协议从torrage.com之类的缓存网站下载种子。最开始我Z量依赖第三方库,使用的是erlang自带的httpc。后来发现程序有内存泄漏Qgoogle发现erlang自带的httpc早ؓ病,当然也有大神说在某个版本之后q个httpc已经很不错。ؓ了省事,我直接换了ibrowseQ替换之后正常很多。但是由于没有具体分析测试过Q加之时间有点远了,我也C太清l节。因为早期的httph部分Q没有做数量限制Q也可能是由于我的用导致的问题?/p>

某个版本后,我才http部分严格Chash处理部分区分开来。相较数据库操作而言Qhttph部分慢了若干数量U。在hash_reader中将q两块分开Q严格限制了提交lhttpc的请求数Q以获得E_性?/p>

对于一个复杂的|络pȝ而言Q分清哪些是耗时的哪些是不大耗时的,才可能获得性能的提升。对于hash_reader而言Q处理一个hash的速度Q虽然很大程度取决于数据库,但相较httphQ已l快很多。它在处理这些hashӞ会将数据库已收录的资源和待下载的资源分离开Q以快的速度处理已存在的Q而将待下载的处理速度交给httpc的响应速度?/p>

erlang httpc ssl

ibrowse处理httpshӞ默认和erlang自带的httpc使用相同的SSL实现。这l常D出现tls_connectionq程挂掉的错误,具体原因不明?/p>

erlang调试

首先合理的日志是Mpȝ调试的必备?/p>

我面临的大部分问题都是内存泄漏相养I所以依赖的erlang工具也是和内存相关的Q?/p>

  • 使用etopQ可以检查内存占用多的进E、消息队列大的进E、CPU消耗多的进E等{:

      spawn(fun() -> etop:start([{output, text}, {interval, 10}, {lines, 20}, {sort, msg_q }]) end).
    
  • 使用erlang:system_info(allocated_areas).查内存用情况,其中会输出系l?code>timer数量

  • 使用erlang:process_info查看某个具体的进E,q个甚至会输出消息队列里的消?/li>

hash_writer/crawler

crawler本n仅收集hashQ然后写入数据库Q所以可以称crawler为hash_writer。这些hash里存在大量的重复。hash_reader从数据库里取些hash然后做处理。处理过E会首先判定该hash对应的资源是否被收录Q没有收录就先通过http获取U子?/p>

在某个版本之后,crawler会简单地预先处理q些hash。它~存一定数量的hashQ接收到新hashӞ合q到hash~存里,以保证缓存里没有重复的hash。这个重复率l过实际数据分析Q大概是50%左右Q即收到?00个请求里Q有50个是重复的。这L优化Q不仅会降低hash数据库的压力Qhash_reader处理的hash数量了Q也会对torrent数据库有很大提升?/p>

当然q一步的Ҏ可以crawler和hash_reader之间交互的这些hash直接攑֜内存中处理,省去中间数据库。但是由于mongodb大量使用虚拟内存的缘故(内存映射文gQ,l常D服务器内存不够(4GQ,内存也就成了珍稀资源。当然这个方案还有个弊端是难以权衡hash~存的管理。crawler收到hash是一个不E_的过E,在某些时间点q些hash可能爆多Q而hash_reader处理hash的速度也会不太E_Q受限于收到的hashcdQ是新增资源q是已存在资源)、种子请求速度、是否有效等?/p>

当然Q也可以限制~存大小Q以及对hash_reader/crawler处理速度建立关系来解册些问题。但另一斚wQ这里的优化是否对目前的pȝ有提升,是否是目前系l面临的最大问题,却是需要考究的事情?/p>

cache indexer

dht_crawler是从torrage.com{网站获取种子文Ӟq些|站看v来都是用了相同的接口,光有一个sync目录Q里面存放了每天每个月烦引的U子hashQ例?http://torrage.com/sync/。这个网站上是否有某个hash对应的种子,可以从q些索引中检查?/p>

hash_reader在处理新资源ӞhU子的过E中发现大部分在q些服务器上都没有找刎ͼ也就是发L很多httph都是404回应Q这不但降低了系l的处理能力、带宽,也降低了索引速度。所以我写了一个工P先手工将sync目录下的所有文件下载到本地Q然后通过q个工具 (cache indexer) 这些烦引文仉的hash全部导入数据库。在以后的运行过E中Q该工具仅下载当天的索引文gQ以更新数据库?hash_reader Ҏ配置Q会首先查某个hash是否存在该数据库中,存在的hash才可能在torrage.com上下载得到?/p>

U子~存

hash_reader可以通过配置Q将下蝲得到的种子保存在本地文gpȝ或数据库中。这可以建立自己的种子缓存,但保存在数据库中会对数据库造成压力Q尤其在当前试服务器硬件环境下Q而保存ؓ本地文gQ又特别占用盘I间?/p>

ZBT协议的种子下?/h3>

通过http从种子缓存里取种子文Ӟ可能会没有直接从P2P|络里取更实时。目前还没来得及查看q些U子~存|站的实现原理。但是通过BT协议获取U子会有炚w烦,因ؓdht_crawler是根?code>get_peerh索引资源的,所以如果要通过BT协议取种子,那么q里q得去DHT|络里查询该U子Q这个查询过E可能会较长Q相比之下会没有http下蝲快。而如果通过announce_peer来烦引新资源的话Q其索引速度会大大降低,因ؓannounce_peerh?code>get_peerh很多,几乎10倍?/p>

所以,q里的方案可能会l合两者,新开一个服务,建立自己的种子缓存?/p>

中文分词

mongodb的全文烦引是不支持中文的。我在之前提刎ͼZ支持搜烦中文Q我字W串拆成了若q子丌Ӏ这L后果是字符串烦引会E稍偏大Q而且目前q一块的代码q特别简单,会将很多非文字字W也在内。后来我加了个中文分词库Q用的是rmmseg-cpp。我其C++部分抽离出来~译成erlang nifQ这可以在我的github上找到?/p>

但是q个库拆分中文句子依赖于词库Q而这个词库不太新Qdhtcrawler爬到的大部分资源cd你们也懂Q那些词汇拆出来的比率不太高Q这会导致搜索出来的l果没你想的那么直白。当然更新词库应该是可以解决q个问题的,目前q没有时间顾q一块?/p>

ȝ

一个老外Ҏ说过Q?#8221;i have 2 children to feed, so i will not do this only for fun”?/p>

你的大部分编E知识来源于|络Q所以稍E回馈一下不会让你丢了饭?/p>

我很IP如果你能让我收获金钱和编E成,q不会嫌我穿得太邋遢Qthat’s really kind of you?/p>



Kevin Lynx 2013-07-20 16:37 发表评论
]]>
使用erlang实现P2P力搜烦-实现http://www.shnenglu.com/kevinlynx/archive/2013/06/20/201179.htmlKevin LynxKevin LynxThu, 20 Jun 2013 12:40:00 GMThttp://www.shnenglu.com/kevinlynx/archive/2013/06/20/201179.htmlhttp://www.shnenglu.com/kevinlynx/comments/201179.htmlhttp://www.shnenglu.com/kevinlynx/archive/2013/06/20/201179.html#Feedback1http://www.shnenglu.com/kevinlynx/comments/commentRss/201179.htmlhttp://www.shnenglu.com/kevinlynx/services/trackbacks/201179.html

?a >上篇Q本谈谈一些实现细节?/p>

q个爬虫E序主要的问题在于如何获取P2P|络中分享的资源Q获取到资源后烦引到数据库中Q搜索就是自然而然的事情?/p>

DHT

DHT|络本质上是一个用于查询的|络Q其用于查询一个资源有哪些计算机正在下载。每个资源都有一?0字节长度的ID用于标示Q称为infohash。当一个程序作为DHT节点加入q个|络Ӟ׃有其他节Ҏ向你查询Q当你做出回应后Q对方就会记录下你。对方还会询问其他节点,当对方开始下载这个infohash对应的资源时Q他׃告诉所有曾l询问过的节点,包括你。这个时候就可以定Q这个infohash对应的资源在q个|络中是有效的?/p>

关于q个|络的工作原理,参看Q?a >P2P中DHT|络爬虫以及写了个磁力搜索的|页?/p>

获取到infohash后能做什么?关键点在于,我们现在使用的磁力链?magnet url)Q是和infohash对应h的。也是拿到infohashQ就{于拿到一个磁力链接。但是这个爬虫还需要徏立资源的信息Q这些信息来源于U子文g。种子文件其实也是对应到一个资源,U子文g包含资源名、描q、文件列表、文件大等信息。获取到infohashӞ其实也获取到了对应的计算机地址Q我们可以在q些计算Z下蝲到对应的U子文g?/p>

但是我ؓ了简单,在获取到infohash后,从一些提供映磁力链到种子文件服务的|站上直接下载了对应的种子。dhtcrawler里用了以下|站Q?/p>

http://torrage.com
https://zoink.it
http://bt.box.n0808.com

使用q些|站Ӟ需提供力哈希Qinfohash可直接{换)Q构建特定的URLQ发出HTTPh卛_?/p>

   U1 = "http://torrage.com/torrent/" ++ MagHash ++ ".torrent",
    U2 = "https://zoink.it/torrent/" ++ MagHash ++ ".torrent",
    U3 = format_btbox_url(MagHash),

format_btbox_url(MagHash) ->
    H = lists:sublist(MagHash, 2),
    T = lists:nthtail(38, MagHash),
    "http://bt.box.n0808.com/" ++ H ++ "/" ++ T ++ "/" ++ MagHash ++ ".torrent".

但是Q以一个节点的w䆾加入DHT|络Q是无法获取大量查询的。在DHT|络中,每个节点都有一个ID。每个节点在查询信息Ӟ仅询问离信息较近的节炏V这里的信息除了infohash外还包含节点Q即节点询问一个节点,q个节点在哪里。DHT的典型实CQKademliaQ,使用两个ID的xor操作来确定距R既然距ȝ计算是基于ID的,Z可能获取整个DHT|络交换的信息,爬虫E序可以徏立尽可能多的DHT节点Q让q些节点的ID均匀地分布在ID取值区间内Q以q样的方式加入网l?/p>

在dhtcrawler中,我用以下方式生了N个大致均匀分布的IDQ?/p>

create_discrete_ids(1) ->
    [dht_id:random()];
create_discrete_ids(Count) ->
    Max = dht_id:max(),
    Piece = Max div Count,
    [random:uniform(Piece) + Index * Piece || Index <- lists:seq(0, Count - 1)].

除了可能多地往DHT|络里部|节点之外,对单个节点而言Q也有些注意事项。例如应可能快地将自己告诉可能多的节点,q可以在启动时进行大量的随机infohash的查询。随着查询q程的深入,该节点会与更多的节点打交道。因为DHT|络里的节点实际上是不稳定的Q它今天在线Q明天后天可能不在线Q所以计你的ID固定Q哪些节点与你较q,本n是个相Ҏc节点在E序退出时Q也最好将自己的\׃息(与自׃互的节点列表Q保存v来,q样下次启动时就可以更快地加入网l?/p>

在dhtcrawler的实CQ每个节Ҏ个一定时_都会向网l中随机查询一个infohashQ这个infohash是随Z生的。其查询目的不在于infohashQ而在于告诉更多的节点Q以及在其他节点上保持自qz跃?/p>

handle_event(startup, {MyID}) ->
    timer:apply_interval(?QUERY_INTERVAL, ?MODULE, start_tell_more_nodes, [MyID]).

start_tell_more_nodes(MyID) ->
    spawn(?MODULE, tell_more_nodes, [MyID]).

tell_more_nodes(MyID) ->
    [search:get_peers(MyID, dht_id:random()) || _ <- lists:seq(1, 3)].

DHT节点的完整实现是比较J琐的,涉及到查询以及繁杂的各种对象的超Ӟ节点、桶、infohashQ,而超时的处理q不是粗暴地做删除操作。因为本w是ZUDP协议Q你得对q些时对象做进一步的查询才能正确地进一步做其他事情。而搜索也是个J杂的事情,递归地查询节点,感觉上,你不一定离目标来近Q由于被查询节点的不定性(无法定Ҏ是否在玩弄你Q或者本w对方就是个傻|Q你很可能接下来要查询的节点反而离目标变远了?/p>

在我W一ơ的DHT实现中,我用了cMtransmission里DHT实现的方法,不断无脑递归Q当搜烦有太久时间没得到响应后终止搜索。第二次实现Ӟ我就使用了etorrent里的实现。这个搜索更聪明Q它记录搜烦q的节点Qƈ且检查是否离目标来远。当q离目标Ӟp为搜索是不太有效的,不太有效的搜索尝试几ơ就可以攑ּ?/p>

实际上,爬虫的实现ƈ不需要完整地实现DHT节点的正常功能?strong>爬虫作ؓ一个DHT节点的唯一动机仅是获取|络里其他节点的查询。而要完成q个功能Q你只需要装得像个正思hp。这里不需要保存infohash对应的peer列表Q面临每一ơ查询,你随便回复几个节点地址可以。但是这里有个责任问题,如果整个DHT|络?000个节点,而你q个爬虫有1000个节点,那么你的随意回复Q就可能DҎҎ找不到正的信息Q这样你依然得不到有效的资源。(可以利用q一点破坏DHT|络Q?/p>

DHT的实现没有用第三方库?/p>

U子

U子文g的格式同DHT|络消息格式一P使用一U称为bencode的文本格式来~码。种子文件分Zc:单个文g和多个文件?/p>

文g的信息无非就是文件名、大。文件名可能包含utf8~码的名字,Z后面处理的方便,dhtcrawler都会优先使用utf8~码?/p>

   {ok, {dict, Info}} = dict:find(<<"info">>, TD),
    case type(Info) of
        single -> {single, parse_single(Info)};
        multi -> {multi, parse_multi(Info)}
    end.
parse_single(Info) ->
    Name = read_string("name", Info),
    {ok, Length} = dict:find(<<"length">>, Info),
    {Name, Length}.

parse_multi(Info) ->
    Root = read_string("name", Info),
    {ok, {list, Files}} = dict:find(<<"files">>, Info),
    FileInfo = [parse_file_item(Item) || {dict, Item} <- Files],
    {Root, FileInfo}.

数据?/h2>

我最开始在选用数据库时Qؓ了不使用W三方库Q打用erlang自带的mnesia。但是因为涉及到字符串匹配搜索,mnesia的查询语句在我看来太不友好,在经q一些资料查阅后q接放弃了?/p>

然后我打用couchdbQ因为它是erlang写的Q而我正在用erlang写程序。第一ơ接触非关系型数据库Q发现NoSQL数据库用v来比SQLcȝ单多了。但是在erlang里要使用couchdb实在太折腾了。我使用的客L库是couchbeam?/p>

因ؓcouchdb暴露的API都是ZHTTP协议的,其数据格式用了jsonQ所以couchbeam实际上就是对各种HTTPh、回应和json的包装。但是它竟然使用了ibrowseq个W三方HTTP客户端库Q而不是erlang自带的。ibrowse又用了jiffyq个解析json的库。这个库更惨烈的是它的解析工作都是交lC语言写的动态库来完成,我还得编译那个C库?/p>

couchdb看v来不支持字符串查询,我得自己创徏一个viewQ这个view里我通过阅了一些资料写了一个将每个doc的name拆分成若q次查询l果的map。这个map在处理每一ơ查询时Q我都得动态更C。couchdb是不支持局部更新的Q这q不大问题。然后很高兴Q终于支持字W串查询了。这里的字符串查询都是基于字W串的子串查询。但是问题在于,太慢了。每一ơ在WEB端的查询Q都直接Derlangq程的call时?/p>

要让couchdb支持字符串查询,要快速,当然是有解决Ҏ的。但是这个时候我已经没有心思l折腾,M一个库、程序如果接口设计得如此不方便,那就可以考虑换一个其他的?/p>

我选择了mongodb。同LZ文档的数据库?.4版本q支持全文搜索。什么是全文搜烦呢,q是一U基于单词的全文搜烦方式?code>hello world我可以搜?code>helloQ基于单词。mongodb会自动拆词。更关键更让人爽的是Q要开启这个功能非常简单:讄启动参数、徏立烦引。没了。mongodb的erlang客户端库mongodb-erlang也只依赖一个bson-erlang库。然后我又埋头苦qԌ几个时候我的这个爬虫程序就可以在浏览器端搜索关键字了?/p>

后来我发玎ͼmongodb的全文搜索是不支持中文的。因为它q不知道中文该怎么拆词。恰好我有个同事做过中文拆词的研IӞ看v来涉及到很复杂的法。直到这个时候,我他妈才醒悟Q我Z么需要基于单词的搜烦。我们大部分的搜索其实都是基于子字符串的搜烦?/p>

于是Q我种子文件的名字拆分成了若干个子字符Ԍ这些子字符串以数组的Ş式作为种子文档的一个键值存储,而我依然q可以用全文烦引,因ؓ全文索引会将整个字符串作为单词比较。实际上Q基于一般的查询方式也是可以的。当Ӟ索引q是得徏立?/p>

使用mongodb时唯一让我很不爽的是mongodb-erlangq个客户端库的文档太Ơ缺。这q不大问题Q因为看看源码参数还是可以大概猜到用法。真正悲剧的是mongodb的有些查询功能它是不支持的。例如通过cursor来排序来限制数量。在cursor模块q没有对应的mongodb接口。最l我只好通过以下方式查询Q我不明白batchsizeQ但它可以工作:

search_announce_top(Conn, Count) ->
    Sel = {'$query', {}, '$orderby', {announce, -1}},
    List = mongo_do(Conn, fun() ->
        Cursor = mongo:find(?COLLNAME, Sel, [], 0, Count), 
        mongo_cursor:rest(Cursor)
    end),
    [decode_torrent_item(Item) || Item <- List].

另一个悲剧的是,mongodb-erlangq不支持文档的局部更斎ͼ它的update接口直接要求传入整个文档。几l折腾,我可以通过runCommand来完成:

inc_announce(Conn, Hash) when is_list(Hash) ->
    Cmd = {findAndModify, ?COLLNAME, query, {'_id', list_to_binary(Hash)}, 
        update, {'$inc', {announce, 1}},
        new, true},
    Ret = mongo_do(Conn, fun() ->
        mongo:command(Cmd)
    end).

Unicode

不知道在哪里我看到过erlang说自己其实是不需要支持unicode的,因ؓq门语言本n是通过list来模拟字W串。对于unicode而言Q对应的list保存的本w就是整数倹{但是ؓ了方便处理,erlangq是提供了一些unicode操作的接口?/p>

因ؓ我需要将U子的名字按字拆分,对于a中文q样的字W串而言Q我需要拆分成以下l果Q?/p>

a
a?
a中文
?
中文
?

那么Q在erlang中当我获取到一个字W串listӞ我就需要知道哪几个整数合v来实际上对应着一个汉字。erlang里unicode模块里有几个函数可以unicode字符串list对应的整数合hQ例如:[111, 222, 333]可能表示的是一个汉字,其转换以下可得?code>[111222333]q样的Ş式?/p>

split(Str) when is_list(Str) ->
    B = list_to_binary(Str), % 必须转换为binary
    case unicode:characters_to_list(B) of
        {error, L, D} ->
            {error, L, D};
        {incomplete, L, D} ->
            {incomplete, L, D};
        UL ->
        {ok, subsplit(UL)}
    end.

subsplit([]) ->
    [];

subsplit(L) ->
    [_|R] = L,
    {PreL, _} = lists:splitwith(fun(Ch) -> not is_spliter(Ch) end, L),
    [unicode:characters_to_binary(lists:sublist(PreL, Len)) 
        || Len <- lists:seq(1, length(PreL))] ++ subsplit(R).

除了q里的拆字之外,URL的编码、数据库的存储都q好Q没遇到问题?/p>

注意Q以上针Ҏ据库本n的吐槽,完全Z我不熟悉该数据库的情况下Q不作ؓ你工具选择的参考?/p>

erlang的稳定?/h2>

都说可以用erlang来编写高定w的服务器E序。看看它的supervisorQ监视子q程Q自动重启子q程。天生的定w功能Q就你宕个几次Q单个进E自动重启,整个E序看v来还E_地在q行Q多牛逼啊。再看看erlang的进E,轻量U的语言Ҏ,像OOP语言里的一个对象一栯量。如果说使用OOP语言写程序得think in objectQ那用erlang你就得think in processQ多牛逼多骇h啊?/p>

实际上,以我的经验来看,你还得以传统的思维ȝ待erlang的进E。一些多U程E序里的问题Q在erlang的进E环境中依然存在Q例如死锁?/p>

在erlang中,对于一些异步操作,你可以通过q程间的交互这个操作包装成同步接口Q例如ping的实玎ͼ可以{到Ҏ回应之后再返回。被d的进E反正很轻量Q其包含的逻辑很单一。这不但是一U良好的包装Q甚臛_以说是一Uerlang-style。但q很Ҏ带来死锁。在最开始的时候我没有注意q个问题Q当爬虫节点C升的时候,|络数据复杂的时候,g出C死锁型宕机(q程互相{待太久Q直接timeoutQ?/p>

另一个容易在多进E环境下出现的问题就是消息依赖的上下文改变问题。当投递一个消息到某个q程Q到q个消息被处理之前,q段旉q个消息兌的逻辑q算所依赖的上下文环境改变了,例如某个ets元素不见了,在处理这个消息时Q你q得以多U程~程的思维来编写代码?/p>

至于supervisorQ这玩意你得端正态度。它不是用来包容你的傻逼错误的。当你写下傻g码导致进E频J崩溃的时候,supervisor屁用没有。supervisor的唯一作用Q仅仅是在一个确实本w可靠的pȝQ确实h品问题万分之一崩溃了,重启它。毕竟,一个重启频率的推荐|是一个小?ơ?/p>



Kevin Lynx 2013-06-20 20:40 发表评论
]]>使用erlang实现P2P力搜烦(开?http://www.shnenglu.com/kevinlynx/archive/2013/06/20/201175.htmlKevin LynxKevin LynxThu, 20 Jun 2013 06:44:00 GMThttp://www.shnenglu.com/kevinlynx/archive/2013/06/20/201175.htmlhttp://www.shnenglu.com/kevinlynx/comments/201175.htmlhttp://www.shnenglu.com/kevinlynx/archive/2013/06/20/201175.html#Feedback0http://www.shnenglu.com/kevinlynx/comments/commentRss/201175.htmlhttp://www.shnenglu.com/kevinlynx/services/trackbacks/201175.html

接上回对DHT|络的研I?/a>Q我用erlang克隆了一?a >力搜烦引擎。我q个实现包含了完整的功能QDHT|络的加入、infohash的接收、种子的获取、资源信息的索引、搜索?/p>

如下图:

screenshot

在我的笔记本上,我开启了100个DHT节点Q大致均匀地分布在DHT|络里,资源索引速度大概?时一万个左右Q包含重复资源)?/p>

q个E序包含三大部分Q?/p>

  • DHT实现QkdhtQ?a >https://github.com/kevinlynx/kdht
  • Z该DHT实现的搜索引擎,dhtcrawlerQ?a >https://github.com/kevinlynx/dhtcrawlerQ该目包含爬虫部分和一个简单的WEB?/li>

q两个项目d包含大概2500行的erlang代码。其中,DHT实现部分DHT|络的加入包装成一个库Q爬虫部分在搜烦U子Ӟ暂时没有使用P2P里的U子下蝲方式Q而是使用现成的磁力链转种子的|站服务Q这h只需要用erlang自带的HTTP客户端就可以获取U子信息。爬虫在获取到种子信息后Q将数据存储到mongodb里。WEB端我Z量用W三方库Q我只好使用erlang自带的HTTP服务器,因此|页内容的创建没有模板系l可用,只好通过字符串构建,~写h不太方便?/p>

使用

整个E序依赖了两个库Qbson-erlang和mongodb-erlangQ但下蝲依赖库的事都可以通过rebar解决Q项目文仉我已l包含了rebar的执行程序。我仅在Windows7上测试过Q但理论上在所有erlang支持的系l上都可以?/p>

  • 下蝲安装mongodb
  • q入mongodb bin目录启动mongodbQ数据库目录保存在db下,需手动建立该目?/p>

      mongod --dbpath db --setParameter textSearchEnabled=true
    
  • 下蝲erlangQ我使用的是R16B版本

  • 下蝲dhtcrawlerQ不需要单独下载kdhtQ待会下载依赖项的时候会自动下蝲

      git clone git@github.com:kevinlynx/dhtcrawler.git
    
  • cmdq入dhtcrawler目录Q下载依赖项前需保证环境变量里有gitQ例?code>D:\Program Files (x86)\Git\cmdQ需注意不要bash的目录加入进来,使用以下命o下蝲依赖?/p>

      rebar get-deps
    
  • ~译

      rebar compile
    
  • 在dhtcrawler目录下,启动erlang

      erl -pa ebin
    
  • 在erlang shell里运行爬虫,erlang语句以点?.)作ؓl束

      crawler_app:start().
    
  • erlang shell里运行HTTP服务?/p>

      crawler_http:start().
    
  • 览器里输入localhost:8000/index.htmlQ这个时候还没有索引到资源,监视|络量以观察爬虫程序是否正工?/p>

爬虫E序启动时会dpriv/dhtcrawler.config配置文gQ该文g里配|了DHT节点的UDP监听端口、节Ҏ量、数据库地址{,可自行配|?/p>

接下来我会谈谈各部分的实现方法?/p>



Kevin Lynx 2013-06-20 14:44 发表评论
]]>
Erlang使用感受http://www.shnenglu.com/kevinlynx/archive/2013/05/09/200138.htmlKevin LynxKevin LynxThu, 09 May 2013 13:24:00 GMThttp://www.shnenglu.com/kevinlynx/archive/2013/05/09/200138.htmlhttp://www.shnenglu.com/kevinlynx/comments/200138.htmlhttp://www.shnenglu.com/kevinlynx/archive/2013/05/09/200138.html#Feedback0http://www.shnenglu.com/kevinlynx/comments/commentRss/200138.htmlhttp://www.shnenglu.com/kevinlynx/services/trackbacks/200138.html

用erlang也算写了些代码了Q主要包?a >使用RabbitMQ的练?/a>Q以及最q写?a >kl_tserver?a >icerl。其中icerl是一个实CIce的erlang库?/p>

erlang的书较少Q我主要读过<Programming Erlang>?lt;Erlang/OTP in Action>。其实erlang本npa来说的话比较单,同ruby一PcMq种本n目标是应用于实际软g目的语a都比较简单,对应的语法书很快可以d?/p>

q里我仅谈谈自己在编写erlang代码q程中的一些感受?/p>

语法

erlang语法很简单,接触q函数式语言的程序员上手会很快。它没有cMcommon lisp里宏q种较复杂的语言Ҏ。其语法元素很紧凑,不存在一些用处不大的Ҏ。在q之前,我学习过ruby和common lisp。ruby代码写的比common lisp多。但是在学习erlang的过E中我的脑v里却不断出现common lisp里的语法Ҏ。这大概是因为common lisp的语法相对ruby来说Q更接近erlang?/p>

~程模式

erlang不是一个面向对象的语言Q它也不同common lisp提供多种~程模式。它的代码就是靠一个个函数l织出来的。面向对象语a在语法上有一点让我很爽的是,其函数调用更自然。erlang的接口调用就像C语言里接口的调用一P

func(Obj, args)
Obj->func(args)

即需要在函数W一个参C递操作对象。但是面向对象语a也会带来一些语法的复杂性。如果一门语a可以用很的语法元素表达很多信息Q那么我觉得q门语言是门优U的语a?/p>

表达?语句

erlang里没有语句,全部是表辑ּQ意思是所有语法元素都是有q回值的。这实在太好了,全世界都有返回值可以让代码写v来简单多了:

    Flag = case func() of 1 -> true; 0 -> false end, 

命名

我之所以不惛_一行python代码的很大一部分原因在于q门语言居然要求我必M用代码羃q来~程Q真是不敢相信。erlang里虽然没有此规定Q却也有不同的语法元素有大小写的限定。变量首字母必须大写Qatom必须以小写字母开_更霸气的是模块命名必d文g名相同?/p>

变量

erlang里的变量是不可更改的。实际上l一个变量赋|严格来说应该?code>boundQ即l定。这个特性完全就是函数式语言里的Ҏ。其带来的好处就像函数式语言宣扬的一Pq会使得代码没有副作?side effect)。因为程序里的所有函C论怎样调用Q其E序状态都不会改变Q因为变量无法被改变?/p>

变量不可更改Q直接意味着全局变量没有存在的意义,也就意味着不论你的pȝ是多么复杂地被构建出来,当系l崩溃时Q其崩溃所在位|的上下文就_扑ֈ问题?/p>

但是变量不可改变也会带来一些代码编写上的不ѝ我惌大概是编E思维的{变问题。erlang的语法特性会人编写非常短的函数Q你大概不愿意看C的函数实现里出现Var1/Var2/Var3q样的变量,而实际上q样的命名在命o式语a里其实指的是同一个变量,只不q其g同而已?/p>

但是我们的程序L应该有状态的。在erlang里我们通过不断创徏新的变量来存储这个状态。我们需要通过这个状态随着我们的程序流E不断地通过函数参数和返回g递下厅R?/p>

atom

atomq个语法Ҏ本w没问题Q它同lisp里的atom一P没什么意义,是一个名字。它主要用在增加代码的可L上。但是这个atom带来的好处,直接Derlang不去内置诸如true/falseq种关键字。erlang使用true/falseq两个atom来作为boolean operator的返回倹{但erlang里严格来说是没有布尔cd的。这其实没什么,p糕的是Q对于一些较常见的函数返回|例如true/falseQerlangE序员之间就得做U定。要表示一个函数执行失败了Q我可以q回false、null、failed、error、nilQ甚至what_the_fuckQ这一度让我迷惘?/p>

list/tuple

erlang里的list当然没有lisp里的list牛|别h整个世界是由list构成的。在一D|间里Q我一直以为list里只能保存相同类型的元素Q而tuple才是用于保存不同cd元素的容器。直到有一天我发现tuple的操作不能满x的需求了Q我才发现list居然是可以保存不同类型的?/p>

list相对于tuple而言Q更厉害的地方就在于头匹配,意思是可以通过匚w来拆分list的头和剩余部分?/p>

匚w(match)

erlang的匹配机制是个好东西。这个东西诏I了整个语言。在我理解看来,匚w机制减少了很多判断代码。它试图用一个期望的cdd配另一个东西,如果q个东西Z错,它就无法完成q个匚w。无法完成匹配就DE序断掉?/p>

匚wq有个方便的地方在于可以很方便地取出record里的成员Q或者tuple和list的某个部分,q其实增Z其他语法元素的能力?/p>

循环

erlang里没有@环语法元素,q真是太好了。函数式语言里ؓ什么要有@环语法呢Qcommon lispq毛要加上那些复杂的循环Q宏Q,每次我遇到需要写循环的场景时Q我都诚惶诚恐,最后还是用递归来解冟?/p>

同样Q在erlang里我们也是用函数递归来解军_@环问题。甚臻I我们q有list comprehension。当我写C++代码Ӟ我很不情愿用循环d那些容器遍历代码Q幸q的是在C++11里通过lambda和STL里那些算法我l于不用再写q样的@环代码了?/p>

if/case/guard

erlang里有条g判定语法ifQ甚臌有类似C语言里的switch…case。这个我一时半会还不敢评hQ好像haskell里也保留了if。erlang里同haskell一hguard的概念,q其实是一U变相的条g判断Q只不过其用场景不一栗?/p>

q程

q发性支持属于erlang的最大亮炏Verlang里的q程概念非常单,Z消息机制Q程序员从来不需要担心同步问题。每个进E都有一个mailboxQ用于缓存发送到此进E的消息。erlang提供内置的语法元素来发送和接收消息?/p>

erlang甚至提供分布式支持,更酷的是你往|络上的其他q程发送消息,其语法和往本地q程发送是一L?/p>

模块加蝲

如果我写了一个erlang库,该如何在另一个erlangE序里加载这个库Q这个问题一度让我迷惘。erlang里貌似有对库打包的功?.ez?)Q按理说应该提供一U整个库加蝲的方式,然后可以通过手动调用函数或者指定代码依赖项来加载。结果不是这栗?/p>

erlang不是按整个库来加载的Q因Z没有方式LqC个库Q应该有W三方的Q。当我们调用某个模块里的函数Ӟerlang会自动从某个目录列表里去搜烦对应的beam文g。所以,可以通过在启动erlangdq个模块文g所在目录来实现加蝲Q这q是自动的。当Ӟ也可以在erlang shell里通过函数dq个目录?/p>

OTP

使用erlang来编写程序,最大的优势可能是其OTP了。OTP基本上就是一些随erlang一起发布的库。这些库中最重要的一个概忉|behaviour。behaviour其实是提供了一U编E框Ӟ应用层提供各U回调函数给q个框架Q从而获得一个健壮的q发E序?/p>

application behaviour

application behaviour用于l织一个erlangE序Q通过一个配|文Ӟ和提供若q回调,可以让我们~写的erlangE序以一U统一的方式启动。我之前写的都是erlang库,q不需要启动,而是提供l应用层使用Q所以也没用该behaviour?/p>

gen_server behaviour

q个behaviour应该是用频率很高的。它装了进E用的l节Q本质上也就是将d收取消息Ҏ了自动收取,收取后再回调l你的模块?/p>

supervisor behaviour

q个behaviour看v来很厉害Q通过对它q行一些配|,你可以把你的q发E序里的所有进E徏立成树状l构。这个结构的牛g处在于,当某个进E挂掉之后,通过supervisor可以自动重新启动q个挂掉的进E,当然重启没这么简单,它提供多U重启规则,以让整个pȝ实通过重启变成正常状态。这实在太牛gQ这意味着你的服务器可?x24时地运行了Q就有问题你也可以立刻获得一个重写工作的pȝ?/p>

热更?/h3>

代码热更新对于一个动态语a而言其实Ҏ不上什么优点,基本上动态语a都能做到q一炏V但是把热更新这个功能加C个用于开发ƈ发程序的语言里,那就很牛g。你再一ơ可以确保你的服务器7x24时不停机维护?/p>

gen_tcp

最开始我以ؓerlang网l部分封装得已经认不出有socketq个概念了。至,你也得有一个牛逼的|络库吧。结果发C然还是socket那一套。然后我很失望。直到后来,发现使用一些behaviourQ加上调整gen_tcp的一些optionQ居然可以以很少的代码写Z个维护大量连接的TCP服务器。是啊,erlang天生是q发的,在传l的|络模型中,我们会觉得用one-thread-per-connection虽然单却不是可行的,因ؓthread是OS资源Q太昂贵。但是在erlang里,one-process-per-connection却是再自然不q的事情。你要是写个erlangE序里面却只有一个process你都不好意思告诉别Z写的是erlang。process是高效的Q对我们q种二流E序员而言Q,它就像C++里一个很普通的对象一栗?/p>

在用gen_tcp的过E中我发C个问题,不管我用哪一U模型,我竟然找不到一U温柔的关闭方式。我查看了几个tutorialQ这些؜蛋竟然没有一个h提到如何L常关闭一个erlang TCP服务器。后来,我没有办法,只好使用API强制关闭服务器进E?/p>

Story

其实Q我和erlang之间是有故事的。我q不是这个月开始才接触erlang。早?009q夏天的时候我学习过q门语言。那时候我q没接触qQ何函数式语言Q那时候lua里的闭包都让我觉得新奇。然后无意间Q我莫名其妙地接触了haskellQ?lt;Real World Haskell>Q,在我军_开始写点什么haskelll习Ӟ我发现我无从下手Q最后,Monads把我吓哭了。haskell实在太可怕了?/p>

紧接着我怀揣着对函数式语言的浓烈好奇心看到了erlang。当我看Cconcurrent programming的章节时Q在一个燥热难耐的下午我的领导扑ֈ了我Q同我探讨verlangҎ们的|游服务器有什么好处。然后,我结束我了的erlang之旅?/p>

旉四年Q这U小众语aQ居然进入了中国E序员的视野Qƈ被用于开发网|戏服务器。时代在q步Q我们L被甩在后面?/p>



Kevin Lynx 2013-05-09 21:24 发表评论
]]>
erlang和RabbitMQ学习ȝhttp://www.shnenglu.com/kevinlynx/archive/2013/04/12/199393.htmlKevin LynxKevin LynxFri, 12 Apr 2013 13:27:00 GMThttp://www.shnenglu.com/kevinlynx/archive/2013/04/12/199393.htmlhttp://www.shnenglu.com/kevinlynx/comments/199393.htmlhttp://www.shnenglu.com/kevinlynx/archive/2013/04/12/199393.html#Feedback0http://www.shnenglu.com/kevinlynx/comments/commentRss/199393.htmlhttp://www.shnenglu.com/kevinlynx/services/trackbacks/199393.html

AMQP和RabbitMQ概述

AMQP(Advanced Message Queue Protocol)定义了一U消息系l规范。这个规范描qC在一个分布式的系l中各个子系l如何通过消息交互。?a >RabbitMQ则是AMQP的一U基于erlang的实现?/p>

AMQP分布式pȝ中各个子pȝ隔离开来,子系l之间不再有依赖。子pȝ仅依赖于消息。子pȝ不关心消息的发送者,也不兛_消息的接受者?/p>

AMQP中有一些概念,用于定义与应用层的交互。这些概念包括:message、queue、exchange、channel, connection, broker、vhost?/p>

注:到目前ؓ止我q没有打用AMQPQ所以没有做更深入的学习Q仅Z找个Z写写erlang代码Q以下信息仅供参考?/em>

  • messageQ即消息Q简单来说就是应用层需要发送的数据
  • queueQ即队列Q用于存储消?/li>
  • exchangeQ有译?#8220;路由”Q它用于投递消息,应用E序在发送消息时q不是指定消息被发送到哪个队列Q而是消息投递给路由Q由路由投递到队列
  • channelQ几乎所有操作都在channel中进行,有点cM一个沟通通道
  • connectionQ应用程序与broker的网l连?/li>
  • brokerQ可单理解ؓ实现AMQP的服务,例如RabbitMQ服务

关于AMQP可以通过一很有名的文章了解更多:RabbitMQ+Python入门l典 兔子和兔子窝

RabbitMQ的运行需要erlang的支持,erlang和RabbitMQ在windows下都可以直接使用安装E序Q非常简单。RabbitMQq支持网늫的管理,q需要开启一些RabbitMQ的插Ӟ可以参?a >官方文档?/p>

RabbitMQ本质上其实是一个服务器Q与q个服务器做交互则是通过AMQP定义的协议,应用可以使用一个实CAMQP协议的库来与服务器交互。这里我使用erlang的一个客LQ对应着RabbitMQ的tutorialQ用erlang实现了一遍。基于这个过E我一些关键实现罗列出来以供记忆:

主要功能使用

关于RabbitMQ erlang client的用说明可以参?a >官方文档。这个client library下蝲下来后是两个ez文gQ其实就是zip文gQ本w是erlang支持的库打包格式Q但据说q个featureq不成熟。M我是直接解压Q然后在环境变量中指?code>ERL_LIBS到解压目录。用时使用include_lib包含库文ӞcMC语言里的头文ӞQ?/p>

    -include_lib("amqp_client/include/amqp_client.hrl").

Connection/Channel

对于q接到本地的RabbitMQ服务Q?/p>

    {ok, Connection} = amqp_connection:start(#amqp_params_network{}),
    {ok, Channel} = amqp_connection:open_channel(Connection),

创徏Queue

每个Queue都有名字Q这个名字可以h为指定,也可以由pȝ分配。Queue创徏后如果不昄删除Q断开|络q接是不会自动删除这个Queue的,q个可以在RabbitMQ的web理端看到?/p>

    #'queue.declare_ok'{queue = Q}
        = amqp_channel:call(Channel, #'queue.declare'{queue = <<"rpc_queue">>}),

但也可以指定Queue会在E序退出后被自动删除,需要指?code>exclusive参数Q?/p>

    QDecl = #'queue.declare'{queue = <<>>, exclusive = true},
    #'queue.declare_ok'{queue = Q} = amqp_channel:call(Channel, QDecl),

上例中queue的名字未指定Q由pȝ分配?/p>

发送消?/h3>

一般情况下Q消息其实是发送给exchange的:

    Payload = <<"hello">>
    Publish = #'basic.publish'{exchange = <<"log_exchange">>},
    amqp_channel:cast(Channel, Publish, #amqp_msg{payload = Payload}),

exchange有一pd规则Q决定某个消息将被投递到哪个队列?/p>

发送消息时也可以不指定exchangeQ这个时候消息的投递将依赖?code>routing_keyQ?code>routing_key在这U场景下对应着目标queue的名字:

    #'queue.declare_ok'{queue = Q}
        = amqp_channel:call(Channel, #'queue.declare'{queue = <<"rpc_queue">>}),
    Payload = <<"hello">>,
    Publish = #'basic.publish'{exchange = <<>>, routing_key = Q},
    amqp_channel:cast(Channel, Publish, #amqp_msg{payload = Payload}),

接收消息

可以通过注册一个消息consumer来完成消息的异步接收Q?/p>

    Sub = #'basic.consume' {queue = Q},
    #'basic.consume_ok'{consumer_tag = Tag} = amqp_channel:subscribe(Channel, Sub, self()),

以上注册了了一个consumerQ监听变?code>Q指定的队列。当有消息到达该队列Ӟpȝ׃向consumerq程对应的mailbox投递一个通知Q我们可以?code>receive来接收该通知Q?/p>

    loop(Channel) ->
        receive 
            % This is the first message received (from RabbitMQ)
            #'basic.consume_ok'{} -> 
                loop(Channel);
            % a delivery
            {#'basic.deliver'{delivery_tag = Tag}, #amqp_msg{payload = Payload}} ->
                echo(Payload),
                % ack the message
                amqp_channel:cast(Channel, #'basic.ack'{delivery_tag = Tag}),
                loop(Channel);
        ...

l定exchange和queue

l定(binding)其实也算AMQP里的一个关键概念,它用于徏立exchange和queue之间的联p,以方便exchange在收到消息后消息投递到队列。我们不一定需要将队列和exchangel定h?/p>

    Binding = #'queue.bind'{queue = Queue, exchange = Exchange, routing_key = RoutingKey},
    #'queue.bind_ok'{} = amqp_channel:call(Channel, Binding)

在绑定的时候需要填入一?code>routing_key的参敎ͼ不同cd的exchange对该值的处理方式不一P例如后面提到fanoutcd的exchangeӞ׃需要该倹{?/p>

更多l节

通过阅读RabbitMQ tutorialQ我们还会获得很多细节信息。例如exchange的种cRbinding{?/p>

exchange分类

exchange有四U类型,不同cd军_了其在收到消息后Q该如何处理q条消息Q投递规则)Q这四种cd为:

  • fanout
  • direct
  • topic
  • headers

fanoutcd的exchange是一个广播exchangeQ它在收到消息后会将消息q播l所有绑定到它上面的队列。绑?binding)用于队列和exchange兌h。我们可以在创徏exchange的时候指定exchange的类型:

    Declare = #'exchange.declare'{exchange = <<"my_exchange">>, type = <<"fanout">>}
    #'exchange.declare_ok'{} = amqp_channel:call(Channel, Declare)

directcd的exchange在收到消息后Q会此消息投递到发送消息时指定?code>routing_key和绑定队列到exchange上时?code>routing_key相同的队列里。可以多ơ绑定一个队列到一个exchange上,每次指定不同?code>routing_keyQ就可以接收多种routing_keycd的消息?strong>注意Q绑定队列时我们可以填入一?code>routing_keyQ发送消息时也可以指定一?code>routing_key?/strong>

topiccd的exchange相当于是direct exchange的扩展,direct exchange在投递消息到队列Ӟ是单U的?code>routing_key做相{判定,而topic exchange则是一?code>routing_key的字W串匚wQ就像正则表辑ּ一栗在routing_key中可以填入一U字W串匚wW号Q?/p>

* (star) can substitute for exactly one word.
# (hash) can substitute for zero or more words.

header exchange tutorial中未提到Q我也不q

消息投递及回应

每个消息都可以提供回应,以RabbitMQ定该消息确实被收到。RabbitMQ重新投递消息仅依靠与consumer的网l连接情况,所以只要网l连接正常,consumer卡死也不会导致RabbitMQ重投消息。如下回应消息:

    amqp_channel:cast(Channel, #'basic.ack'{delivery_tag = Tag}),

其中Tag来源于接收到消息旉?code>Tag?/p>

如果有多个consumer监听了一个队列,RabbitMQ会依ơ把消息投递到q些consumer上。这里的投递原则用了round robinҎQ也是轮流方式。如前所qͼ如果某个consumer的处理逻辑耗时严重Q则导致多个consumer出现负蝲不均衡的情况Q而RabbitMQq不兛_consumer的负载。可以通过消息回应机制来避免RabbitMQ使用q种消息数^均的投递原则:

    Prefetch = 1,
    amqp_channel:call(Channel, #'basic.qos'{prefetch_count = Prefetch})

消息可靠?/h3>

RabbitMQ可以保证消息的可靠性,q需要设|消息和队列都ؓdurable的:

    #'queue.declare_ok'{queue = Q} = amqp_channel:call(Channel, #'queue.declare'{queue = <<"hello_queue">>, durable = true}),

    Payload = <<"foobar">>,
    Publish = #'basic.publish'{exchange = "", routing_key = Queue},
    Props = #'P_basic'{delivery_mode = 2}, %% persistent message
    Msg = #amqp_msg{props = Props, payload = Payload},
    amqp_channel:cast(Channel, Publish, Msg)

参?/h2>

除了参考RabbitMQ tutorial外,q可以看看别Z用erlang是如何实现这些tutorial的,github上有一个这L目Q?a >rabbitmq-tutorials。我自己也实C一份,包括rabbitmq-tutorials中没实现的RPC。后来我发现原来rabbitmq erlang client的实现里已经包含了一个RPC模块?/p>



Kevin Lynx 2013-04-12 21:27 发表评论
]]> þԭav| þ޾ƷĻ| ҹþӰԺ| AVþ| ޾ƷþþþþͼƬ| ҹƷþþþþӰ777| Ʒþþþþ| þþƷձҰ| ھƷ˾þþþø| ŷһþþþþþôƬ| þ޾Ʒһ| þùƷƵ| ޾þһ| ľþۺĻ| þҹ³˿Ƭϼ| þǿdŮվ| þ޾Ʒ˳ۺ| þ޹vwww| þٸ۲AV| þþþùƷ۲ӰԺ| þˬˬ| 97þþƷҹһ| Ʒþþþþþþþþþþþþ | þþù׾Ʒ| ޳ɫwwwþվҹ | ˾þô߽avһ| avԾþþþa鶹| þ99ۺϾƷ| 뾫Ʒþþ| ۺϾþһ| þҹ³Ƭ| ˾þ| þպƬ| Ļþ| þþþþþþþþѾƷ| ޹ƷƬþ| ޹Ʒþþþ| Ļɫ͵͵þ| þwww˳ɿƬ| þþþAV| պAVëƬƷþþ|