Ping笔记(二)

函数介绍(续)

重要的函数: socket

int socket(int family, int type, int protocol);
返回值:成功时返回文件描述符 / 失败时返回-1
头文件: #include <sys/socket.h>
函数作用:创建一个套接字
一般为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数

函数参数

第一个参数family:指明套接字中使用的协议族信息。
常见值有:

第二个参数type:指明套接口类型,也即套接字的数据传输方式。
常见值有:

在常见的使用socket进行网络编程中,经常使用SOCK_STREAM和SOCK_DGRAM,也就是TCP和UDP编程。在本项目中,我们将使用SOCK_RAW(原始套接字)。

原始套接字的主要作用在三个方面:

1.通过原始套接字发送/接收 ICMP 协议包。
2.接收发向本级的,但 TCP/IP 协议栈不能处理的IP包。
3.用来发送一些自己制定源地址特殊作用的IP包(自己写IP头)。

ping 命令使用的就是 ICMP 协议,因此我们不能直接通过建立一个 SOCK_STREAM或SOCK_DGRAM 来发送协议包,只能自己构建 ICMP 包通过 SOCK_RAW 来发送。

第三个参数 protocol:指明协议类型。

常见值有: 图片描述信息

参数 protocol 指明了所要接收的协议包。

如果指定了 IPPROTO_ICMP,则内核碰到ip头中 protocol 域和创建 socket 所使用参数 protocol 相同的 IP 包,就会交给我们创建的原始套接字来处理。

因此,一般来说,要想接收什么样的数据包,就应该在参数protocol里来指定相应的协议。当内核向我们创建的原始套接字交付数据包的时候,是包括整个IP头的,并且是已经重组好的IP包。如下所示: 图片描述信息

这里的数据也就是前面所说的时间戳。

但是,当我们发送IP包的时候,却不用自己处理IP首部,IP首部由内核自己维护,首部中的协议字段被设置成调用 socket 函数时传递给它的第三个参数。

我们发送 IP 包时,发送数据时从 IP 首部的第一个字节开始的,所以只需要构造一个如下所示的数据缓冲区就可以了。 图片描述信息

如果想自己处理 IP 首部,则需要设置 IP_HDRINCL 的 socket 选项,如下所示:

int flag = 1;
setsocketopt(sockfd, IPPROTO_TO, IP_HDRINCL, &flag, sizeof(int));
此时,我们需要构造如下所示的数据缓冲区。

注意,我们自己填充 IP 首部时,也不是填充 IP 首部的所有字段,而是应该将 IP 首部的 id 字段设置为0,表示让内核来处理这个字段。同时,内核还会自动完成 IP 首部的校验和的计算并填充。

最后介绍发送和接收 IP 包的两个函数:recvfrom 和 sendto。

#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void * buff, size_t nbytes, int flags, struct sockaddr * from, socklen_t * addrlen);

ssize_t sendto(int sockfd, const void * buff, size_t nbytes, int flags,const struct sockaddr * to, socklen_t addrlen);
成功时返回读写的字节数,失败时返回-1。

sockfd参数:套接字描述符。
buff参数:指向读入或写出缓冲区的指针。
nbytes参数:读写字节数。
flags参数:本项目中设置为0。
recvfrom 的 from 参数指向一个将由该函数在返回时填写数据发送者的地址信息的结构体,而该结构体中填写的字节数则放在 addrlen 参数所指的整数中。
sendto 的 to 参数指向一个含有数据报接收者的地址信息的结构体,其大小由addrlen参数指定。

校验和算法

检验和算法在 TCP/IP 协议族中是比较常见的算法。
IP、ICMP、UDP和TCP报文头部都有校验和字段,不过IP、TCP、UDP只针对首部计算校验和,
而 ICMP 对首部和报文数据一起计算校验和。

检验和算法可以分成两步来实现。

首先在发送端,有以下三步:
1.把校验和字段置为0。
2.对需要校验的数据看成以16bit为单位的数字组成,依次进行二进制求和。
3.将上一步的求和结果取反,存入校验和字段。

其次在接收端,也有相应的三步:

  1. 对需要校验的数据看成以16bit为单位的数字组成,依次进行二进制求和,包括校验和字段。
  2. 将上一步的求和结果取反。
  3. 判断最终结果是否为0。如果为0,说明校验和正确。如果不为0,则协议栈会丢掉接收到的数据。

从上可以看出,归根到底,校验和算法就是二进制反码求和。由于先取反后相加与先相加后取反,得到的结果是一样的,所以上面的步骤都是先求和后取反。

下面用C语言来实现校验和算法,代码如下:

/**
 * addr 指向需校验数据缓冲区的指针
 * len  需校验数据的总长度(字节单位)
 */
