8.1.2 数据包发送实例
为了更好地理解在网络分析中如何根据实际要求发送数据包,本节将使用WinPcap所提供的函数来演示各种发送方式,同时还分别提供了典型的使用实例程序与简单的测试结果。
1.发送单个数据包
下面的代码将演示如何通过WinPcap提供的pcap_sendpacket函数发送单个数据包一次,完整的代码见[ch8/send工程]。
/生成数据包的函数/
void gen_packet(unsigned char*buf,int len);
int main()
{
/获得系统的网络适配器设备列表,并选择一个的适配器设备,打开它/
……
/发送数据包/
//生成数据包
int packetlen=100;//数据包长度为100字节
unsigned charbuf=(unsigned char)malloc(packetlen);
memset(buf,0x0,packetlen);
gen_packet(buf,packetlen);//获得生成的数据包,长度为packetlen
//开始数据包发送
if((ret=pcap_sendpacket(adhandle,buf,packetlen))==-1)
{
//发送失败,释放资源,程序返回
……
return-1;
}
/*
*释放资源,
*包括关闭打开的适配器,释放适配器设备列表,释放存储待发数据包的内存
*/
……
return 0;
}
此实例程序在打开了合适的网络适配器后,为了存储数据包,会执行内存分配并清零。然后调用gen_packet函数生成一个数据包,该数据包的长度为100字节。最后调用WinPcap库的pcap_sendpacket函数把该数据包发送到网络上。其中gen_packet函数负责生成数据包,其参数buf用来保存所生成数据包的内容,len为要求生成的数据包的长度。
注意 该函数所生成的数据包没有任何实际意义,仅起演示作用。
gen_packet函数的具体实现如下:
void gen_packet(unsigned char*buf,int len)
{
int i=0;
//给数据包的第0到5字节填充目标MAC地址:01:01:01:01:01
for(i=0;i<6;i++)
{
buf[i]=0x01;
}
//给数据包的第6到11字节填充源MAC地址:02:02:02:02:02
for(i=6;i<12;i++)
{
buf[i]=0x02;
}
//给数据包的第12到13字节填充协议标识0xcd,无任何实际意义
buf[12]=0xc;
buf[13]=0xd;
//从第14字节开始填充数据包内容,从0开始递增,添数
for(i=14;i<len;i++)
{
buf[i]=i-14;
}
}
在上面的实例程序中使用了下列代码生成待发的数据包,数据包长度为100字节:
//数据包长度为100字节
int packetlen=100;
//分配内存
unsigned charbuf=(unsigned char)malloc(packetlen);
//内存清零
memset(buf,0x0,packetlen);
//生成数据包
gen_packet(buf,packetlen);
代码最终生成的数据包内容如图8-1所示。
图 8-1 生成的数据包
换句话说,在网络上实际接收到的数据包内容也应该与此相同。我们使用Wireshark工具来接收该实例程序所发送的数据包,实验结果如图8-2所示。
图 8-2 Wireshark所接收的数据包
从图8-2所示可知,其所接收的数据包与图8-1所示一致,这表示实例代码是正确的。
2.重复发送单个数据包
在内核中重复发送单个数据包的方法其实很简单,在[send工程]的源文件main.cpp中添加下面的代码即可(完整的代码见[ch8/send_n工程])。
include<pcap-int.h>
/调用Packet.dll库提供的PacketSetNumWrites函数设置重复发送次数/
//重复50次
PacketSetNumWrites((LPADAPTER)(adhandle->adapter),50);
因为wpcap.dll库中并没有提供设置重复发送次数的函数,所以此处只能调用Packet.dll库提供的PacketSetNumWrites函数来设置相应的值。
注意 因为实例使用了Packet.dll库,所以需要给[send_n工程]的Linker->Input->Additional Dependencies选项添加所需依赖的库文件Packet.lib,同时从WinPcap库源代码的wpcap\libpcap目录下复制pcap-int.h文件到E:\WpdPack\Include目录下(Visual Studio所设定的Include文件目录),才可顺利编译。
运行实例程序,用Wireshark工具接收实例程序所发送的数据包,其结果如图8-3所示。
图 8-3 Wireshark所接收的数据包
从图8-3所示可以看出,数据包的确重复发送了50次,而且数据包的内容也与图8-1所示一致,由此可见实例代码是正确的。
3.使用发送队列发送数据包(同步方式)
下面采用WinPcap提供的pcap_sendqueue_transmit函数来演示如何以同步方式(按每个数据包要求的时间戳)来发送大量的数据包,代码如下所示(完整的代码见[ch8/send_queue工程])。
/执行发送队列发送的函数/
int send_queue(pcap_t*fp,unsigned int npacks,unsigned int dus);
/生成数据包的函数/
void gen_packet(unsigned char*buf,int len);
/增加时间戳的函数,间隔为dus,单位为微秒(µs)/
timeval add_stamp(timeval*ptv,unsigned int dus);
int main()
{
/获得系统的网络适配器设备列表,并选择一个的适配器设备,打开它/
……
/*
*开始数据包发送,
*发送100个数据包,每个数据包之间的时间间隔为20µs
*/
send_queue(adhandle,100,20);
/*
*释放资源,
*包括关闭打开的适配器,释放适配器设备列表,释放存储待发数据包的内存
*/
……
return 0;
}
上述代码中,send_queue函数负责生成与释放发送队列,并把发送队列发送出去,其原型如下:
int send_queue(pcap_t*fp,unsigned int npacks,unsigned int dus);
上述函数中,参数fp标识发送数据包的网络适配器;参数npacks为要发送的数据包个数;参数dus为每个数据包的发送时间间隔。
send_queue函数执行成功,则返回0,失败返回-1。
send_quene函数的具体代码实现如下:
int send_queue(pcap_t*fp,unsigned int npacks,
unsigned int dus)
{
char errbuf[PCAP_ERRBUF_SIZE];
int i;
unsigned int res;
pcap_send_queue*squeue;//申明发送队列
const int packetlen=100;//设置数据包长度
struct pcap_pkthdr mpktheader;//申明数据包头信息
struct pcap_pkthdr*pktheader;
pktheader=&mpktheader;
timeval tv;//申明时间戳
tv.tv_sec=0;
tv.tv_usec=0;
/分配发送队列/
squeue=pcap_sendqueue_alloc((unsigned int)(
(packetlen+sizeof(struct pcap_pkthdr))*npacks));
/用数据包填充发送队列/
unsigned chartempbuf=(unsigned char)malloc(packetlen);
for(i=0;i<npacks;i++)
{
memset(tempbuf,0x0,packetlen);
gen_packet(tempbuf,packetlen);//获得生成的数据包
//设置数据包的包头
pktheader->ts=tv;
pktheader->caplen=packetlen;
pktheader->len=packetlen;
//把数据包添加到发送队列中
if(pcap_sendqueue_queue(squeue,pktheader,tempbuf)==-1)
{/数据包添加失败,比如缓冲区太小,函数返回/
……
return-1;
}
add_stamp(&tv,dus);//增加时间戳
pktheader->ts=tv;//更新数据包头的时间戳
}
free(tempbuf);
/发送队列到网络/
if((res=pcap_sendqueue_transmit(fp,squeue,1))<
squeue->len)//以同步方式发送
{
/发送失败,函数返回/
……
pcap_sendqueue_destroy(squeue);
return-1;
}
/释放发送队列/
pcap_sendqueue_destroy(squeue);
return 0;
}
本实例代码演示了使用发送队列发送数据包的全过程,其中send_queue函数是该实例的核心函数。为了使读者更好地理解这个重要的过程,下面以图的形式演示该过程,如图8-4所示。
图 8-4 发送队列发送过程示意图
为了更新每个数据包的时间戳,我们编写了add_stamp函数,其原型如下:
timeval add_stamp(timeval*ptv,unsigned int dus)
上述函数中,参数ptv为时间戳结构体指针,参数dus为时间戳增加间隔,以微秒(µs)为单位。
add_stamp函数的具体实现代码如下:
timeval add_stamp(timeval*ptv,unsigned int dus)
{
ptv->tv_usec=ptv->tv_usec+dus;
if(ptv->tv_usec>=1000000)
{
ptv->tv_sec=ptv->tv_sec+1;
ptv->tv_usec=ptv->tv_usec-1000000;
}
return*ptv;
}
add_stamp函数主要处理因为结构体timeval的两个字段tv_usec与tv_sec的单位不一致而存在的转换问题。
上面的实例程序在打开合适的网络适配器后,会调用下列代码发送数据包。
send_queue(adhandle,100,20);
上面所设定的发送数据包的个数为100,每个数据包之间的时间间隔为20µs。最终将会在send_queue函数中调用WinPcap库的pcap_sendpacket函数把该数据包队列发送到网络。
由以上内容可知,实例程序的正确结果应该是:发送100个长度为100字节的数据包,并且每个数据包发送的时间间隔为20µs。运行实例程序,使用Wireshark工具接收实例程序所发送的数据包,其结果如图8-5所示。
图 8-5 Wireshark所接收的数据包
从图8-5中Wireshark概要区域的Time字段可以看出,接收的时间戳间隔为20µs,精度差别为2µs左右,100个数据包总共耗费1966µs(理论上应该为20×(100-1)=1980µs);协议Protocol字段显示为0x0c0d。在Wireshark详情区域中可看到目标MAC地址为01:01:01:01:01:01,源MAC地址为02:02:02:02:02:02。从Wireshark数据区域中可看到数据包的内容从0开始递增,直到0x55(十进制85)为止。图中内容验证了实例程序是正确的。
4.使用发送队列发送数据包(异步方式)
最后我们继续通过使用WinPcap提供的pcap_sendqueue_transmit函数,来演示如何以异步的方式发送大量的数据包。具体实现时只需要对[ch8/send_queue工程]的send_queue函数做如下修改即可,主要修改pcap_sendqueue_transmit函数的Sync参数值,将其设置为0,表示以异步方式发送。
void send_queue(pcap_t*fp,unsigned int npacks,unsigned int dus)
{
……
//if((res=pcap_sendqueue_transmit(fp,squeue,1))<squeue->len)//同步发送
if((res=pcap_sendqueue_transmit(fp,squeue,0))<squeue->len)//异步发送
……
}
使用Wireshark工具接收实例程序所发送的数据包,结果如图8-6与图8-7所示。
图 8-6 Wireshark所接收的数据包(起始的数据包)
图 8-7 Wireshark所接收的数据包(结束的数据包)
从图8-6与图8-7所示可知,发送100个数据包花费了990µs的时间,比同步方式快,同时每个数据包发送的时间间隔并未依据数据包的时间戳来进行安排。