腾讯云上 Winpcap 网络编程四之主机通信

学习资源

我的腾讯云主机预装了 Windows Server ,截图如下:

两台主机通信实战

在上一篇我给大家介绍了《腾讯云上Winpcap网络编程三之ARP协议获得MAC地址表》

接下来我们让用户输入要发送的 IP 地址和要发送的数据

u_int ip1,ip2,ip3,ip4;
		scanf_s("%d.%d.%d.%d",&ip1,&ip2,&ip3,&ip4);
		printf("请输入你要发送的内容:\n");
		getchar();
		gets_s(TcpData);
		printf("要发送的内容:%s\n",TcpData);

声明一下 TcpData

	char TcpData[20];   //发送内容

接下来就是重头戏了,需要声明各种结构体,我们发送的是 TCP 数据,这样, TCP 的 TcpData 就作为真正的内容,然后在前面加上 TCP 头,IP 头,帧头,还有校验和要正确。

最后构成一个完整的帧,那么另外声明的结构体如下,前面代码声明过的帧头部结构体就去掉了。

//IP地址格式
struct IpAddress
{
    u_char byte1;
    u_char byte2;
    u_char byte3;
    u_char byte4;
};

//帧头部结构体,共14字节
struct EthernetHeader
{
    u_char DestMAC[6];    //目的MAC地址 6字节
    u_char SourMAC[6];   //源MAC地址 6字节
    u_short EthType;         //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp  2字节
};

//IP头部结构体,共20字节
struct IpHeader
{
    unsigned char Version_HLen;   //版本信息4位 ,头长度4位 1字节
    unsigned char TOS;                    //服务类型    1字节
    short Length;                              //数据包长度 2字节
    short Ident;                                 //数据包标识  2字节
    short Flags_Offset;                    //标志3位,片偏移13位  2字节
    unsigned char TTL;                   //存活时间  1字节
    unsigned char Protocol;          //协议类型  1字节
    short Checksum;                       //首部校验和 2字节
	IpAddress SourceAddr;       //源IP地址   4字节
	IpAddress DestinationAddr; //目的IP地址  4字节
};

//TCP头部结构体,共20字节
struct TcpHeader
{
    unsigned short SrcPort;                        //源端口号  2字节
    unsigned short DstPort;                        //目的端口号 2字节
    unsigned int SequenceNum;               //序号  4字节
    unsigned int Acknowledgment;         //确认号  4字节
    unsigned char HdrLen;                         //首部长度4位,保留位6位 共10位
    unsigned char Flags;                              //标志位6位
    unsigned short AdvertisedWindow;  //窗口大小16位 2字节
    unsigned short Checksum;                  //校验和16位   2字节
    unsigned short UrgPtr;						  //紧急指针16位   2字节
};

//TCP伪首部结构体 12字节
struct PsdTcpHeader
{
	IpAddress SourceAddr;                     //源IP地址  4字节
	IpAddress DestinationAddr;             //目的IP地址 4字节
    char Zero;                                                    //填充位  1字节
    char Protcol;                                               //协议号  1字节
    unsigned short TcpLen;                           //TCP包长度 2字节
};

继续 main 函数中对各种结构体的数据进行初始化赋值,并计算校验和。

