解析ping原理及实现

PING

一、概述

ping属于一个通信协议,是TCP/IP协议的一部分。利用“ping”命令可以检查网络是否通畅或者网络连接速度,很好地分析和判定网络故障。

Ping发送一个ICMP(Internet Control Messages Protocol),即因特网信报控制协议;接收端回声消息给目的地并报告是否收到所希望的ICMPecho (ICMP回声应答)。它的原理是:利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,通过对方回复的数据包来确定两台网络机器是否连接相通,时延是多少。

ping是端对端连通,通常用来作为可用性的检查,但是某些病毒木马会强行远程执行大量ping命令抢占你的网络资源,导致系统变慢,网速变慢。严禁ping入侵作为大多数防火墙的一个基本功能提供给用户进行选择。通常的情况下你如果不用作服务器或者进行网络测试,可以放心的选中它,保护你的电脑。


二、ICMP概述

定位

ping属于哪一层?ping命令使用的tcp报文还是udp报文呢?

首先,答案是:PING是应用层直接使用网络层ICMP的一个例子,它没有通过运输层的TCP或UDP。

PING命令是我们能够直接使用的命令,像HTTP、FTP,属于应用层。ping命令底层使用的是ICMP,ICMP报文封装在ip包里,所以ICMP属于网络层协议。

分层协议
补充IGMP(是G)

IGMP即Internet工作组管理协议(Internet Group Management Protocol),IGMP主要用来解决网络上广播时占用带宽的问题。

当网络上的信息要传输给所有工作站时,就发出广播(broadcast)信息(即IP地址主机标识位全为1),交换机会将广播信息不经过滤地发给所有工作站;但当这些信息只需传输给某一部分工作站时,通常采用组播(multicast,也称多点广播)的方式,这就要求交换机支持IGMP。支持IGMP的交换机会识别组播信息并将其转发至相应的组,从而使不需要这些信息的工作站的网络带宽不被浪费。IGMP对于提高多媒体传输时的网络性能尤为重要。

IP和ICMP报文的关系

报文内的具体字段可以暂时抛开,可以看到ICMP报文是内嵌在IP报文内的,也就是说IP报文里包含着ICMP报文,ICMP报文是IP报文的一部分。

IP和ICMP报文

三、ICMP实现

由于PING命令是使用ICMP实现的,那么ICMP是如何实现的对了解PING是至关重要的。

1. ICMP报文类型

ICMP类型分为两大类:

  1. 差错报告
  2. 询问报告

ICMP通过一个整数数字来表示不同的报文类型,双方通过该类型值来识别报文的目的,并作出不同的反应。

种类 类型值 ICMP报文的类型
差错报告 3 终点不可达
差错报告 11 时间超过
差错报告 12 参数问题
差错报告 5 改变路由
询问报告 8/0 回送请求/回答
询问报告 13/14 时间戳请求/回答

对于PING命令,我们需要的到达的效果是检查是否联通。那么只需要我们的请求方带上数字标识8(回送请求),如果对方回送的数值是0,那么证明两者是联通的。

2. Checksum 检验和

校验和是一个从数据包计算出来的值来检查其完整性。通过完整性,我们可以检查收到的数据是否没有错误。由于最底层的网络通信是通过电路传输的,期间包含很多不定因素,可能导致部分数据丢失、改变。所以网络协议大都需要通过某种方法来确保收到的数据是正确的。检验和是常用的一种方式。

ICMP协议中使用了检验和来保证接收到正确的数据。在源端,计算校验和并将其作为字段设置在报文中。在目标端,再次计算校验和,并用报文中现有的校验和值进行交叉检查,看看数据包是否正常。

如何计算

icmp检验和的计算与IP检验和类似,不同的是,icmp需要通过其报文信息与数据主体一起校验,IP只需校验头部信息。

因为通常在IP报头之后的数据(如ICMP,TCP等)具有自己的校验和

就算法而言,imcp校验和是:imcp报文中所有16位字的补码总和的16位补码。具体操作如:

  1. 将校验和字段置为0。
  2. 将每两个字节(16位)相加(二进制求和)直到最后得出结果,若出现最后还剩一个字节继续与前面结果相加。
  3. (溢出)将高16位与低16位相加,直到高16位为0为止。
  4. 将最后的结果(二进制)取反。

关于二进制求和:

# 例:
# 1. 不溢出时

4500  - > 0100 0101 0000 0000
003c  - > 0000 0000 0011 1100

453C  - > 0100 0101 0011 1100  # 结果

