谈谈自己对于DHT的一些理解
这篇只是随便说说自己对于DHT的一些理解,并不太深入,只是能够正常弄出爬虫就行了~
网络协议
DHT是基于Kademila,运行于UDP协议上,所以咱们的爬虫也需要使用运行在UDP上。
传输格式
在DHT中,所有的请求与回复都使用一种统一的编码格式,就是bencoded编码格式,这里简称为B编码。
五种信息
DHT网络有四种请求,分别是:ping、find_node、get_peers、announce_peer
还有一种信息,就是错误信息
而咱们需要用的的就是find_node、get_peers、announce_peer三种请求
ping
和平时命令行使用的ping也差不多是一样一样的,就是确认你是否在线之类的功能。
请求格式
{"t":"aa", "y":"q","q":"ping", "a":{"id":"abcdefghij0123456789"}}
B编码
d1:ad2:id20:abcdefghij0123456789e1:q4:ping1:t2:aa1:y1:qe
回复格式
{"t":"aa", "y":"r", "r":{"id":"mnopqrstuvwxyz123456"}}
B编码
d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
find_node
这个请求是向服务器发出寻找节点的请求,咱们发起请求,其中包含了一个id,这个是自己的id,还有一个target,是表明要查找那个节点,这个请求是比较重要的请求,因为咱们就是通过它来认识新朋友的。
请求格式
{"t":"aa", "y":"q","q":"find_node", "a":{"id":"abcdefghij0123456789","target":"mnopqrstuvwxyz123456"}}
B编码
d1:ad2:id20:abcdefghij01234567896:target20:mnopqrstuvwxyz123456e1:q9:find_node1:t2:aa1:y1:qe
回复格式
{"t":"aa", "y":"r", "r":{"id":"0123456789abcdefghij", "nodes":"def456..."}}
B编码
d1:rd2:id20:0123456789abcdefghij5:nodes9:def456...e1:t2:aa1:y1:re
get_peers
在P2P中,要通过种子文件下载一个资源,需要知道整个P2P网络中有哪些计算机正在下载/上传该资源。这里将这些提供某个资源下载的计算机定义为peer,而get_peers请求就是发送info_hash信息到服务器中,查找这些peer的一个请求,请求发送到服务器,服务器会在路由表中查找对应的peer,客户端获取到peer后就可以通过这些peer下载资源了。
这里需要注意,在服务器发送回复时,可能遇到两种情况,一种是服务器的路由表中存在info_hash对应的peer,那么服务器会直接返回一个带有values信息的数据,这其中包含了peer信息;还有一种情况,就是服务器中并没有存放对应的peer,那么服务器会返回一个nodes信息,表示与info_hash对应的peer最相近的一些节点列表,而咱们在爬虫中用到的仅仅是nodes而已。
同时,在get_peers中,还要注意,服务器在回复咱们的时候,会存在一个token,这个token就是后面announce_peer时验证使用的。
请求格式
{"t":"aa", "y":"q","q":"get_peers", "a":{"id":"abcdefghij0123456789","info_hash":"mnopqrstuvwxyz123456"}}
B编码
d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz123456e1:q9:get_peers1:t2:aa1:y1:qe
回复格式
{"t":"aa", "y":"r", "r":{"id":"abcdefghij0123456789", "token":"aoeusnth","values": ["axje.u", "idhtnm"]}}{"t":"aa", "y":"r", "r":{"id":"abcdefghij0123456789", "token":"aoeusnth","nodes": "def456..."}}
B编码
d1:rd2:id20:abcdefghij01234567895:token8:aoeusnth6:valuesl6:axje.u6:idhtnmee1:t2:aa1:y1:red1:rd2:id20:abcdefghij01234567895:nodes9:def456...5:token8:aoeusnthe1:t2:aa1:y1:re
announce_peer
当客户端发送get_peers请求并获取到peer信息后,客户端开始下载资源,这时就会发送一个信息给服务端,表明自个儿正在哪里哪里下载着什么资源,咱们爬虫最需要的就是这个信息了!
首先,请求中包含有一个info_hash信息,这个就是磁力链的关键,infohash,其次,会包含一个port,表明下载资源所用的端口,如果需要直接下载资源,就需要用到这个端口号,不过咱们只是要info_hash,所以可以略过,还有一个信息就是token,上一步服务器回复时发送了token,这时客户端再发起announce_peer请求,就会把这个token也同时发过来,提供给服务器验证使用。
请求格式
{"t":"aa", "y":"q","q":"announce_peer", "a":{"id":"abcdefghij0123456789","info_hash":"mnopqrstuvwxyz123456", "port":6881, "token": "aoeusnth"}}
B编码
d1:ad2:id20:abcdefghij01234567899:info_hash20:mnopqrstuvwxyz1234564:porti6881e5:token8:aoeusnthe1:q13:announce_peer1:t2:aa1:y1:qe
回复格式
{"t":"aa", "y":"r", "r":{"id":"mnopqrstuvwxyz123456"}}
B编码
d1:rd2:id20:mnopqrstuvwxyz123456e1:t2:aa1:y1:re
错误信息
除了四种请求格式外,作为一个完整的协议,不可避免的包含了错误信息,否则如果像HTTP一样遇到404,咱们怎么来说呢? DHT包含了四种错误格式,分别为:
201:一般错误 202:服务错误 203:协议错误,比如不规范的包,无效的参数,或者错误的token 204:未知方法
报文格式
{"t":"aa", "y":"e", "e":[201,"A Generic Error Ocurred"]}
B编码
d1:eli201e23:AGenericErrorOcurrede1:t2:aa1:y1:ee
爬虫原理
嗯,明白了DHT的基础原理,那么爬虫也就明白一半了!
首先,我们要制作的是爬虫,不是要实现一个完整的DHT协议,所以,我们只需要选择对于爬虫有用的东西就行了。
ping请求我们需要么?不需要,因为我们无需去验证谁在线,我管他谁在线呢,我只需要不停的找朋友就行,那么ping回复呢?我们也无需保留,直接略过就可,但这里又存在一个问题!
DHT协议中,每个节点都有一张路由表,路由表中记录了众多的节点信息,而这些节点信息会根据情况来分为很好、可疑、坏的三种状态,如果来划分这三种状态?发送ping就是一种方法,当我的朋友发送ping请求给我时,若我不回复,那么我的朋友可能会认为我网络卡了,也许是被那啥给屏蔽了,朋友将再次发送一次请求给我,如果我还是不回复呢?说不定朋友就把你列入黑名单,认为你是坏人了,很可能在下一秒钟就会把你给踢出自己的路由表中。
SO,为了保持自己的活跃度,让大家都认为你是值得深交的朋友,我个人建议保留ping回复,去除ping请求即可。
在DHT网络中,要想更多的获取到我们需要的info_hash,就必须要去更多的认识新朋友,也就是find_node,显然,这个请求对于我们是很有用的,通过它我们可以找到很多很多朋友,那这个请求我们保留,相应的,与ping一样,为了保持自己的青春活力,个人建议同样保留find_node回复。
当我们认识了很多很多的朋友时,如果让朋友们发送我们需要的info_hash呢?announce_peer就是我们需要的,当朋友们发送announce_peer请求给我们时,我们就可以记录下这个info_hash信息,也就可以通过info_hash来生成一个磁力链了,很显然,announce_peer我们是需要保留的,那么是保留请求还是回复呢?
对于爬虫来说,我们并不需要下载任何东西,我们需要的仅仅是info_hash而已,那既然不需要下载,只需要一个字段,而这个字段还是朋友们塞给我的,咱们还需要请求么?当然不需要了,咱们只需要对朋友们发来的announce_peer做下回应就行,故此,保留announce_peer回复,去除announce_peer请求。
刚才说到,获取info_hash是通过announce_peer来获取,那么announce_peer的前提条件又是什么呢?还记得刚才说过的么?客户端发送get_peers请求,服务器找到peer或者nodes列表,客户端通过peer或者nodes列表查找到资源并开始下载,将会再次对服务器发起一个请求,通知服务器:感谢你,哥们儿,我找到爱情动作片了,正在XXX.XXX.XXX.XXX的XX端口下载呢,你要不要一起来看看?
显而易见,要获得info_hash需要通过announce_peer,要得到announce_peer则需要通过get_peers,而get_peers也与announce_peer一样的选择,记住我们是爬虫,我们无需下载资源,我们只是接受资源,那么,get_peers同样的保留回复,去除请求。
最后,还有一个错误信息,这个需要保留么?当然需要!至少发个错误信息给人家也还是可以保持活力的!
好了,要留下什么也知道了,那么爬虫要如何运行呢?
咱们自己先把自己变为一个节点,也就是生成一个node id,这是一个唯一标识,和IP一样一样的,当咱们有了自己的node id,有了合法身份,爬虫先生就利用这个合法身份伪装一下,骗过网络中那些诚实的朋友,见到一个就握握手,Say hello,朋友一多,爬虫先生的交友圈一广,朋友们就开始来找爬虫先生办事儿了:我最近想找苍老师的片子,你这里有没有? 爬虫先生赶紧翻了翻电脑,然后抱歉的说:哦,朋友,非常抱歉,我这里没有,不过我知道谁有,你去找他们看看!朋友屁颠屁颠的去找爬虫先生推荐的朋友,终于找到了片子,又乐呵呵的找到爬虫先生:嘿!朋友,我已经找到片子了,你需要么?直接登录XXX.XXX.XXX.XXX,端口号XXX就可以下载了,哦对了,她的番号是XXX-222!爬虫先生喜滋滋的送别了朋友,悄悄记下了番号……
好吧,说了这么多,咱们要认识朋友,就需要发送find_node请求,而发送find_node之前,我们是不认识任何朋友的,那第一个朋友又如何来结交呢?幸好咱们有一个交友网站,也就是爬虫先生知道几个圈内人,只需要找到圈内人,向他们来索取朋友信息就行了!
当爬虫先生喜滋滋的和圈内人索要朋友信息,却半天也收不到回复,颇为奇怪,再次询问才知道:靠,原来爬虫先生连门都没开,圈内人把朋友信息送到了门口,爬虫先生却接收不到,哎呀!这个脑子啊!
于是爬虫先生赶紧打开自家大门,也就是监听了一个端口,然后再次向圈内人发起find_node请求,圈内人将朋友信息通过这个端口发送给了爬虫先生,爬虫先生乐呵呵的接收了,再逐条的向朋友信息发送着find_node请求,通过这些朋友再次认识新朋友,正是:有了新朋友,不忘老朋友~
于是,在爬虫先生不断的find_node中,爬虫先生认识了无数的朋友,朋友们开始在圈内找寻着各种苍老师、松岛老师……
每次朋友向爬虫先生发送get_peers请求,找各位老师,爬虫先生都热心的找到nodes给朋友,朋友找到了需要的资源,又乐呵呵的发送announce_peer请求给爬虫先生,告诉爬虫先生下片地址,爬虫先生嘿嘿笑了笑:小样儿,咱不下片,咱只是大番号的搬运工!
好吧,总结下: 爬虫开启端口,伪装正常node,向固定node发送find_node请求,查找到node列表,对node列表逐条解析,并逐条发送find_node请求,获取到node列表又再次逐条发送find_node请求,如此这般无限循环……
当爬虫发送足够多的find_node请求后,接收过爬虫请求的node会将爬虫伪装的node信息保留下来,并在需要的时候向爬虫发出get_peers请求,爬虫对请求进行回复,当node查找到对应资源,将向爬虫发送一个announce_peer请求,通知爬虫正在下载,爬虫记录下资源info_hash。
这就是爬虫的运行原理,至于怎么实现?这就仁者见仁,智者见智了~反正我现在正调试着~