linux原始套接字实战
2017-12-22 16:04
162 查看
本文的主线是一个使用原始套接字发送数据的程序,工作在数据链路层,采用自定义以太网帧协议,靠MAC地址识别目标主机。所以不涉及到IP地址和端口号。程序主要用于互联的两台机器之间进行丢帧率计算。以下部分都是围绕它而展开说明的。内容分为以下几部分:
原始套接字概述
原始套接字的创建
自定义协议
发送端程序流程、实现
接收端程序的开发
我们知道,当进行网络编程的时候,通常会用到socket函数。而且主要有两种,一种是第二个参数为SOCK_STREAM,面向连接的 Socket,针对于面向连接的TCP 服务应用,另外一种是第二个参数为SOCK_DGRAM,面向无连接的 Socket,针对于无连接的 UDP 服务应用。对于TCP或UDP,我们不能修改其头部的格式,只能依照系统开放给我们定义好的头部进行编程开发。而今天,介绍的原始套接字却跟前面两种大不相同。原始套接字可以提供普通的TCP和UDP套接字不支持的能力。比如:
发送一个自定义的以太网帧。(这将是本节实现的重点)
发送一个 ICMP 协议包。
发送一个自定义的 IP 包。
分析所有经过网络的包,而不管这样包是否是发给自己的。
伪装本地的 IP 地址。
注意:原始套接字需要在root权限下使用!
这里把socket函数第一个参数指定为PF_PACKET,因为本文程序利用PF_PACKET接口操作链路层的数据。
第二个参数指定为SOCK_RAW。第三个参数指定为一个字符串”0x980A”来标识自定义协议。
创建完成之后就可以利用函数sendto函数和recvfrom函数来发送和接收数据链路层的数据包了。
其中,attribute ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
上面的代码,先解析出命令行参数,并把参数赋给结构体stbTestCfg。然后调用stbTestSocketInit函数初始化socket。接着调用stbTestInitThread函数创建发送数据stbTestThread_S线程和接收数据stbTestThread_R的线程。最后调用pthread_join等待线程接收。程序的实现的大体思路是:向另一台主机发送type=0x980A、opcode=0x08,带序列号和当前时间的单播报文。另一台主机接收到报文之后修改报文源MAC、目标MAC、opcode=0x00并返回给发送主机。发送主机计算现在的时间和报文中的时间值之差,就可以判断这份报文一个来回经过了多长的时间。通过时间的长短,我们可以判断是否丢帧了。我们这里定义当测试报文从发送到接收的时间间隔超过4000ms(包括4000ms)时或没有接受到发送的报文,这个报文当做丢帧。丢帧率计算为:在一个测试周期内(具体测试周期参见规范),按照规范要求的发包频率(具体发包频率参见规范)进行单播探测交互,统计出本周期内的所有丢帧数,计算丢帧率=丢帧数/发包总数×100.00%,丢帧率单位为百分比,精确到小数点2位。程序还包含了时延、抖动的计算,操作的数据同样来源于时间值,这里不详细说明。我们来看看stbTestSocketInit函数里面干了什么:
以上最主要的就是通过调用socket函数创建原始套接字,我们再来看看stbTestInitThread函数干了什么:
以上主要是关于线程属性的设置和创建,可以查看之前的文章linux 线程属性控制。主函数通过stbTestInitThread函数创建了两个线程,一个用于发送,一个用于接收。我们先来看看发送线程函数:
程序比较长,其实就是填充如下结构体,这个结构体是我们要发送给远端主机的,然后调用send_to_stb函数。
发送数据真正的地方在send_to_stb这个函数里面。以下程序根据我们定义的频率、周期,在for循环调用stbTestSocketSend函数发送数据,这个函数真正调用的是sendto函数。发送完数据之后等待三秒。这是因为要等对端主机接受完数据之后发送回来之后。这样数据才经历了一个来回。我们才能抓到数据包里面的时间值。根据发送时间和接收时间来计算丢帧率、时延等。计算出结果之后,把数据写进新创建的文件/var/run/stb_test_result。
接收函数相对简单,关键是调用stbTestSocketReceive函数接收数据,这个函数最终是调用recvfrom函数来接收数据的。接收完数据之后,再把时间值处理一下。然后流程是到了发送线程去计算最后的结果。之前等待三秒就是为了等待这个函数执行完。
程序有点长,五百多行,上面的代码并不完全,只是关键的一部分。
原始套接字概述
原始套接字的创建
自定义协议
发送端程序流程、实现
接收端程序的开发
一、原始套接字概述
先来看看socket函数原型:int socket(int domain, int type, int protocol);
我们知道,当进行网络编程的时候,通常会用到socket函数。而且主要有两种,一种是第二个参数为SOCK_STREAM,面向连接的 Socket,针对于面向连接的TCP 服务应用,另外一种是第二个参数为SOCK_DGRAM,面向无连接的 Socket,针对于无连接的 UDP 服务应用。对于TCP或UDP,我们不能修改其头部的格式,只能依照系统开放给我们定义好的头部进行编程开发。而今天,介绍的原始套接字却跟前面两种大不相同。原始套接字可以提供普通的TCP和UDP套接字不支持的能力。比如:
发送一个自定义的以太网帧。(这将是本节实现的重点)
发送一个 ICMP 协议包。
发送一个自定义的 IP 包。
分析所有经过网络的包,而不管这样包是否是发给自己的。
伪装本地的 IP 地址。
注意:原始套接字需要在root权限下使用!
二、原始套接字的创建
创建原始套接字有如下步骤:这里把socket函数第一个参数指定为PF_PACKET,因为本文程序利用PF_PACKET接口操作链路层的数据。
第二个参数指定为SOCK_RAW。第三个参数指定为一个字符串”0x980A”来标识自定义协议。
int sock; //这里把protocol自定义为0x980A if((sock = socket(PF_PACKET, SOCK_RAW, htons(protocol))) < 0) { perror("socket"); return -1; }
创建完成之后就可以利用函数sendto函数和recvfrom函数来发送和接收数据链路层的数据包了。
三、自定义协议
自定义数据包格式:typedef struct { unsigned char tmac[6]; //目的主机mac地址 unsigned char smac[6]; //本机mac地址 unsigned short type; //自定义为0x980A unsigned short len; //暂定为46 unsigned char reserve[3]; //保留,暂填写为00 unsigned char opcode; //探测请求 = 0x08;响应 = 0x00 unsigned short seqnum; //报文序号,从0-65535 unsigned int datetime; //填充当前发送测试包时间,精确到毫秒 unsigned char content[34];//填充,以0123456798ABCDEF0123456……模式循环 } __attribute__((packed)) stbtest_packet_t ;
其中,attribute ((packed)) 的作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
四、发送端程序流程、实现
先来看下程序的主函数:int main(int argc, char **argv){ stbtest_packet_t packet; struct sockaddr_ll addrSrc; u_int nLen = 0; int ret = -1,i = 0; int fd = -1; char pidstr[20] = {0}; struct sockaddr_ll socketAddrReply; //解析程序输入参数,采用./stb_test 50 30 00:00:00:00:00:00 11:11:11:11:11:11格式 //50 表示发送频率,30表示发送周期,00:00:00:00:00:00表示本机的mac地址,11:11:11:11:11:11表示目的mac for(i=0; i<argc; i++) { printf("%d ",i); printf("=%s\n",argv[i]); } if(argc < 4) { printf("usage:%s <freqency> <send-period> <src-mac> <dst-mac>\r\n", argv[0]); printf("eg: ./stb_test 50 30 dc:0e:a1:68:6a:98 dc:0e:a1:68:6a:98\r\n"); exit(1); } stbTestCfg.freqency = atoi(argv[1]); stbTestCfg.send_period = atoi(argv[2]); str_to_macNum(argv[3], stbTestCfg.onuMacAddr);//source mac str_to_macNum(argv[4], stbTestCfg.dstMacAddr);//dest mac if(stbTestSocketInit()< 0 ) { printf("=> Error: stbTestSocketInit fail!\n"); stbTestSocketDestroy(); return -1; } stbTestInitThread(&stbTestThread_S, (void *)stbTestSendTask); stbTestInitThread(&stbTestThread_R, (void *)stbTestReceiveTask); sleep(1);//防止线程未创建就开始执行pthread_join函数,导致等待线程退出失败 pthread_join(stbTestThread_R,NULL); pthread_join(stbTestThread_S,NULL); stbTestSocketDestroy(); return 0; }
上面的代码,先解析出命令行参数,并把参数赋给结构体stbTestCfg。然后调用stbTestSocketInit函数初始化socket。接着调用stbTestInitThread函数创建发送数据stbTestThread_S线程和接收数据stbTestThread_R的线程。最后调用pthread_join等待线程接收。程序的实现的大体思路是:向另一台主机发送type=0x980A、opcode=0x08,带序列号和当前时间的单播报文。另一台主机接收到报文之后修改报文源MAC、目标MAC、opcode=0x00并返回给发送主机。发送主机计算现在的时间和报文中的时间值之差,就可以判断这份报文一个来回经过了多长的时间。通过时间的长短,我们可以判断是否丢帧了。我们这里定义当测试报文从发送到接收的时间间隔超过4000ms(包括4000ms)时或没有接受到发送的报文,这个报文当做丢帧。丢帧率计算为:在一个测试周期内(具体测试周期参见规范),按照规范要求的发包频率(具体发包频率参见规范)进行单播探测交互,统计出本周期内的所有丢帧数,计算丢帧率=丢帧数/发包总数×100.00%,丢帧率单位为百分比,精确到小数点2位。程序还包含了时延、抖动的计算,操作的数据同样来源于时间值,这里不详细说明。我们来看看stbTestSocketInit函数里面干了什么:
//LAN_IF定义为"eth0",ETH_P_STBTEST_S定义为0x980A int stbTestSocketInit(void) { stbTestSocket_S = strCreateSocket(LAN_IF, ETH_P_STBTEST_S, &stbTestSocketAddr_S) ; if(stbTestSocket_S == -1) { printf("=> Error: create stbtest_s socket failed\n") ; return -1 ; } stbTestSocket_R = strCreateSocket(LAN_IF, ETH_P_STBTEST_S, &stbTestSocketAddr_R) ; if(stbTestSocket_R == -1) { printf("=> Error: create stbtest_r socket failed\n") ; return -1 ; } return 0 ; } int strCreateSocket(char *iface, int protocol, struct sockaddr_ll *sll) { int sock; struct ifreq ifr; int sockopt = 0; //这里就是创建原始套接字的地方,PF_PACKET表明操作的是数据链路层的数据,协议这里指定自定义的0x980A if((sock = socket(PF_PACKET, SOCK_RAW, htons(protocol))) < 0) { perror("socket"); return -1; } if(iface != NULL) { memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, iface, sizeof(ifr.ifr_name)); //这里把eth0接口的索引存入ifr.ifr_ifindex中 if ( ioctl(sock, SIOCGIFINDEX, &ifr) < 0) { perror("ioctl(SIOCGIFINDEX)"); close(sock); return -1; } memset(sll, 0, sizeof(struct sockaddr_ll)); sll->sll_family = AF_PACKET; sll->sll_ifindex = ifr.ifr_ifindex; sll->sll_protocol = htons(protocol);//填入自定义协议,这里是0x980A if (bind(sock, (struct sockaddr *)sll, sizeof(struct sockaddr_ll)) == -1) { perror("bind()"); close(sock); return -1; } } return sock; }
以上最主要的就是通过调用socket函数创建原始套接字,我们再来看看stbTestInitThread函数干了什么:
int stbTestInitThread(pthread_t *thread, void *func){ int ret; pthread_attr_t attr; //初始化线程属性 ret = pthread_attr_init(&attr); if(ret != 0) { printf("\r\n stbTestInitThread attribute creation fail!"); return -1; } //设置线程堆栈大小 ret = pthread_attr_setstacksize(&attr, 16384); if(ret != 0) { printf("\r\nSet stacksize fail!"); return -1; } //设置线程分离状态 ret = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); if(ret != 0) { printf("\r\nSet attribute fail!"); return -1; } //创建线程 ret = pthread_create(thread , &attr, (void *)func, NULL); if(ret != 0) { printf("\r\nCreate thread fail!"); return -1; } //销毁线程属性结构体attr pthread_attr_destroy(&attr); return 0; }
以上主要是关于线程属性的设置和创建,可以查看之前的文章linux 线程属性控制。主函数通过stbTestInitThread函数创建了两个线程,一个用于发送,一个用于接收。我们先来看看发送线程函数:
void *stbTestSendTask(void){ stbtest_packet_t packet; unsigned short nCount = 0; char buf[10] = {0}; char data[REPEAT_DATA_LEN] = {0x01,0x23,0x45,0x67,0x89,0xAB,0xCD,0xEF}; unsigned int send_period = 0, freqency = 0, factor = 5; int iRet,ret1, ret2; memset(&packet , 0, sizeof(stbtest_packet_t)); memcpy(packet.smac, stbTestCfg.onuMacAddr,6); packet.type = htons(ETH_P_STBTEST_S); packet.len = htons(STBTEST_LOAD_LEN); packet.opcode = STBTEST_OPCODE_REQUEST; for(nCount=0; (nCount+1)*REPEAT_DATA_LEN < 34; nCount++){ memcpy(&packet.content[nCount*REPEAT_DATA_LEN], data, REPEAT_DATA_LEN); } memcpy(&packet.content[nCount*REPEAT_DATA_LEN], data, 34-nCount*REPEAT_DATA_LEN); //获取发送参数 get_send_para(&send_period, &freqency, &factor); ret1 = send_to_stb(&packet, 2, send_period, freqency, factor); if (ret1 == 0 ) { memset(stbTestCfg.state , 0, sizeof(stbTestCfg.state)); strcpy(stbTestCfg.state,"Complete"); printf("now state is : %s\n",stbTestCfg.state); } else { memset(stbTestCfg.state , 0, sizeof(stbTestCfg.state)); strcpy(stbTestCfg.state,"Stop"); printf("now state is : %s\n",stbTestCfg.state); } printf("\n======>stbTestSendTask, do thread exit\n"); stbTestThread_S = 0; }
程序比较长,其实就是填充如下结构体,这个结构体是我们要发送给远端主机的,然后调用send_to_stb函数。
typedef struct { unsigned char tmac[6]; unsigned char smac[6]; unsigned short type; unsigned short len; unsigned char reserve[3]; unsigned char opcode; unsigned short seqnum; unsigned int datetime; unsigned char content[34]; } __attribute__((packed)) stbtest_packet_t ;
发送数据真正的地方在send_to_stb这个函数里面。以下程序根据我们定义的频率、周期,在for循环调用stbTestSocketSend函数发送数据,这个函数真正调用的是sendto函数。发送完数据之后等待三秒。这是因为要等对端主机接受完数据之后发送回来之后。这样数据才经历了一个来回。我们才能抓到数据包里面的时间值。根据发送时间和接收时间来计算丢帧率、时延等。计算出结果之后,把数据写进新创建的文件/var/run/stb_test_result。
int send_to_stb(stbtest_packet_t *p_packet, int lan_index, unsigned int send_period, unsigned int freqency, unsigned int factor) { unsigned short nCount = 0, nPeriod = 0, send_count = 0, l_seqnum = 0; unsigned short rxCount = 0; char buf[10] = {0}; char stbMac[32] = {0}; char resultBuff[64] = {0}; unsigned int timeResponseTotal = 0; unsigned int timeShakeTotal = 0; unsigned int sen_cnt = 0, freq_cnt = 0, last_cnt = 0, real_cnt = 0; float lostrate = 0; int learn_flag = 0; if (p_packet == NULL) return -1; memcpy(p_packet->tmac, stbTestCfg.dstMacAddr, 6); send_count = (send_period * freqency); g_seqnum = send_count; sen_cnt = (send_period * factor); freq_cnt = (freqency / factor); last_cnt = (freqency % factor); l_seqnum = 0; for (nPeriod = 0; nPeriod < sen_cnt; nPeriod++) { if ((nPeriod % factor) == 0) real_cnt = freq_cnt + last_cnt; else real_cnt = freq_cnt; for(nCount=0; nCount < real_cnt; nCount++){ int seqnum = l_seqnum++; p_packet->seqnum = seqnum; gettimeofday(&startTime[seqnum], 0); p_packet->datetime = startTime[seqnum].tv_sec*1000+startTime[seqnum].tv_usec/1000; timeResponse[seqnum] = RESPONSE_TIMEOUT; stbTestSocketSend(stbTestSocket_S, sizeof(stbtest_packet_t), p_packet, &stbTestSocketAddr_S); printf("send packet seq:%d,datetime:%u\n",seqnum,p_packet->datetime); } usleep(1000*1000/factor - 15*1000); } sleep(3); /* send finish, so collect the information */ rxCount = 0; timeResponseTotal = 0; timeShakeTotal = 0; for(nCount=0; nCount < send_count; nCount++){ if(timeResponse[nCount] < RESPONSE_TIMEOUT){ rxCount++; timeResponseTotal += timeResponse[nCount]; } else { timeResponseTotal += RESPONSE_TIMEOUT; timeResponse[nCount] = RESPONSE_TIMEOUT; } if(nCount>0){ if(timeResponse[nCount] - timeResponse[nCount-1] > 0) timeShakeTotal += timeResponse[nCount] - timeResponse[nCount-1]; else timeShakeTotal += timeResponse[nCount-1] - timeResponse[nCount]; } } //printf("stb_test:LAN%d recv packet complete,recv_count=%d\n",lan_index,rxCount); /* if response packet can be receive or mac learn, it is stb */ if (rxCount > 0 || learn_flag) { //break; } if (send_count <= 0) { return -1; } // rx count lostrate = (float)(((float)send_count - (float)rxCount)*100.00/(float)send_count); //delay int timeResponseRate = timeResponseTotal/send_count; // shake int timeShakeRate = timeShakeTotal/(send_count-1); int fd = open(STB_TEST_RESULT, O_CREAT|O_EXCL|O_RDWR, 0666); if (fd < 0){ printf("stb_test can't create stb_test_result file, exit.\n"); exit(1); } sprintf(stbMac,"%02x:%02x:%02x:%02x:%02x:%02x",stbTestCfg.dstMacAddr[0],stbTestCfg.dstMacAddr[1],stbTestCfg.dstMacAddr[2], stbTestCfg.dstMacAddr[3],stbTestCfg.dstMacAddr[4],stbTestCfg.dstMacAddr[5]); ToUpperCase(stbMac); sprintf(resultBuff,"%s:%3.2f:%d:%d",stbMac,lostrate,timeResponseRate,timeShakeRate); write(fd, resultBuff, strlen(resultBuff)); close(fd); return 0; }
接收函数相对简单,关键是调用stbTestSocketReceive函数接收数据,这个函数最终是调用recvfrom函数来接收数据的。接收完数据之后,再把时间值处理一下。然后流程是到了发送线程去计算最后的结果。之前等待三秒就是为了等待这个函数执行完。
void *stbTestReceiveTask(void){ stbtest_packet_t packet; struct sockaddr_ll addrSrc; struct timeval revTime; u_int nLen = 0; int ret = -1; int count = 0, count2 = 0; while(1){ ret = stbTestSocketReceive(stbTestSocket_R, &nLen, &packet, &addrSrc); if((ret == 0) && (packet.opcode == STBTEST_OPCODE_REPLY) && (memcmp(packet.smac, stbTestCfg.dstMacAddr, 6) == 0)){ if(packet.seqnum>=0 && packet.seqnum<g_seqnum){ gettimeofday(&revTime, 0); timeResponse[packet.seqnum] = (revTime.tv_sec*1000+revTime.tv_usec/1000)-packet.datetime; count2++; } count++; if (count >= g_seqnum) { count = 0; count2 = 0; break; } } } stbTestThread_R = 0; }
程序有点长,五百多行,上面的代码并不完全,只是关键的一部分。
五、接收端程序的开发
接收端的还没开发,以后有时间再看看。接收端肯定更加简单,因为另一台主机接收到报文之后只是需要修改下报文源MAC、目标MAC、opcode=0x00并返回给发送主机就可以了。相关文章推荐
- Linux 网络编程——原始套接字实例:发送 UDP 数据包
- Linux原始套接字之ARP协议实现
- Linux网络编程:原始套接字的魔力【下】
- linux网络编程--原始套接字
- linux原始套接字-发送ICMP报文
- Linux网络编程--10. 原始套接字 --11. 后记
- Linux原始套接字编程
- linux sock_raw原始套接字编程 (转)和Linux下Libpcap源码分析和包过滤机制
- Linux原始套接字实现分析
- Linux原始套接字----ping命令的实现
- Linux网络编程:原始套接字的魔力【续】
- Linux网络编程:原始套接字的魔力【上】
- linux下的原始套接字
- Linux网络编程:原始套接字的魔力【续】
- linux网络协议栈(四)链路层 (4)原始套接字
- Linux原始套接字实现分析
- Linux网络编程:原始套接字的魔力【续】
- Linux 原始套接字抓包实例
- 使用Linux 原始套接字抓取数据链路层上IEC61850-9-2(LE) SV数据包并显示的参考程序
- Linux用户态下利用原始套接字接收和发送自定义链路帧