使用socket+Python实现ping

import os
import socket
import struct
import select
import time

# 计算校验和,用于确保数据的完整性
def checksum(source_string):
    sum = 0
    count = 0
    max_count = len(source_string)
    
    # 处理成对的字节
    while count < max_count - 1:
        val = source_string[count + 1] * 256 + source_string[count]
        sum = sum + val
        sum = sum & 0xffffffff  # 保持sum为32位
        count = count + 2
    
    # 处理最后一个字节(如果长度为奇数)
    if max_count % 2:
        sum = sum + source_string[-1]
        sum = sum & 0xffffffff
    
    # 折叠高16位和低16位
    sum = (sum >> 16) + (sum & 0xffff)
    sum = sum + (sum >> 16)
    # 取反得到最终的校验和
    answer = ~sum
    answer = answer & 0xffff
    # 最终调整顺序(大端或小端)
    answer = answer >> 8 | (answer << 8 & 0xff00)
    
    return answer

# 创建 ICMP echo 请求包
def create_packet(id):
    # 头部类型为8(ICMP echo请求),代码为0,校验和为0,id为传入的id,序列号为1
    header = struct.pack('bbHHh', 8, 0, 0, id, 1)
    data = 256 * b'Q'  # 数据部分
    my_checksum = checksum(header + data)  # 计算校验和
    # 重新打包头部,包含正确的校验和
    header = struct.pack('bbHHh', 8, 0, socket.htons(my_checksum), id, 1)
    return header + data

# 执行 ping 操作
def ping(dest_addr, timeout=1, count=4):
    icmp = socket.getprotobyname("icmp")  # 获取 ICMP 协议的编号
    # 创建原始套接字
    my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
    
    my_ID = os.getpid() & 0xFFFF  # 生成一个唯一的ID
    
    sent_count = 0  # 发送的ping包数量
    received_count = 0  # 收到的ping包数量
    
    while sent_count < count:
        sent_count += 1
        packet = create_packet(my_ID)
        
        my_socket.sendto(packet, (dest_addr, 1))  # 发送 ICMP echo 请求
        
        while True:
            started_select = time.time()
            # 监听套接字是否有数据可读
            what_ready = select.select([my_socket], [], [], timeout)
            how_long_in_select = (time.time() - started_select)
            if what_ready[0] == []:  # 超时
                print("请求超时。")
                break
            
            time_received = time.time()
            received_packet, addr = my_socket.recvfrom(1024)  # 接收数据包
            icmp_header = received_packet[20:28]  # 提取 ICMP 头部
            type, code, checksum, packet_ID, sequence = struct.unpack(
                "bbHHh", icmp_header
            )
            
            if packet_ID == my_ID:  # 确认是发自我们的请求
                bytes_In_double = struct.calcsize("d")
                time_sent = struct.unpack("d", received_packet[28:28 + bytes_In_double])[0]
                print("来自 {} 的回复: 字节={} 时间={:.2f}ms".format(
                    addr[0], len(received_packet), (time_received - time_sent) * 1000)
                )
                received_count += 1
                break
            
            time_left = timeout - how_long_in_select
            if time_left <= 0:
                print("请求超时。")
                break
    
    my_socket.close()
    return received_count

# 使用示例
if __name__ == '__main__':
    dest = input("输入要 ping 的主机: ")
    print("正在用 Python ping {}:".format(dest))
    ping(dest)

# 处理成对的字节
    while count < max_count-1:
        val = source_string[count + 1]*256 + source_string[count]
        sum = sum + val
        sum = sum & 0xffffffff
        count = count + 2

    1.以成对的字节进行遍历,将源字符串(source_string)中的成对字节合并成16位的整数,并将这些整数累加到 sum 变量中。源字符串是要发送的数据包的内容。

    2.val = source_string[count + 1]*256 + source_string[count] 将源字符串中的当前字节(source_string[count])和下一个字节(source_string[count + 1])结合起来形成一个16位的整数。由于在大多数计算机中,整数是以小端序存储的,所以 count + 1 位置的字节是高字节,需要乘以256(即左移8位)以放在结果整数的高位,然后加上 count 位置的字节作为低位。

    3.sum = sum + val 这里将刚刚计算出的16位整数 val 加到累加器 sum 上。这个累加器最终会包含所有16位整数的和。

    4.print(source_string)的结果为source_string:  b'\x08\x00\x00\x00\xe4H\x01\x00QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'