//结构体初始化为0序列
		memset(&ethernet, 0, sizeof(ethernet));
		BYTE destmac[8];
		//目的 MAC 地址,此处没有对帧的 MAC 地址进行赋值,因为网卡设置的混杂模式,可以接受经过该网卡的所有帧。当然最好的方法是赋值为ARP刚才获取到的MAC地址,当然不赋值也可以捕捉到并解析,在此处仅做下说明。
		destmac[0] = 0x00;
		destmac[1] = 0x11;
		destmac[2] = 0x22;
		destmac[3] = 0x33;
		destmac[4] = 0x44;
		destmac[5] = 0x55;
		//赋值目的MAC地址
		memcpy(ethernet.DestMAC, destmac, 6);
		BYTE hostmac[8];
		//源MAC地址
		hostmac[0] = 0x00;
		hostmac[1] = 0x1a;
		hostmac[2] = 0x4d;
		hostmac[3] = 0x70;
		hostmac[4] = 0xa3;
		hostmac[5] = 0x89;
		//赋值源MAC地址
		memcpy(ethernet.SourMAC, hostmac, 6);
		//上层协议类型,0x0800代表IP协议
		ethernet.EthType = htons(0x0800);
		//赋值SendBuffer
		memcpy(&SendBuffer, ðernet, sizeof(struct EthernetHeader));
		//赋值IP头部信息
		ip.Version_HLen = 0x45;
		ip.TOS = 0;
		ip.Length = htons(sizeof(struct IpHeader) + sizeof(struct TcpHeader) + strlen(TcpData));
		ip.Ident = htons(1);
		ip.Flags_Offset = 0;
		ip.TTL = 128;
		ip.Protocol = 6;
		ip.Checksum = 0;
		//源IP地址
		ip.SourceAddr.byte1 = 127;
		ip.SourceAddr.byte2 = 0;
		ip.SourceAddr.byte3 = 0;
		ip.SourceAddr.byte4 = 1;
		//目的IP地址
		ip.DestinationAddr.byte1 = ip1;
		ip.DestinationAddr.byte2 = ip2;
		ip.DestinationAddr.byte3 = ip3;
		ip.DestinationAddr.byte4 = ip4;
		//赋值SendBuffer
		memcpy(&SendBuffer[sizeof(struct EthernetHeader)], &ip, 20);
		//赋值TCP头部内容
		tcp.DstPort = htons(102);
		tcp.SrcPort = htons(1000);
		tcp.SequenceNum = htonl(11);
		tcp.Acknowledgment = 0;
		tcp.HdrLen = 0x50;
		tcp.Flags = 0x18;
		tcp.AdvertisedWindow = htons(512);
		tcp.UrgPtr = 0;
		tcp.Checksum = 0;
		//赋值SendBuffer
		memcpy(&SendBuffer[sizeof(struct EthernetHeader) + 20], &tcp, 20);
		//赋值伪首部
		ptcp.SourceAddr = ip.SourceAddr;
		ptcp.DestinationAddr = ip.DestinationAddr;
		ptcp.Zero = 0;
		ptcp.Protcol = 6;
		ptcp.TcpLen = htons(sizeof(struct TcpHeader) + strlen(TcpData));
		//声明临时存储变量,用来计算校验和
		char TempBuffer[65535];
		memcpy(TempBuffer, &ptcp, sizeof(struct PsdTcpHeader));
		memcpy(TempBuffer + sizeof(struct PsdTcpHeader), &tcp, sizeof(struct TcpHeader));
		memcpy(TempBuffer + sizeof(struct PsdTcpHeader) + sizeof(struct TcpHeader), TcpData, strlen(TcpData));
		//计算TCP的校验和
		tcp.Checksum = checksum((USHORT*)(TempBuffer), sizeof(struct PsdTcpHeader) + sizeof(struct TcpHeader) + strlen(TcpData));
		//重新把SendBuffer赋值,因为此时校验和已经改变,赋值新的
		memcpy(SendBuffer + sizeof(struct EthernetHeader) + sizeof(struct IpHeader), &tcp, sizeof(struct TcpHeader));
		memcpy(SendBuffer + sizeof(struct EthernetHeader) + sizeof(struct IpHeader) + sizeof(struct TcpHeader), TcpData, strlen(TcpData));
		//初始化TempBuffer为0序列,存储变量来计算IP校验和
		memset(TempBuffer, 0, sizeof(TempBuffer));
		memcpy(TempBuffer, &ip, sizeof(struct IpHeader));
		//计算IP校验和
		ip.Checksum = checksum((USHORT*)(TempBuffer), sizeof(struct IpHeader));
		//重新把SendBuffer赋值,IP校验和已经改变
		memcpy(SendBuffer + sizeof(struct EthernetHeader), &ip, sizeof(struct IpHeader));
		//发送序列的长度
		int size = sizeof(struct EthernetHeader) + sizeof(struct IpHeader) + sizeof(struct TcpHeader) + strlen(TcpData);
		int result = pcap_sendpacket(adhandle, SendBuffer,size);
		if (result != 0)
		{
			printf("Send Error!\n");
		} 
		else
		{
			printf("Send TCP Packet.\n");
			printf("Dstination Port:%d\n", ntohs(tcp.DstPort));
			printf("Source Port:%d\n", ntohs(tcp.SrcPort));
			printf("Sequence:%d\n", ntohl(tcp.SequenceNum));
			printf("Acknowledgment:%d\n", ntohl(tcp.Acknowledgment));
			printf("Header Length:%d*4\n", tcp.HdrLen >> 4);
			printf("Flags:0x%0x\n", tcp.Flags);
			printf("AdvertiseWindow:%d\n", ntohs(tcp.AdvertisedWindow));
			printf("UrgPtr:%d\n", ntohs(tcp.UrgPtr));
			printf("Checksum:%u\n", ntohs(tcp.Checksum));
			printf("Send Successfully!\n");
		}

