import os
import socket
import struct
import select
import time# 计算校验和,用于确保数据的完整性
def checksum(source_string):sum = 0count = 0max_count = len(source_string)# 处理成对的字节while count < max_count - 1:val = source_string[count + 1] * 256 + source_string[count]sum = sum + valsum = 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 = ~sumanswer = answer & 0xffff# 最终调整顺序(大端或小端)answer = answer >> 8 | (answer << 8 & 0xff00)return answer# 创建 ICMP echo 请求包
def create_packet(id):# 头部类型为8(ICMP echo请求),代码为0,校验和为0,id为传入的id,序列号为1header = 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 # 生成一个唯一的IDsent_count = 0 # 发送的ping包数量received_count = 0 # 收到的ping包数量while sent_count < count:sent_count += 1packet = 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("请求超时。")breaktime_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 += 1breaktime_left = timeout - how_long_in_selectif time_left <= 0:print("请求超时。")breakmy_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 字节。