QT 使用Winpcap抓取Modbus Tcp通信内容

需求:项目中集成Modbus Tcp进行通信,需要显示实际收发内容。

方案:通过集成Winpcap来抓取数据进行显示。

流程:

        1、首先下载WpdPack(Developer's pack),地址:https://www.winpcap.org/devel.htm

        2、下载安装驱动,https://www.winpcap.org/install/default.htm

        3、将WpdPack中Include、Lib引入项目中,在.pro中配置
                 

INCLUDEPATH+=Include
LIBS += -L$$PWD\Lib -lwpcap
LIBS += -lws2_32
LIBS += -liphlpapi

        4、参照WinPcap手册http://www.ferrisxu.com/WinPcap/html/index.html编写抓取功能,草稿代码:

头文件:

#ifndef WINPCAPPROTDATATHREAD_H
#define WINPCAPPROTDATATHREAD_H

#include <QThread>
#include <QDateTime>
#include "pcap.h"
#include "modebusdatachange.h"

class WinpcapProtDataThread: public QThread
{
    Q_OBJECT
public:
    WinpcapProtDataThread(QObject *parent);
    virtual  void run();
    void setIpAndProt(QString ip ,QString prot);
    void setShowTimer(bool b);//是否显示时间
    void setShowHex(bool b);//是否显示hex前缀

    bool isStartPcapLoop = false;
};

#endif // WINPCAPPROTDATATHREAD_H

源文件:

#include "winpcapprotdatathread.h"
#include <QDebug>

/* 4字节的IP地址 */
typedef struct ip_address{
    u_char byte1;
    u_char byte2;
    u_char byte3;
    u_char byte4;
}ip_address;

/* IPv4 首部 */
typedef struct ip_header{
    u_char  ver_ihl;        // 版本 (4 bits) + 首部长度 (4 bits)
    u_char  tos;            // 服务类型(Type of service)
    u_short tlen;           // 总长(Total length)
    u_short identification; // 标识(Identification)
    u_short flags_fo;       // 标志位(Flags) (3 bits) + 段偏移量(Fragment offset) (13 bits)
    u_char  ttl;            // 存活时间(Time to live)
    u_char  proto;          // 协议(Protocol)
    u_short crc;            // 首部校验和(Header checksum)
    ip_address  saddr;      // 源地址(Source address)
    ip_address  daddr;      // 目的地址(Destination address)
    u_int   op_pad;         // 选项与填充(Option + Padding)
}ip_header;

// ICMP
typedef struct _ICMPHeader{
    u_char type;            // reply:0 ,request:8
    u_char code;            // 代码
    u_short checkSum;       // 校验和
    u_short ident;          //
    u_short seq;            // 序列号
    //u_char data[32];      //可变长度
}ICMPHeader;

// TCP数据包的头部 20 bytes
typedef struct _TCPPacketHeader {
    u_short	SrcPort;		//源端口
    u_short	DestPort;		//目的端口
    u_int	Seq;			//序列号
    u_int	Ack;			//确认序列号
    //u_short	Lenres;			//数据偏移4+保留区6+URG+ACK+PSH+RST+SYN+FIN,可以将u_short分为两个u_char
    u_short doff : 4, hlen : 4, fin : 1, syn : 1, rst : 1, psh : 1, ack : 1, urg : 1, ece : 1, cwr : 1; //4 bits 首部长度,6 bits 保留位,6 bits 标志位

    u_short	Win;			//窗口大小
    u_short	Sum;			//校验和
    u_short	Urp;			//紧急指针
}TCPPacketHeader;

// 12字节的TCP伪首部,参与校验和计算
typedef struct _PsedoTCPHead{
    u_char  source_addr[4];
    u_char  dest_addr[4];
    u_char  zero;
    u_char  protocol;
    u_short seg_len;
}PsedoTCPHead;

/* UDP 首部*/
typedef struct udp_header{
    u_short sport;          // 源端口(Source port)
    u_short dport;          // 目的端口(Destination port)
    u_short len;            // UDP数据包长度(Datagram length)
    u_short crc;            // 校验和(Checksum)
}udp_header;


void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);

WinpcapProtDataThread::WinpcapProtDataThread(QObject *parent) : QThread(parent)
{
}

