文章目录

一、通信协议的设计

说到通信,我们肯定会想到OSI七层模型,想到TCP/IP,想到Socket。但是如果我们需要直接和物理设备通信,尤其是对实时性、安全性要求较高的时候,采用在数据链路层发送自己设计的裸包的方法是最好不过的了:

第一,安全性可控。自己设计的通信协议当然可以控制想要加密什么东西了。

第二,实时性。不需要经过高层的封包解包,直接向MAC地址发送裸包。

第三,也是最重要的,可裁剪。我们可以裁剪掉不需要的功能,增加需要的功能,这对于有内存闪存大小限制的嵌入式设备是很有意义的。

那么,该如何去设计这个通信协议呢?最简单的协议可以考虑这些内容:

序号协议字段名详细描述
 1协议标识 标记这个包是用的你的协议
 2协议版本 当协议有多个版本后,可以协调兼容问题
 3包类型 握手包、心跳包、数据包、断开包
 4包序号 发送者设定序号,接受者回复同样的号
 5数据长度 数据字段有多少字节
 6数据 要传输的数据,可以为空 
 7校验和  校验包在传输过程中是否发生了错误

当然,更深层次的协议可以去设计错误码、重传、分片、加密、压缩、FLAG字段、透传token等等,甚至还需要考虑各种开源代码是否可以使用(如果你不想开源的话,使用MIT License的代码是很好的选择)。

简单包中的1~4可以看做传说中的包头(Header),我们定义自己的一个简单通信协议,称为BTP(BWB Transport Protocol):

其中:

序号BTP字段名说明
 1协议标识 0x42(大写的'B')
 2协议版本 0x01(1.0版本)
 3包类型 握手请求包:0x01
 握手响应包:0x02
 心跳请求包:0x03
 心跳响应包:0x04
 数据包:0x05
 断开请求包:0x06
 断开响应包:0x07
 4包序号 0x00~0xFF循环使用
 5数据长度 0x0000~0xFFFF
 6数据 要传输的数据,可以为空 
 7校验和  采用经典的CRC32

一个简单BTP传输协议就设计好啦。

二、使用winpcap进行简单收发包测试

实验工具和平台

操作系统:windows 8 / VMware14(windows xp sp3)

开发软件:Visual Studio 2013 / Wireshark 2.6.2(可以运行在windows 8 上) / Wireshark  1.4.9(可以运行在windows xp上)[百度网盘下载(密码:bclc)]

插件:WinPcap 4.1.2 源码包  [百度网盘下载(密码:a03g)] 

实验拓扑

PA端是真实的电脑,PB端是vmware开的windows xp虚拟机,虚拟机网络连接方式选择NAT模式。这样,主机上的VMnet8 Adapter就能够和虚拟机的虚拟网卡处于同一个网段中来通信了。

收发包基础环境搭建

【PA发包】

1、搭建Visual Studio可用环境

(1)文件→新建→项目→Visual C++空项目→命名为BTPsender

(2)右键→添加→新建项:新建一个头文件BTPsender.h和源文件BTPsender.c

(3)屏蔽安全报错

相信不少VS老玩家都应该知道_s的安全报错,直接在代码里写并不是一个好的方法,修改预处理器更好,当然你也可以直接屏蔽所有的警告,这里不再赘述。

调试→属性→配置属性→C/C++→预处理器→预处理器定义,添加:

_CRT_SECURE_NO_WARNINGS;

(4)写个helloworld测试一下:

BTPsender.h

#include <stdio.h>
#include <stdlib.h>

BTPsender.c

#include "BTPsender.h"
int main(){
	printf("1");
	return 0;
}

2、添加winpcap支持

(1)前往winpcap官网下载源码包

(2)将源码包解压到合适的位置,注意路径不要带中文

(3)调试→属性→配置属性→C/C++→预处理器→预处理器定义,添加:

HAVE_REMOTE;WPCAP;

(4)调试→属性→配置属性→链接器→输入→附加依赖项,添加:

ws2_32.lib;wpcap.lib;Packet.lib;

(5)调试→属性→VC++目录,包含目录添加E:\WpdPack\Include,库目录添加E:\WpdPack\Lib,这两个路径就是你刚才解压winpcap源码的路径。

