Tracert 与 Ping程序设计与实现(二)

2019/12/24update:已解决线程并发导致主界面假死问题。


今天是冬至,祝大家有个愉快的的冬天。o(* ̄▽ ̄*)o

花了一天的时间给程序做了一个GUI,在调试多线程的时候遇到了困难,昨天晚上2点多才弄完,但是感觉这个多线程有点奇怪(雾

目录:

提示

我用的GUI开发环境是Qt Creator。Qt的下载地址和安装步骤看 这里

Qt creator的语法和C++一样,只不过在C++的基础上增加了专门用于图像界面的库,C++代码也可在Creator上流畅运行。

开始之前先新创一个项目,命名为Tracert。然后在Tracert.pro文件中添加我们需要用到的库,在任意行添加 “LIBS += -lWs2_32” 即可。

添加库文件

然后新建一个头文件threadtest.h和一个资源文件threadtest.cpp,用于创建多线程。在Forms目录下有一个布局文件mainwindow.ui,点击这个ui文件,我们可快捷编辑GUI布局。

编辑界面

现在我们的资源目录为:

资源目录

代码

mainwindow.h中是我们主要的类,用于GUI和声明一些全局变量:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "threadtest.h"
class TestThread;
extern int ipnumber;
extern int nowrow;
extern QString nowip;
extern int outtime;
namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void DisplayMsg(QString,bool,QString,QString);
    void on_pushButton_clicked();

    void on_pushButton_2_clicked();

private:
    Ui::MainWindow *ui;
    TestThread* t[255];
    QString ip1;
    QString ip2;
};

#endif // MAINWINDOW_H

threadtest.h 头文件用于创建线程类:

#ifndef TESTTHREAD_H
#define TESTTHREAD_H

#include <QThread>

#include "mainwindow.h"

class TestThread : public QThread
{
    Q_OBJECT
public:
    explicit TestThread(QObject *parent = 0);

protected:
    void run();

signals:
    void TestSignal(QString,bool,QString,QString);

private:
    int tnc=4;
};

#endif // TESTTHREAD_H

main.cpp不需要做任何改动:

#include "mainwindow.h"
#include <QApplication>
#include <iostream>


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

mainwindow.cpp 就是我们需要重点关注的地方了,GUI的绑定事件及GUI主界面的更新都在此文件内操作,所以代码有点长:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>

#include <cstring>

#include <stdlib.h>
#include <stdio.h>

#include <string.h>
using namespace std;
QString nowip;
int nowrow;
int ipnumber;
int outtime;

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    WSADATA wsa;
    WSAStartup(MAKEWORD(2,2),&wsa);
    ui->tableWidget->setColumnCount(4);
    //ui->tableWidget->setRowCount(255);
    ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
}

MainWindow::~MainWindow()
{
    delete ui;
}

//跳转到下一个IP地址
QString toNextIp(QString str1)
{
    //QString2char
    char *tempIp = (char *)malloc(17);
    QByteArray ba = str1.toLatin1();
    strcpy(tempIp,ba.data());
    char temp[4][4];
    int ip1=0;
    int ip2=0;
    for(int i=0; i < strlen(tempIp); i++)
    {
        if(tempIp[i]=='.')
        {
            temp[ip1][ip2] = '\0';
            ip1++;
            ip2=0;
            continue;
        }
        temp[ip1][ip2] = tempIp[i];
        ip2++;
    }

    temp[ip1][ip2] ='\0';
    if(atoi(temp[3])==254)
    {
        strcpy(temp[3],"1");
    }
    else
    {
        int t1 = atoi(temp[3])+1;
        itoa(t1,temp[3],10);
    }
    //cout <<temp[3];
    char tip[17];
    strcpy(tip,temp[0]);
    for(int i=1; i<4; i++)
    {
        strcat(tip,".");
        strcat(tip,temp[i]);
    }
    QString qs = QString::fromLocal8Bit(tip);
    return qs;
}

