其中UDPPinggerServer.py是已经给出的,需要我们实现的是UDPPingger.py
from socket import *
import random
serverSocket = socket(AF_INET, SOCK_DGRAM) #创建UDP套接字
serverSocket.bind(('', 12000)) #绑定本机IP地址和端口号
while True: #用无限循环来模拟监听客户端发来的数据报
rand = random.randint(1, 10) #生成一个[1,10]的随机数
message, address = serverSocket.recvfrom(1024) #接收客户端消息
message = message.upper() #将数据报内容转换为大写
if rand < 4: #如果随机数是1,2或3,则不做出响应,模拟30%丢包率
continue
serverSocket.sendto(message, address) #否则,服务器向客户端做出响应
from socket import *
import time
serverName = '192.168.202.1' #服务器地址设置为本机IP地址,与UDPPinggerServer中一致
serverPort = 12000 #服务器指定端口为12000,与UDPPinggerServer中一致
clientSocket = socket(AF_INET, SOCK_DGRAM) #创建UDP套接字
clientSocket.settimeout(1) #设置超时值1秒
for i in range(10): #发送10条ping消息
sendTime = time.time()
message = ('Ping %d %s' % (i+1, sendTime)).encode() #生成数据报,编码为bytes以便发送
try:
clientSocket.sendto(message, (serverName, serverPort)) #向服务器发送数据报
upperedMessage, serverAddress = clientSocket.recvfrom(1024) #接收服务器的响应信息和服务器地址
RTT = time.time() - sendTime #计算RTT
print('Sequence %d: Reply from %s RTT = %.3fs' %(i+1, serverName, RTT)) #打印成功接收的信息
except:
print('Sequence %d: Request timed out.' %(i+1)) #打印丢包信息
现在终端运行 UDPPinggerServer.py,监听数据报
再另开一个终端运行 UDPPingger.py
ICMP协议(Internet Control Message Protocl,因特网控制报文协议)是一种面向无连接的协议,用于传输出错报告控制信息,属于网络层协议,主要用于在和路由器之间传递控制控制信息
一个ICMP报文包括IP报头(至少 20 字节)、ICMP报头(至少 8 字节)和ICMP报文(即 ICMP 报文的数据部分),当IP报头中的协议字段值为 1 时,就说明这是一个ICMP报文
ICMP报头结构
类型(Type):标识ICMP报文的类型,第一类是取值为1-127的差错报文,第二类是取指128以上的信息报文。
代码(Code):它与类型字段一起共同标识了ICMP报文的详细类型
校验和(Checksum):对包括ICMP报文数据部分在内的整个ICMP数据报的校验和,以检验报文在传输过程中是否出现了差错
本实验中涉及到的ICMP报文类型
(1)ping操作中包括了请求ICMP报文(类型字段值为8)和应答ICMP报文(类型字段值为0):
一台主机向一个节点发送一个类型字段值为8的ICMP报文,如果途中没有异常,则目标返回类型字段值为0的ICMP报文,说明这台主机存在
(2)时间戳请求报文(类型字段值为13)和时间戳应答报文(类型字段值为14)用于测试两台主机之间数据报来回一次的传输时间。传输时,主机填充原始时间戳,接受方收到请求后填充接受时间戳后以类型值字段14的报文格式返回,发送方计算这个时间差
在begin和and之间的是需要我们实现的部分
import socket
import os
import struct
import time
import select
ICMP_ECHO_REQUEST = 8
#生成校验和
def checksum(str):
csum = 0
countTo = (len(str) / 2) * 2
count = 0
while count < countTo:
thisVal = str[count + 1] * 256 + str[count]
csum = csum + thisVal
csum = csum & 0xffffffff
count = count + 2
if countTo < len(str):
csum = csum + str[len(str) - 1].decode()
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
#发送一次Ping数据包
#先生成一个校验和为0的虚假的头部,然后计算出虚假头部与数据的校验和,用其生成真正的头部,最后由真正的头部和数据data构成发送的数据包packect
#头部结构:请求类型(8bits),代码8(bits),新校验和(16bits),报文ID(16bits),报文序号Sequence(16bits)
def sendOnePing(mySocket, ID, sequence, destAddr):
myChecksum = 0
########## Begin ##########
# 生成一个校验和为0的虚拟头部
# struct.pack() -- 将字符串信息封装成二进制数据
# ! -- 以大端模式标准对齐
# B -- python 1字节int类型
# H -- python 2字节int类型
# d -- python 8字节float类型
header = struct.pack("!2B3H", ICMP_ECHO_REQUEST, 0, myChecksum, ID, sequence)
data = struct.pack("!d", time.time())
# 计算头部和数据的校验和
myChecksum = checksum(header + data)
#用新的校验和生成真正的头部
header = struct.pack("!2B3H", ICMP_ECHO_REQUEST, 0, myChecksum, ID, sequence)
#由真正的头部和数据data构成要发送的包
packet = header + data
########## End ##########
mySocket.sendto(packet, (destAddr, 1))
#接收一次Ping的返回消息
def receiveOnePing(mySocket, ID, sequence, destAddr, timeout):
timeLeft = timeout
while 1:
startedSelect = time.time()
whatReady = select.select([mySocket], [], [], timeLeft)
howLongInSelect = (time.time() - startedSelect)
if whatReady[0] == []: # Timeout
return None
timeReceived = time.time()
#计算并返回报文延迟、TTL及double类型数据长度
########## Begin ##########
#接收报文
recPacket, addr = mySocket.recvfrom(1024)
#取出ICMP报文头部(前20字节是IP报头,第21-28字节是ICMP报头)
header = recPacket[20: 28]
#解析ICMP头部字段(和sendOnePing中的pack相对应)
type, code, checksum, packetID, sequence = struct.unpack("!2B3H", header)
#计算延迟及TTL信息
if type == 0 and packetID == ID: # type should be 0
byte_in_double = struct.calcsize("!d") #计算数据部分所占的字节数(在本例中,数据中只有发送时间)
#注意unpack返回值是元组,只pack了一个对象,那么unpack得到的就是一元组
timeSent = struct.unpack("!d", recPacket[28: 28 + byte_in_double])[0]
delay = timeReceived - timeSent #计算数据报传输时延
# IP报头的第8个字节是TTL(Time To Live)字段,ord函数将字符转换成对应的整数,即用于获取ASCII给定字符的值
ttl = ord(struct.unpack("!c", recPacket[8:9])[0].decode())
return (delay, ttl, byte_in_double)
########## End ##########
timeLeft = timeLeft - howLongInSelect
if timeLeft <= 0:
return None
#向指定地址发送Ping消息
def doOnePing(destAddr, ID, sequence, timeout):
icmp = socket.getprotobyname("icmp")
########## Begin ##########
mySocket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) #创建使用ICMP协议的原始套接字
########## End ##########
sendOnePing(mySocket, ID, sequence, destAddr)
delay = receiveOnePing(mySocket, ID, sequence, destAddr, timeout)
mySocket.close()
return delay
#主函数Ping
def ping(host, timeout=1):
# timeout=1指: 如果1秒内没从服务器返回,客户端认为Ping或Pong丢失。
dest = socket.gethostbyname(host)
print("Pinging " + dest + " using Python:")
#每秒向服务器发送一次Ping请求
myID = os.getpid() & 0xFFFF # 返回进程ID
loss = 0
for i in range(4):
result = doOnePing(dest, myID, i, timeout)
if not result:
print("Request timed out.")
loss += 1
else:
delay = int(result[0]*1000)
ttl = result[1]
bytes = result[2]
print("Received from " + dest + ": byte(s)=" + str(bytes) + " delay=" + str(delay) + "ms TTL=" + str(ttl))
time.sleep(1) # one second
print("Packet: sent = " + str(4) + " received = " + str(4-loss) + " lost = " + str(loss))
return
ping("www.baidu.com")