简介
几周前,我完成了多线程端口检查器的Python实现,但对获得的结果(速度)并不满意。我需要它更快,所以我用CPP(还有一点C)构建了另一个。
如果有人想看的话,这里是Python代码。
此刻,我没有为我的脚本实现线程模块,因为我想就什么是最明智的选择获得一些建议(在此处禁止添加该部分的答案,我不介意看到它的示例)

说明

该程序将以下内容作为命令行参数:

每行包含一个域名的文本文件
将检查上述域的端口号
线程数
超时
一个文本文件,将在其中写入打开端口的域(及其IP地址),例如:google.com:172.217.22.46



该程序当前正在做的是将每个域转换为IP地址,并检查给定的端口是否打开。如果已打开,则将domain:ip写入输出文件。
可以使用以下命令编译并运行该程序:
g++ port_checker.cpp -o checker
./checker domains.txt 80 2 2 output.txt


代码
#include <iostream>
#include <fstream>
#include <vector>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>

std::vector<std::string> get_domains_from_file(std::string domains_file) {
    std::vector<std::string> domains_array;
    std::ifstream file(domains_file);
    std::string domain;

    if (!file.is_open()) {
        std::cerr << "Unable to open domains file!" << std::endl;
        std::exit(-1);
    }

    while (file >> domain) {
        domains_array.push_back(domain);
    }

    return domains_array;
}

int main(int argc, char *argv[]) {
    struct hostent *h;
    struct sockaddr_in servaddr;

    int sd, rval;

    if (argc != 6) {
        std::cerr << "The number of arguments should be 5 e.g: ./checker domains.txt [port] [threads] [timeout] output.txt" << std::endl;
        std::exit(-1);
    }

    std::string domains_file = argv[1];
    std::string output_file  = argv[5];
    int port                 = atoi(argv[2]);
    int threads              = atoi(argv[3]);
    int timeout              = atoi(argv[4]);

    std::vector<std::string> domains = get_domains_from_file(domains_file);
    
    std::ofstream myfile;
    myfile.open(output_file);

    for (int i = 0; i < domains.size(); i++) {
        if ((h=gethostbyname(domains[i].c_str())) == NULL) {
            std::cerr << "Error when using gethostbyname" << std::endl;
            std::exit(-1);
        }
        // std::cout << inet_ntoa(*((struct in_addr *)h->h_addr)) << std::endl;

        sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (sd == -1) {
            std::cout << "Error when trying to create socket !" << std::endl;
            std::exit(-1);
        }

        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);

        memcpy(&servaddr.sin_addr, h -> h_addr, h -> h_length);

        rval = connect(sd, (struct sockaddr *) &servaddr, sizeof(servaddr));

        if (rval == -1) {
            // std::cout << "Port is closed for domain: " << domains[i] << std::endl;
            continue;
        }

        else {
            myfile << domains[i] << ":" << inet_ntoa(*((struct in_addr *)h->h_addr))  << std::endl;
        }
        close(sd);
    }
    return 0;
}

我最想从这篇评论中得到的是:

我如何改善性能? (这是我首先关心的问题)
还有其他检查开放端口的方法吗?
如何在可读性/最佳实践方面改进我的代码?

注意:请记住,这是我很长时间以来第一个C ++程序,因此请确保在您的评论中添加尽可能多的信息。

评论

嗨!通常人们使用C ++标记进行搜索,因此既然您已经用完了全部五个,就可以用C ++标记替换最不重要的一个吗? C ++ 11将是一个不错的选择,您可以在文章的开头提到它。

这种事情完全是网络绑定的,python和C ++一样好(使用相同的方法)。 python可能不会像您那样泄漏打开的套接字。

当您检查多个主机时,使用异步网络API(例如在Boost.Asio的帮助下)将获得最佳性能提升。

由于您有两个解决方案,因为第一个解决方案的速度不够快,您是否也对两个方案都进行了基准测试?

@桅杆这几天我一定会这样做!

#1 楼

检查TCP端口和多线程