# 2. 溢出时 高16位与低16位相加
E188  - > 1110 0001 1000 1000 
AC10  - > 1010 1100 0001 0000

18D98->1 1000 1101 1001 1000
8D99  - >  1000 1101 1001 1001   # 结果


# 最后结果取反
0A0C  - > 0000101000001100  # 最后一次累加
4E19  - > 0100111000011001  # 取反得最终结果
代码实现
  # 检验和
    def chesksum(data):
        n = len(data)
        m = n % 2
        sum = 0
        for i in range(0, n - m, 2):
            # 传入data以每两个字节(十六进制)通过ord转十进制,第一字节在低位,第二个字节在高位
            # ?????为什么第二个字节在高位
            sum += (data[i]) + ((data[i+1]) << 8)
            sum = (sum >> 16) + (sum & 0xffff)
        if m:
            sum += (data[-1])
            sum = (sum >> 16) + (sum & 0xffff)
    
        # 取反
        answer = ~sum & 0xffff
        #  主机字节序转网络字节序列
        answer = answer >> 8 | (answer << 8 & 0xff00)
        return answer
补充:小/大端序

不同CPU中,4字节整数在内存空间的存储方式是不同的。若不考虑这些就收发数据会发生问题,因为保存顺序的不同意味着对接收数据的解析顺序也不同。一般分为两种:

  • 大端序(Big Endian):高位字节存放到低位地址(高位字节在前)。
  • 小端序(Little Endian):高位字节存放到高位地址(低位字节在前)。
保存4字节 int 型数据 0x12345678

大端序

image

小端序

image

不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。

四、实现PING功能

简单流程

  1. 执行 ping 192.168.0.5
Ping命令会构建一个固定格式的ICMP请求数据包,
然后由ICMP协议将这个数据包连同地址“192.168.0.5”一起交给IP层协议
  1. IP层相关操作
IP层协议将以地址“192.168.0.5”作为目的地址,本机IP地址作为源地址,
加上一些其他的控制信息,构建一个IP数据包发往192.168.0.5。
  1. 目的主机相关操作
接收后检查该数据帧,将IP数据包从帧中提取出来,交给本机的IP层协议。
IP层检查后,将有用的信息提取后交给ICMP协议
ICMP协议后者处理后,马上构建一个ICMP应答包,发送给主机A
代码实现
# encoding:utf-8
import time
import struct
import socket
import select


# 检验和
def chesksum(data):
    n = len(data)
    m = n % 2
    sum = 0
    for i in range(0, n - m, 2):
        # 传入data以每两个字节(十六进制)通过ord转十进制,第一字节在低位,第二个字节在高位
        sum += (data[i]) + ((data[i+1]) << 8)
        sum = (sum >> 16) + (sum & 0xffff)
    if m:
        sum += (data[-1])
        sum = (sum >> 16) + (sum & 0xffff)

    # 取反
    answer = ~sum & 0xffff
    #  主机字节序转网络字节序列
    answer = answer >> 8 | (answer << 8 & 0xff00)
    return answer


def request_ping(data_type, data_code, data_checksum, data_ID, data_Sequence, payload_body):
    #  把字节打包成二进制数据
    imcp_packet = struct.pack('>BBHHH32s', data_type, data_code, data_checksum, data_ID, data_Sequence, payload_body)
    # 获取校验和
    icmp_chesksum = chesksum(imcp_packet)
    #  把校验和传入,再次打包
    imcp_packet = struct.pack('>BBHHH32s', data_type, data_code, icmp_chesksum, data_ID, data_Sequence, payload_body)
    return imcp_packet

# 初始化套接字,并发送
def raw_socket(dst_addr,imcp_packet):
    # 实例化一个socket对象,ipv4,原套接字(普通套接字无法处理ICMP等报文),分配协议端口
    rawsocket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname("icmp"))
    # 记录当前请求时间
    send_request_ping_time = time.time()
    # 发送数据到网络
    rawsocket.sendto(imcp_packet, (dst_addr, 80))
    return send_request_ping_time, rawsocket