(6)测试一下是否引入成功:

BTPsender.h添加:

#include <pcap.h>

运行,发现报错,找不到sys/time.h:

我们点击这个报错进去看,发现是这几行代码:

很容易能读懂,就是没有定义WIN32这个常量,导致被识别为UNIX系统,引入了UNIX的头文件,当然找不到了。解决办法就是直接注释掉整段,强行写一个#include:

保存,再次运行,成功通过编译。

3、发包测试

(1)打开windows xp虚拟机PB,网络适配器选择NAT模式,用ipconfig /all查看虚拟网卡MAC地址,这里是00-0C-29-86-B8-C8:

(2)回到主机PA,同样用ipconfig /all查看本机VMnet8的MAC地址,这里是00-50-56-C0-00-08:

(3)构造一个以太网帧,数据字段就随便填写一个字符串,然后使用winpcap发送出去。

这里有个知识点就是以太网帧的格式,这里使用最常见的Ethernet II,有关以太网帧的比较可以参看《四种格式的以太网帧结构》,以后也许会有机会自己写一篇。

FCS:FCS就是目的MAC到数据之间的内容得到的校验和,一般用CRC32。这一部分是不需要自己添加的,网卡驱动会自行计算。

数据:至于为什么数据最少要有46字节,简单的来说就是为了及时检测冲突,如果包小于64字节,那么对于相距很远的主机,很可能这边发送完了认为发送成功,那边冲突的信号还没传过来。我们所设计的协议包就是放在以太网帧的数据字段里面,所以这里先随便填写一个字符串,测试发送的通畅性。

类型:类型字段表明这个帧到底是什么,比如常见的IP包,这里会写0x0800;ARP包,这里会写0x0806;PPPoE发现阶段为0x8863等等。这里随便填一个,填类型为IP包即可。

不难写出这样的发包程序:

BTPsender.h

#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>
#include <winsock.h>
	
#define ETHERTYPE_IP					0x0800 /* IP */ 
#define ERROR_GENERAL					-1
#define ERROR_FINDALLDEVS_FAILURE		-2
#define ERROR_INTERFACES_NOT_FOUND		-3
#define ERROR_BAD_INPUT					-4
#define ERROR_OPEN_ADAPTER_FAILURE		-5
#define ERROR_SENDING_FAILURE			-6
#define SEND_BUFSIZE					1024
#define SEND_TIMES						10000
#define SEND_INTVAL						1000

typedef struct ETH_HEADER
{
	u_char dest_mac[6];
	u_char src_mac[6];
	u_short etype;
}ETH_HEADER;

BTPsender.c

#include "BTPsender.h"