对于顺序程序,您的性能应该不错。检查TCP端口是否打开的另一种方法是仅发送初始SYN并等待ACK。这样,您不必进行完整的TCP握手。但是,这也意味着您必须自己实现TCP协议的这一部分。此外,某些平台不允许为受限制的用户使用原始IP套接字,这可能会带来另一个问题。

但是,您目前无法实现多线程,至少不能超过当前水平for循环。 gethostbyname不是线程安全的。您必须在此处使用另一个功能,否则最终将导致数据争用,从而导致未定义的行为。 GNU提供了gethostbyaddr_r,它是POSIX标准的扩展。

可以这么说,让我们看一下您当前的代码。

正确包含

#include <fstream>
#include <vector>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <unistd.h>


您缺少几个包含项。 coutcerr需要<iostream>memcpymemset需要<cstring>exit需要cstdlib

更加用户友好

我们可以将您的get_domains_from_file用作案例研究:

std::vector<std::string> get_domains_from_file(std::string domains_file) {
    std::vector<std::string> domains_array;
    std::ifstream file(domains_file);
    std::string domain;

    if (!file.is_open()) {
        std::cerr << "Unable to open domains file!" << std::endl;
        std::exit(-1);
    }

    while (file >> domain) {
        domains_array.push_back(domain);
    }

    return domains_array;
}


此刻,只要遇到问题,您只需exit(-1)。我在那里看到两个问题:


退出代码不产生任何信息,除了“成功”或“失败”
,您无法恢复该错误,因为即使您未能为第6000个域创建套接字也要继续运行的示例。

请考虑备用输入和输出流

此外,因为我们目前在在UNIX平台上,您的程序不是非常友好的管道。一个人只能从一个文件中获取域,但是在实际情况下,您可能需要检查一些日志,例如

grep "acme" /var/log/attacks | ./domain-checker 80 4 2


如果我们在此处使用istream,至少可以使您的程序准备就绪:

您可以使用get_domains_from_stream实现您的其他功能:

std::vector<std::string> get_domains_from_stream(std::istream & in) {
    std::vector<std::string> domains_array;
    std::string domain;

    while (in >> domain) {
        domains_array.push_back(domain);
    }

    return domains_array;
}


请注意,在这种情况下,我返回了一个空向量。无论是返回空资源,引发异常还是执行其他操作,在这里或多或少都是一个优先点。但是从用户的角度来看,“我指定了一个空文件,哎呀”和“我指定了一个不存在的文件,哎呀”是同一种错误:用户给程序输入了错误的参数。

更好的用法消息和输入处理

虽然我们出现错误消息,但是您可以改善参数用法错误消息:

std::vector<std::string> get_domains_from_file(std::string domains_file) {
    std::ifstream file(domains_file);

    if (!file.is_open()) {
        std::cerr << "Unable to open domains file!" << std::endl;
        return {};
    } else {
        return get_domains_from_stream(file);
    }
}


您不知道在用户PC上调用该应用程序是什么,因此只需使用arg[0]

谈到参数,您的程序如何处理无效输入? br />
std::cerr << "The number of arguments should be 5 e.g: "
          << arg[0] << " domains.txt [port] [threads] [timeout] output.txt" 
          << std::endl;


没有错误消息,没有异常。这是由于atoi,它仅返回0。更可取的是std::stoi,如果出现问题,它会引发异常: cout通常适合输出。如果用户要写入文件,则可以使用重定向:

$ ./a.out domains.txt abc def ghi blarg.out


尽可能限制变量的范围

仍然在程序的同一部分中,定义int sdint rvalstruct hostent *h;和其他的太早了。您总是尝试限制变量的范围。因此,在这一点上摆脱它们。另外,请经常使用const,以确保您不进行任何更改。在某些情况下,它还将帮助编译器优化程序。 for -loop

现在,在上述讨论之后,让我们看一下您的for循环。由于您使用的是C ++ 11,请考虑基于范围的for -loop:

struct hostent *h;             // hm, what are these?
struct sockaddr_in servaddr;   // where do you use them?

int sd, rval;                  // what are these ints?

