使用C++编写一个DHT爬虫,实现从DHT网络爬取BT种子_c++实现dht

for (auto addr : ip_addr)
{
    struct addrinfo hints, \*info;
    memset(&hints, 0, sizeof(hints));
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_family = AF_UNSPEC;

    int error = getaddrinfo(addr.first, addr.second, &hints, &info);
    if (error)
    {
        log_error << "getaddrinfo fail, error=" << error << ", errstr=" << gai\_strerror(error);
    }
    else
    {
        struct addrinfo\* p = info;
        while (p)
        {
            if (p->ai_family == AF_INET)
            {
                send\_ping((struct sockaddr_in\*)p->ai_addr, "");
                log_debug << addr.first << ":" << addr.second << " is AF\_INET";
            }
            else
            {
                log_debug << addr.first << ":" << addr.second << " is no support the family(" << p->ai_family << ")";
            }

            p = p->ai_next;
        }
        freeaddrinfo(info);
    }
}

}


#### 4.2.3、报文解析


收到其他节点发过来的报文之后,进行报文解析,DHT网络中互相之间通信的格式是B编码,不了解B编码的可以去看这篇文章《[B编码与BT种子文件分析,以及模仿json-cpp写一个B编码解析器](https://bbs.csdn.net/topics/618317507)》,解析报文的代码如下:



// private
int DhtSearch::parse(const char* buf, int len, std::string& tid, std::string& id,
std::string& info_hash, unsigned short& port, std::string& nodes)
{
#define XX(str)
log_error << str;
return -1

int ret;
BEncode::Value root;
size_t start = 0;
if (BEncode::decode(buf, start, len, &root) || root.getType() != BEncode::Value::BCODE_DICTIONARY)
{
    XX("bencode message is invalid");
}

// tid(始终在顶层)
{
    auto value = root.find("t");
    if (value != root.end())
    {
        if (value->getType() != BEncode::Value::BCODE_STRING)
        {
            XX("\"t\" value is must be string");
        }
        tid = value->asString();
    }
}

// y(始终在顶层)
auto type_y = root.find("y");
if (type_y != root.end() && type_y->getType() == BEncode::Value::BCODE_STRING)
{
    std::string value = type_y->asString();
    if (value == "r")
        ret = REPLY;
    else if (value == "e")
    {
        XX("remote reply ERROR value");
    }
    else if (value == "q")
    {
        auto type_q = root.find("q");
        if (type_q != root.end() && type_q->getType() == BEncode::Value::BCODE_STRING)
        {
            std::string v = type_q->asString();
            if (v == "ping")
                ret = PING;
            else if (v == "find\_node")
                ret = FIND_NODE;
            else if (v == "get\_peers")
                ret = GET_PEERS;
            else if (v == "announce\_peer")
                ret = ANNOUNCE_PEER;
            else if (v == "vote" || v == "sample\_infohashes")
                return -1;
            else
            {
                XX("\"q\" value(" + v + ") is invaild");
            }
        }
        else
        {
            XX("not found \"q\" value");
        }
    }
    else
    {
        XX("\"y\" value(" + value + ") is invaild");
    }
}
else
{
    XX("not found \"y\" value");
}

BEncode::Value::iterator body_value;
if (ret == REPLY)
{
    body_value = root.find("r");
    if (body_value == root.end() || body_value->getType() != BEncode::Value::BCODE_DICTIONARY)
    {
        XX("not found \"r\" value");
    }
}
else
{
    body_value = root.find("a");
    if (body_value == root.end() || body_value->getType() != BEncode::Value::BCODE_DICTIONARY)
    {
        XX("not found \"a\" value");
    }
}

// id
{
    auto value = body_value->find("id");
    if (value != body_value->end())
    {
        if (value->getType() != BEncode::Value::BCODE_STRING)
        {
            XX("\"id\" value is must be string");
        }
        id = value->asString();
        if (id.size() != 20)
            id.clear();
    }
    else
        id.clear();
}

// info\_hash
{
    auto value = body_value->find("info\_hash");
    if (value != body_value->end())
    {
        if (value->getType() != BEncode::Value::BCODE_STRING)
        {
            XX("\"info\_hash\" value is must be string");
        }
        info_hash = value->asString();
        if (info_hash.size() != 20)
            info_hash.clear();
    }
    else
        info_hash.clear();
}

// port
{
    auto value = body_value->find("port");
    if (value != body_value->end())
    {
        if (value->getType() != BEncode::Value::BCODE_INTEGER)
        {
            XX("\"port\" value is must be int");
        }
        port = (unsigned short)(value->asInt());
    }
    else
        port = 0;
}

// nodes
{
    auto value = body_value->find("nodes");
    if (value != body_value->end())
    {
        if (value->getType() != BEncode::Value::BCODE_STRING)
        {
            XX("\"nodes\" value is must be string");
        }
        nodes = value->asString();
    }
    else
        nodes.clear();
}
return ret;

#undef XX
}


#### 4.2.4、对不同类型报文进行处理、回复


解析完成后,如果报文有效,则进行后续处理,由于我们的需求只是爬取其他人的种子,自己不进行主动查询,所以并不需要完整实现DHT协议,即不缓存其他节点信息,别人的请求有用的就接受,没用的返回一些假的信息给请求节点,通过这种骗、偷袭的方法可以使得编写出的爬虫的复杂度大大降低,接下来分析各个请求的回应方法(不知道DHT协议的请看这篇文章《[DHT协议介绍](https://bbs.csdn.net/topics/618317507)》,请务必看完,不然接下来的内容很有可能无法看懂)




| 请求类型 | 回复方法 |
| --- | --- |
| PING | 直接按标准格式回复PONG就行 |
| FIND\_NODE | 由于我们并没有缓存其他节点信息,来我们这里查找节点是不可能做到的,所以返回一个空的节点列表给它 |
| GET\_PEERS | 这个对于我们是有用的,我们要通过GET\_PEERS请求的发起者来下载种子文件,但是由于我们既没有缓存节点,也没有缓存peer,所以回复它一个空列表 |
| ANNOUNCE\_PEER | 和GET\_PEERS处理方式一样 |
| REPLY | 由于我们始终没有在主动查询任何资源,所以基本不太可能受到回复,收到的话检测报文中有没有nodes,有的话把里面的节点拿出来ping一遍,加入到更多的网络之中 |


#### 4.2.5、隐藏自己,防止被其他节点拉进黑名单


由于整个过程中欺骗其他节点的成分很大,所以每次回复别人错误信息的时候最好修改一下自己的node id,防止被其他节点加入黑名单


#### 4.2.6、获取info\_hash和peer


通过获取GET\_PEERS或者ANNOUNCE\_PEER消息中的info\_hash还有对端地址就可以开始使用BitTorrent协议来下载种子信息了(此时将对端节点视为peer,下载失败的概率会挺大,毕竟对端节点也有可能只是在找种子而已,而不是持有种子在下载资源)


### 4.3、实现BitTorrent协议


要想实现BitTorrent协议,就得先仔细看完下面两篇官方文档  
 <http://www.bittorrent.org/beps/bep_0009.html>  
 <http://www.bittorrent.org/beps/bep_0010.html>  
 里面的介绍非常简短,建议全部看完


#### 4.3.1、HandShake(握手)


从`bep_0010`中可以看到,握手的报文消息格式为:`19的ASCII码` + `BitTorrent protocol` + `\x00\x00\x00\x00\x00\x10\x00\x04` + `infohash的十六进制解码` + `二十字节长的nodeid`,infohash是种子的hash,nodeid就是我们自己的id了,需要注意的是`BitTorrent协议`除了握手消息之外的其他所有的消息的开头四个字节是消息长度(不包含长度域),对端收到消息之后,会给你返回一个至少68字节的回复信息(为什么是至少,下面扩展握手那里会讲),至于如何判断对端是接受了我们的握手呢,判断返回信息的第25位和27位即可(这个是看其他开源代码这样写的,具体原因没去深究,通过测试之后证明确实是这样)



// 握手
std::string handshake_message;
handshake_message.resize(28);
handshake_message[0] = 19;
memcpy(&handshake_message[1], "BitTorrent protocol", 19);
char ext[8];
memset(ext, 0x00, sizeof(ext));
ext[5] = 0x10;
ext[7] = 0x04;
memcpy(&handshake_message[20], ext, 8);
handshake_message += m_info_hash + m_node_id;
m_sock->send(&handshake_message[0], handshake_message.size());
int len = m_sock->recv(buf, BUF_LEN);
if (len < 68)
{
    log_debug << COMMON_PART << "(handshake) message size=" << len
        << " is too short(must be >= 68)";
    delete buf;
    return false;
}
std::string handshake\_reply(buf, 68);
std::string ext_message;
if (len > 68)
    ext_message = std::string(buf + 68, len - 68);
if (handshake_reply.substr(0, 20) != handshake_message.substr(0, 20))
{
    log_debug << COMMON_PART << "(handshake) protocol fail, message:"
        << std::endl << dump(handshake_reply);
    delete buf;
    return false;
}
if ((int)handshake_reply[25] & 0x10 == 0)
{
    log_debug << COMMON_PART << "(handshake) peer does not support extension protocol, message:"
        << std::endl << dump(handshake_reply);
    delete buf;
    return false;
}
if ((int)handshake_reply[27] & 0x04 == 0)
{
    log_debug << COMMON_PART << "(handshake) peer does not support fast protocol, message:"
        << std::endl << dump(handshake_reply);
    delete buf;
    return false;
}

下面是请求报文示例  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210505002608282.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc5ODg4Nw==,size_16,color_FFFFFF,t_70#pic_center)  
 下面是响应报文示例,大家可以自己算一下,从第四行第7个字节0x13开始算起到报文结尾,长度确实是超过了68  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210505003338546.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc5ODg4Nw==,size_16,color_FFFFFF,t_70#pic_center)


#### 4.3.2、Extend HandShake(扩展握手)


从`bep_0010`中可以看到,握手之后就要进行扩展握手了,而扩展握手是至关重要的,报文消息格式为:`消息长度` + `MSG_ID的ASCII` + `EXTEND_ID的ASCII` + `B编码的字典{‘m’:{‘ut_metadata’:1}}`  
 其中MSG\_ID为20,由于是扩展握手,EXTEND\_ID是0,完成之后,peer的响应报文里面会包含了两个我们下一步用得到的键值:ut\_metadata、和metadata\_size,这两个非常重要,拿到之后要找个变量存起来



> 
> 注意事项:协议中本来是要求握手协议和扩展握手是分开两步进行的,但是在实际测试中发现了很多peer会直接在第一次握手时就把全部数据发过来了,也就是把原本属于扩展握手的消息的应答也一并发过来,而且还有几率发不全。刚开始在写代码的时候,由于不知道这点,导致一直扩展握手失败,差点怀疑智商和码生,到后来通过抓包才了解到这个东西,所以在最终实现时必须这样做,就是第一次握手之后,如果数据量大于68个字节,把多余的内容保存下来,然后进行扩展握手,扩展握手后,把握手剩余的内容和扩展握手的内容一加,就得到正确的扩展握手数据了
> 
> 
> 


代码实现如下:



// 扩展握手
std::string ext_handshake_message;
ext_handshake_message.append(1, 20);
ext_handshake_message.append(1, 0);
ext_handshake_message += “d1:md11:ut_metadatai2ee1:v” + std::to_string(m_v.size()) + “:” + m_v + “e”;
std::string ext_handshake_message_size_str;
ext_handshake_message_size_str.resize(4);
uint32_t ext_handshake_message_size = ext_handshake_message.size();
ext_handshake_message_size = littleByteSwap(ext_handshake_message_size);
memcpy(&ext_handshake_message_size_str[0], &ext_handshake_message_size, 4);
ext_handshake_message = ext_handshake_message_size_str + ext_handshake_message;
m_sock->send(&ext_handshake_message[0], ext_handshake_message.size());
len = 0;
while (1)
{
int cur_len = m_sock->recv(buf + len, BUF_LEN - len);
if (cur_len <= 0)
break;
len += cur_len;
if (len >= BUF_LEN)
break;
}
std::string ext_reply;
if (len > 0)
ext_reply = ext_message + std::string(buf, len);
else if (!ext_message.empty())
ext_reply = ext_message;
else
{
log_debug << COMMON_PART << “(ext handshake) fail”;
delete buf;
return false;
}
// 摘取数据
// ut_metadata
size_t pos = ext_reply.find(“ut_metadata”);
if (pos == std::string::npos)
{
log_debug << COMMON_PART << “(ext handshake) parse ut_metadata fail, message:”
<< std::endl << dump(ext_reply);
delete buf;
return false;
}
pos += 12;
size_t pos_e = ext_reply.find(“e”, pos);
if (pos_e == std::string::npos)
{
log_debug << COMMON_PART << “(ext handshake) parse ut_metadata fail, message:”
<< std::endl << dump(ext_reply);
delete buf;
return false;
}
std::string ut_metadata_str = ext_reply.substr(pos, pos_e - pos);
uint32_t ut_metadata = atoi(ut_metadata_str.c_str());

// metadata\_size
pos = ext_reply.find("metadata\_size");
if (pos == std::string::npos)
{
    log_debug << COMMON_PART << "(ext handshake) parse metadata\_size fail, message:"
        << std::endl << dump(ext_reply);
    delete buf;
    return false;
}
pos += 14;
pos_e = ext_reply.find("e", pos);
if (pos_e == std::string::npos)
{
    log_debug << COMMON_PART << "(ext handshake) parse metadata\_size fail, message:"
        << std::endl << dump(ext_reply);
    delete buf;
    return false;
}
std::string metadata_size_str = ext_reply.substr(pos, pos_e - pos);
int64\_t metadata_size = atoll(metadata_size_str.c\_str());

下面是请求报文示例  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210505003845520.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc5ODg4Nw==,size_16,color_FFFFFF,t_70#pic_center)  
 下面是正常响应报文示例  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210505004336651.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc5ODg4Nw==,size_16,color_FFFFFF,t_70#pic_center)  
 下面是需要用拼接的响应报文示例(和握手剩余内容拼接),可以很想看出报文没有以字母d开头(B编码表示的报文都需要d开头表示整体是一个对象)  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210505004725345.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80Mzc5ODg4Nw==,size_16,color_FFFFFF,t_70#pic_center)


#### 4.3.3、获取metadata


我们在握手完毕,收到ut\_metadata、metadata\_size后就能进行下载了,为什么需要这两个值,因为请求的格式为:`消息长度` + `MSG_ID的ASCII` + `ut_metadata的ASCII` + `B编码的字典{‘msg_type’:0,‘piece’:piece}`  
 这里MSG\_ID为20,ut\_metadata必须为2,不然peer不会给你回复的,piece值为分片标记,协议中说,一个piece分片的长度为 16KB=16\*1024B,所以我们需要拿metadata\_size和16\*1024除法计算分片标记,代码如下



std::string data;
int piece = 0;
while (metadata_size > 0)
{
    std::string get_metadata_message;
    get_metadata_message.append(1, 20);
    get_metadata_message.append(1, 2);
    get_metadata_message += "d8:msg\_typei0e5:piecei" + std::to\_string(piece) + "ee";
    std::string get_metadata_message_size_str;
    get_metadata_message_size_str.resize(4);
    uint32\_t get_metadata_message_size = get_metadata_message.size();
    get_metadata_message_size = littleByteSwap(get_metadata_message_size);
    memcpy(&get_metadata_message_size_str[0], &get_metadata_message_size, 4);
    get_metadata_message = get_metadata_message_size_str + get_metadata_message;
    m_sock->send(&get_metadata_message[0], get_metadata_message.size());
    len = 0;
    while (1)
    {
        int cur_len = m_sock->recv(buf + len, BUF_LEN - len);
        if (cur_len <= 0)
            break;
        len += cur_len;
        if (len >= BUF_LEN)
            break;

做了那么多年开发,自学了很多门编程语言,我很明白学习资源对于学一门新语言的重要性,这些年也收藏了不少的Python干货,对我来说这些东西确实已经用不到了,但对于准备自学Python的人来说,或许它就是一个宝藏,可以给你省去很多的时间和精力。

别在网上瞎学了,我最近也做了一些资源的更新,只要你是我的粉丝,这期福利你都可拿走。

我先来介绍一下这些东西怎么用,文末抱走。


(1)Python所有方向的学习路线(新版)

这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

最近我才对这些路线做了一下新的更新,知识体系更全面了。

在这里插入图片描述

(2)Python学习视频

包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。

在这里插入图片描述

(3)100多个练手项目

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。

在这里插入图片描述

(4)200多本电子书

这些年我也收藏了很多电子书,大概200多本,有时候带实体书不方便的话,我就会去打开电子书看看,书籍可不一定比视频教程差,尤其是权威的技术书籍。

基本上主流的和经典的都有,这里我就不放图了,版权问题,个人看看是没有问题的。

(5)Python知识点汇总

知识点汇总有点像学习路线,但与学习路线不同的点就在于,知识点汇总更为细致,里面包含了对具体知识点的简单说明,而我们的学习路线则更为抽象和简单,只是为了方便大家只是某个领域你应该学习哪些技术栈。

在这里插入图片描述

(6)其他资料

还有其他的一些东西,比如说我自己出的Python入门图文类教程,没有电脑的时候用手机也可以学习知识,学会了理论之后再去敲代码实践验证,还有Python中文版的库资料、MySQL和HTML标签大全等等,这些都是可以送给粉丝们的东西。

在这里插入图片描述

这些都不是什么非常值钱的东西,但对于没有资源或者资源不是很好的学习者来说确实很不错,你要是用得到的话都可以直接抱走,关注过我的人都知道,这些都是可以拿到的。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里无偿获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO系列是基于深度学习的端到端实时目标检测方法。 PyTorch版的YOLOv5轻量而高性能,更加灵活和易用,当前非常流行。 本课程将手把手地教大家使用labelImg标注和使用YOLOv5训练自己的数据集。课程实战分为两个项目:单目标检测(足球目标检测)和多目标检测(足球和梅西同时检测)。  本课程的YOLOv5使用ultralytics/yolov5,在Windows和Ubuntu系统上分别做项目演示。包括:安装YOLOv5、标注自己的数据集、准备自己的数据集(自动划分训练集和验证集)、修改配置文件、使用wandb训练可视化工具、训练自己的数据集、测试训练出的网络模型和性能统计。 除本课程《YOLOv5实战训练自己的数据集(Windows和Ubuntu演示)》外,本人推出了有关YOLOv5目标检测的系列课程。请持续关注该系列的其它视频课程,包括:《YOLOv5(PyTorch)目标检测:原理与源码解析》课程链接:https://edu.csdn.net/course/detail/31428《YOLOv5目标检测实战:Flask Web部署》课程链接:https://edu.csdn.net/course/detail/31087《YOLOv5(PyTorch)目标检测实战:TensorRT加速部署》课程链接:https://edu.csdn.net/course/detail/32303《YOLOv5目标检测实战:Jetson Nano部署》课程链接:https://edu.csdn.net/course/detail/32451《YOLOv5+DeepSORT多目标跟踪与计数精讲》课程链接:https://edu.csdn.net/course/detail/32669《YOLOv5实战口罩佩戴检测》课程链接:https://edu.csdn.net/course/detail/32744《YOLOv5实战中国交通标志识别》课程链接:https://edu.csdn.net/course/detail/35209 《YOLOv5实战垃圾分类目标检测》课程链接:https://edu.csdn.net/course/detail/35284  

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值