int main()
{
	pcap_t *adapter;						/* 网卡句柄 */
	char errbuf[PCAP_ERRBUF_SIZE];			/* 错误信息buffer */
	ETH_HEADER eth_header;					/* 以太网包头 */

	char package[] = { "BTP test.<46" };		/* 测试用的字符串 */
	int index;								/* 发送buffer偏移 */
	u_char sendbuf[SEND_BUFSIZE];			/* 发送buffer */

	pcap_if_t *alldevs;	/* 全部网卡列表 */
	pcap_if_t *d;		/* 一个网卡 */
	int did;			/* 选择的网卡ID */

	int i;				/* 迭代 */

	/* 查找网卡 */
	if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1) {
		fprintf(stderr, "[ERROR] pcap_findalldevs error: %s\n", errbuf);
		return ERROR_FINDALLDEVS_FAILURE;
	}

	/* 选择网卡d */
	for (d = alldevs, i = 0; d; d = d->next) {
		if (d->description)
			printf("NO.%d: %s\n", ++i, d->description);
		else
			printf("[WARN] No description available\n");
	}

	if (i == 0) {
		printf("[ERROR] No interfaces found! Make sure WinPcap is installed.\n");
		return ERROR_INTERFACES_NOT_FOUND;
	}

	printf("[INFO] Enter the interface number (1-%d):", i);
	scanf("%d", &did);

	if (did < 1 || did > i) {
		printf("[ERROR] Interface number out of range.\n");
		pcap_freealldevs(alldevs);
		return ERROR_BAD_INPUT;
	}

	for (d = alldevs, i = 0; i < did - 1; d = d->next, i++);

	/* 打开网卡 */
	if ((adapter = pcap_open_live(d->name,	/* 设备名 */
							65536,			/* 捕获数据包的长度(65536捕获所有数据包) */
							1,				/* 混杂模式(非0表示使用混杂模式) */
							1000,			/* 超时时间(0表示没有超时限制) */
							errbuf			/* 错误缓存(存储错误信息) */
		)) == NULL) {
		fprintf(stderr, "[ERROR] Unable to open the adapter. %s is not supported by WinPcap\n");
		pcap_freealldevs(alldevs);
		return ERROR_OPEN_ADAPTER_FAILURE;
	}

	/*目的PB的mac地址*/
	eth_header.dest_mac[0] = 0x00;
	eth_header.dest_mac[1] = 0x0C;
	eth_header.dest_mac[2] = 0x29;
	eth_header.dest_mac[3] = 0x86;
	eth_header.dest_mac[4] = 0xB8;
	eth_header.dest_mac[5] = 0xC8;

	/*源PA的mac地址*/
	eth_header.src_mac[0] = 0x00;
	eth_header.src_mac[1] = 0x50;
	eth_header.src_mac[2] = 0x56;
	eth_header.src_mac[3] = 0xC0;
	eth_header.src_mac[4] = 0x00;
	eth_header.src_mac[5] = 0x08;

	eth_header.etype = htons(ETHERTYPE_IP);



	memcpy(sendbuf, &eth_header, sizeof(eth_header));
	index = sizeof(eth_header);
	memcpy(&sendbuf[index], package, sizeof(package));
	index += sizeof(package);
	
	/* 发包 */
	for (i = 0; i < SEND_TIMES; i++) {
		if (pcap_sendpacket(adapter,	/* 网卡句柄 */
							sendbuf,	/* 要发送的帧 */
							index		/* 帧的大小 */
			) != 0) {
			fprintf(stderr, "[ERROR] Error sending the packet: %s\n", pcap_geterr(adapter));
			return ERROR_SENDING_FAILURE;
		}
		printf("packet send successed!\n");
		Sleep(SEND_INTVAL);
	}

	pcap_close(adapter);
	return 0;
}

这里要注意的是,引入了#include <winsock.h>,并且使用了eth_header.etype = htons(ETHERTYPE_IP);的写法,我们来看看不这样做会发生什么,用wireshark抓vmnet8的包:

被识别为了802.3的帧,长度为8,巧合的是设置的IP类型也为0x0800。没错,由于网络字节序,0x0800被存储为了0x0008,导致被识别为802.3协议。一种改法是采用类似MAC地址的那种字节数组,然而这不利于使用宏定义;还有一种就是修改宏定义,改为0x0008,但是这又不利于阅读了。所以我们最好写的时候正常写,发包的时候再利用htons(也就是host to network short)函数转换:

还有一点值得注意的是,发送的数据包小于46字节,也没有报错。我们先来看看在虚拟机windows xp上低版本的wireshark抓到的包是什么样的:

原来后面都填充了0,一共60字节(有4字节校验码没有显示),所以推测wireshark高版本不再显示这些自动填充字节了。PA发包,PB收到相同的包,说明发包成功了~

【PB收包】

1、搭建Visual Studio可用环境 

与前面一样,项目名BTPrecver,文件BTPrecver.h / BTPrecver.c

2、添加winpcap支持  

与前面一样。

3、支持xp

这一步的原因是因为懒得去配置windows xp下的编程环境了,我们直接在windows 8上采用vs2013来编写和编译,然后把生成的exe发到虚拟机运行即可,因此需要让程序支持xp。

(1)调试→属性→配置属性→常规→平台工具集,选择Visual Studio 2013 - Windows Xp(v120_xp)

(2)调试→属性→配置属性→链接器→系统→子系统,选择窗口(/SUBSYSTEM:WINDOWS),同时注意一下所需最低版本是不是5.01:

(3)调试→属性→配置属性→链接器→命令行,添加:

/SUBSYSTEM:CONSOLE,"5.01"

这一步非常重要,否则你的程序跑在windows xp上就会提示“不是有效的win32应用程序”。