QString tcpData;
void WinpcapProtDataThread::run()
{
    pcap_if_t *alldevs;
    pcap_if_t *d;
    int inum;
    int i=0;
    pcap_t *adhandle;
    QList<pcap_if_t*> mark;

    int selectCurrenPcapIndex = -1;

    char errbuf[PCAP_ERRBUF_SIZE];
    u_int netmask;
//    char packet_filter[] = "src or dst host 20008";
    char packet_filter[] = "ip and tcp";
    struct bpf_program fcode;

        /* 获取本机设备列表 */
        if (pcap_findalldevs(&alldevs,errbuf)==-1)
        {
            fprintf(stderr,"Error in pcap_findalldevs: %s\n", errbuf);
            exit(1);
        }

        /* 打印列表 */
        for(d=alldevs; d; d=d->next)
        {
            qDebug("%d. %s", ++i, d->name);
            mark.append(d);
            if (d->description){
                QString networkCard = QString(d->description);
                if(networkCard.contains("Realtek")){//查询到是本机信息,则打开
                    selectCurrenPcapIndex = i-1;
                }

                qDebug(" (%s)\n", d->description);

            }else{
                qDebug(" (No description available)\n");
            }

        }

        if(i==0 || selectCurrenPcapIndex < 0)
        {
            /* 释放设备列表 */
            pcap_freealldevs(alldevs);
            printf("\nNo interfaces found! Make sure WinPcap is installed.\n");
            return ;
        }


        /* 打开设备 */
        QString name = mark.at(selectCurrenPcapIndex)->name;
        if ( (adhandle= pcap_open_live(name.toLatin1().data(),          // 设备名
                                  65535,            // 65535保证能捕获到不同数据链路层上的每个数据包的全部内容
                                     1,    // 混杂模式
                                  -1,             // 读取超时时间,这里如果是接收数据后立马输出,则设置成-1
                                  errbuf            // 错误缓冲池
                                  ) ) == NULL)
        {
            fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n",name.toLatin1().data());
            /* 释放设备列表 */
            pcap_freealldevs(alldevs);
            return ;
        }

        /* 检查数据链路层,为了简单,我们只考虑以太网 */
            if(pcap_datalink(adhandle) != DLT_EN10MB)
            {
                fprintf(stderr,"\nThis program works only on Ethernet networks.\n");
                /* 释放设备列表 */
                pcap_freealldevs(alldevs);
                return ;
            }

            if(mark.at(selectCurrenPcapIndex)->addresses != NULL)
                /* 获得接口第一个地址的掩码 */
                netmask=((struct sockaddr_in *)(mark.at(selectCurrenPcapIndex)->addresses->netmask))->sin_addr.S_un.S_addr;
            else
                /* 如果接口没有地址,那么我们假设一个C类的掩码 */
                netmask=0xffffff;


            //编译过滤器
            if (pcap_compile(adhandle, &fcode, packet_filter, 1, netmask) <0 )
            {
                fprintf(stderr,"\nUnable to compile the packet filter. Check the syntax.\n");
                /* 释放设备列表 */
                pcap_freealldevs(alldevs);
                return ;
            }

            //设置过滤器
            if (pcap_setfilter(adhandle, &fcode)<0)
            {
                fprintf(stderr,"\nError setting the filter.\n");
                /* 释放设备列表 */
                pcap_freealldevs(alldevs);
                return ;
            }

        /* 释放设备列表 */
        pcap_freealldevs(alldevs);

        /* 开始捕获 */
        pcap_loop(adhandle, 0, packet_handler, NULL);

}

QString tcpProt;
QString tcpIp;
void WinpcapProtDataThread::setIpAndProt(QString ip, QString prot)
{
    if(ip.isEmpty()){
        return;
    }
    tcpIp = ip;
    tcpProt = prot;
}

bool isShowTimer = false;
void WinpcapProtDataThread::setShowTimer(bool b)
{
    isShowTimer = b;
}

bool isShowHePrefix= false;
void WinpcapProtDataThread::setShowHex(bool b)
{
    isShowHePrefix = b;
}

int readMark = 0;//0=目标Ip为接收,1=目标IP为发送端
struct tm *ltime;
ip_header *ih;
_TCPPacketHeader *tcp;
char timestr[16];
u_int ip_len;
u_int tcp_len;
u_short sport,dport;
time_t local_tv_sec;
char *data;

