winpcap/npcap 提高抓包效率 发UDP包失败

最近使用winpcap在win7下编写网络抓包程序,记录一下。

winpcap是比较老的网络抓包库,在win7和win10上依赖都不是很好,看网上有针对的win10版本的winpcap安装包,这里分享了win10下的winpcap安装包和编译好的库以及winpcap的源程序,连接如下win10pcap安装包
关于win10下重新编译wpcap可参考另外一篇,主要记录了遇到的问题win7编译wpcap

言归正传,本文目的是为了记录下如何提高winpcap的抓包效率,先描述下问题。
问题描述:在PC端使用winpcap编写抓UDP包程序,千兆网卡,另一端是FPGA编写的数据发送端,带宽可达到800M/s,安装官方给的用例实现,存在以下问题。

  • 数据发送失败
  • 丢包非常严重

1、数据发送失败
使用winpcap编写的发送UDP数据的功能,在本地使用wireshark可以抓到包,但在另一端无法收到,wireshark也抓不到,分析的原因是数据包没有送出网卡,无论调用pcap_sendpacket函数还是pcap_sendqueue_transmit都发送不成功,最终将winpcap替换为Npcap,发送数据成功。有次看来还是winpcap的驱动在win7或者win10上依然存在bug。关于Npcap的详细信息可以查看官网Npcap,其实就是优化后的winpcap。
关于组UDP包的代码

//以太网数据头结构如下
#define ETHER_ADDR_LEN 6
typedef struct ether_header{
	u_char ether_dhost[ETHER_ADDR_LEN];
	u_char ether_shost[ETHER_ADDR_LEN];
	u_short ether_type;	//如果上一层为IP协议。则ether_type的值就是0x0800
}ether_header;
/* 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{
	unsigned   char		ihl : 4;				//ip   header   length    
	unsigned   char		version : 4;			//version   
	u_char  tos;            // 服务类型(Type of service) 
	u_short tot_len;           // 总长(Total length) 
	u_short id;				// 标识(Identification)
	u_short flag_off;       // 标志位(Flags) (3 bits) + 段偏移量(Fragment offset) (13 bits)
	u_char  ttl;            // 存活时间(Time to live)
	u_char  protocol;          // 协议(Protocol)
	u_short check;            // 首部校验和(Header checksum)
	in_addr  saddr;      // 源地址(Source address)
	in_addr  daddr;      // 目的地址(Destination address)
//	u_int   op_pad;         // 选项与填充(Option + Padding)
}ip_header;

/* 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;
struct Psd_Header {
	ULONG sourceip; //源IP地址
	ULONG destip; //目的IP地址
	BYTE mbz; //置空(0)
	BYTE ptcl; //协议类型
	USHORT plen; //TCP/UDP数据包的长度(即从TCP/UDP报头算起到数据包结束的长度 单位:字节)
};

组UDP包的过程

int  makeframe_udp(u_char*frame, char*data, int datalen)
	{
		int UserDataLen = datalen;
		int TotolLen = UserDataLen+sizeof(ether_header) + sizeof(ip_header) + sizeof(udp_header);
		char buffer[1024] = { 0 };
		ether_header* pether_header = (ether_header*)buffer;
		ip_header* pip_herder = (ip_header*)(buffer + sizeof(ether_header));
		udp_header* pudp_herder = (udp_header*)(buffer + sizeof(ether_header) + sizeof(ip_header));

		//以太网帧头
		memcpy(pether_header->ether_dhost, _eheader.ether_dhost, ETHER_ADDR_LEN);
		memcpy(pether_header->ether_shost, _eheader.ether_shost, ETHER_ADDR_LEN);
		USHORT TmpType = 8;
		pether_header->ether_type = TmpType;
		//IP头
		pip_herder->ihl = sizeof(ip_header) / 4;
		pip_herder->version = 4;
		pip_herder->tos = 0;
		pip_herder->tot_len = htons(TotolLen - sizeof(ether_header));
		pip_herder->id = htons(0x370d);//????
		pip_herder->flag_off = htons(0);
		pip_herder->ttl = 0x40;
		pip_herder->protocol = IPPROTO_UDP;
		pip_herder->check = 0;
		pip_herder->saddr.S_un.S_addr = inet_addr(_sourceaddress);
		pip_herder->daddr.S_un.S_addr = inet_addr(_dstaddress);
		pip_herder->check = in_cksum((u_short*)pip_herder, sizeof(ip_header));
		
		//构建UDP数据头;
		pudp_herder->dport = htons(_ndPort);
		pudp_herder->sport = htons(_nsPort);
		pudp_herder->len = htons(UserDataLen + 8);
		pudp_herder->crc = 0;
		int headlen = sizeof(ether_header) + sizeof(ip_header) + sizeof(udp_header);

		char buffer2[1024] = { 0 };
		Psd_Header* psd = (Psd_Header*)buffer2;
		psd->sourceip = inet_addr(_sourceaddress);
		psd->destip = inet_addr(_dstaddress);
		psd->ptcl = IPPROTO_UDP;
		psd->plen = htons(UserDataLen + 8);
		psd->mbz = 0;
		memcpy(buffer2 + sizeof(Psd_Header), (void*)pudp_herder, sizeof(udp_header));
		memcpy(buffer2 + sizeof(Psd_Header) + sizeof(udp_header), data, UserDataLen);
		pudp_herder->crc = in_cksum((u_short *)buffer2, UserDataLen + 8 + sizeof(Psd_Header));
		
		memcpy(frame, buffer, headlen);
		memcpy((void*)(frame + headlen), (void*)data, UserDataLen);
		return TotolLen;
	}

在这其中需要计算校验和,计算代码

u_short in_cksum(u_short * addr, int len)
	{
		int     nleft = len;
		u_int sum = 0;
		u_short *w = addr;
		u_short answer = 0;

		/*
		* Our algorithm is simple, using a 32 bit accumulator (sum), we add
		* sequential 16 bit words to it, and at the end, fold back all the
		* carry bits from the top 16 bits into the lower 16 bits.
		*/
		while (nleft > 1) {
			sum += *w++;
			nleft -= 2;
		}
		/* mop up an odd byte, if necessary */
		if (nleft == 1) {
			*(unsigned char *)(&answer) = *(unsigned char *)w;
			sum += answer;
		}

		/* add back carry outs from top 16 bits to low 16 bits */
		sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
		sum += (sum >> 16);     /* add carry */
		answer = ~sum;     /* truncate to 16 bits */
		return (answer);
	}