4、使用静态编译

这样就不需要动态链接库了,把所有依赖都打包进exe:

调试→属性→配置属性→C/C++→代码生成→运行库,选择多线程(/MT):

5、收包测试

BTPrecver.h

#include <stdio.h>
#include <stdlib.h>
#include <pcap.h>
#include <winsock.h>

#define ERROR_GENERAL					-1
#define ERROR_FINDALLDEVS_FAILURE		-2
#define ERROR_INTERFACES_NOT_FOUND		-3
#define ERROR_BAD_INPUT					-4
#define ERROR_OPEN_ADAPTER_FAILURE		-5
#define ERROR_SENDING_FAILURE			-6
#define ERROR_INVALID_DATALINK_TYPE		-7
#define ERROR_COMPILE_FILTER_FALIURE	-8
#define ERROR_SET_FILTER_FALIURE		-9

typedef struct ETH_HEADER
{
	u_char dest_mac[6];
	u_char src_mac[6];
	u_short etype;
}ETH_HEADER;


void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);/* 抓包回调函数 */
void format_mac(LPSTR lpHWAddrStr, const unsigned char *HWAddr);/* mac地址格式化函数 */

BTPrecver.c

#include "BTPrecver.h"

int main()
{
	pcap_t *adapter;				/* 网卡句柄 */
	char errbuf[PCAP_ERRBUF_SIZE];	/* 错误信息buffer */
	u_int netmask;					/* 掩码信息 */
	char packet_filter[] = "ether src 00:50:56:C0:00:08 and ether dst 00:0C:29:86:B8:C8"; /* 过滤规则 */
	struct bpf_program fcode;		/* 存储编译好的过滤码 */

	pcap_if_t *alldevs;	/* 全部网卡列表 */
	pcap_if_t *d;		/* 一个网卡 */
	int did;			/* 选择的网卡ID */

	int i = 0;			/* 迭代 */

	/*查找网卡*/
	if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1) {
		fprintf(stderr, "[ERROR] pcap_findalldevs error: %s\n", errbuf);
		return ERROR_FINDALLDEVS_FAILURE;
	}

	/* 选择网卡d */
	for (d = alldevs, i = 0; d; d = d->next) {
		if (d->description)
			printf("NO.%d: %s\n", ++i, d->description);
		else
			printf("[WARN] No description available\n");
	}

	if (i == 0) {
		printf("[ERROR] No interfaces found! Make sure WinPcap is installed.\n");
		return ERROR_INTERFACES_NOT_FOUND;
	}

	printf("[INFO] Enter the interface number (1-%d):", i);
	scanf("%d", &did);

	if (did < 1 || did > i) {
		printf("[ERROR] Interface number out of range.\n");
		pcap_freealldevs(alldevs);
		return ERROR_BAD_INPUT;
	}

	for (d = alldevs, i = 0; i < did - 1; d = d->next, i++);

	/* 打开网卡 */
	if ((adapter = pcap_open_live(d->name,	/* 设备名 */
								  65536,	/* 捕获数据包的长度(65536捕获所有数据包) */
								  1,		/* 混杂模式(非0表示使用混杂模式) */
								  1000,		/* 超时时间(0表示没有超时限制) */
								  errbuf	/* 错误缓存(存储错误信息) */
		)) == NULL) {
		fprintf(stderr, "[ERROR] Unable to open the adapter. %s is not supported by WinPcap\n");
		pcap_freealldevs(alldevs);
		return ERROR_OPEN_ADAPTER_FAILURE;
	}

	/* 检查链路层类型 */
	if (pcap_datalink(adapter) != DLT_EN10MB) /* DLT_EN10MB指10Mb以太网 */
	{
		fprintf(stderr, "[ERROR] This program works only on Ethernet networks.\n");
		pcap_freealldevs(alldevs);
		return ERROR_INVALID_DATALINK_TYPE;
	}

	/* 检查地址类型 */
	if (d->addresses != NULL) /* 如果有IP地址 */
		netmask = ((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr; /* 使用第一个掩码 */
	else /* 如果没有IP地址,说明是C类网络(局域网) */
		netmask = 0xffffff;	/* 掩码设置为255.255.255.0 */


	/* 编译过滤器 */
	if (pcap_compile(adapter, &fcode, packet_filter, 1, netmask) < 0) /* 1表示自动进行优化 */
	{
		fprintf(stderr, "[ERROR] Unable to compile the packet filter. Check the syntax.\n");
		pcap_freealldevs(alldevs);
		return ERROR_COMPILE_FILTER_FALIURE;
	}

	/* 应用过滤器 */
	if (pcap_setfilter(adapter, &fcode)<0)
	{
		fprintf(stderr, "[ERROR] Error setting the filter.\n");
		pcap_freealldevs(alldevs);
		return ERROR_SET_FILTER_FALIURE;
	}

	/* 开始抓包 */
	printf("listening on %s...\n", d->description);
	pcap_freealldevs(alldevs);
	pcap_loop(adapter, 0, packet_handler, NULL);

	return 0;
}

/* 抓包回调函数 */
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
	time_t local_tv_sec;	/* 时间戳 */
	struct tm *ltime;		/* 本地时间 */
	char timestr[16];		/* 格式化后的本地时间 */
	ETH_HEADER *eth_header;	/* 以太网帧包头 */
	char str_mac[50];		/* 源MAC地址 */
	char dest_mac[50];		/* 目的MAC地址 */
	char *data;		/* 数据 */

	/* 没有使用param  */
	(VOID)(param);

	/* 格式化当前时间 */
	local_tv_sec = header->ts.tv_sec;
	ltime = localtime(&local_tv_sec);
	strftime(timestr, sizeof timestr, "%H:%M:%S", ltime);

	/* 解析以太网帧 */
	eth_header = (ETH_HEADER *)pkt_data;
	format_mac(str_mac, eth_header->src_mac);
	format_mac(dest_mac, eth_header->dest_mac);

	printf("[ %s.%.6d ] receive package    \nlength=\t%d   \neth type=\t0x%x    \nsrc mac=\t%s    \ndest mac=\t%s\n", 
						timestr, header->ts.tv_usec, header->len, ntohs(eth_header->etype), str_mac, dest_mac);

	/* 解析数据域 */
	data = (char *)(pkt_data + 14);
	printf("data= \t%s\n", data);
}


