Python 高度复现Windows的ping指令

Python 高度复现Windows的ping指令


前言

这都要从这学期的课程安排说起,这里要吐槽一下教务系统的安排,课表排得不任性化就算了,让学校两头的学生跨区上课,还要再雪上加霜,把下学期计算机网络的课设搬到这学期完成,好在负责的老师比较佛系。
在众多的选题中我选中了这个,既不会进入调包深坑学不到知识,也不至于难度太大导致各种不愉快。

题目要求如下:
设计并实现程序,实现类似Windows自带PING程序的功能,可以向指定的域名或IP地址发送Echo 请求报文,并根据响应报文显示出Ping的结果。程序仅支持-t选项即可。
实现本程序需要了解网络基础知识,掌握Ping命令的使用、ICMP报文的作用和结构、高级语言及网络编程知识,可以选择任意语言实现。

一、Ping与ICMP的关系?

1. 理解Ping如何实现前首先要知道Ping是什么

ping属于一个通信协议,是TCP/IP协议的一部分。利用“ping”命令可以检查网络是否连通,可以很好地分析和判定网络故障。
PING ,用于测试网络连接量的程序。Ping发送一个ICMP(Internet Control Messages Protocol)即因特网信报控制协议;回声请求消息给目的地并报告是否收到所希望的ICMPecho (ICMP回声应答)。它是用来检查网络是否通畅或者网络连接速度的命令。
它所利用的原理是这样的:利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,再要求对方返回一个同样大小的数据包来确定两台网络机器是否连接相通,时延是多少。

2. ICMP数据报文

显然Ping的实现和ICMP这个协议密不可分,实际上Ping的原理就是ICMP协议。
ICMP报文结构如下:
在这里插入图片描述
本篇博客旨在指引大家更加快速地获取相关知识,更加详细说明可参考:
用Python实现PING
Ping 本质( ICMP )
ping 原理
ping工作原理

二、代码实现

1.摸爬滚打

一开始的时候,也有个念头那就是有没有现成的第三方库可以参考,这样就可以在原来的代码上做改进,果不其然在Python社区里已经有人写好了关于Ping的实现代码,那就是ping3这个第三方库,网址为ping3 PyPI,简单地pip install ping3即可拥有。
然而呢在查阅网上资料和阅读ping3实现源码的时候总觉得哪里不对头,感觉少了点什么。
真实的ping:
在这里插入图片描述
来自网友的代码,不完全的ping:
在这里插入图片描述
来自ping3第三方库,虚假的ping:
在这里插入图片描述
尽管如此,ping3的源码很规范依然具有很强参考性,可以和网友代码进行结合,实现高仿ping。

2.引入库

import time  # 时间模块
import struct # 构造和解析数据包
import socket # 网络编程,套接字
import select # 实现阻塞
import os 

3.源代码

class Icmp_param:  # ICMP数据参数结构体
    def __init__(self, type_, code, checksum, ID, sequence, content):
        self.data_type = type_
        self.data_code = code
        self.data_checksum = checksum
        self.data_ID = ID
        self.data_Sequence = sequence
        self.payload_body = content.encode()


