需求:项目中集成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中无法使用信号槽,因此弄了一个全局变量来进行数据输出,各位有啥高招吗?