void format_mac(char* lpHWAddrStr, const unsigned char *HWAddr)
{
	int i;
	short temp;
	char szStr[3];

	strcpy(lpHWAddrStr, "");
	for (i = 0; i<6; ++i)
	{
		temp = (short)(*(HWAddr + i));
		_itoa(temp, szStr, 16);
		if (strlen(szStr) == 1)
			strcat(lpHWAddrStr, "0");
		strcat(lpHWAddrStr, szStr);
		if (i<5)
			strcat(lpHWAddrStr, ":"); 
	}
}

然后把生成的EXE文件复制到虚拟机运行:

这里值得注意的有:
(1)解包的时候要使用ntohs(eth_header->etype)来把字节序展示为正常的字节序

(2)过滤器的写法可以参考《WinPcap笔记(6):过滤数据包》

PA端运行发包程序,然后PB端运行收包程序,最后的结果:

到这里,已经可以学会如何设计通信协议、以及使用winpcap进行发包收包了,完整的工程文件可以在这里下载(密码:qdnu)。那么接下来应用我们自己的通信协议试一试,如果有兴趣请继续浏览《用winpcap测试自己的通信协议(二)》

参考资料

1、《自己设计系统之间的通信协议》

2、《如何自定义一个通信协议》

3、《四种格式的以太网帧结构》

4、《c语言中网络字节序和主机字节序的转换》

5、《WinPcap基础知识(第八课:发送数据包)》

6、《关于以太网(Ethernet II)这个网络的个人理解以及应用(1)》

7、《以太网帧 类型字段及值》

8、《WinPcap发送接收裸包(一)》

9、《WinPcap发送接收裸包(二)》

10、《学习整理——以太帧、ip帧、udp/tcp帧、http报文结构》

11、《以太网原理 最大帧长 最小帧长》

12、《ARP欺骗源码(基于WinPcap实现)》

13、《怎么让VS2015编写的程序在XP中顺利运行》

14、《WinPcap笔记(6):过滤数据包》


转载请注明出处http://www.bewindoweb.com/211.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