//更新主界面
void MainWindow::DisplayMsg(QString aip,bool is_online,QString qsmac,QString qsname){
    int row_c = ui->tableWidget->rowCount();
    ui->tableWidget->insertRow(row_c);
    ui->tableWidget->setItem(row_c,0,new QTableWidgetItem(aip));
    if(is_online){
        ui->tableWidget->setItem(row_c,3,new QTableWidgetItem(QString("Online")));
    }else{
        ui->tableWidget->setItem(row_c,3,new QTableWidgetItem(QString("Away")));
    }
    ui->tableWidget->setItem(row_c,1,new QTableWidgetItem(qsname));
    ui->tableWidget->setItem(row_c,2,new QTableWidgetItem(qsmac));
}
//按钮监听
void MainWindow::on_pushButton_clicked()
{
    ip1 = ui->lineEdit->text();
    ip2 = ui->lineEdit_2->text();
    QString outtime2 = ui->lineEdit_3->text();
    ui->label_6->setText("from:"+ip1);
    ui->label_7->setText("to:"+ip2);
    outtime = outtime2.toInt();

    int number=0;
    while((QString::compare(ip1,ip2))<0){
        number++;
        ip1 = toNextIp(ip1);
    }
    ipnumber = number;
    ip1 = ui->lineEdit->text();
    int flag=0;
    while(flag<ipnumber){
        nowrow = flag;
        nowip = ip1;
        ip1 = toNextIp(ip1);
        t[flag] = new TestThread();//新建线程
        connect(t[flag], SIGNAL(TestSignal(QString,bool,QString,QString)), this, SLOT(DisplayMsg(QString,bool,QString,QString)));
        //执行子线程
        t[flag]->start();
        Sleep(100);
        flag++;
    }
}

void MainWindow::on_pushButton_2_clicked()
{
    exit(0);
}

threadtest.cpp 文件中包含了线程函数的参数传递,以及一下耗时的并发运算:

#include "threadtest.h"
#include "mainwindow.h"
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>

#include <cstring>

#include <stdlib.h>
#include <stdio.h>

#include <string.h>
using namespace std;

TestThread::TestThread(QObject *parent) :
    QThread(parent)
{
}
//IP 报头
typedef struct
{
    unsigned char hdr_len:4; //4 位头部长度
    unsigned char version:4; //4 位版本号
    unsigned char tos; //8 位服务类型
    unsigned short total_len; //16 位总长度
    unsigned short identifier; //16 位标识符
    unsigned short frag_and_flags; //3 位标志加 13 位片偏移
    unsigned char ttl; //8 位生存时间
    unsigned char protocol; //8 位上层协议号
    unsigned short checksum; //16 位校验和
    unsigned long sourceIP; //32 位源 IP 地址
    unsigned long destIP; //32 位目的 IP 地址
} IP_HEADER;

//ICMP 报头
typedef struct
{
    BYTE type; //8 位类型字段
    BYTE code; //8 位代码字段
    USHORT cksum; //16 位校验和
    USHORT id; //16 位标识符
    USHORT seq; //16 位序列号
} ICMP_HEADER;

//报文解码结构
typedef struct
{
    USHORT usSeqNo; //序列号
    DWORD dwRoundTripTime; //往返时间
    in_addr dwIPaddr; //返回报文的 IP 地址
} DECODE_RESULT;