class Ping3:
    def __init__(self, host, order=0):
        self.host = host # 输入IP地址
        self.order = order # 指令类型,默认为无-t模式

    def check_sum(self, source: bytes) -> int:  # 计算校验和
        len_source = len(source)
        if len_source % 2:  # 如果总长度为奇数,则填充一个八位零来计算校验和
            source += b'\x00'
        sum = 0
        for i in range(0, len_source, 2):
            # 传入data以每两个字节(十六进制)通过ord转十进制,第一字节在低位,第二个字节在高位
            sum += (source[i]) + ((source[i + 1]) << 8)
            sum = (sum >> 16) + (sum & 0xffff)
        # 取反
        answer = ~sum & 0xffff
        #  主机字节序转网络字节序列
        answer = answer >> 8 | (answer << 8 & 0xff00)
        return answer

    def icmp_pack(self, type, code, checksum, ID, sequence, payload_body):
        #  把字节打包成二进制数据
        icmp_package = struct.pack('>BBHHH32s', type, code, checksum, ID, sequence, payload_body)
        # 获取校验和
        icmp_chesksum = self.check_sum(icmp_package)
        #  把校验和传入,再次打包
        icmp_package = struct.pack('>BBHHH32s', type, code, icmp_chesksum, ID, sequence, payload_body)
        return icmp_package

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

    def header2dict(self, names, struct_format, data):  # 解析ICMP数据包
        unpacked_data = struct.unpack(struct_format, data)
        return dict(zip(names, unpacked_data))

    def reply(self, request_time, rawsocket, data_sequence, timeout=2):
        # 实例化select对象(非阻塞),可读,可写为空,异常为空,超时时间
        content = select.select([rawsocket], [], [], timeout)
        # 等待时间
        wait_for_time = time.time() - request_time
        # 没有返回可读的内容,判断超时
        if not content[0]:  # Timeout
            return -1
        # 记录接收时间
        received_time = time.time()
        # 设置接收的包的字节为1024
        received_package, _ = rawsocket.recvfrom(1024)
        # 提取TTL
        ip_header = self.header2dict(names=[
            "version", "type", "length",
            "id", "flags", "ttl", "protocol",
            "checksum", "src_ip", "dest_ip"
        ], struct_format="!BBHHHBBHII", data=received_package[:20])
        ttl = ip_header['ttl']
        # print(ttl)
        # 获取接收包的icmp头
        icmpHeader = received_package[20:28]
        # 反转编码
        type, _, _, _, sequence = struct.unpack(">BBHHH", icmpHeader)
        if type == 0 and sequence == data_sequence:  # 判断收发数据的类型和内容是否一致
            return received_time - request_time, ttl
        # 数据包的超时时间判断
        timeout -= wait_for_time
        if timeout <= 0:
            return -1, -1

    def run(self):
        # 统计最终 已发送、 接受、 丢失
        send, accept, lost = 0, 0, 0
        sumtime, shorttime, longtime, avgtime = 0, 1000, 0, 0
        # 8回射请求 11超时 0回射应答
        icmp_param = Icmp_param(8, 0, 0, 0, 1, content="我是一个数据包") # content可以为任意内容
        # 将主机名转ipv4地址格式,返回以ipv4地址格式的字符串,如果主机名称是ipv4地址,则它将保持不变
        dest_addr = socket.gethostbyname(self.host)
        print("正在 Ping {0} [{1}] 具有 32 字节的数据:".format(self.host, dest_addr))
        # 默认发送4次
        send_time = 0
        timeout_delay = 1  # 请求超时延迟时间
        while True:
            # 请求ping数据包的二进制转换
            icmp_packet = self.icmp_pack(icmp_param.data_type, icmp_param.data_code, icmp_param.data_checksum,
                                         icmp_param.data_ID, icmp_param.data_Sequence + send_time,
                                         icmp_param.payload_body)
            # 连接套接字,并将数据发送到套接字
            send_request_ping_time, rawsocket = self.raw_socket(dest_addr, icmp_packet)
            # 数据包传输时间
            times, ttl = self.reply(send_request_ping_time, rawsocket, icmp_param.data_Sequence + send_time)
            if times > 0:
                print("来自 {0} 的回复: 字节=32 时间={1}ms TTL={2}".format(dest_addr, int(times * 1000), ttl))
                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.5)
            else:
                lost += 1
                print("请求超时")
                time.sleep(timeout_delay)
                timeout_delay *= 2  # 惩罚延迟时间翻倍

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


def shell():  # 交互界面
    print(os.path.abspath(__file__))
    print("支持指令:ping IP地址 和 ping -t IP地址")
    while True:
        input_order = input(">>>")
        try:
            segments = input_order.strip().split(" ")
            if segments[1] == "-t":
                Ping3(segments[2], 1).run()
            else:
                Ping3(segments[1]).run()

        except Exception as e:
            print(e)
            continue


if __name__ == "__main__":
    shell()

4.关键讲解

其实不管是网上的代码还是ping3里的代码都对ICMP的构造发送等实现很完备了,然而在解析数据包的时候还有些欠缺,所以就出现了TTL这部分的确实,实际上返回的ICMP数据包中有所有我们想要的信息,只要我们按照ICMP数据包的格式提取即可,这里参考从通过Python原始套接字接收的ICMP消息中读取TTL
其中最精髓的地方就在于对数据格式的提取,以json的形式提取数据然后从中选取所需数据,快捷高效。
体现在我的代码里就是header2dict函数和在reply函数中对其的调用!

5.完成效果

在这里插入图片描述

总结

大家都喜欢做的事情未必就是正确的,大家都喜欢白嫖,然而白嫖并不是解决问题的可行方法,这只是在逃避问题而没有去面对问题,在拿到难题或者任务时首先从网络上广泛查阅知识,这个阶段是为了让我们对要做的事情和问题有个宏观的理解。然后着眼于细微之处比如如何实现,先看网上代码如何,然后选择去留,不满意则自己按照理解实现,选择保留则建议在原有基础上将他人代码编写成含有自己特色和风格的代码,否则只是生搬照抄,这些知识永远不属于你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值