def reply_ping(send_request_ping_time, rawsocket, data_Sequence, timeout=2):
    while True:
        '''
        select函数,直到inputs中的套接字被触发(在此例中,套接字接收到客户端发来的握手信号,从而变得可读,满足select函数的“可读”条件),
        返回被触发的套接字(服务器套接字);                
        '''
        # 实例化select对象(非阻塞),可读,可写为空,异常为空,超时时间
        what_ready = select.select([rawsocket], [], [], timeout)
        # 等待时间
        # wait_for_time = (time.time() - started_select)
        wait_for_time = (time.time() - send_request_ping_time)
        # 没有返回可读的内容,判断超时
        if what_ready[0] == []:  # Timeout
            return -1
        # 记录接收时间
        time_received = time.time()
        # 设置接收的包的字节为1024
        received_packet, addr = rawsocket.recvfrom(1024)
        # 获取接收包的icmp头
        # print(icmpHeader)
        icmpHeader = received_packet[20:28]
        # 反转编码
        type, code, r_checksum, packet_id, sequence = struct.unpack(
            ">BBHHH", icmpHeader
        )

        if type == 0 and sequence == data_Sequence:
            return time_received - send_request_ping_time

        # 数据包的超时时间判断
        timeout = timeout - wait_for_time
        if timeout <= 0:
            return -1


def dealtime(dst_addr, sumtime, shorttime, longtime, accept, i, time):
    sumtime += time
    print(sumtime)
    if i == 4:
        print("{0}的Ping统计信息:".format(dst_addr))
        print("数据包:已发送={0},接收={1},丢失={2}({3}%丢失),\n往返行程的估计时间(以毫秒为单位):\n\t最短={4}ms,最长={5}ms,平均={6}ms".format(i+1,accept,i+1-accept,(i+1-accept)/(i+1)*100,shorttime,longtime,sumtime))


def ping(host):
    # 统计最终 已发送、 接受、 丢失
    send, accept, lost = 0, 0, 0
    sumtime, shorttime, longtime, avgtime = 0, 1000, 0, 0
    # TODO icmp数据包的构建
    # 8回射请求 11超时 0回射应答
    data_type = 8
    data_code = 0
    # 检验和
    data_checksum = 0
    # ID
    data_ID = 0
    # 序号
    data_Sequence = 1
    # 可选的内容
    payload_body = b'abcdefghijklmnopqrstuvwabcdefghi' #data

    # 将主机名转ipv4地址格式,返回以ipv4地址格式的字符串,如果主机名称是ipv4地址,则它将保持不变
    dst_addr = socket.gethostbyname(host)
    print("正在 Ping {0} [{1}] 具有 32 字节的数据:".format(host, dst_addr))
    # 默认发送4次
    for i in range(0, 4):
        send = i + 1
        # 请求ping数据包的二进制转换
        icmp_packet = request_ping(data_type, data_code, data_checksum, data_ID, data_Sequence + i, payload_body)
        # 连接套接字,并将数据发送到套接字
        send_request_ping_time, rawsocket = raw_socket(dst_addr, icmp_packet)
        # 数据包传输时间
        times = reply_ping(send_request_ping_time, rawsocket, data_Sequence + i)
        if times > 0:
            print("来自 {0} 的回复: 字节=32 时间={1}ms".format(dst_addr, int(times*1000)))

            accept += 1
            return_time = int(times * 1000)
            sumtime += return_time
            if return_time > longtime:
                longtime = return_time
            if return_time < shorttime:
                shorttime = return_time
            time.sleep(0.7)
        else:
            lost += 1
            print("请求超时。")

        if send == 4:
            print("{0}的Ping统计信息:".format(dst_addr))
            print("\t数据包:已发送={0},接收={1},丢失={2}({3}%丢失),\n往返行程的估计时间(以毫秒为单位):\n\t最短={4}ms,最长={5}ms,平均={6}ms".format(
                i + 1, accept, i + 1 - accept, (i + 1 - accept) / (i + 1) * 100, shorttime, longtime, sumtime/send))


if __name__ == "__main__":
    i = input("请输入要ping的主机或域名\n")
    ping(i)
补充:SOCK_RAW

实际上,我们常用的网络编程都是在应用层的报文的收发操作,也就是大多数程序员接触到的流式套接字(SOCK_STREAM)和数据包式套接字(SOCK_DGRAM)。而这些数据包都是由系统提供的协议栈实现,用户只需要填充应用层报文即可,由系统完成底层报文头的填充并发送。

然而在某些情况下需要执行更底层的操作,比如修改报文头、避开系统协议栈等。这个时候就需要使用其他的方式来实现。

原始套接字(SOCK_RAW)是一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心。首先来说,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。总体来说,SOCK_RAW可以处理普通的网络报文之外,还可以处理一些特殊协议报文以及操作IP层及其以上的数据。

引用自:

https://blog.csdn.net/zhj082/article/details/80531628

https://www.jianshu.com/p/17f16256008d

《计算机网络》第七版

https://blog.csdn.net/newnewman80/article/details/8000404

你可能感兴趣的:(解析ping原理及实现)