const std::string domains_file = argv[1];
const std::string output_file  = argv[5];
const int port                 = std::stoi(argv[2]);
const int threads              = std::stoi(argv[3]);
const int timeout              = std::stoi(argv[4]);


我认为这里不适合使用std::exit。在domain.txt的第一行中有一个小的错字应该不会阻止其余部分的检查。但是,更多一点的信息将是很好的:

$ grep "acme.evil" | ./checker 80 4 2 > evil.ip


请注意,hstrerrorh_errno被认为已过时,但gesthostbyname也已被淘汰。

资源泄漏

如果sdrval,则您忘记关闭-1。这可能会导致“句柄过多”错误,并且您无法继续扫描所有这些端口。解决该问题的一种方法是不使用continue

for(const auto & domain : domains){
    struct hostent * h = gethostbyname(domain.c_str());


但是,这也容易出错。输入RAII:

    if (h == nullptr) {
        std::cerr << "Error when using gethostbyname: "
                  << hstrerror(h_errno)
                  << " (on domain: " << domain << ")"
                  << std::endl;
        continue;
    }


这基本上是int的包装。但是,最重要的部分是它将自动在该close上调用int,因此您不会忘记它。这将使您的代码更健壮。请注意,移动操作仅是一笔小小的奖金,因此这是一个过大的杀伤力。您可能不需要它们。但是,仍然必须禁止复制该包装程序。我们最终得到:

if(rval != -1){
    myfile << domains[i] << ":" << inet_ntoa(*reinterpret_cast<struct in_addr *>(h->h_addr))  << std::endl;
}
close(sd);


我们不必担心忘记关闭套接字,因为它现在会自动关闭。请注意,生成的代码与您当前的代码几乎相同。这样更安全。

使用正确的强制转换

您可能已经在上面的代码中看到了。使用适当的演员表。 C样式强制转换应是最后的选择。

关于性能的说明

程序中有两个瓶颈:


获取域名的地址信息。
连接到给定的地址。

请注意,在连接时可以获取其他域的地址信息。因此,对于多线程的第一个实现,您可以在一个线程中获取地址信息,然后在另一个线程中进行连接。通过线程安全的可关闭队列连接它们。

但是,由于其阻塞性质,如果端口未打开,则connect调用可能会阻塞一段时间。如果要保留在POSIX领域,则必须使用多个线程同时连接。一个工作池可能适合于此。诸如Boost.ASIO的某些库为您提供异步IO操作,这使此操作变得更加容易。 Boost还提供了线程安全的无锁队列,因此,如果您确实在争夺最后的性能,则不必自己实现它们。

但还是一如既往:在开始之前优化,分析您的程序。对于顺序程序,此刻您实际上无能为力。您读取一个域名,获得其网络地址,尝试连接,并记住结果。这正是您所做的。

评论


\ $ \ begingroup \ $
<< arg [0] <<“ domains.txt [port] [threads] [timeout] output.txt”不应在domains.txt之前有空格吗?我认为否则会丢失输出中的空格(例如./checkerdomains.txt ...),但是我不是C开发人员,因此可能会有一个我不知道的尾随空格。
\ $ \ endgroup \ $
– Pokechu22
17年2月19日在17:39

\ $ \ begingroup \ $
@ Pokechu22不,您是对的。固定。
\ $ \ endgroup \ $
– Zeta
17-2-19在17:39



\ $ \ begingroup \ $
这是一篇很棒的评论,其中涵盖了许多方面,并提供了精心编写的示例。荣誉
\ $ \ endgroup \ $
– isanae
17年2月19日在17:59

\ $ \ begingroup \ $
@Zeta是一个令人难以置信的答案。我没想到你还会有其他事情。我将应用您明天建议的更改。谢谢 !
\ $ \ endgroup \ $
– Grajdeanu Alex
17年2月19日在19:49

\ $ \ begingroup \ $
Nitpick:对于socket_wrapper,您使用蛇形保护套,那么,为什么还要对isValid使用驼峰式保护套呢?
\ $ \ endgroup \ $
–someonewithpc
17年2月20日在12:48

#2 楼

以下是一些可以帮助您改进程序的内容。

使用必需的#include s

代码使用memcpymemset,这意味着应该使用#include <cstring>。重要的是,必须在程序中包含所有适当的#include行,以确保可移植性,即使在没有在特定计算机上编译它们的情况下也可以进行移植。也缺少<string>

不要误导用户

程序的用法字符串在方括号中显示“ [端口] [线程] [超时]”。请注意,在那种情况下将内容放在方括号中通常意味着该参数是可选的,在这里似乎不是目的。

使用标准算法

get_domains_from_file例程可以完全由标准算法代替。特别是,可以使用以下代码代替:

std::string domains_file = argv[1];
std::vector<std::string> domains = get_domains_from_file(domains_file);


您可以使用以下代码:

std::ifstream in{argv[1]};
std::vector<std::string> domains;
std::copy(std::istream_iterator<std::string>(in),
    std::istream_iterator<std::string>(), 
    std::back_inserter(domains));


声明并初始化第一步

该代码当前包括以下两行:

std::ofstream myfile;
myfile.open(output_file);


我主张像这样组合它们:

std::ofstream myfile{output_file};


将程序分解为较小的部分

现在,几乎所有代码都在main中,这不一定是错误的,但这意味着不仅困难重用,但也很难排除故障。更好的方法是将代码分成小块。它既易于理解,又易于修复或改进。在这种情况下,我想说一个有意义的功能是取一个域名并返回一个bool,表示端口已打开,或者返回一个非空字符串,其中包含发现打开的地址端口。

不要使用过时的功能

gethostbyname()inet_ntoa一样已过时。相反,您可以使用getaddrinfoinet_ntop或更好的东西,例如boost asio库。将此建议与前一个建议结合起来,我们可以编写这样的函数:

std::string isPortOpen(const std::string &domain, const std::string &port)
{
    addrinfo *result;
    addrinfo hints{};    // aggregate initialization (no need for memset)
    hints.ai_family = AF_UNSPEC;   // either IPv4 or IPv6
    hints.ai_socktype = SOCK_STREAM; 
    char addressString[INET6_ADDRSTRLEN];  // big enough for IPv4 or IPv6
    const char *retval = nullptr;
    if (0 != getaddrinfo(domain.c_str(), port.c_str(), &hints, &result)) {
        return "";
    }
    for (addrinfo *addr = result; addr != nullptr; addr = addr->ai_next) {
        int handle = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
        if (handle == -1) {
            continue;
        }
        if (connect(handle, addr->ai_addr, addr->ai_addrlen) != -1) {
            switch(addr->ai_family) {
                case AF_INET:
                    retval = inet_ntop(addr->ai_family, 
                        &(reinterpret_cast<sockaddr_in *>(addr->ai_addr)->sin_addr), 
                        addressString, INET6_ADDRSTRLEN);
                    break;
                case AF_INET6:
                    retval = inet_ntop(addr->ai_family, 
                        &(reinterpret_cast<sockaddr_in6 *>(addr->ai_addr)->sin6_addr), 
                        addressString, INET6_ADDRSTRLEN);
                    break;
                default:
                    // unknown family
                    retval = nullptr;
            }
            close(handle);
            break;
        }
    }
    freeaddrinfo(result);
    return retval==nullptr ? "" : retval;
}


与现有代码相比,它具有多个优点。首先,它要短得多(至少对我来说!)更容易阅读和理解。其次,它不使用过时的功能。第三,无需修改代码即可处理IPv4或IPv6。第四,代码是可重入的,如果您决定使该线程成为多线程,这将很重要。

使用“ range for”并简化您的代码

这里是替代实现使用上面发布的功能的程序的代码:

int main(int argc, char *argv[]) {
    if (argc != 6) {
        std::cerr << "Usage: " << argv[0] << " domains.txt port threads timeout output.txt\n";
        return 0;
    }

    std::string domains_file{argv[1]};
    std::string output_file{argv[5]};
    std::string port{argv[2]};
    int threads {atoi(argv[3])};
    int timeout {atoi(argv[4])};

    std::ifstream in{domains_file};
    std::vector<std::string> domains;
    std::copy(std::istream_iterator<std::string>(in),
        std::istream_iterator<std::string>(), 
        std::back_inserter(domains));

    std::ofstream myfile{output_file};

    // this is the range for syntax
    for (const auto &domain: domains) {
        std::string addr = isPortOpen(domain, port);
        if (addr.length()) {
            myfile << domain << ":" << addr << "\n";
        }
    }
    myfile.close();
}


使用std::future简化多线程处理,使用std::future实际上非常简单。一种方法是用以下代码替换上面for中显示的main循环:此代码未指定std::async的启动策略。可以轻松地将启动策略添加为std::async的第一个参数,但是在这种情况下,它可能并没有多大区别。

如果真的不需要std::endl,请不要使用

std::endl'\n'之间的区别在于,'\n'只是发出换行符,而std::endl实际上是刷新流。在具有大量I / O的程序中,这可能很耗时,实际上很少需要。最好仅在有充分的理由冲洗流时才使用std::endl,而对于像这样的简单程序,它并不是经常需要的。避免在使用std::endl时习惯使用'\n'的习惯,这将在将来为您带来回报,因为您编写具有更多I / O的更复杂的程序并且需要最大化性能。 br />
当C或C ++程序到达return 0的末尾时,编译器将自动生成返回0的代码,因此无需将main显式放在return 0;的末尾。

注意:当我提出此建议时,几乎总是在以下两种评论中的一种:“我不知道。”或“这是个坏建议!”我的理由是,依靠该标准明确支持的编译器行为既安全又有用。对于C,自C99起;参见ISO / IEC 9899:1999第5.1.2.2.3节:从初始调用到main函数的返回等效于使用以下命令调用main函数: exit函数作为其参数返回的值;对于C ++,自1998年的第一个标准以来,到达终止main函数的}返回值0。请参见ISO / IEC 14882:1998第3.6.1节:


如果控制到达main的末尾而没有遇到return语句,则其作用是执行return 0;


从那时起,两个标准的所有版本(C99和C ++ 98)都保持了相同的想法。我们依靠C ++中自动生成的成员函数,很少有人在main函数的末尾编写显式的return;语句。拒绝的原因似乎可以归结为“看起来很奇怪”。如果像我一样,您对更改C标准的理由感到好奇,请阅读此问题。还应注意,在1990年代初,这被视为“草率做法”,因为当时它是不确定的行为(尽管得到广泛支持)。

所以我主张省略它;其他人则不同意(通常是非常激烈!)无论如何,如果遇到忽略它的代码,您就会知道该标准已明确支持它,并且您会知道它的含义。

全部放入一起

这里是怎么做:

std::vector<std::future<std::string>> results;
for (const auto &domain: domains) {
    results.push_back(std::async(isPortOpen, domain, port));
}
std::for_each(results.begin(), results.end(), 
    [&myfile](std::future<std::string>& f){myfile << f.get() << '\n';});


这就是全部,可以使用以下命令进行编译:


g ++-墙-Wextra -pedantic -std = c ++ 11 portchek.cpp -o portchek -lpthread


评论


\ $ \ begingroup \ $
好答案@爱德华。再想一想,您能否编辑您的答案并在使用所有给定建议后将您认为应该的整个代码添加为最后一步? ^ _ ^我无法使其正常运行
\ $ \ endgroup \ $
– Grajdeanu Alex
17-2-26在12:01

\ $ \ begingroup \ $
非常感谢。显然我没有像您写的那样运行它:)另一个问题(仅在您需要时回答:有没有办法将int线程的线程数传递给它?我在文档中找不到任何内容
\ $ \ endgroup \ $
– Grajdeanu Alex
17-2-26在14:23

\ $ \ begingroup \ $
您可以将std :: launch :: async添加为std :: async每次调用的第一个参数,然后在线程组中启动它们,但是随后您必须跟踪每个线程何时完成推出一个新的。换句话说,这可能比付出更大的努力。普通的旧std :: async可能会更高效且更易于使用。
\ $ \ endgroup \ $
–爱德华
17年2月26日在14:37