10.2 数据接收的幕后
图10-4所示为与各种接收方式相关的主要函数之间的调用关系,后续各节将依据从用户空间到内核空间的顺序,来详细描述各重要函数的具体实现。
图 10-4 主要函数的调用关系图
10.2.1 wpcap.dll库中相应函数的实现
1.pcap_dispatch函数
pcap_dispatch函数接收并处理一组NPF驱动程序所接收的数据包,其原型如下:
int pcap_dispatch(pcap_tp,int cnt,pcap_handler callback,u_charuser)
上述函数中,参数cnt描述了函数返回时所需捕获的最大数据包个数。对一个可用捕获源进行读取时,每次仅会对存储数据包的缓冲区执行一次读操作,且少于cnt个数的数据包可被处理。如果参数cnt设为-1,则表示对读取可用的捕获源而言是读取放置在缓冲区中的所有数据包,而对读取一个savefile文件而言,是读取文件的所有数据包。
参数callback描述了所需的回调函数,其原型如下:
void packet_handler(u_char*user,
const struct pcap_pkthdrpkt_header,const u_charpkt_data)
上述代码中,参数user是一个用户定义的参数,由pcap_dispatch函数传入,它包含了捕获会话的状态,其对应pcap_dispatch函数的user参数;参数pkt_header是驱动程序给数据包附加的一个信息头,而不是数据包的协议头;参数pkt_data指向数据包的数据,包括数据包的协议头。
pcap_dispatch函数返回所读取数据包的个数(≥1)。如果没有数据包被捕获,函数返回0。如果函数出现错误则返回-1,可使用pcap_perror函数或pcap_geterr函数获得描述错误的文本信息。如果在没有数据包被处理之前调用了pcap_breakloop函数,循环将被终止,返回-2。所以,如果应用程序使用了pcap_breakloop函数,应该确保会显式地检查返回值是-1还是-2,而不仅是检查返回值是否小于0。
pcap_dispatch函数的主要实现代码如下:
int pcap_dispatch(pcap_t*p,int cnt,pcap_handler callback,
u_char*user)
{
return p->read_op(p,cnt,callback,user);
}
在pcap_activate_win32函数中已经指明p->read_op为pcap_read_win32_npf函数,所以由pcap_read_win32_npf函数负责具体的数据读取与处理,主要实现代码如下:
static int pcap_activate_win32(pcap_t*p)
{
……
p->read_op=pcap_read_win32_npf;
……
}
下面来介绍pcap_read_win32_npf函数。
该函数负责读取一组数据包,并解析为一个一个单独的数据包,然后对每个数据包调用一次回调函数,把驱动程序所获得的数据包推向应用程序。函数各参数的含义与pcap_dispatch函数一样,其主要实现代码如下:
static int pcap_read_win32_npf(pcap_t*p,int cnt,
pcap_handler callback,u_char*user)
{
int cc;//记录待处理的数据包数据字节数
int n=0;//已处理的数据包个数
register u_charbp,ep;//记录待处理数据包数据的起始与终止位置
cc=p->cc;
/如果待处理数据包数据的字节数为0,就需要捕获数据包/
if(p->cc==0)
{
//pcap_breakloop函数被调用了吗?
if(p->break_loop){//break_loop为1表示终止循环
//调用了该函数,把该标识清零,并返回-2,表示退出了该循环
p->break_loop=0;
return(-2);
}
//捕获数据包
if(PacketReceivePacket(p->adapter,p->Packet,TRUE)==FALSE)
{//数据包读取失败
return(-1);
}
//所读取数据的字节数
cc=p->Packet->ulBytesReceived;
//存放数据包数据的缓冲区起始地址
bp=p->Packet->Buffer;
}
else
bp=p->bp;
/从缓冲区中解析出每个数据包/
defne bhp((struct bpf_hdr*)bp)
ep=bp+cc;//设置待处理数据包数据的终止位置(起始位置+字节数)
while(1){
register int caplen,hdrlen;
if(p->break_loop){//调用pcap_breakloop函数了吗?
//如调用了,则立即返回
if(n==0){
//如果没有读取任何数据包(n=0)
//则清除该标识(break_loop)并返回-2,表示跳出了该循环
p->break_loop=0;
return(-2);
}else{
//否则保持该标识的设置(n不等于0的情况),
//因此下一个调用将跳出该循环,而没有读取任何数据包,
//并返回已处理的数据包个数(n)
p->bp=bp;
p->cc=ep-bp;
return(n);
}
}
//数据处理结束,退出循环
if(bp>=ep)
break;
caplen=bhp->bh_caplen;//获得数据包的捕获长度
hdrlen=bhp->bh_hdrlen;//获得数据包头的长度
//调用回调函数,一个bpf_hdr结构体匹配一个pcap_pkthdr结构体
(callback)(user,(struct pcap_pkthdr)bp,bp+hdrlen);
bp+=BPF_WORDALIGN(caplen+hdrlen);//与存储时字对齐一致
if(++n>=cnt&&cnt>0){//处理的数据包个数达到cnt个
p->bp=bp;
p->cc=ep-bp;
return(n);
}
}
undef bhp
/数据处理已结束,退出循环,把p->cc清零,并返回处理的数据包数n/
p->cc=0;
return(n);
}
上述代码中,结构体pcap_pkthdr用于在wpcap.dll库中描述NPF驱动程序为每个所捕获数据包添加的头信息,其定义如下:
struct pcap_pkthdr{
struct timeval ts;/数据包接收的时间戳/
bpf_u_int32 caplen;/数据包所捕获的长度/
bpf_u_int32 len;/数据包的原始长度/
};
2.pcap_loop函数
pcap_loop函数与pcap_dispatch函数非常相似,只是pcap_dispatch函数在超时时间到了就会返回,而pcap_loop函数则不会因此而返回,直到当cnt个数据包被捕获后它才会返回。因此pcap_loop函数会在一小段时间内阻塞网络。对于本章这些简单的实例程序来说,pcap_loop函数就可以满足要求了,而对于比较复杂的程序来说,选择pcap_dispatch函数会更合适。
pcap_loop与pcap_dispatch这两个函数都有一个参数packet_handler回调函数,它指向一个可以接收数据包的函数。该回调函数会在每收到一个新的数据包,且收到一个事件通知时被调用。
pcap_loop函数用于接收一组数据包,其原型如下:
int pcap_loop(pcap_tp,int cnt,pcap_handler callback,u_charuser)
上述函数中,参数cnt表示读取的数据包个数达到cnt个时,函数将返回0。若cnt为负值,则表示函数将会永远循环(或者是出现了一个错误才返回)。如果函数出现错误则返回-1。要是在没有数据包被处理之前调用了pcap_breakloop函数,循环将被终止,函数返回-2。如果应用程序使用了pcap_breakloop函数,应该确保显式地检查了返回值是-1还是-2,而不是仅检查返回值是否小于0。
pcap_loop函数有一个回调参数packet_handler,它指向一个可以接收数据包的函数。该回调函数会在每收到一个新数据包时被wpcap.dll库调用。此函数的主要实现代码如下:
int pcap_loop(pcap_tp,int cnt,pcap_handler callback,u_charuser)
{
register int n;
for(;;)//循环读取
{
if(p->sf.rfile!=NULL)//读取文件
{
/如果返回0,则表示EOF(文件结尾),将不会再循环/
n=pcap_offine_read(p,cnt,callback,user);
}
else
{
/保持读取,直到达到返回条件(n!=0)/
do{
n=p->read_op(p,cnt,callback,user);
}while(n==0);
}
if(n<=0)
return(n);
if(cnt>0){
cnt-=n;
/如果达到cnt个数据包就返回,否则继续循环/
if(cnt<=0)
return(0);
}
}
}
上述代码中,p->read_op就是调用的pcap_read_win32_npf函数。
3.pcap_next函数
pcap_next函数用于返回下一个可用的数据包,其原型如下:
u_charpcap_next(pcap_tp,struct pcap_pkthdr*h);
此函数会读取下一个数据包(通过调用pcap_dispatch函数来实现,参数cnt设置为1),并返回一个u_char类型的指针,指向数据包数据(不包含给数据包添加的头信息pcap_pkthdr结构体)。如果出现了错误或者在savefile文件中没有有效的数据包,则返回NULL(0)。
pcap_next函数的实现代码如下:
const u_charpcap_next(pcap_tp,struct pcap_pkthdr*h)
{
struct singleton s;
s.hdr=h;//头信息指针
if(pcap_dispatch(p,1,pcap_oneshot,(u_char*)&s)<=0)
return(0);
return(s.pkt);
}
/描述每个数据包的头信息与数据包数据的结构体/
struct singleton{
struct pcap_pkthdr*hdr;//头信息
const u_char*pkt;//数据包数据
};
/获得数据包与头信息的回调函数/
static void pcap_oneshot(u_char*userData,
const struct pcap_pkthdrh,const u_charpkt)
{
struct singletonsp=(struct singleton)userData;
sp->hdr=h;
sp->pkt=pkt;
}
4.pcap_next_ex函数
pcap_next_ex函数与pcap_next相比,pcap_next有以下几点不足:①它效率低下,尽管它隐藏了回调的方式,但它依然依赖于pcap_dispatch函数。②它不能检测到文件末尾的状态(EOF),因此,如果数据包是需要从文件读取的,则其就不大实用了。值得注意的是,pcap_next_ex函数在成功、超时、出错或遇到文件EOF等的情况下,会返回不同的值。
pcap_next_ex函数会从一个设备接口或一个文件中读取一个数据包,其原型如下:
int pcap_next_ex(pcap_tp,struct pcap_pkthdr*pkt_header,
const u_char**pkt_data)
pcap_next_ex函数分别用指向数据包头信息的指针与数据包数据来填充pkt_header与pkt_data参数。此函数的各返回值含义如下:
❑1:数据包读取成功。
❑0:如果pcap_open_live设置的超时时间逝去,则pkt_header与pkt_data都不指向有用的数据包。
❑-1:出现错误。
❑-2:离线捕获(文件操作)遇到文件尾部的EOF。
pcap_next_ex函数的实现代码如下:
int pcap_next_ex(pcap_tp,struct pcap_pkthdr*pkt_header,
const u_char**pkt_data)
{
struct pkt_for_fakecallback s;
s.hdr=&p->pcap_header;
s.pkt=pkt_data;
/保存指向数据包头信息的指针/
*pkt_header=&p->pcap_header;
if(p->sf.rfile!=NULL){//读取文件
int status;
status=pcap_offine_read(p,1,pcap_fakecallback,(u_char*)&s);
if(status==0)
return(-2);
else
return(status);
}
/*
*pcap_read函数(实际可能就是pcap_read_win32_npf函数)的返回值为:
*0:超时
*-1:错误
*-2:循环被pcap_breakloop函数中止
*>=1:正确
*/
return(p->read_op(p,1,pcap_fakecallback,(u_char*)&s));
}
上述代码中,pkt_for_fakecallback结构体描述了每个数据包头信息与数据包数据,该结构体的定义如下:
struct pkt_for_fakecallback{
struct pcap_pkthdr*hdr;//头信息
const u_char**pkt;//数据包数据
};
上述代码中,pcap_fakecallback回调函数用于获得数据包与头信息,其实现代码如下:
static void pcap_fakecallback(u_char*userData,
const struct pcap_pkthdrh,const u_charpkt)
{
struct pkt_for_fakecallbacksp=(struct pkt_for_fakecallback)userData;
sp->hdr=h;//头信息
*sp->pkt=pkt;//数据包数据
}
pcap_offline_read函数的返回值含义如下:
❑0:EOF,文件结尾
❑-1:错误
❑>=1:正确
第一种返回值(0)与pcap_read函数返回的0值(表示一个在线捕获“超时之前没有数据包到达,再尝试”的情况)冲突,若把它映射为-2,就能在"savefile"的EOF与在线捕获的“在超时之前没有数据包到达,再尝试”之间进行区别了。
实际上,如果p->read_op是pcap_read_win32_npf函数,该函数的返回值如下:
❑0:超时
❑-1:错误
❑-2:循环被pcap_breakloop函数中止
❑>=1:正确
5.pcap_breakloop函数
pcap_breakloop函数会设置一个标识来强制pcap_dispatch或pcap_loop函数返回,使其不再继续循环。该函数的实现代码如下:
void pcap_breakloop(pcap_t*p)
{
p->break_loop=1;
}