在看《计算机网络:自顶向下方法》第四章的时候,有一个编程作业是用python编写一个ping小程序,我在实现的时候加深了一写对python的了解和对IP报文头部和ICMP报文头部信息有了一定理解,故记录一下。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from socket import *
import os
import sys
import struct
import time
import select
import binascii
ICMP_ECHO_REQUEST = 8
def checksum(strr):
csum = 0
countTo = (len(strr) / 2) * 2
count = 0
while count < countTo:
thisVal = strr[count+1] * 256 + strr[count]
csum = csum + thisVal
csum = csum & 0xffffffff
count = count + 2
if countTo < len(strr):
csum = csum + str[len(strr) - 1]
csum = csum & 0xffffffff
csum = (csum >> 16) + (csum & 0xffff)
csum = csum + (csum >> 16)
answer = ~csum
answer = answer & 0xffff
answer = answer >> 8 | (answer << 8 & 0xff00)
return answer


def receiveOnePing(mySocket, ID, timeout, destAddr):
timeLeft = timeout
while 1:
startedSelect = time.time()
whatReady = select.select([mySocket], [], [], timeLeft)
howLongInSelect = (time.time() - startedSelect)
if whatReady[0] == []: # Timeout
return "请求超时。"
timeReceived = time.time()
recPacket, addr = mySocket.recvfrom(1024)
#Fill in start
#Fetch the ICMP header from the IP packet
t, c, checksum, recID, seq = struct.unpack("bbHHh", recPacket[20:28])
ttl, = struct.unpack("b", recPacket[8:9])
if t == 3:
if c == 0:
return "Destination Network Unreachable."
elif c == 1:
return "Destination Host Unreachable."
if t != 0 or c != 0 or recID != ID or seq != 1:
return "Recieve error."
#Fill in end
timeLeft = timeLeft - howLongInSelect
if timeLeft <= 0:
return "请求超时。"
return [1-timeLeft, ttl, len(recPacket)]

def sendOnePing(mySocket, destAddr, ID):
# Header is type (8), code (8), checksum (16), id (16), sequence (16)
myChecksum = 0
# Make a dummy header with a 0 checksum.
# struct -- Interpret strings as packed binary data
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, myChecksum, ID, 1)
data = struct.pack("d", time.time())
# Calculate the checksum on the data and the dummy header.
myChecksum = checksum(header + data)
# Get the right checksum, and put in the header
if sys.platform == 'darwin':
myChecksum = htons(myChecksum) & 0xffff
#Convert 16-bit integers from host to network byte order.
else:
myChecksum = htons(myChecksum)
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, myChecksum, ID, 1)
packet = header + data
mySocket.sendto(packet, (destAddr, 1)) # AF_INET address must be tuple, not str
#Both LISTS and TUPLES consist of a number of objects
#which can be referenced by their position number within the object


def doOnePing(destAddr, timeout):
icmp = getprotobyname("icmp")
#SOCK_RAW is a powerful socket type. For more details see: http://sock-raw.org/papers/sock_raw
#Fill in start
mySocket = socket(AF_INET, SOCK_RAW, icmp)
#Fill in end
myID = os.getpid() & 0xFFFF #Return the current process i
sendOnePing(mySocket, destAddr, myID)
delay = receiveOnePing(mySocket, myID, timeout, destAddr)
mySocket.close()
return delay
def ping(host, timeout=1):
dest = gethostbyname(host)
print("Pinging " + dest + " using Python:")
print()
pingTimes = 4
lost = 0
delayList = []
#Send ping requests to a server separated by approximately one second
for i in range(pingTimes):
res = doOnePing(dest, timeout)
if type(res) == str:
lost+=1;
print(res)
continue
delay, TTL, packetSize = res
delay = int(delay * 1000)
delayList.append(delay)
print("来自",dest,"的回复: 字节=",packetSize," 时间=",delay,"ms TTL=",TTL)
time.sleep(1)# one second

print(dest + " 的 Ping 统计信息:")
print(" 数据包: 已发送 = ",pingTimes," ,已接收 = ",pingTimes-lost," ,丢失 = ",lost, " ( ",lost/pingTimes*100,"% 丢失)")
if len(delayList) > 0:
print("往返行程的估计时间(以毫秒为单位):")
print("最短 = ",min(delayList),"ms,最长 = ",max(delayList),"ms,平均 = ",sum(delayList)/len(delayList),"ms")

if len(sys.argv) <= 1:
print("请输入要ping的主机地址!")
exit()
ping(sys.argv[1])

作业中主要填写两个空,一个在doOnePing函数中,一个在receiveOnePing函数中。

首先看doOnePing函数,根据上下文可以猜测出这里需要定义一个socket变量,然后我们在第二章中做的实验知道,创建TCP类型的socket的语句是:

1
socket(AF_INET, SOCK_STREAM)

创建UDP类型的socket的语句是:

1
socket(AF_INET, SOCK_DGRAM)

而我们要发送的是ICMP报文,要知道ICMP工作在网络层,所以不能用传输层的协议来传输,进一步根据注释知道了使用SOCK_RAW来创建一个原始套接字,通过查API知道创建包含ICMP协议的原始套接字的语句是:

1
2
icmp = getprotobyname("icmp")
mySocket = socket(AF_INET, SOCK_RAW, icmp)

第一个空完成!


接下来做receiveOnePing函数中的空。

这个函数要结合着sendOnePing函数看,因为一个是从当前主机往目标主机发数据,一个是从目标主机往当前主机接收数据,所以用什么方法打包数据,就要用同样的方法反向解包。

可以看到sendOnePing函数是通过struct.pack()来将header和data打包起来发送的,所以在receiveOnePing函数使用struct.unpack()来进行解包。注意这里recPacket是IP报文数据,又因为一般的IP首部占20个字节,所以从第20个字节开始才是ICMP报文。所以 recPacket[20:28] 就是ICMP的报文头,然后用struct.unpack()来把数据解包。注释中 Header is type (8), code (8), checksum (16), id (16), sequence (16) 可以帮助理解ICMP的报文头部信息。根据ICMP报文头部的信息确定ICMP的类型,我们需要的是TYPE为0,CODE为0,该报文类型为Echo Reply——回显应答(Ping应答),到这里我们解析ICMP报文的任务就完成了。

为了做的和windows中自带的ping看起来更像一些,我适当的做了一些修改。

比如字节数和TTL,由于TTL是在IP报文头部的第八个字节中存储的,所以也单独的提取出来。