void packet_handler(u_char *param, const pcap_pkthdr *header, const u_char *pkt_data)
{

    /* 将时间戳转换成可识别的格式 */
    local_tv_sec = header->ts.tv_sec;
    ltime=localtime(&local_tv_sec);
    strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);

    /* 打印数据包的时间戳和长度 */
//    qDebug("%s.%.6d len:%d ", timestr, header->ts.tv_usec, header->len);

    /* 获得IP数据包头部的位置 */
    ih = (ip_header *) (pkt_data + 14); //以太网头部长度

    /* 获得TCP首部的位置 */
    ip_len = (ih->ver_ihl & 0xf) * 4;
    tcp = (TCPPacketHeader *) ((u_char*)ih + ip_len);

    /* 将网络字节序列转换成主机字节序列 */
    tcp_len = tcp->hlen*4;
    data = (char*)tcp + tcp_len;
    u_int data_len = ntohs(ih->tlen) - ip_len - tcp_len;
    char buffer[20000];
    memcpy(buffer, data, data_len);
    buffer[data_len] = '\0';

    /* 将网络字节序列转换成主机字节序列 */
    sport = ntohs(tcp->SrcPort);
    dport = ntohs(tcp->DestPort);
    /* 打印IP地址和端口 */
//    qDebug("%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n",
//        ih->saddr.byte1,
//        ih->saddr.byte2,
//        ih->saddr.byte3,
//        ih->saddr.byte4,
//        sport,
//        ih->daddr.byte1,
//        ih->daddr.byte2,
//        ih->daddr.byte3,
//        ih->daddr.byte4,
//        dport);


    //Ip及端口
    QString ipProt = QString("%1.%2.%3.%4.%5->%6.%7.%8.%9.%10").arg(ih->saddr.byte1).
                             arg(ih->saddr.byte2).
                             arg(ih->saddr.byte3).
                             arg(ih->saddr.byte4).
                             arg(sport).
                             arg(ih->daddr.byte1).
                             arg(ih->daddr.byte2).
                             arg(ih->daddr.byte3).
                             arg(ih->daddr.byte4).
                             arg(dport);

    //比对一下是不是和modbus连接的一样
    QString ipAndProt = tcpIp+"."+tcpProt;
    if(ipAndProt.length() <= 1 || !ipProt.contains(ipAndProt) || data_len <= 0){
        return;
    }

    //一发一收处理一下,比较Low的方式
    if(readMark == 0){//接收时
        if(ipProt.split("->")[1] != ipAndProt){
            return;
        }
        readMark = 1;
    }else if(readMark == 1){//发送端时
        if(ipProt.split("->")[0] != ipAndProt){
            return;
        }
        readMark = 0;
    }

    qDebug("ip:%d tcp:%d datalen:(%d)\n",ip_len,tcp_len, data_len);
    QStringList completeData;
    for(int i=0; i<data_len; i++){//输出内容,转16
//        QString str = QString::number(buffer[i]);
        QString str ;
        if(isShowHePrefix){
            str =  QString("0x%1").arg(buffer[i]&0xff,2,16,QLatin1Char('0'));
        }else{
            str =  QString("%1").arg(buffer[i]&0xff,2,16,QLatin1Char('0'));
        }
        completeData.append(str);
    }

    QDateTime current_date_time =QDateTime::currentDateTime();
    QString current_date =current_date_time.toString("yyyy-MM-dd hh:mm:ss.zzz");

    //自己整理一下数据,供其他地方使用
    tcpData = (readMark == 1?"Send:":"RECV:")+completeData.join(",").replace(","," ");
    if(isShowTimer){
        tcpData = current_date+" "+tcpData;
    }
    ModebusDataChange::modebusData.push_front(tcpData);
    if(readMark == 1){
         ModebusDataChange::sendLength += data_len;
    }else{
         ModebusDataChange::recceivedLength += data_len;
    }    

}


使用的地方开启线程即可,退出时记得关闭winpcap等!!!

注:pcap_open_live中超时参数,如果不是-1,则会填充满后才会进行返回,如果是接收立马返回则需要设置成-1。

遇到的问题:pcap_loop中无法使用信号槽,因此弄了一个全局变量来进行数据输出,各位有啥高招吗?

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值