由于字符串长度为264,循环将运行132次(假设字符串有偶数个字节)。第一次循环会处理索引为0和1的字节(\x08 和 \x00),第二次循环处理索引为2和3的字节(\x00 和 \x00),以此类推,直到处理完所有的字节对。(一个字节是8位)

# 最终调整顺序

answer = answer >> 8 | (answer << 8 & 0xff00)

    answer >> 8:这将 answer 的所有位向右移动8位。这个操作会将原始 answer 的高8位移动到低8位的位置。
    answer << 8:这将 answer 的所有位向左移动8位。这个操作会将原始 answer 的低8位移动到高8位的位置。
    answer << 8 & 0xff00:& 0xff00 是一个掩码操作,它将确保左移操作后只保留高8位的值,低8位将被清零。0xff00 是一个16位的整数,其中高8位是1,低8位是0。
    answer >> 8 | (answer << 8 & 0xff00):| 是按位或操作,它将上述两个操作的结果组合起来。右移的结果会在新值的低8位,左移并掩码后的结果会在新值的高8位。 

以 4660 为例操作:
    右移8位 (answer >> 8):将 00010010 01100100 右移8位变为 00000000 00010010,在十进制中这是 18。
    左移8位并应用掩码 (answer << 8 & 0xff00):首先,将 00010010 01100100 左移8位变为 01100100 00000000,这个操作后在十进制中为 25856。然后应用掩码 0xff00(在二进制中为 11111111 00000000),结果仍为 01100100 00000000(25856)。
    合并两个结果:使用按位或操作将 00000000 00010010(18)和 01100100 00000000(25856)合并,得到 01100100 00010010,这在十进制中为 25618。

header = struct.pack('bbHHh', 8, 0, 0, id, 1) 

    struct.pack() 是 Python struct 模块中的一个函数,它的作用是将给定的值打包成特定格式的二进制数据。在网络编程和二进制文件处理中经常使用这个函数,因为它能够根据指定的格式将Python数据类型转换为字节字符串,这些字节字符串可以被发送到网络或写入文件。

socket.htons(my_checksum) 

    socket.htons() 函数在 Python 中用于将一个16位的正短整数从主机字节序转换为网络字节序。在网络编程中经常需要这样做,因为不同的计算机系统有不同的整数存储方式,即字节序问题。 

my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)

    调用 socket.socket(socket.AF_INET, socket.SOCK_RAW) 的作用是创建一个原始套接字(raw socket),用于低级网络通信,其中可以发送和接收特定网络协议的数据包。 
    socket.AF_INET: 这个参数指定地址族为 IPv4。它是用来确定套接字使用的网络层协议,AF_INET 表示使用 IPv4 地址。
    socket.SOCK_RAW: 这个参数指定套接字类型为“原始套接字”。原始套接字允许你访问底层协议,如 ICMP、TCP、UDP 等。使用原始套接字,你可以构造自己的数据包头部,进行更为灵活的网络操作。
    icmp: 这是 socket.getprotobyname("icmp") 返回的 ICMP 协议编号。它告诉套接字使用 ICMP 协议。ICMP 通常用于发送控制消息,如 ping 请求和响应。

my_ID = os.getpid() & 0xFFFF 

    os.getpid() 函数用于获取当前进程的进程 ID(PID)。在 Python 中,这个函数返回一个整数,代表当前运行的进程的唯一标识符。 
    在网络编程,特别是在构建 ICMP Echo 请求(如 ping 命令)时,通常需要一个标识符来区分不同的 Echo 请求。使用当前进程的 PID 作为标识符是一个常见的做法,因为它能够为每个不同的进程提供一个唯一的标识。然而,由于 PID 的大小可能超出了 ICMP Echo 请求头中标识符字段所能容纳的范围(通常为 16 位),所以通过与 0xFFFF 进行按位与运算,可以确保得到的标识符适合在该字段中使用,即将 PID 限制在 0 到 65535 (即 0xFFFF)的范围内。这样做既保留了一定程度的唯一性,又符合协议的要求。(这个my_ID就是传入create_packet()函数中header = struct.pack('bbHHh', 8, 0, 0, id, 1) 所用的id参数。)

my_socket.sendto(packet, (dest_addr, 1)) 

   调用 my_socket.sendto(packet, (dest_addr, 1)) 的作用是通过之前创建的原始套接字 my_socket 发送一个数据包 packet 到指定的目标地址 dest_addr 上的端口号 1。

what_ready = select.select([my_socket], [], [], timeout)

if what_ready[0] == []:  # Timeout

                print("请求超时")

                break

