多线程端口扫描器
实验目的
实现一个多线程端口扫描程序
实验要求
- 能同时扫描5个IP地址
- 针对每个IP地址开设100个线程对其进行扫描
- 如果端口打开,使用函数getservbyport获取其服务名,在屏幕上打印:IP port servername,如果是未知服务,则屏幕显示:ip port unkonown
相关知识及程序设计
端口扫描原理
在Linux系统下实现端口扫描程序的原理是通过网络套接字编程来进行网络通信。套接字编程基于TCP/IP协议栈,通过套接字可以建立客户端与服务器之间的连接,并在连接中进行数据传输。对于端口扫描程序来说,它需要尝试连接到目标主机的各个端口,以判断该端口是否开放或关闭。通过使用套接字编程,可以实现向特定IP地址和端口发送连接请求,并根据返回结果来判断端口的状态。
- 创建一个套接字(Socket):使用
socket()函数创建一个套接字,指定协议族(AF_INET或AF_INET6)和套接字类型(SOCK_STREAM或SOCK_DGRAM)。\ - 设置目标主机信息:设置目标主机的IP地址和端口号等信息。
- 打开套接字连接:使用
connect()函数将套接字连接到目标主机。 - 发送和接收数据:使用
send()和recv()函数发送和接收数据。 - 关闭套接字连接:使用
close()函数关闭套接字连接。
多线程原理
多线程是通过操作系统提供的线程机制来实现的。Linux使用了POSIX线程标准(Pthreads)来支持多线程编程。
- 线程创建:通过调用
pthread_create()函数创建新线程。该函数接受线程标识符指针、线程属性、线程函数和参数作为参数,并创建一个新线程开始执行指定的线程函数。 - 线程上下文切换:操作系统负责对线程进行调度,并在不同的时间片之间进行线程上下文切换。当一个线程被抢占或主动放弃CPU时,操作系统会保存当前线程的上下文,并加载下一个线程的上下文以继续执行。
- 共享资源:多个线程可以共享进程的地址空间,使得它们可以访问相同的全局变量、堆内存和文件描述符等共享资源。同时,也需要注意对共享资源的互斥访问,以避免竞争条件和数据不一致性问题。
- 线程同步:多个线程之间可能会存在并发访问共享资源的情况,为了保证数据的正确性,需要使用同步机制,如互斥锁、条件变量、信号量等,来确保对共享资源的互斥访问和协调线程之间的执行顺序。
- 线程终止:线程的执行可以通过多种方式结束,包括线程函数返回、调用
pthread_exit()函数、或者其他线程调用pthread_cancel()来终止指定线程。线程终止时,它的资源会被释放,但进程中的其他线程仍然可以继续执行。
程序流程图