最终的以太网帧还需要加上CRC校验和,和头部的数据,但是在使用winpcap的发送接口时,这些数据段都不需要添加计算。

2、提高抓包效率
npcap的抓包过程包含两级缓存,即内核缓存和用户缓存,内核将网卡接收到的数据拷贝到内核缓存中,wpcap又讲内核缓存中的数据拷贝到用户缓存中,应用层再调用pcap_loop或者pcap_next_ex获取每一包的数据。
通过分析npcap的抓包过程,由于千兆网下瞬时速度太快,当发送数据较多如1M,PC端总是会出现不同程度的丢包现象,使用wireshark也无法抓到,wireshark和自己写的抓包程序现象一样。
想过修改源码,但看到源码,实在没有时间细嚼慢咽了,还是通过经验慢慢摸索下吧。

  • 尝试一
    增大两级缓存,内核缓存默认为1M,用户缓存默认为512K,按道理说内核缓存大小为1M,我发送1M的数据,缓存是够的,在使用默认缓存和增大缓存后,现象却不如人意,依然丢了很多包。初步分析该数量级上的数据包,丢包并不是因为缓存不足引起的,可能数据量大的时候需要更大缓存,因为上层处理数据总是比较慢的。但是大的缓存总比小的好,在内存允许的情况下缓存越大越好了。
  • 尝试二
    尝试改变接收方式,使用pcap_next_ex代替回调pcap_loop,总觉得函数的频繁调用是要消耗系统资源的,在接收线程中,只做了数据包个数累计,不做任何费时操作,但是丢包现象依旧。看来也不是这的原因。
  • 尝试三
    网上有人说将pcap_open_live中的接收数据长度设置的小一点(一般都写成65535),这样加快内核存储空间的分配,加快内核对接受数据的处理,猛一听有点道理,尝试一把,黯然神伤!理论上可能会加快,但不足以解决丢包的问题
  • 尝试四
    将pcap_open_live中的timeout设置的尽量小,比如1ms,这样虽然会有一定程度上占用cpu资源,但是对数据包的响应速度会比较快。
  • 尝试五
    还是要分析原理啊,通过各种尝试,初步认为数据丢包发生在内核处理过程,即送网卡取数据的过程慢了,可如何加快内核的处理过程呢,这个npcap并没有提供接口,但是,npcap提供了内核拷贝最小数据设置的接口pcap_setmintocopy,这个接口很关键,默认值是16k,适当的增加这个设置,就会减少内核数据的拷贝次数,换言之就是减少了内核的操作次数,这就变相的加快了内存处理速度,通过增加这个参数,再配合增加两级缓存,丢包的问题终于得到了有效的缓解。当然我说的是缓解,因为千兆网下UPD的数据包,有要求实时性,怎么敢保证一包不丢呢。

你可能感兴趣的:(网络)