实验任务
-
编写端口扫描程序
- 具有界面(使用QtCreator)
- 具有多线程处理能力
- 使用简单的
connect
确定端口是否开放即可 - 可以提前结束扫面,安全的结束线程
-
效果示例(源码)
-
界面
-
扫描局域网内开放的端口
-
扫描百度
-
多线程加速
-
提前结束扫描
-
实验步骤一、熟悉QtCreator编程
- C语言中文网Qt教程,按序看10~15篇
- Qt弹窗,QString,用于和用户交互,发出警告/提示
- Qt多线程,本实验要用到
- Qt Socket,Qt有封装好的socket函数
实验步骤二、设计界面
-
拖拉拽即可,注意一点就是控件的
objectName
属性值,通过该值可以在代码中访问、更新UI,如更新结果显示区内容时,使用
ui->resultArea
访问该控件对象void MainWindow::updateResult(QString info, bool opened) { /* other code */ /* 向结果区增加记录 */ ui->resultArea->append(info + " " + (opened ? "opened" : "closed")); /* other code */ }
也可以在代码中设置控件的一些属性,如
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); /* 设置每个IP输入框的长度限制 */ ui->startIP_1->setMaxLength(3); ui->startIP_2->setMaxLength(3); ui->startIP_3->setMaxLength(3); ui->startIP_4->setMaxLength(3); ui->endIP_1->setMaxLength(3); ui->endIP_2->setMaxLength(3); ui->endIP_3->setMaxLength(3); ui->endIP_4->setMaxLength(3); /* other code */ }
将按钮的点击事件和处理函数绑定(Qt称为连接信号与槽)
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { /* other code */ /* 绑定(连接)开始扫描和结束扫描按钮点击事件(信号)对应的处理函数(槽) */ QObject::connect(ui->beginScan, &QPushButton::clicked, this, &MainWindow::scan); QObject::connect(ui->stopScan, &QPushButton::clicked, this, &MainWindow::terminateScan); }
实验步骤二、确定通信对象,声明信号和槽
-
为UI中的按钮创建对应的槽
mainwindow.h
class MainWindow : public QMainWindow { /* other code */ /* 声明槽 */ public slots : void scan(); /* 开始扫描 */ void terminateScan(); /* 结束扫描 */ void updateResult(QString info, bool open); /* 子线程扫描结束对应的槽 */ };
-
子线程扫描结束信号,及主线程对应的槽(上述第三个)
scanthread.h
class ScanThread : public QThread { /* other code */ /* 声明信号 */ signals: void scanOver(QString info, bool opened); /* 子线程扫描完成一个端口时发出信号,主线程更新UI */ };
实验步骤三、主线程之参数校验
-
合法IP地址、IP范围校验
-
合法端口校验
-
合理线程校验
qDebug() << "检查线程\n"; /* 获取控件参数 */ int numOfThread = ui->threadCount->text().toInt(); if(numOfThread == NULL || numOfThread < 0 || numOfThread > 1000) { QMessageBox::warning( this, tr("提示"), tr("请输入正确的线程数(max=1000)!"), QMessageBox::Ok); return ; }
实验步骤四、主线程之任务分配
-
分配方式
-
待扫描的IP数量小于等于线程数:
每个线程只需扫描一个IP地址,按端口数量分配任务;
有可能出现刚好跨IP的情况(前一个IP的最后几个端口和下一个IP的前几个端口),方便起见,遇到这种情况时,前一个线程只需扫描完当前IP的最后几个端口即可;而原本在它工作范围之内的剩余IP均分给剩余线程
-
带扫描的IP数量大于线程数
每个线程要扫描多个IP地址,则按IP数量分配任务,每个线程完成分配到的IP的所有端口的扫描
-
每次给当线程分配完任务后都动态调整剩余任务的分配
-
-
分配实现
if(numOfThread >= ipCount) { // 按线程分配,一个线程最多扫描一个IP int taskForSingleThread = totCount / numOfThread, dispatchTask = 0; for(int i = 0; i < numOfThread; i++) { // 计算出线程需要扫描的IP及端口 currentIP = netAddr + QString::number(hostAddr + scannedIP, 10); currentPort = scannedPort % portCount + startPort; // 最后一个线程完成剩下的任务即可 if(i == numOfThread - 1) taskForSingleThread = totCount - scannedPort; // 如果一个IP剩下的端口小于task,那么该线程少分配一些任务,扫描完当前IP即可 // 而未分配到的任务将均分至剩下的线程中 if(endPort - currentPort + 1 < taskForSingleThread) { dispatchTask = endPort - currentPort + 1; } else { dispatchTask = taskForSingleThread; } ScanThread* thread = new ScanThread(currentIP, 1, currentPort, dispatchTask); QObject::connect(thread, &ScanThread::scanOver, this, &MainWindow::updateResult); threadPool[i] = thread; thread->start(); scannedPort += dispatchTask; scannedIP = scannedPort / portCount; // 修正平均工作量 if((totCount - scannedPort) * 1.0 / (numOfThread - i - 1) > taskForSingleThread) { taskForSingleThread++; } } } else { // 按IP分配,每个线程扫描多IP int taskForSingleThread = ipCount / numOfThread; for(int i = 0; i < numOfThread; i++) { currentIP = netAddr + QString::number(hostAddr + scannedIP, 10); if(i == numOfThread - 1) { taskForSingleThread = ipCount - scannedIP; } ScanThread* thread = new ScanThread(currentIP, taskForSingleThread, startPort, portCount); QObject::connect(thread, &ScanThread::scanOver, this, &MainWindow::updateResult); /* 记录所有开启的线程 */ threadPool[i] = thread; thread->start(); scannedIP += taskForSingleThread; if((ipCount - scannedIP) * 1.0 / (numOfThread - i - 1) > taskForSingleThread) { taskForSingleThread++; } } } }
实验步骤五、主线程之更新UI
-
收到子线程传递的扫描结果,将其显示在相应区域
void MainWindow::updateResult(QString info, bool opened) { /* 提前结束扫描的标志 */ if(this->isOver) return ; /* 显示扫描结果 */ ui->resultArea->append(info + " " + (opened ? "opened" : "closed")); /* 记录开启的端口 */ if(opened) { this->openedPort.append(info); } this->finTask++; /* 所有任务完成 */ if(this->finTask == this->totTask) { this->endTime = QTime::currentTime(); ui->resultArea->append("扫描结束,耗时: " + QString::number(this->startTime.msecsTo(this->endTime) / 1000.0)); /* 显示所有开启的端口 */ this->showAllOpenedPorts(); } }
-
最后显示所有开启的端口
void MainWindow::showAllOpenedPorts() { ui->resultArea->append("-------------------------------------------\n开启的IP及端口号如下"); int numOfOpenedPort = this->openedPort.size(); for(int i = 0; i < numOfOpenedPort; i++) { ui->resultArea->append(this->openedPort.at(i)); } ui->resultArea->append("-------------------------------------------\n本次扫描完成\n"); qDebug() << "over"; }
实验步骤六、主线程之结束子线程
-
由于提供了提前终止扫描的按钮,需要适当的处理子线程
void MainWindow::terminateScan() { /* 提示用户确定终止扫描 */ int confirm = QMessageBox::warning( this, tr("提示"), tr("您确定要提前结束扫描吗?"), QMessageBox::Ok, QMessageBox::Cancel); if(confirm != QMessageBox::Ok) { return ; } /* 设置终止标志位,组织UI继续更新 */ this->isOver = true; /* 遍历所有子线程 */ for(int i = 0; i < 64; i++) { if(threadPool[i] != NULL) { /* 向子线程发出终止请求 */ threadPool[i]->requestInterruption(); /* 等待子线程退出 */ threadPool[i]->wait(); qDebug() << "stop" << threadPool[i] << "\n"; } } ui->resultArea->append("-------------------------------------------\n本次扫描已终止\n"); qDebug() << "stopped\n"; }
实验步骤七、子线程之扫描端口
-
主线程给子线程分配任务时,传递了以下参数
ip
:待扫描的起始IPipCount
:待扫描的IP数量port
:待扫描的起始端口portCount
:待扫描的端口数量
-
扫描实现
void ScanThread::run() { QTcpSocket* conn = new QTcpSocket(); /* 获得网络号 */ QString netAddr = ip.left(ip.lastIndexOf('.') + 1); /* 获得主机号 */ int hostAddr = ip.right(ip.length() - ip.lastIndexOf('.') - 1).toInt(); /* 遍历所有主机(IP) */ QString curIP; int curPort; /* 每次循环开始时检查主线程是否发出了终止请求 */ for(int i = 0; !isInterruptionRequested() && i < ipCount; i++) { /* 获得待扫描的IP */ curIP = netAddr + QString::number(hostAddr + i); /* 扫描端口 */ for(int j = 0; !isInterruptionRequested() && j < portCount; j++) { qDebug() << "线程" << QThread::currentThreadId() << " 开始扫描 " << curIP << ":" << port << "\n"; curPort = port + j; /* 向该端口发送连接请求 */ conn->connectToHost(curIP, curPort); /* 等待一段时间,得到连接结果 */ bool res = conn->waitForConnected(1000); /* 在向主线程发送更新信号时,判断一下当前扫描是否已终止,这是由于信号的延迟会导致UI冲突 */ if(!isInterruptionRequested()) { /* 向主线程发送扫描完成的信号并传递结果 */ emit scanOver(curIP + ":" + QString::number(curPort), res); } if(res) { qDebug() << "opened"; } else { qDebug() << "closed"; } /* 断开连接 */ conn->disconnectFromHost(); } } qDebug() << "线程" << QThread::currentThreadId() << " 扫描完成" << "\n"; }
总结
- 时间比较紧,处理线程任务分配和提前终止最麻烦