//计算网际校验和函数
USHORT checksum(USHORT *pBuf,int iSize)
{
    unsigned long cksum=0;
    while(iSize>1)
    {
        cksum+=*pBuf++;
        iSize-=sizeof(USHORT);
    }
    if(iSize)
    {
        cksum+=*(UCHAR *)pBuf;
    }
    cksum=(cksum>>16)+(cksum&0xffff);
    cksum+=(cksum>>16);
    return (USHORT)(~cksum);
}
//对数据包进行解码,结果封装到Decode当中
/*
buf:接收到的buf,字符串
ipacketsize:接收到的数据长度
解析结果封装到DecodeResult
ICMP类型, echo_reply是一个常量,放到全局也行
ICMP类型
*/
BOOL DecodeIcmpResponse(char * pBuf,int iPacketSize,DECODE_RESULT &DecodeResult,BYTE ICMP_ECHO_REPLY,BYTE ICMP_TIMEOUT)
{
    //检查数据报大小的合法性
    IP_HEADER* pIpHdr = (IP_HEADER*)pBuf; //将接收到的数据转换成ip抱头
    int iIpHdrLen = pIpHdr->hdr_len * 4;//计算报文长度
    //如果接收的报文长度小于IP报文长度,就退出
    if (iPacketSize < (int)(iIpHdrLen+sizeof(ICMP_HEADER)))
        return FALSE;
    //根据 ICMP 报文类型提取 ID 字段和序列号字段
    ICMP_HEADER *pIcmpHdr=(ICMP_HEADER *)(pBuf+iIpHdrLen); //根据字符串结构体指针自动匹配长度
    USHORT usID,usSquNo;
    //如果返回的是回显应答报文
    if(pIcmpHdr->type==ICMP_ECHO_REPLY) //ICMP 回显应答报文
    {
        usID=pIcmpHdr->id; //报文 ID
        usSquNo=pIcmpHdr->seq; //报文序列号
    }
    //如果返回的是超时差错报文
    else if(pIcmpHdr->type==ICMP_TIMEOUT)//ICMP 超时差错报文
    {
        char * pInnerIpHdr=pBuf+iIpHdrLen+sizeof(ICMP_HEADER); //载荷中的 IP 头
        int iInnerIPHdrLen=((IP_HEADER *)pInnerIpHdr)->hdr_len*4; //载荷中的 IP 头长
        ICMP_HEADER * pInnerIcmpHdr=(ICMP_HEADER *)(pInnerIpHdr+iInnerIPHdrLen);//载荷中的 ICMP 头
        usID=pInnerIcmpHdr->id; //报文 ID
        usSquNo=pInnerIcmpHdr->seq; //序列号
    }
    //如果都不是,则退出
    else
    {
        return false;
    }
    //检查 ID 和序列号以确定收到期待数据报
    //检查ID是否为当前进程ID,获取的序列号是否为指定序列号
    if(usID!=(USHORT)GetCurrentProcessId()||usSquNo!=DecodeResult.usSeqNo)
    {
        return false;
    }
    //记录 IP 地址并计算往返时间
    DecodeResult.dwIPaddr.s_addr=pIpHdr->sourceIP;
    DecodeResult.dwRoundTripTime=GetTickCount()-DecodeResult.dwRoundTripTime;
    //处理正确收到的 ICMP 数据报
    if (pIcmpHdr->type == ICMP_ECHO_REPLY ||pIcmpHdr->type == ICMP_TIMEOUT)
    {
        //输出往返时间信息
        if(DecodeResult.dwRoundTripTime)
            cout<<" "<<DecodeResult.dwRoundTripTime<<"ms"<<flush;
        else
            cout<<" "<<"<1ms"<<flush;
    }
    return true;
}

