通信协议的设计
说到通信,我们肯定会想到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上)
- 插件:WinPcap 4.1.2
实验拓扑

PA端是真实的电脑,PB端是vmware开的windows xp虚拟机,虚拟机网络连接方式选择NAT模式。这样,主机上的VMnet8 Adapter就能够和虚拟机的虚拟网卡处于同一个网段中来通信了。
收发包基础环境搭建
PA发包
1、搭建Visual Studio可用环境
文件→新建→项目→Visual C++空项目→命名为
BTPsender右键→添加→新建项:新建一个头文件
BTPsender.h和源文件BTPsender.c
屏蔽安全报错
相信不少VS老玩家都应该知道_s的安全报错,直接在代码里写并不是一个好的方法,修改预处理器更好,当然你也可以直接屏蔽所有的警告,这里不再赘述。
调试→属性→配置属性→C/C++→预处理器→预处理器定义,添加:
c
_CRT_SECURE_NO_WARNINGS;- 写个helloworld测试一下
BTPsender.h
c
#include <stdio.h>
#include <stdlib.h>BTPsender.c
c
#include "BTPsender.h"
int main(){
printf("1");
return 0;
}2、添加winpcap支持
前往winpcap官网下载源码包


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

调试→属性→配置属性→C/C++→预处理器→预处理器定义,添加:
c
HAVE_REMOTE;WPCAP;
- 调试→属性→配置属性→链接器→输入→附加依赖项,添加:
c
ws2_32.lib;wpcap.lib;Packet.lib;
调试→属性→VC++目录,包含目录添加
E:\WpdPack\Include,库目录添加E:\WpdPack\Lib,这两个路径就是你刚才解压winpcap源码的路径。
测试一下是否引入成功
BTPsender.h添加:
c
#include <pcap.h>运行,发现报错,找不到sys/time.h:
我们点击这个报错进去看,发现是这几行代码:
很容易能读懂,就是没有定义WIN32这个常量,导致被识别为UNIX系统,引入了UNIX的头文件,当然找不到了。解决办法就是直接注释掉整段,强行写一个#include:
保存,再次运行,成功通过编译。
3、发包测试
- 打开windows xp虚拟机PB,网络适配器选择NAT模式,用
ipconfig /all查看虚拟网卡MAC地址,这里是00-0C-29-86-B8-C8:
- 回到主机PA,同样用
ipconfig /all查看本机VMnet8的MAC地址,这里是00-50-56-C0-00-08:
- 构造一个以太网帧,数据字段就随便填写一个字符串,然后使用winpcap发送出去。
这里有个知识点就是以太网帧的格式,这里使用最常见的Ethernet II,有关以太网帧的比较可以参看《四种格式的以太网帧结构》,以后也许会有机会自己写一篇。 
- FCS:FCS就是目的MAC到数据之间的内容得到的校验和,一般用CRC32。这一部分是不需要自己添加的,网卡驱动会自行计算。
- 数据:至于为什么数据最少要有46字节,简单的来说就是为了及时检测冲突,如果包小于64字节,那么对于相距很远的主机,很可能这边发送完了认为发送成功,那边冲突的信号还没传过来。我们所设计的协议包就是放在以太网帧的数据字段里面,所以这里先随便填写一个字符串,测试发送的通畅性。
- 类型:类型字段表明这个帧到底是什么,比如常见的IP包,这里会写
0x0800;ARP包,这里会写0x0806;PPPoE发现阶段为0x8863等等。这里随便填一个,填类型为IP包即可。
不难写出这样的发包程序:
c
// 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;c
// 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, ð_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。
调试→属性→配置属性→常规→平台工具集,选择Visual Studio 2013 - Windows Xp(v120_xp)

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

调试→属性→配置属性→链接器→命令行,添加:
c
/SUBSYSTEM:CONSOLE,"5.01"
这一步非常重要,否则你的程序跑在windows xp上就会提示“不是有效的win32应用程序”。
- 使用静态编译 这样就不需要动态链接库了,把所有依赖都打包进exe。
调试→属性→配置属性→C/C++→代码生成→运行库,选择多线程(/MT): 
- 收包测试
c
// 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地址格式化函数 */c
// 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文件复制到虚拟机运行: 
这里值得注意的有:
- 解包的时候要使用
ntohs(eth_header->etype)来把字节序展示为正常的字节序 - 过滤器的写法可以参考《WinPcap笔记(6):过滤数据包》
PA端运行发包程序,然后PB端运行收包程序,最后的结果: 
到这里,已经可以学会如何设计通信协议、以及使用winpcap进行发包收包了。



粤公网安备44030602007943号