(以上代码可以省去)

    调用 select.select([my_socket], [], [], timeout) 的作用是使用 select 模块来监视套接字的状态,检查套接字 my_socket 是否有数据可读,是否可以无阻塞地进行读操作。这是一种多路复用输入/输出的方式,用于在多个通信渠道上等待事件发生,从而避免程序在单个通信渠道上阻塞。

    在 select.select() 调用中,参数有以下含义:
    第一个参数 [my_socket] 是一个套接字列表,select 将监视这个列表中的套接字以查看它们是否变得可读(即是否有数据到达套接字,可以进行读操作)。
    第二个参数 [] 是一个空列表,用于指定需要检查是否可写的套接字列表。在这个调用中,我们不关心套接字是否可写,所以传入一个空列表。
    第三个参数 [] 同样是一个空列表,用于指定需要检查是否有错误的套接字列表。同样,在这个调用中,我们不关心套接字是否有错误,所以传入一个空列表。
    第四个参数 timeout 是一个超时值,指定 select 等待的最长时间(以秒为单位)。如果指定了超时时间,即使没有套接字变成可读,select 也会在超时后返回。如果 timeout 是 None,select 将会无限期地等待直到至少有一个套接字变得可读。

    例如,当你发送了一个 ICMP Echo 请求后,你可能会使用 select.select() 来等待一个响应,而不会阻塞程序的运行。如果 select 在超时时间内检测到 my_socket 有数据可读,它会返回一个包含 my_socket 的列表,这意味着你可以从套接字读取数据而不会阻塞。如果在超时时间内没有数据可读,select 将返回一个空列表,并且你可以决定是否重新发送请求、继续等待或进行其他操作。

 

received_packet, addr = my_socket.recvfrom(1024)

    调用 received_packet, addr = my_socket.recvfrom(1024) 的作用是接收通过网络传输到达指定的原始套接字 my_socket 的数据,并且读取最多 1024 字节的数据。

    在这个函数调用中:
    received_packet 是接收到的数据内容。这个数据包含了发送方发送的原始数据,可能包括IP头部、ICMP头部以及随后的数据部分。
    addr 是一个包含发送方地址信息的元组,通常形式为 (IP地址, 端口号)。在接收到的是 ICMP 消息的情况下,端口号通常是不相关的,因为 ICMP 是网络层的协议,不使用传输层的端口号。
my_socket 是之前创建的原始套接字,用于在网络上发送和接收低级别的协议数据包。
    1024 是指定的缓冲区大小,以字节为单位。它告诉 recvfrom 方法在单次调用中最多可以接收多少字节的数据。

    因此,当调用 my_socket.recvfrom(1024) 时,它会阻塞当前线程,直到有数据到达套接字或者套接字关闭。一旦接收到数据,它会停止阻塞,并将数据和发送方的地址赋值给 received_packet 和 addr 变量。这个操作通常用于网络通信中的数据接收,例如,在实现ping程序时,用来接收ICMP Echo响应。

icmp_header = received_packet[20:28]

    提取接收到的数据包中的 ICMP 协议头部分,ICMP 协议头通常包含类型、代码和校验和等信息,长度为 8 字节。

 

 

相关推荐

  1. Mac 上可以使用 ping 端口

    2024-05-10 12:16:04       42 阅读
  2. 使用Python编写Ping监测程序

    2024-05-10 12:16:04       31 阅读
  3. Golang WebSocket Ping Pong

    2024-05-10 12:16:04       43 阅读
  4. UDP Ping程序实现--第1关:Ping服务端创建UDP套接字

    2024-05-10 12:16:04       62 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-10 12:16:04       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-10 12:16:04       101 阅读
  3. 在Django里面运行非项目文件

    2024-05-10 12:16:04       82 阅读
  4. Python语言-面向对象

    2024-05-10 12:16:04       91 阅读

热门阅读

  1. C++面向对象学习笔记四

    2024-05-10 12:16:04       35 阅读
  2. Linux学习笔记2

    2024-05-10 12:16:04       28 阅读
  3. SpringBoot学习记录(1)

    2024-05-10 12:16:04       34 阅读
  4. leetcode题目1

    2024-05-10 12:16:04       29 阅读
  5. C++ 509. 斐波那契数

    2024-05-10 12:16:04       29 阅读
  6. 1376:信使(msner)

    2024-05-10 12:16:04       32 阅读
  7. LinuxC 鼠标应用编程 input_event

    2024-05-10 12:16:04       34 阅读
  8. MADbench2

    2024-05-10 12:16:04       32 阅读
  9. Node.js

    2024-05-10 12:16:04       29 阅读