校验和方法如下:

//获得校验和的方法
unsigned short checksum(unsigned short *data, int length)
{
    unsigned long temp = 0;
    while (length > 1)
    {
        temp +=  *data++;
        length -= sizeof(unsigned short);
    }
    if (length)
    {
        temp += *(unsigned short*)data;
    }
    temp = (temp >> 16) + (temp &0xffff);
    temp += (temp >> 16);
    return (unsigned short)(~temp);
}

记得在声明一下这个方法。如果放在 main 函数前当然就不用声明啦。

另外需要声明的变量有

		struct EthernetHeader ethernet;    //以太网帧头
    struct IpHeader ip;                            //IP头
    struct TcpHeader tcp;                      //TCP头
    struct PsdTcpHeader ptcp;             //TCP伪首部

unsigned char SendBuffer[200];       //发送队列

接下来的运行结果:

获取MAC地址完毕,请输
121.250.216.112
请输入你要发送的内容
what is tcp
要发送的内容:what i
Send TCP Packet.
Dstination Port:102
Source Port:1000
Sequence:11
Acknowledgment:0
Header Length:5*4
Flags:0x18
AdvertiseWindow:512
UrgPtr:0
Checksum:17149
Send Successfully!

截图如下:

好啦,发送帧到此就告一段落啦!如果有疑问请留言。

帧的接收很简单,直接贴源码如下:

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


char *iptos(u_long in);       //u_long即为 unsigned long
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);
//struct tm *ltime;					//和时间处理有关的变量

struct IpAddress
{
    u_char byte1;
    u_char byte2;
    u_char byte3;
    u_char byte4;
};

//帧头部结构体,共14字节
struct EthernetHeader
{
    u_char DestMAC[6];    //目的MAC地址 6字节
    u_char SourMAC[6];   //源MAC地址 6字节
    u_short EthType;         //上一层协议类型,如0x0800代表上一层是IP协议,0x0806为arp  2字节
};

//IP头部结构体,共20字节
struct IpHeader
{
    unsigned char Version_HLen;   //版本信息4位 ,头长度4位 1字节
    unsigned char TOS;                    //服务类型    1字节
    short Length;                              //数据包长度 2字节
    short Ident;                                 //数据包标识  2字节
    short Flags_Offset;                    //标志3位,片偏移13位  2字节
    unsigned char TTL;                    //存活时间  1字节
    unsigned char Protocol;           //协议类型  1字节
    short Checksum;                        //首部校验和 2字节
    IpAddress SourceAddr;           //源IP地址   4字节
    IpAddress DestinationAddr;   //目的IP地址  4字节
};

//TCP头部结构体,共20字节
struct TcpHeader
{
    unsigned short SrcPort;                        //源端口号  2字节
    unsigned short DstPort;                        //目的端口号 2字节
    unsigned int SequenceNum;               //序号  4字节
    unsigned int Acknowledgment;         //确认号  4字节
    unsigned char HdrLen;                         //首部长度4位,保留位6位 共10位
    unsigned char Flags;                              //标志位6位
    unsigned short AdvertisedWindow;  //窗口大小16位 2字节
    unsigned short Checksum;                  //校验和16位   2字节
    unsigned short UrgPtr;						  //紧急指针16位   2字节
};