unsigned short checkSum(unsigned short *addr, int len){
    unsigned int sum = 0;
    while(len > 1){
        sum += *addr++;
        len -= 2;
    }

    // 处理剩下的一个字节
    if(len == 1){
        sum += *(unsigned char *)addr;
    }

    // 将32位的高16位与低16位相加
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);

    return (unsigned short) ~sum;
}

上面的代码首先定义了一个32位无符号整型的变量sum,用来保存16bit二进制数字相加的结果,由于16bit相加可能会产生进位,所以这里使用32位变量来保存结果,其中高16bit保存的是相加产生的进位。

然后下面的 while 循环,对数据按16bit累加求和。

接下来的if语句判断是否还剩下8bit(一字节)。如果校验的数据为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加。

之后的两行代码作用是将 sum 高16bit的值加到低16bit上,即把累加中最高位的进位加到最低位上。(sum >> 16)将高16bit右移到低16bit,(sum & 0xffff)将高16bit全部置为0。注意,这两步都不会改变sum原来的值。

进行了两次相加可以保证 sum 高16bit都为0,没有进位了。

最后取反,并返回。

扩展:

为什么使用二进制反码求和,而不是原码或补码呢?

这是因为,使用反码计算校验和比较简单和快速。对于网络通信来说,最重要的就是效率和速度。

编码实现

整个程序的流程图如下所示: 图片描述信息

第一步,首先创建原始套接字。

第二步,封装 ICMP 报文,向目的IP地址发送 ICMP 报文,1秒后接收 ICM P响应报文,并打印 TTL,RTT。

第三步:循环第二步N次,本项目设置为5。

第四步:输出统计信息。

栗子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <netdb.h>

#define ICMP_SIZE (sizeof(struct icmp))
#define ICMP_ECHO 8
#define ICMP_ECHOREPLY 0
#define BUF_SIZE 1024
#define NUM   5    // 发送报文次数

#define UCHAR  unsigned char
#define USHORT unsigned short
#define UINT   unsigned int

// ICMP报文数据结构
struct icmp{
    UCHAR           type;      // 类型
    UCHAR           code;      // 代码
    USHORT          checksum;  // 校验和
    USHORT          id;        // 标识符
    USHORT          sequence;  // 序号
    struct timeval  timestamp; // 时间戳
};

// IP首部数据结构
struct ip{
    // 主机字节序判断
    #if __BYTE_ORDER == __LITTLE_ENDIAN
    UCHAR   hlen:4;        // 首部长度
    UCHAR   version:4;     // 版本
    #endif
    #if __BYTE_ORDER == __BIG_ENDIAN
    UCHAR   version:4;
    UCHAR   hlen:4;
    #endif
    UCHAR   tos;             // 服务类型
    USHORT  len;             // 总长度
    USHORT  id;                // 标识符
    USHORT  offset;            // 标志和片偏移
    UCHAR   ttl;            // 生存时间
    UCHAR   protocol;       // 协议
    USHORT  checksum;       // 校验和
    struct in_addr ipsrc;    // 32位源ip地址
    struct in_addr ipdst;   // 32位目的ip地址
};

char buf[BUF_SIZE] = {0};

USHORT checkSum(USHORT *, int); // 计算校验和
float timediff(struct timeval *, struct timeval *); // 计算时间差
void pack(struct icmp *, int);  // 封装一个ICMP报文
int unpack(char *, int, char *);        // 对接收到的IP报文进行解包

int main(int argc, char * argv[]){
    struct hostent *host;
    struct icmp sendicmp;
    struct sockaddr_in from;
    struct sockaddr_in to;
    int fromlen = 0;
    int sockfd;
    int nsend = 0;
    int nreceived = 0;
    int i, n;
    in_addr_t inaddr;

    memset(&from, 0, sizeof(struct sockaddr_in));
    memset(&to, 0, sizeof(struct sockaddr_in));

    if(argc < 2){
        printf("use : %s hostname/IP address \n", argv[0]);
        exit(1);
    }

    // 生成原始套接字
    if((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1){
        printf("socket() error \n");
        exit(1);
    }

    // 设置目的地址信息
    to.sin_family = AF_INET;

    // 判断是域名还是ip地址
    if(inaddr = inet_addr(argv[1]) == INADDR_NONE){
        // 是域名
        if((host = gethostbyname(argv[1])) == NULL){
            printf("gethostbyname() error \n");
            exit(1);
        }
        to.sin_addr = *(struct in_addr *)host->h_addr_list[0];
    }else{
        // 是ip地址
        to.sin_addr.s_addr = inaddr;
    }

    // 输出域名ip地址信息
    printf("ping %s (%s) : %d bytes of data.\n", argv[1], inet_ntoa(to.sin_addr), (int)ICMP_SIZE);

    //循环发送报文、接收报文
    for(i = 0; i < NUM; i++){
        nsend++;  // 发送次数加1
        memset(&sendicmp, 0, ICMP_SIZE);
        pack(&sendicmp, nsend);

        // 发送报文
        if(sendto(sockfd, &sendicmp, ICMP_SIZE, 0, (struct sockaddr *)&to, sizeof(to)) == -1){
            printf("sendto() error \n");
            continue;
        }

        // 接收报文
        if((n = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&from, &fromlen)) < 0){
            printf("recvform() error \n");
            continue;
        }
        nreceived++;  // 接收次数加1
        if(unpack(buf, n, inet_ntoa(from.sin_addr)) == -1){
            printf("unpack() error \n");
        }

        sleep(1);
    }

    // 输出统计信息
    printf("---  %s ping statistics ---\n", argv[1]);
    printf("%d packets transmitted, %d received, %%%d packet loss\n", nsend, nreceived,
            (nsend - nreceived) / nsend * 100);

    return 0;
}

