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报文的一部分。
三、ICMP实现
由于PING命令是使用ICMP实现的,那么ICMP是如何实现的对了解PING是至关重要的。
1. ICMP报文类型
ICMP类型分为两大类:
- 差错报告
- 询问报告
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位补码。具体操作如:
- 将校验和字段置为0。
- 将每两个字节(16位)相加(二进制求和)直到最后得出结果,若出现最后还剩一个字节继续与前面结果相加。
- (溢出)将高16位与低16位相加,直到高16位为0为止。
- 将最后的结果(二进制)取反。
关于二进制求和:
# 例:
# 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
大端序
小端序
不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
四、实现PING功能
简单流程
- 执行 ping 192.168.0.5
Ping命令会构建一个固定格式的ICMP请求数据包,
然后由ICMP协议将这个数据包连同地址“192.168.0.5”一起交给IP层协议
- IP层相关操作
IP层协议将以地址“192.168.0.5”作为目的地址,本机IP地址作为源地址,
加上一些其他的控制信息,构建一个IP数据包发往192.168.0.5。
- 目的主机相关操作
接收后检查该数据帧,将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