//TCP伪首部结构体 12字节
struct PsdTcpHeader
{
    unsigned long SourceAddr;                     //源IP地址  4字节
    unsigned long DestinationAddr;             //目的IP地址 4字节
    char Zero;                                                    //填充位  1字节
    char Protcol;                                               //协议号  1字节
    unsigned short TcpLen;                           //TCP包长度 2字节
};


int main(){

	EthernetHeader *ethernet;    //以太网帧头
    IpHeader *ip;                            //IP头
    TcpHeader *tcp;                      //TCP头
    PsdTcpHeader *ptcp;             //TCP伪首部

	pcap_if_t  * alldevs;       //所有网络适配器
	pcap_if_t  *d;					//选中的网络适配器
	char errbuf[PCAP_ERRBUF_SIZE];   //错误缓冲区,大小为256
	char source[PCAP_ERRBUF_SIZE];
	pcap_t *adhandle;           //捕捉实例,是pcap_open返回的对象
	int i = 0;                            //适配器计数变量
	struct pcap_pkthdr *header;    //接收到的数据包的头部
    const u_char *pkt_data;			  //接收到的数据包的内容
	int res;                                    //表示是否接收到了数据包
	u_int netmask;                       //过滤时用的子网掩码
	char packet_filter[] = "tcp";        //过滤字符
	struct bpf_program fcode;                     //pcap_compile所调用的结构体

	u_int ip_len;                                       //ip地址有效长度
	u_short sport,dport;                        //主机字节序列
	u_char packet[100];                       //发送数据包目的地址
	pcap_dumper_t *dumpfile;         //堆文件

	//time_t local_tv_sec;				//和时间处理有关的变量
    //char timestr[16];					//和时间处理有关的变量

	
	//获取本地适配器列表
	if(pcap_findalldevs_ex(PCAP_SRC_IF_STRING,NULL,&alldevs,errbuf) == -1){
		//结果为-1代表出现获取适配器列表失败
		fprintf(stderr,"Error in pcap_findalldevs_ex:\n",errbuf);
		//exit(0)代表正常退出,exit(other)为非正常退出,这个值会传给操作系统
		exit(1);
	}
	//打印设备列表信息
	for(d = alldevs;d !=NULL;d = d->next){
		printf("-----------------------------------------------------------------\nnumber:%d\nname:%s\n",++i,d->name);
		if(d->description){
			//打印适配器的描述信息
			printf("description:%s\n",d->description);
		}else{
			//适配器不存在描述信息
			printf("description:%s","no description\n");
		}
		//打印本地环回地址
		printf("\tLoopback: %s\n",(d->flags & PCAP_IF_LOOPBACK)?"yes":"no");
		
		 pcap_addr_t *a;       //网络适配器的地址用来存储变量
		 for(a = d->addresses;a;a = a->next){
			 //sa_family代表了地址的类型,是IPV4地址类型还是IPV6地址类型
			 switch (a->addr->sa_family)
			 {
				 case AF_INET:  //代表IPV4类型地址
					 printf("Address Family Name:AF_INET\n");
					 if(a->addr){
						 //->的优先级等同于括号,高于强制类型转换,因为addr为sockaddr类型,对其进行操作须转换为sockaddr_in类型
						 printf("Address:%s\n",iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
					 }
					if (a->netmask){
						 printf("\tNetmask: %s\n",iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
					}
					if (a->broadaddr){
						   printf("\tBroadcast Address: %s\n",iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
					 }
					 if (a->dstaddr){
						   printf("\tDestination Address: %s\n",iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
					 }
        			 break;
				 case AF_INET6: //代表IPV6类型地址
					 printf("Address Family Name:AF_INET6\n");
					 printf("this is an IPV6 address\n");
					 break;
				 default:
					 break;
			 }
		 }
	}
	//i为0代表上述循环未进入,即没有找到适配器,可能的原因为Winpcap没有安装导致未扫描到
	if(i == 0){
		printf("interface not found,please check winpcap installation");
	}

	int num;
	printf("Enter the interface number(1-%d):",i);
	//让用户选择选择哪个适配器进行抓包
	scanf_s("%d",&num);
	printf("\n");

	//用户输入的数字超出合理范围
	if(num<1||num>i){
		printf("number out of range\n");
		pcap_freealldevs(alldevs);
		return -1;
	}
	//跳转到选中的适配器
	for(d=alldevs, i=0; i< num-1 ; d=d->next, i++);

	//运行到此处说明用户的输入是合法的
	if((adhandle = pcap_open(d->name,		//设备名称
														65535,       //存放数据包的内容长度
														PCAP_OPENFLAG_PROMISCUOUS,  //混杂模式
														1000,           //超时时间
														NULL,          //远程验证
														errbuf         //错误缓冲
														)) == NULL){
        //打开适配器失败,打印错误并释放适配器列表
		fprintf(stderr,"\nUnable to open the adapter. %s is not supported by WinPcap\n", d->name);
        // 释放设备列表 
        pcap_freealldevs(alldevs);
        return -1;
	}
	

	//打印输出,正在监听中
	printf("\nlistening on %s...\n", d->description);

	//所在网络不是以太网,此处只取这种情况
	if(pcap_datalink(adhandle) != DLT_EN10MB)
    {
        fprintf(stderr,"\nThis program works only on Ethernet networks.\n");
        //释放列表
        pcap_freealldevs(alldevs);
        return -1;
    }

	//先获得地址的子网掩码
	if(d->addresses != NULL)
        //获得接口第一个地址的掩码 
        netmask=((struct sockaddr_in *)(d->addresses->netmask))->sin_addr.S_un.S_addr;
    else
        // 如果接口没有地址,那么我们假设一个C类的掩码
        netmask=0xffffff;

	//pcap_compile()的原理是将高层的布尔过滤表
	//达式编译成能够被过滤引擎所解释的低层的字节码
	if(pcap_compile(adhandle,	//适配器处理对象
										&fcode,
										packet_filter,   //过滤ip和UDP
										1,                       //优化标志
										netmask           //子网掩码
										)<0)
	{
		//过滤出现问题
		fprintf(stderr,"\nUnable to compile the packet filter. Check the syntax.\n");
        // 释放设备列表
        pcap_freealldevs(alldevs);
        return -1;
	}

	//设置过滤器
    if (pcap_setfilter(adhandle, &fcode)<0)
    {
        fprintf(stderr,"\nError setting the filter.\n");
        //释放设备列表
        pcap_freealldevs(alldevs);
        return -1;
    }


	//利用pcap_next_ex来接受数据包
	while((res = pcap_next_ex(adhandle,&header,&pkt_data))>=0)
	{
		if(res ==0){
			//返回值为0代表接受数据包超时,重新循环继续接收
			continue;
		}else{
			//运行到此处代表接受到正常从数据包
			//header为帧的头部
			printf("%.6ld len:%d ", header->ts.tv_usec, header->len);
			// 获得IP数据包头部的位置
			ip = (IpHeader *) (pkt_data +14);    //14为以太网帧头部长度
			//获得TCP头部的位置
			ip_len = (ip->Version_HLen & 0xf) *4;
			printf("ip_length:%d ",ip_len);
			tcp = (TcpHeader *)((u_char *)ip+ip_len);
			char * data;
			 data = (char *)((u_char *)tcp+20);
			 //将网络字节序列转换成主机字节序列
			sport = ntohs( tcp->SrcPort );
			dport = ntohs( tcp->DstPort );
			printf("srcport:%d desport:%d\n",sport,dport);
			printf("%d.%d.%d.%d.%d -> %d.%d.%d.%d.%d\n",
					ip->SourceAddr.byte1,
					ip->SourceAddr.byte2,
					ip->SourceAddr.byte3,
					ip->SourceAddr.byte4,
				    sport,
				    ip->DestinationAddr.byte1,
				    ip->DestinationAddr.byte2,
				    ip->DestinationAddr.byte3,
				    ip->DestinationAddr.byte4,
				    dport);
			printf("%s\n",data);
		}

	}

	
	//释放网络适配器列表
	pcap_freealldevs(alldevs);

	/**
	int pcap_loop  ( pcap_t *  p,  
								  int  cnt,  
								  pcap_handler  callback,  
								  u_char *  user   
								 );
     typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *,
                 const u_char *);
	*/
	//开始捕获信息,当捕获到数据包时,会自动调用这个函数
	//pcap_loop(adhandle,0,packet_handler,NULL);

	int inum;
	scanf_s("%d", &inum);

	return 0;

}

/* 每次捕获到数据包时,libpcap都会自动调用这个回调函数 */
/**
pcap_loop()函数是基于回调的原理来进行数据捕获的,如技术文档所说,这是一种精妙的方法,并且在某些场合下,
它是一种很好的选择。但是在处理回调有时候会并不实用,它会增加程序的复杂度,特别是在多线程的C++程序中
*/
/*
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data)
{
    struct tm *ltime = NULL;
    char timestr[16];
    time_t local_tv_sec;

    // 将时间戳转换成可识别的格式
    local_tv_sec = header->ts.tv_sec;
    localtime_s(ltime,&local_tv_sec);
    strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);

    printf("%s,%.6ld len:%d\n", timestr, header->ts.tv_usec, header->len);

}
*/
/* 将数字类型的IP地址转换成字符串类型的 */
#define IPTOSBUFFERS    12
char *iptos(u_long in)
{
    static char output[IPTOSBUFFERS][3*4+3+1];
    static short which;
    u_char *p;

    p = (u_char *)&in;
    which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
    sprintf_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
    return output[which];
}

运行截图如下

Thank You

如有问题,欢迎留言~

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员的碎碎念

redis对于key的操作命令

搞懂了前面的内容,我相信进入到redis操作命令就很舒服啦;redis操作命令学习没什么技巧,多看多用多拼。在这里分享燕十八里的redis学习笔记(当然官网才是...

1262
来自专栏程序猿DD

程序员你为什么这么累【续】:编码习惯之工具类规范

一个项目不可能没有工具类,工具类的初衷是良好的,代码重用,但到了后面工具类越来越乱,有些项目工具类有几十个,看的眼花缭乱,还有不少重复。如何编写出好的工具类,我...

2076
来自专栏蓝天

一个简单的支持MySQL和SQLite3的DB接口

simple_db.zip 相关联代码:https://github.com/eyjian/mooon/tree/master/common_library/...

812
来自专栏逍遥剑客的游戏开发

在C#中派生C++的抽象类

1894
来自专栏逆向技术

16位汇编第九讲汇编指令以及逆向中的花指令

                                      16位汇编第九讲汇编指令以及逆向中的花指令 一丶LOOP指令(循环指令) 作用:  ...

17610
来自专栏Java后端生活

JDBC(四)JDBC元数据和反射的利用

Java 通过JDBC获得连接以后,得到一个Connection 对象,可以从这个对象获得有关数据库管理系统的各种信息,包括数据库中的各个表,表中的各个列,数据...

472
来自专栏GreenLeaves

存储过程详解

存储过程简介 什么是存储过程:存储过程可以说是一个记录集吧,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表...

18410
来自专栏WindCoder

MySQL数据库对象与应用-MySQL程序开发单元测验

这是微专业参加单元测试后的试题及答案整理,分享出来,供大家参考,所有标红的为答案。

491
来自专栏前端儿

在PHP中使用MySQL Mysqli操作数据库 ,以及类操作方法

先来操作函数部分,普遍的MySQL 函数方法,但随着PHP5的发展,有些函数使用的要求加重了,有些则将废弃不用,有些则参数必填...

1023
来自专栏Kevin-ZhangCG

Oracle学习笔记四

在写java程序中有集合的概念,那么在pl/sq中也会用到多条记录,这时候我们就要用到游标,游标可以存储查询返回的多条数据。

432

扫码关注云+社区