/**
 * addr 指向需校验数据缓冲区的指针
 * len  需校验数据的总长度(字节单位)
 */
USHORT checkSum(USHORT *addr, int len){
    UINT sum = 0;
    while(len > 1){
        sum += *addr++;
        len -= 2;
    }

    // 处理剩下的一个字节
    if(len == 1){
        sum += *(UCHAR *)addr;
    }

    // 将32位的高16位与低16位相加
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);

    return (USHORT) ~sum;
}

/**
 * 返回值单位:ms
 * begin 开始时间戳
 * end   结束时间戳
 */
float timediff(struct timeval *begin, struct timeval *end){
    int n;
    // 先计算两个时间点相差多少微秒
    n = ( end->tv_sec - begin->tv_sec ) * 1000000
        + ( end->tv_usec - begin->tv_usec );

    // 转化为毫秒返回
    return (float) (n / 1000);
}

/**
 * icmp 指向需要封装的ICMP报文结构体的指针
 * sequence 该报文的序号
 */
void pack(struct icmp * icmp, int sequence){
    icmp->type = ICMP_ECHO;
    icmp->code = 0;
    icmp->checksum = 0;
    icmp->id = getpid();
    icmp->sequence = sequence;
    gettimeofday(&icmp->timestamp, 0);
    icmp->checksum = checkSum((USHORT *)icmp, ICMP_SIZE);
}

/**
 * buf  指向接收到的IP报文缓冲区的指针
 * len  接收到的IP报文长度
 * addr 发送ICMP报文响应的主机IP地址
 */
int unpack(char * buf, int len, char * addr){
   int i, ipheadlen;
   struct ip * ip;
   struct icmp * icmp;
   float rtt;          // 记录往返时间
   struct timeval end; // 记录接收报文的时间戳

   ip = (struct ip *)buf;

   // 计算ip首部长度,即ip首部的长度标识乘4
   ipheadlen = ip->hlen << 2;

   // 越过ip首部,指向ICMP报文
   icmp = (struct icmp *)(buf + ipheadlen);

   // ICMP报文的总长度
   len -= ipheadlen;

   // 如果小于ICMP报文首部长度8
   if(len < 8){
        printf("ICMP packets\'s length is less than 8 \n");
        return -1;
   }

   // 确保是我们所发的ICMP ECHO回应
   if(icmp->type != ICMP_ECHOREPLY ||
           icmp->id != getpid()){
       printf("ICMP packets are not send by us \n");
       return -1;
   }

   // 计算往返时间
   gettimeofday(&end, 0);
   rtt = timediff(&icmp->timestamp, &end);

   // 打印ttl,rtt,seq
   printf("%d bytes from %s : icmp_seq=%u ttl=%d rtt=%fms \n",
           len, addr, icmp->sequence, ip->ttl, rtt);

   return 0;

执行提示 socket() error 错误,也就是调用 socket 函数的时候出现了错误。这是因为我们创建的是原始套接字,原始套接字必须有 root 权限才能创建,所以我们可以加 sudo 执行

Ping笔记(一)

参考文献

参考文献一

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容

  • 定义 网络协议为计算机网络中进行数据交换而建立的规则、标准或约定的集合。网络协议主要由三个要素组成:语义、语法及时...
    FlyAndroid阅读 988评论 0 10
  • 个人认为,Goodboy1881先生的TCP /IP 协议详解学习博客系列博客是一部非常精彩的学习笔记,这虽然只是...
    贰零壹柒_fc10阅读 5,053评论 0 8
  • 网络编程 一.楔子 你现在已经学会了写python代码,假如你写了两个python文件a.py和b.py,分别去运...
    go以恒阅读 2,007评论 0 6
  • 地址解析协议ARP 物理这一级,主机和路由器是用物理地址来区别的。物理地址是一个本地地址,管辖范围是本地网络,所以...
    顾慎为阅读 1,080评论 0 1
  • 我10月份的早起率是不达标的 不知从什么时候开始,我的生物钟变成晚上12点到早上7点了,每天都闹铃,每次按了继续睡...
    丁靓阅读 139评论 0 0