linux原始套接字实战

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

  1. 原始套接字概述
  2. 原始套接字的创建
  3. 自定义协议
  4. 发送端程序流程、实现
  5. 接收端程序的开发

一、原始套接字概述

先来看看socket函数原型:

int socket(int domain, int type, int protocol);

我们知道,当进行网络编程的时候,通常会用到socket函数。而且主要有两种,一种是第二个参数为SOCK_STREAM,面向连接的 Socket,针对于面向连接的TCP 服务应用,另外一种是第二个参数为SOCK_DGRAM,面向无连接的 Socket,针对于无连接的 UDP 服务应用。对于TCP或UDP,我们不能修改其头部的格式,只能依照系统开放给我们定义好的头部进行编程开发。而今天,介绍的原始套接字却跟前面两种大不相同。原始套接字可以提供普通的TCP和UDP套接字不支持的能力。比如:

  1. 发送一个自定义的以太网帧。(这将是本节实现的重点
  2. 发送一个 ICMP 协议包。
  3. 发送一个自定义的 IP 包。
  4. 分析所有经过网络的包,而不管这样包是否是发给自己的。
  5. 伪装本地的 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并返回给发送主机就可以了。

猜你喜欢

转载自blog.csdn.net/u014530704/article/details/78874274