//判断目标IP是否可达
bool tracert(char* IpAddress,int outtime)
{
    bool is_reach = false;
    //得到 IP 地址
    u_long ulDestIP=inet_addr(IpAddress);
    //转换不成功时按域名解析
    if(ulDestIP==INADDR_NONE)
    {
        hostent * pHostent=gethostbyname(IpAddress);
        if(pHostent)
        {
            ulDestIP=(*(in_addr*)pHostent->h_addr).s_addr;
        }
        else
        {
            cout<<"输入的 IP 地址或域名无效!"<<endl;
            WSACleanup();
            return 0;
        }
    }
    cout<<"\nTracing route to "<<IpAddress<<" with a maximum of 30 hops.\n"<<endl;
    //填充目地端 socket 地址
    sockaddr_in destSockAddr;
    ZeroMemory(&destSockAddr,sizeof(sockaddr_in));
    destSockAddr.sin_family=AF_INET;//协议IPV4
    destSockAddr.sin_addr.s_addr=ulDestIP;//目的IP地址
    //创建原始套接字
    SOCKET sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED);
    //超时时间
    int iTimeout=outtime;
    //接收超时
    setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char *)&iTimeout,sizeof(iTimeout));
    //发送超时
    setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char *)&iTimeout,sizeof(iTimeout));

    //构造 ICMP 回显请求消息,并以 TTL 递增的顺序发送报文
    //ICMP 类型字段
    const BYTE ICMP_ECHO_REQUEST=8; //请求回显
    const BYTE ICMP_ECHO_REPLY=0; //回显应答
    const BYTE ICMP_TIMEOUT=11; //传输超时

    //其他常量定义
    const int DEF_ICMP_DATA_SIZE=32; //ICMP 报文默认数据字段长度
    const int MAX_ICMP_PACKET_SIZE=1024;//ICMP 报文最大长度(包括报头)
    const DWORD DEF_ICMP_TIMEOUT=3000; //回显应答超时时间
    const int DEF_MAX_HOP=30; //最大跳站数

    //填充 ICMP 报文中每次发送时不变的字段
    //发送缓冲区
    char IcmpSendBuf[sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE];//发送缓冲区
    memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf)); //初始化发送缓冲区,用0填充
    //接收缓冲区
    char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; //接收缓冲区
    memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区,用0填充

    ICMP_HEADER * pIcmpHeader=(ICMP_HEADER*)IcmpSendBuf; //发送报头
    pIcmpHeader->type=ICMP_ECHO_REQUEST; //类型为请求回显,8
    pIcmpHeader->code=0; //代码字段为 0
    pIcmpHeader->id=(USHORT)GetCurrentProcessId(); //ID 字段为当前进程号

    memset(IcmpSendBuf+sizeof(ICMP_HEADER),'E',DEF_ICMP_DATA_SIZE);//数据字段,用'E'填充
    USHORT usSeqNo=0; //ICMP 报文序列号

    int iTTL=1; //TTL 初始值为 1
    BOOL bReachDestHost=FALSE; //循环退出标志
    int iMaxHot=DEF_MAX_HOP; //循环的最大次数 30
    DECODE_RESULT DecodeResult; //传递给报文解码函数的结构化参数
    while(!bReachDestHost&&iMaxHot--)
    {
        //设置 IP 报头的 TTL 字段
        setsockopt(sockRaw,IPPROTO_IP,IP_TTL,(char *)&iTTL,sizeof(iTTL));
        cout<<iTTL<<flush; //输出当前序号
        //填充 ICMP 报文中每次发送变化的字段
        ((ICMP_HEADER *)IcmpSendBuf)->cksum=0; //校验和先置为 0
        ((ICMP_HEADER *)IcmpSendBuf)->seq=htons(usSeqNo++); //填充序列号
        ((ICMP_HEADER *)IcmpSendBuf)->cksum=checksum((USHORT *)IcmpSendBuf,sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE); //计算校验和
        //记录序列号和当前时间
        DecodeResult.usSeqNo=((ICMP_HEADER*)IcmpSendBuf)->seq; //当前序号
        DecodeResult.dwRoundTripTime=GetTickCount(); //当前时间
        //发送 TCP 回显请求信息
        sendto(sockRaw,IcmpSendBuf,sizeof(IcmpSendBuf),0,(sockaddr*)&destSockAddr,sizeof(destSockAddr));
        //接收 ICMP 差错报文并进行解析处理
        sockaddr_in from; //对端 socket 地址
        int iFromLen=sizeof(from); //地址结构大小
        int iReadDataLen; //接收数据长度
        while(1)
        {
            //接收数据
            iReadDataLen=recvfrom(sockRaw,IcmpRecvBuf,MAX_ICMP_PACKET_SIZE,0,(sockaddr*)&from,&iFromLen);
            if(iReadDataLen!=SOCKET_ERROR)//有数据到达
            {
                //对数据包进行解码
                if(DecodeIcmpResponse(IcmpRecvBuf,iReadDataLen,DecodeResult,ICMP_ECHO_REPLY,ICMP_TIMEOUT))
                {
                    //到达目的地,退出循环
                    if(DecodeResult.dwIPaddr.s_addr==destSockAddr.sin_addr.s_addr)bReachDestHost=true;
                    //输出 IP 地址
                    cout<<'\t'<<inet_ntoa(DecodeResult.dwIPaddr)<<endl;
                    if(DecodeResult.dwIPaddr.s_addr == destSockAddr.sin_addr.s_addr)
                    {
                        //cout << inet_ntoa(destSockAddr.sin_addr)<<"可达!"<<endl;
                        is_reach = true;
                    }
                    break;
                }
            }
            else if(WSAGetLastError()==WSAETIMEDOUT) //接收超时,输出*号
            {
                cout<<" *"<<'\t'<<"Request timed out."<<endl;
                break;
            }
            else
            {
                break;
            }
        }
        iTTL++; //递增 TTL 值
    }
    return is_reach;
}

// get mac and pcName
int _System(const char * cmd, char *pRetMsg, int msg_len,char *mac,char*name)
{
    FILE * fp;
    char * p = NULL;
    int res = -1;
    char mac1[128];
    char name1[128];
    if (cmd == NULL || pRetMsg == NULL || msg_len < 0)
    {
        printf("Param Error!\n");
        return -1;
    }
    if ((fp = _popen(cmd, "r")) == NULL)
    {
        printf("Popen Error!\n");
        return -2;
    }
    else
    {
        memset(pRetMsg, 0, msg_len);
        //get lastest result
        int index = 0;
        while (fgets(pRetMsg, msg_len, fp) != NULL)
        {
            if(index==8){
                //printf("Msg:%s", pRetMsg); //print all info
               // printf(strlen(pRetMsg));
               strcpy(mac1,pRetMsg);
                int m1=0;
               for(int i=4;mac1[i]!=' ';i++){
                    mac[m1++] = mac1[i];
               }
               //printf("%s\n",mac);
            }else if(index==13){
                //printf("Msg:%s", pRetMsg); //print all info
                strcpy(name1,pRetMsg);
                int n1=0;
                for(int i=15;name1[i]!=' ';i++){
                    name[n1++] = name1[i];
                }
               //printf("%s\n",name);
            }
            index++;
        }

        if ((res = _pclose(fp)) == -1)
        {
            printf("close popenerror!\n");
            return -3;
        }
        pRetMsg[strlen(pRetMsg) - 1] = '\0';
        return 0;
    }
}

void TestThread::run()
{
    //触发信号
    //tnc = 5;
    //emit TestSignal(nowip,outtime);
    char *tempIp = (char *)malloc(17);
    QByteArray ba = nowip.toLatin1();
    strcpy(tempIp,ba.data());
    cout << tempIp <<endl;
    bool is_online=false;
    char mac[50]={0};
    char name[50] = {0};
    QString qsname;
    QString qsmac;
    if(tracert(tempIp,outtime)){
        is_online = true;
        //ui->tableWidget->setItem(row_c,3,new QTableWidgetItem(QString("Online")));
        char cmd[50] = "nbtstat -a ";
        strcat(cmd,tempIp);
        char a8Result[128] = { 0 };
        int ret = 0;


        ret = _System(cmd, a8Result, sizeof(a8Result),mac,name);
        if(!ret){
//            printf("%s\n",mac);
//            printf("%s\n",name);
            qsname = QString::fromLocal8Bit(name);
            qsmac = QString::fromLocal8Bit(mac);
            cout << name<<" "<<mac<<endl;
            //ui->tableWidget->setItem(row_c,1,new QTableWidgetItem(qsname));
            //ui->tableWidget->setItem(row_c,2,new QTableWidgetItem(qsmac));
        }
    }
    TestSignal(nowip,is_online,qsmac,qsname);
}

以上便是全部代码了。

运行演示

程序能正常运行。 😬 😬 😬

(上午更新了获取MAC地址。

注意!!!获取MAC地址的方法是代替cmd执行 nbtstat -a [ip],但是命令只能在Windows 7/XP & Linux上才能运行,也就是说如果在Windows7/XP上执行程序能获取Mac&PcName,在Windows 8/10系统运行不会获取 Mac&PcName ,但不影响程序运行。示例图有个乱码不用介意)