代码思路
-
定义一个
ThreadArg结构体,用于传递给每个线程需要扫描的IP地址、起始端口和结束端口 -
scan_ports函数根据传入的参数(ThreadArg结构体)进行端口扫描,并输出结果(这就是新线程执行的函数)。如果端口打开,使用函数getservbyport获取其服务名,并在屏幕上打印出ip port servername;如果是未知服务,屏幕将显示ip port unknown
该函数中的重要部分及解释:for (i = start_port; i <= end_port; i++) { int sockfd; struct sockaddr_in target; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket() error"); continue; } target.sin_family = AF_INET; target.sin_port = htons(i); inet_pton(AF_INET, ip, &(target.sin_addr)); if (connect(sockfd, (struct sockaddr *)&target, sizeof(struct sockaddr)) == 0) { service = getservbyport(htons(i), "tcp"); if (service != NULL) { printf("%s %d %s\n", ip, i, service->s_name); } else { printf("%s %d unknown\n", ip, i); } } close(sockfd); }socket(AF_INET, SOCK_STREAM, 0)- 这个函数用于创建一个套接字,用于网络通信。
- 参数
AF_INET表示使用 IPv4 地址族。 - 参数
SOCK_STREAM表示使用 TCP 协议。 - 参数
0表示根据给定的地址族和协议选择默认的协议。
inet_pton(AF_INET, ip, &(target.sin_addr))- 这个函数用于将字符串形式的 IP 地址转换为网络字节顺序的二进制形式。
- 参数
AF_INET表示使用 IPv4 地址族。 - 参数
ip是要转换的 IP 地址。 - 参数
&(target.sin_addr)是指向存储转换结果的目标变量的指针。
connect(sockfd, (struct sockaddr *)&target, sizeof(struct sockaddr))- 这个函数用于与指定的目标地址建立连接。
- 参数
sockfd是之前创建的套接字文件描述符。 - 参数
(struct sockaddr *)&target是指向目标地址结构体的指针。 - 参数
sizeof(struct sockaddr)表示目标地址结构体的大小。
getservbyport(htons(i), "tcp")- 这个函数用于获取指定端口对应的服务名称。
- 参数
htons(i)用于将端口号转换为网络字节顺序。 - 参数
"tcp"表示使用 TCP 协议。
-
main函数中,循环遍历要扫描的IP地址,为每个IP地址创建100个线程进行分段扫描。每个线程负责扫描一个端口范围。通过计算出每个线程负责的起始端口和结束端口,并将对应的ThreadArg结构体传递给线程函数scan_ports。
该函数重要部分及解释:pthread_t threads[NUM_THREADS]; struct ThreadArg thread_args[NUM_THREADS]; int i, j; for (i = 0; i < NUM_IPS; i++) { int port_range = 65535 / NUM_THREADS; int start_port = 1; int end_port = port_range; for (j = 0; j < NUM_THREADS; j++) { thread_args[j].ip = ips[i]; thread_args[j].start_port = start_port; thread_args[j].end_port = end_port; pthread_create(&threads[j], NULL, scan_ports, &thread_args[j]); start_port = end_port + 1; end_port += port_range; if (end_port > 65535) { end_port = 65535; } } for (j = 0; j < NUM_THREADS; j++) { pthread_join(threads[j], NULL); } }pthread_t threads[NUM_THREADS];:定义了一个线程数组threads,用于存储创建的线程ID。struct ThreadArg thread_args[NUM_THREADS];:定义了一个结构体数组thread_args,用于传递给线程函数的参数。- 外层循环
for (i = 0; i < NUM_IPS; i++):遍历待扫描的IP地址。 - 内层循环
for (j = 0; j < NUM_THREADS; j++):遍历多个线程,每个线程负责扫描一定范围内的端口。thread_args[j].ip = ips[i];:将当前IP地址赋值给线程参数的ip字段。thread_args[j].start_port = start_port;:将起始端口号赋值给线程参数的start_port字段。thread_args[j].end_port = end_port;:将结束端口号赋值给线程参数的end_port字段。pthread_create(&threads[j], NULL, scan_ports, &thread_args[j]);:创建一个新线程,并将线程参数传递给函数scan_ports。start_port = end_port + 1;:更新起始端口号为当前范围的下一个端口。end_port += port_range;:更新结束端口号为当前范围的上限。if (end_port > 65535) { end_port = 65535; }:如果结束端口号超过了最大端口号65535,则将其设置为最大端口号。
- 内层循环结束后,使用
pthread_join()函数等待所有线程执行完毕。 - 外层循环继续遍历下一个IP地址,直到所有IP地址都被扫描完毕。
实验代码
scanner.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <pthread.h>
#define NUM_THREADS 100
#define NUM_IPS 5
struct ThreadArg {
char *ip;
int start_port;
int end_port;
};
void *scan_ports(void *arg) {
struct ThreadArg *thread_arg = (struct ThreadArg *)arg;
char *ip = thread_arg->ip;
int start_port = thread_arg->start_port;
int end_port = thread_arg->end_port;
struct servent *service;
int i;
for (i = start_port; i <= end_port; i++) {
int sockfd;
struct sockaddr_in target;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket() error");
continue;
}
target.sin_family = AF_INET;
target.sin_port = htons(i);
inet_pton(AF_INET, ip, &(target.sin_addr));
if (connect(sockfd, (struct sockaddr *)&target, sizeof(struct sockaddr)) == 0) {
service = getservbyport(htons(i), "tcp");
if (service != NULL) {
printf("%s %d %s\n", ip, i, service->s_name);
} else {
printf("%s %d unknown\n", ip, i);
}
}
close(sockfd);
}
pthread_exit(NULL);
}
int main() {
char *ips[NUM_IPS] = {"127.0.0.1", "127.0.0.1", "127.0.0.1", "127.0.0.1", "127.0.0.1"};
pthread_t threads[NUM_THREADS];
struct ThreadArg thread_args[NUM_THREADS];
int i, j;
for (i = 0; i < NUM_IPS; i++) {
int port_range = 65535 / NUM_THREADS;
int start_port = 1;
int end_port = port_range;
for (j = 0; j < NUM_THREADS; j++) {
thread_args[j].ip = ips[i];
thread_args[j].start_port = start_port;
thread_args[j].end_port = end_port;
pthread_create(&threads[j], NULL, scan_ports, &thread_args[j]);
start_port = end_port + 1;
end_port += port_range;
if (end_port > 65535) {
end_port = 65535;
}
}
for (j = 0; j < NUM_THREADS; j++) {
pthread_join(threads[j], NULL);
}
}
return 0;
}
编译运行
实验环境:ubuntu20.04

本文介绍在Linux系统(Ubuntu 20.04)下实现多线程端口扫描器。阐述了端口扫描基于网络套接字编程、多线程基于POSIX线程标准的原理,给出程序流程图和代码思路,要求能同时扫描5个IP地址,每个IP开100个线程扫描,最后提供实验代码及编译运行说明。

被折叠的 条评论
为什么被折叠?



