您的位置:首页 > 运维架构 > Linux

linux原始套接字实战

2017-12-22 16:04 162 查看
本文的主线是一个使用原始套接字发送数据的程序,工作在数据链路层,采用自定义以太网帧协议,靠MAC地址识别目标主机。所以不涉及到IP地址和端口号。程序主要用于互联的两台机器之间进行丢帧率计算。以下部分都是围绕它而展开说明的。内容分为以下几部分:

原始套接字概述

原始套接字的创建

自定义协议

发送端程序流程、实现

接收端程序的开发

一、原始套接字概述

先来看看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并返回给发送主机就可以了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: