一、网络聊天程序的设计与实现
1、实验目的
使用Socket编程,了解Socket通信的原理,会使用Socket进行简单的网络编程,并在此基础上编写聊天程序,运行服务器端和客户端,实现多个客户端通过服务器端进行通信。
2、总体设计
(1)背景知识
① TCP/IP协议与WinSock网络编程接口
TCP/IP协议是一种四层协议,包含各种软硬件需求的定义,其中UDP协议(User Datagram Protocol 用户数据报协议),是一种保护消息边界的,不保障可靠数据的传输。TCP协议(Transmission Control Protocol传输控制协议), 是一种流传输的协议,提供可靠的、有序的、双 向的、面向连接的传输。
保护消息边界是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次就只能接收发送端发出的一个数据包;面向流则是无保护消息边界的,如果发送端连续发送数据,接收端就有可能在一次接收动作中接收两个或者更多的数据包。
WinSock编程是一种网络编程接口,实际上是作为TCP/IP协议的一种封装。可以通过调用WinSock的接口函数来调用TCP/IP的各种功能。
② 使用TCP服务的常用系统调用阶段
(i)连接建立阶段
当套接字被创建后,它的端口号和IP地址都是空的,因此应用进程要调用bind来指明套接字的本地地址(本地端口号和本地IP地址)。在服务器端调用bind时就是把熟知端口号和本地IP地址填写到已创建的套接字中,即把本地地址绑定到套接字。在客户端也可以不调用bind,由操作系统内核自动分配一个动态端口号(通信结束后由系统收回)。
服务器在调用bind后,还必须调用listen把套接字设置为被动方式,以便随时接受客户的服务请求。
服务器紧接着就调用accept,以便把远地客户进程发来的连接请求提取出来。系统调用accept的一个变量就是要指明是从哪一个套接字发起的连接。
在任一时刻,服务器中总是有一个主服务器进程和零个或多个从属服务器进程。主服务器进程用原来的套接字接受连接请求,而从属服务器进程用新创建的套接字和相应的客户建立连接并可进行双向传送数据。
当使用TCP协议的客户己经调用socket创建了套接字后,客户进程就调用connect,以便和远地服务器建立连接(即主动打开,相当于客户发出的连接请求)。在connect系统调用中,客户必须指明远地端点(即远地服务器的IP地址和端口号)。
(ii)数据传输阶段
客户和服务器都在TCP连接上使用send系统调用传送数据,使用recv系统调用接收数据。通常客户使用send发送请求,而服务器使用send发送回答。服务器使用recv接收客户用send调用发送的请求。客户在发完请求后用recv接收回答。
调用send需要三个变量:数据要发往的套接字的描述符、要发送的数据的地址以及数据的长度。通常send调用把数据复制到操作系统内核的缓存中。若系统的缓存已满,send就暂时阻塞,直到缓存有空间存放新的数据。
调用recv也需要三个变量:要使用的套接字的描述符、缓存的地址以及缓存空间的长度。
(iii)连接释放阶段
客户或服务器结束使用套接字,就把套接字撤销。调用close释放连接和撤销套接字。注意,有些系统调用在一个TCP连接中可能会循环使用,而UDP服务器由于只提供无连接服务,因此不使用listen和accept系统调用。
③ Python中的常用Socket编程方法
- socket.bind(address)
将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址;
- socket.listen(backlog)
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。其中backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5,这个值不能无限大,因为要在内核中维护连接队列;
- socket.accept()
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址;
- socket.connect(address)
连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误;
- socket.close()
关闭套接字;
- socket.recv(bufsize[,flag])
接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略;
- socket.send(string[,flag])
将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小,即可能未将指定内容全部发送。
(2)模块介绍
程序总共分为两大部分,分别是服务器端和客户端:
① 服务器端
(i)统计在线人员模块
统计客户端登录的情况,获取当前在线人员的列表。若用户断开连接,将其从用户列表中删除并更新用户列表。
(ii)接收消息模块
接受来自客户端的用户名。如果用户名为空,则使用用户的IP与端口作为用户名;若用户名出现重复,则在用户名后依此加上后缀"2"、"3"、"4"等;获取用户名后便会不断地接受用户端发来的消息,结束后关闭连接;如果用户断开连接,将该用户从用户列表中删除,然后更新用户列表。
(iii)发送数据模块
服务端在接受到数据后,会对其进行一些处理然后发送给客户端:对于聊天内容,服务端直接发送给客户端;而对于用户列表,便由json.dumps处理后发送。
② 客户端
(i)登录模块
通过tkinter绘制UI,获取IP、PORT和用户名(可实现防重名),退出登录界面时会弹出确认提示,确定后则退出程序;
(ii)接收消息模块
保持连接状态,获取数据信息,并识别"message+username+chatwith"格式的消息,对聊天状态进行判断,进行相应的显示。
(iii)发送消息模块
消息从聊天框发送后,将以"message+username+chatwith"的格式送出,触发条件是sendButton()方法对应的“发送”按钮。
(3)设计步骤
① 服务器端
- 导入socket库,通过socket.socket()方法加载socket库,并创建socket;
- 通过bind()方法绑定socket到一个IP和一个PORT上;
- 通过listening()方法将socket设置为监听模式等待连接请求;
- 当客户端通过connect()方法传来请求后,接收请求,通过accept()方法返回一个新的对应于此次连接的socket;
- 通过send()方法和recv()方法用返回的socket和客户端进行通信;
- 返回,等待另一连接请求;
- 通过close()方法关闭socket和加载的socket库。
② 客户端
- 导入socket库,通过socket.socket()方法加载socket库,并创建socket;
- 通过connect()方法向服务器发出连接请求;
- 通过send()方法和recv()方法和客户端进行通信;
- 通过close()方法关闭socket和加载的socket库。
3、详细设计
(1)程序流程图
(2)关键代码
① 服务器端
# 接受来自客户端的用户名,如果用户名为空,使用用户的IP与端口作为用户名,如果用户名出现重复,则在出现的用户名依此加上后缀"2","3","4"……
def receive(self, conn, addr): # 接收消息
# recv:接受套接字的数据,数据以字符串形式返回,bufsize指定最多可以接收的数量,flag提供有关消息的其他信息,通常可以忽略
user = conn.recv(1024) # 用户名称
user = user.decode()
if user == '用户名不存在':
user = addr[0] + ':' + str(addr[1])
tag = 1
temp = user
for i in range(len(users)): # 检验重名,则在重名用户后加数字
if users[i][0] == user:
tag = tag + 1
user = temp + str(tag)
users.append((user, conn))
USERS = Onlines()
self.Load(USERS, addr)
# 在获取用户名后便会不断地接受用户端发来的消息(即聊天内容),结束后关闭连接
# noinspection PyBroadException
try:
while True:
# recv:接受套接字的数据,数据以字符串形式返回,bufsize指定最多可以接收的数量,flag提供有关消息的其他信息,通常可以忽略
message = conn.recv(1024) # 发送消息
message = message.decode()
message = user + ':' + message
self.Load(message, addr)
# close:关闭套接字
conn.close()
# 如果用户断开连接,将该用户从用户列表中删除,然后更新用户列表
except:
j = 0 # 用户断开连接
for man in users:
if man[0] == user:
users.pop(j) # 服务器端删除退出的用户
break
j = j + 1
USERS = onlines()
self.Load(USERS, addr)
conn.close()
# 服务端在接受到数据后,会对其进行一些处理然后发送给客户端,对于聊天内容,服务端直接发送给客户端,而对于用户列表,便由json.dumps处理后发送
def sendData(): # 发送数据
while True:
if not messages.empty():
message = messages.get()
if isinstance(message[1], str):
for i in range(len(users)):
data = ' ' + message[1]
# send:将string中的数据发送到连接的套接字,返回值是要发送的字节数量
users[i][1].send(data.encode())
print(data)
print('\n')
if isinstance(message[1], list):
data = json.dumps(message[1])
for i in range(len(users)):
# noinspection PyBroadException
try:
# send:将string中的数据发送到连接的套接字,返回值是要发送的字节数量
users[i][1].send(data.encode())
except:
pass
② 客户端
def send():
message = entryIuput.get() + '~' + user + '~' + chat
s.send(message.encode())
INPUT.set('')
def receive():
global uses
while True:
# noinspection PyBroadException
try:
data = s.recv(1024)
data = data.decode()
print(data)
# noinspection PyBroadException
try:
uses = json.loads(data)
listbox1.delete(0, tkinter.END)
listbox1.insert(tkinter.END, "当前在线用户")
listbox1.insert(tkinter.END, "------Group chat-------")
for x in range(len(uses)):
listbox1.insert(tkinter.END, uses[x])
users.append('------Group chat-------')
except:
data = data.split('~')
message = data[0]
userName = data[1]
chatwith = data[2]
message = '\n' + message
if chatwith == '------Group chat-------': # 群聊
if userName == user:
listbox.insert(tkinter.END, message)
else:
listbox.insert(tkinter.END, message)
elif userName == user or chatwith == user: # 私聊
if userName == user:
listbox.tag_config('tag2', foreground='red')
listbox.insert(tkinter.END, message, 'tag2')
else:
listbox.tag_config('tag3', foreground='green')
listbox.insert(tkinter.END, message, 'tag3')
listbox.see(tkinter.END)
except:
pass
4、实验结果与分析
(1)运行结果
① 登录界面(输入IP地址和用户名)
② 聊天界面(登录后界面)
③ 群聊示例(输入格式:message)
④ 私聊示例(输入格式:message+username+chatwith)
⑤ 服务器的开启与退出
⑥ 演示视频
网络聊天程序
(2)实验分析
首先运行服务器端,创建ChatServer对象,构造函数创建Thread线程,并通过调用socket.socket()方法加载socket库,并创建socket,同时开始接收来自客户端的登录信息;运行客户端,分别输入IP地址和用户名,按下登录按钮后调用send()方法将user数据送至服务器端,并通过connect()方法向服务器请求连接。服务器端通过recv()方法接收信息,验证后通过Online()方法更新用户列表,调用accept()方法接受请求,三个客户端A、B、C进入聊天界面。
客户端在输入框内输入消息,调用send()方法发送给服务器端。服务端通过recv()方法接受数据后,会对其进行处理然后发送给客户端,对于聊天内容,服务端直接发送给客户端;而对于用户列表,便由json.dumps来处理后发送。
若发送的消息中只有消息内容(即消息格式为message),此时客户端识别chatwith== '------Group chat-------',同时将消息显示在所有客户端的聊天界面上;而发送的消息包含指向信息或私聊(即消息格式为message+username+chatwith),客户端会将消息按’~’进行分片,分别提取出message、username和chatwith,此时客户端分别识别username == user和chatwith == user,并按对应的字体颜色分别显示在对应的聊天界面上。
5、小结与心得体会
通过学习编写网络聊天程序,对Socket编程有了初步的了解,熟悉了TCP服务的各个常用系统调用阶段,并借此学习了Python中Socket库中的常用方法调用以及tkinter库提供的界面,通过学习基本的服务器端和客户端的通信,进而扩展学习了多个客户端之间的通信,并通过编写客户端的条件判断结构实现了程序的私聊功能。
6、完整代码
(1)Client1.py、Client2.py、Client3.py
import socket
import tkinter
import tkinter.messagebox
import threading
import json
import tkinter.filedialog
from tkinter.scrolledtext import ScrolledText
IP = ''
PORT = ''
user = ''
listbox1 = '' # 用于显示在线用户的列表框
show = 1 # 用于判断是开还是关闭列表框
users = [] # 在线用户列表
chat = '------Group chat-------' # 聊天对象
# 登陆窗口
root0 = tkinter.Tk()
root0.geometry("300x150")
root0.title('用户登陆窗口')
root0.resizable(0, 0)
one = tkinter.Label(root0, width=300, height=150, bg="LightBlue")
one.pack()
IP0 = tkinter.StringVar()
IP0.set('')
USER = tkinter.StringVar()
USER.set('')
labelIP = tkinter.Label(root0, text='IP地址', bg="LightBlue")
labelIP.place(x=20, y=20, width=100, height=40)
entryIP = tkinter.Entry(root0, width=60, textvariable=IP0)
entryIP.place(x=120, y=25, width=100, height=30)
labelUSER = tkinter.Label(root0, text='用户名', bg="LightBlue")
labelUSER.place(x=20, y=70, width=100, height=40)
entryUSER = tkinter.Entry(root0, width=60, textvariable=USER)
entryUSER.place(x=120, y=75, width=100, height=30)
def Login():
global IP, PORT, user
IP, PORT = entryIP.get().split(':')
user = entryUSER.get()
if not user:
tkinter.messagebox.showwarning('warning', message='用户名为空!')
else:
root0.destroy()
loginButton = tkinter.Button(root0, text="登录", command=Login, bg="Yellow")
loginButton.place(x=135, y=110, width=40, height=25)
root0.bind('<Return>', Login)
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
root0.destroy()
exit()
root0.protocol("WM_DELETE_WINDOW", Exit)
root0.mainloop()
# 建立连接
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# connect:连接到address处的套接字,一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误
s.connect((IP, int(PORT)))
if user:
s.send(user.encode()) # 发送用户名
else:
s.send('用户名不存在'.encode())
user = IP + ':' + PORT
# 聊天窗口
root1 = tkinter.Tk()
root1.geometry("640x480")
root1.title('67xChat')
root1.resizable(0, 0)
# 消息界面
listbox = ScrolledText(root1)
listbox.place(x=5, y=0, width=640, height=320)
listbox.tag_config('tag1', foreground='red', background="Yellow")
listbox.insert(tkinter.END, '欢迎进入群聊,大家开始聊天吧!', 'tag1')
INPUT = tkinter.StringVar()
INPUT.set('')
entryIuput = tkinter.Entry(root1, width=120, textvariable=INPUT)
entryIuput.place(x=5, y=320, width=580, height=170)
# 在线用户列表
listbox1 = tkinter.Listbox(root1)
listbox1.place(x=510, y=0, width=130, height=320)
def send():
message = entryIuput.get() + '~' + user + '~' + chat
s.send(message.encode())
INPUT.set('')
sendButton = tkinter.Button(root1, text="\n发\n\n\n送", anchor='n', command=send, font=('Helvetica', 18),
bg='LightBlue')
sendButton.place(x=585, y=320, width=55, height=300)
root1.bind('<Return>', send)
def receive():
global uses
while True:
# noinspection PyBroadException
try:
data = s.recv(1024)
data = data.decode()
print(data)
# noinspection PyBroadException
try:
uses = json.loads(data)
listbox1.delete(0, tkinter.END)
listbox1.insert(tkinter.END, "当前在线用户")
listbox1.insert(tkinter.END, "------Group chat-------")
for x in range(len(uses)):
listbox1.insert(tkinter.END, uses[x])
users.append('------Group chat-------')
except:
data = data.split('~')
message = data[0]
userName = data[1]
chatwith = data[2]
message = '\n' + message
if chatwith == '------Group chat-------': # 群聊
if userName == user:
listbox.insert(tkinter.END, message)
else:
listbox.insert(tkinter.END, message)
elif userName == user or chatwith == user: # 私聊
if userName == user:
listbox.tag_config('tag2', foreground='red')
listbox.insert(tkinter.END, message, 'tag2')
else:
listbox.tag_config('tag3', foreground='green')
listbox.insert(tkinter.END, message, 'tag3')
listbox.see(tkinter.END)
except:
pass
r = threading.Thread(target=receive)
r.setDaemon(True)
r.start() # 开始线程接收信息
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
tkinter.messagebox.showinfo("提示", "退出成功!")
root1.destroy()
s.close()
exit()
root1.protocol("WM_DELETE_WINDOW", Exit)
root1.mainloop()
(2)Server.py
import socket
import threading
import queue
import json # json.dumps(some)打包 json.loads(some)解包
import os
import os.path
import sys
import tkinter
import tkinter.messagebox
IP = '127.0.0.1'
PORT = 8000 # 端口
messages = queue.Queue()
users = [] # 0:userName 1:connection
lock = threading.Lock()
def Onlines(): # 统计当前在线人员
online = []
for i in range(len(users)):
online.append(users[i][0])
return online
def Load(data, addr):
# 获取锁
# 当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止
lock.acquire()
try:
messages.put((addr, data))
finally:
# 释放锁
# 获得锁的线程用完后一定要释放锁lock.release(),否则等待锁的线程将永远等待下去
lock.release()
# 接受来自客户端的用户名,如果用户名为空,使用用户的IP与端口作为用户名,如果用户名出现重复,则在出现的用户名依此加上后缀"2","3","4"……
def receive(conn, addr): # 接收消息
# recv:接受套接字的数据,数据以字符串形式返回,bufsize指定最多可以接收的数量,flag提供有关消息的其他信息,通常可以忽略
user = conn.recv(1024) # 用户名称
user = user.decode()
if user == '用户名不存在':
user = addr[0] + ':' + str(addr[1])
tag = 1
temp = user
for i in range(len(users)): # 检验重名,则在重名用户后加数字
if users[i][0] == user:
tag = tag + 1
user = temp + str(tag)
users.append((user, conn))
USERS = Onlines()
Load(USERS, addr)
# 在获取用户名后便会不断地接受用户端发来的消息(即聊天内容),结束后关闭连接
# noinspection PyBroadException
try:
while True:
# 将地址与数据(需发送给客户端)存入messages队列
# recv:接受套接字的数据,数据以字符串形式返回,bufsize指定最多可以接收的数量,flag提供有关消息的其他信息,通常可以忽略
message = conn.recv(1024) # 发送消息
message = message.decode()
message = user + ':' + message
Load(message, addr)
# close:关闭套接字
conn.close()
# 如果用户断开连接,将该用户从用户列表中删除,然后更新用户列表
except:
j = 0 # 用户断开连接
for man in users:
if man[0] == user:
users.pop(j) # 服务器端删除退出的用户
break
j = j + 1
USERS = Onlines()
Load(USERS, addr)
conn.close()
# 服务端在接受到数据后,会对其进行一些处理然后发送给客户端,对于聊天内容,服务端直接发送给客户端,而对于用户列表,便由json.dumps处理后发送
def sendData(): # 发送数据
while True:
if not messages.empty():
message = messages.get()
if isinstance(message[1], str):
for i in range(len(users)):
data = ' ' + message[1]
# send:将string中的数据发送到连接的套接字,返回值是要发送的字节数量
users[i][1].send(data.encode())
print(data)
print('\n')
if isinstance(message[1], list):
data = json.dumps(message[1])
for i in range(len(users)):
# noinspection PyBroadException
try:
# send:将string中的数据发送到连接的套接字,返回值是要发送的字节数量
users[i][1].send(data.encode())
except:
pass
class ChatServer(threading.Thread):
global users, que, lock
def __init__(self): # 构造函数
threading.Thread.__init__(self)
self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
os.chdir(sys.path[0])
def run(self):
# bind:将套接字绑定到地址,address地址的格式取决于地址族,在AF_INET下,以元组(host, port)的形式表示地址
self.s.bind((IP, PORT))
'''
开始监听传入连接,backlog指定在拒绝连接之前,可以挂起的最大连接数量
backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5
这个值不能无限大,因为要在内核中维护连接队列
'''
self.s.listen(5)
q = threading.Thread(target=sendData)
q.start()
while True:
# accept:接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据,address是连接客户端的地址
conn, addr = self.s.accept()
t = threading.Thread(target=receive, args=(conn, addr))
t.start()
self.s.close()
def Start():
tkinter.messagebox.showinfo("提示", "启动成功!")
server = ChatServer()
server.setDaemon(True)
server.start()
root = tkinter.Tk()
root.geometry("200x100")
root.title("67x")
root.resizable(False, False)
one = tkinter.Label(root, width=200, height=100, background="LightBlue")
one.pack()
startButton = tkinter.Button(root, text="START", command=Start, background="yellow")
startButton.place(x=50, y=10, width=100, height=35)
startButton.bind('<Return>', Start)
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
tkinter.messagebox.showinfo("提示", "退出成功!")
root.destroy()
exit(0)
root.protocol("WM_DELETE_WINDOW", Exit)
exitButton = tkinter.Button(root, text="EXIT",
command=lambda: (tkinter.messagebox.showinfo("提示", "退出成功!"), root.destroy(), exit(0)),
background="Red")
exitButton.place(x=50, y=50, width=100, height=35)
exitButton.bind('<Return>', lambda: (tkinter.messagebox.showinfo("提示", "退出成功!"), root.destroy(), exit(0)))
root.mainloop()
二、Tracert与Ping程序设计与实现
1、实验目的
- 了解Tracert程序的实现原理并调试通过;
- 学习Ping的基本原理,了解ICMP报文,并在此基础上编写Ping程序,测试两台主机之间的连通性。
2、总体设计
(1)背景知识
① Tracert的工作原理
Tracert 命令用 IP 生存时间 (TTL) 字段和 ICMP 错误消息来确定从一个主机到网络上其他主机的路由。
首先,Tracert送出一个TTL=1的IP 数据报到目的地,当路径上的第一个路由器收到这个数据报时,将TTL减1。此时TTL变为0,所以该路由器会将此数据报丢掉,并送回一个“ICMP time exceeded”消息(包括发IP包的源地址、IP包的所有内容及路由器的IP地址),Tracert 收到这个消息后便知道这个路由器存在于这个路径上,接着Tracert 再送出另一个TTL=2 的数据报,以此类推,Tracert 每次将送出的数据报的TTL 加1来发现下一个路由器,且每对应每一个TTL值,源主机都要发送3次同样的IP数据包这个重复的动作,一直持续到某数据报成功抵达目的地后,该主机则不会送回“ICMP time exceeded”消息,由于Tracert通过UDP数据报向不常见的端口(30000以上)发送了数据报,因此将会收到“ICMP port unreachable”消息,故可判断到达目的地。
Tracert 有一个固定的时间等待响应(ICMP TTL到期消息)。如果这个时间过了,它将打印出一系列的*号表明:在这个路径上,这个设备不能在给定的时间内发出ICMP TTL到期消息的响应。然后,Tracert给TTL记数器加1,继续进行(默认是最多30跳结束)。
以控制台执行"tracert www.baidu.com"为例说明:
使用Wireshark软件查看ICMP回送请求和回送回答报文:
使用Wireshark软件查看ICMP超时差错报告报文:
② Ping的工作原理
简单来说,Ping 是基于 ICMP 协议(Internet Control Message Protocol,Internet 控制报文协议)来工作的。Ping首先会发送一份ICMP回送请求报文给目标主机,等待目标主机返回ICMP回送回答报文。由于ICMP协议要求目标主机收到消息之后,必须返回ICMP回送回答报文给源主机,因此如果源主机在一定时间内收到了目标主机的应答,则表明两台主机之间网络是可达的。
以控制台执行"ping www.baidu.com"为例说明:
使用Wireshark软件查看ICMP回送请求和回送回答报文:
(2)模块介绍
Tracert程序主要分为三个部分,分别是:
① 计算校验和
- 把校验和字段置为0;
- 将ICMP包以16位为一组,并将所有组进行二进制求和;
- 若高16位不为0,则将高16位与低16位反复相加,直到高16位的值为0,从而获得一个只有16位长度的值;
- 将此16位值进行按位求反操作,将所得值替换到校验和字段。
② 测试连接
设置超时时间,使用struct模块创建一个ICMP_ECHO_REQUEST数据报,将查验请求的数据发往目的地址。在未到达超时时间之前socket处于阻塞状态等待响应,当有数据传回时就接受响应,然后提取包含ID的ICMP报文首部和ICMP内容,根据请求响应的延时与超时时间对比和路由情况给定返回值。
③ 跟踪路由
设置TTL的初始值和最大值,获取远程主机的DNS主机名和数据包类型,Tracert 每次将送出的数据报的TTL 加1来发现下一个路由器,一直持续到某个数据报成功抵达目的地,退出程序。
Ping程序主要分为三个部分,分别是:
- 检验校验和(同Tracert程序)
- 发送Ping数据报文:获取远程主机的DNS主机名,使用struct模块创建ICMP_ECHO_REQUEST数据报,将回送请求数据报发送到目标主机(在发送前也需要进行检验校验和)。
- 接收ping数据报文:在未到达超时时间之前socket处于阻塞状态一直等待响应,当有数据传回时就接受响应,然后提取包含标识符ID的ICMP报文首部和包含发送时间值的ICMP内容部分,计算请求响应的延时间隔。
3、详细设计
(1)程序流程图
(2)关键代码
① Tracert.py
def calculate_checksum(packet):
checksum = 0
for i in range(0, len(packet), 2):
word = packet[i] + (packet[i + 1] << 8)
checksum = checksum + word
overflow = checksum >> 16
while overflow > 0:
checksum = checksum & 0xFFFF
checksum = checksum + overflow
overflow = checksum >> 16
overflow = checksum >> 16
while overflow > 0:
checksum = checksum & 0xFFFF
checksum = checksum + overflow
overflow = checksum >> 16
checksum = ~checksum
checksum = checksum & 0xFFFF
return checksum
def send_ping(ttl, destination_address, Socket):
timeout = 1
temp_header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, 0, 0, 1)
checksum = calculate_checksum(temp_header)
main_header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, checksum, 0, 1)
Socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl)
Socket.sendto(main_header, (destination_address, 33434))
if not select.select([Socket], [], [], timeout)[0]:
print("%02d 连接超时" % ttl)
return False
IP = Socket.recvfrom(1024)[1][0]
print("%02d IP:" % ttl, IP)
if IP == destination_address:
return True
return False
def tracert(host):
ttl = 1
max_ttl = 30
destination_address = socket.gethostbyname(host)
icmp_protocol = socket.getprotobyname("icmp")
while ttl <= max_ttl:
Socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp_protocol)
if send_ping(ttl, destination_address, Socket):
Socket.close()
break
ttl += 1
Socket.close()
sys.exit()
② Ping.py
def do_checksum(source_string):
sum = 0
max_count = (len(source_string) / 2) * 2
count = 0
while count < max_count:
val = source_string[count + 1] * 256 + source_string[count]
sum = sum + val
sum = sum & 0xffffffff
count = count + 2
if max_count < len(source_string):
sum = sum + ord(source_string[len(source_string) - 1])
sum = sum & 0xffffffff
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
answer = ~sum
answer = answer & 0xffff
answer = answer >> 8 | (answer << 8 & 0xff00)
return answer
def send_ping(self, sk, ID):
target_addr = socket.gethostbyname(self.target_host)
my_checksum = 0
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1)
bytes_In_double = struct.calcsize("d")
data = (192 - bytes_In_double) * "R"
data = struct.pack("d", time.time()) + bytes(data.encode('utf-8'))
my_checksum = do_checksum(header + data)
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1)
packet = header + data
sk.sendto(packet, (target_addr, 1))
def ping_once(self):
global sock
icmp = socket.getprotobyname("icmp")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
except socket.error as e:
if e.errno == 1:
# Not superuser, so operation not permitted
e.msg += "ICMP 消息只能从根用户进程发送"
raise socket.error(e.msg)
except Exception as e:
print("Exception: %s" % e)
my_ID = os.getpid() & 0xFFFF
self.send_ping(sock, my_ID)
delay = receive_ping(sock, my_ID, self.timeout)
sock.close()
return delay
def receive_ping(sk, ID, timeout):
time_remaining = timeout
while True:
start_time = time.time()
readable = select.select([sk], [], [], time_remaining)
time_spent = (time.time() - start_time)
if not readable[0]: # 超时
return
time_received = time.time()
recv_packet, addr = sk.recvfrom(1024)
icmp_header = recv_packet[20:28]
type, code, checksum, packet_ID, sequence = struct.unpack("bbHHh", icmp_header)
if packet_ID == ID:
bytes_In_double = struct.calcsize("d")
time_sent = struct.unpack("d", recv_packet[28:28 + bytes_In_double])[0]
return time_received - time_sent
time_remaining = time_remaining - time_spent
if time_remaining <= 0:
return
def ping(self):
for i in range(self.count):
try:
delay = self.ping_once()
except socket.gaierror:
return -2
print("Ping failed. (socket error: '%s')" % e[1])
break
if delay is None:
return -2
print("Ping failed. (timeout within %ssec.)" % self.timeout)
else:
delay = delay * 1000
return delay
4、实验结果与分析
(1)运行结果
① Tracert.py
以tracert("www.baidu.com")为例
跟踪结果与控制台命令结果对比一致
② Ping.py
③ 演示视频
Tracert与Ping程序
(2)实验分析
① Tracert.py
首先tracert("www.baidu.com")将路由追踪的目标地址送入send_ping()方法,并设置超时时间,使用struct模块创建一个ICMP_ECHO_REQUEST类型的数据报,将查验请求的数据发往目的地址,计算校验和。在未到达超时时间之前socket将处于阻塞状态等待响应,当有数据传回时就接受响应,然后提取包含ID的ICMP报文首部和ICMP内容,根据请求响应的延时与超时时间对比和路由情况给定返回值:如果响应时间超过设定的超时时间,则显示超时信息,反之则显示当前追踪到的路由器IP地址,并返回False并继续追踪;如果当前追踪的IP地址与目的IP地址相同,即有IP = destination_address,则返回True并停止追踪。
② Ping.py
首先使用tkinter库设计并绘制UI,输入待ping的IP地址范围、超时时间和线程数,之后对给定IP地址范围的所有IP地址依次执行三次ping()方法,主要过程与Tracert程序类似,本机向目的IP地址发送ICMP回送请求报文,并接收对应的ICMP回送回答和超时差错报告报文,将平均时延或超时信息显示在UI上。
5、小结与心得体会
通过学习Tracert程序原理,对路由追踪的过程有了初步了解, 同时通过学习Ping的基本原理,了解了ICMP报文的类型和发送过程,并借此学习了Python中Socket库中的常用方法调用以及tkinter库提供的界面,同时在此基础上编写了Ping程序,完成了测试不同主机之间的连通性的任务。
6、完整代码
(1)Tracert.py
import socket
import struct
import sys
import select
ICMP_ECHO_REQUEST = 8
def calculate_checksum(packet):
checksum = 0
for i in range(0, len(packet), 2):
word = packet[i] + (packet[i + 1] << 8)
checksum = checksum + word
overflow = checksum >> 16
while overflow > 0:
checksum = checksum & 0xFFFF
checksum = checksum + overflow
overflow = checksum >> 16
overflow = checksum >> 16
while overflow > 0:
checksum = checksum & 0xFFFF
checksum = checksum + overflow
overflow = checksum >> 16
checksum = ~checksum
checksum = checksum & 0xFFFF
return checksum
def send_ping(ttl, destination_address, Socket):
timeout = 1
temp_header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, 0, 0, 1)
checksum = calculate_checksum(temp_header)
main_header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, checksum, 0, 1)
Socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl)
Socket.sendto(main_header, (destination_address, 33434))
if not select.select([Socket], [], [], timeout)[0]:
print("%02d 连接超时" % ttl)
return False
IP = Socket.recvfrom(1024)[1][0]
print("%02d IP:" % ttl, IP)
if IP == destination_address:
return True
return False
def tracert(host):
ttl = 1
max_ttl = 30
destination_address = socket.gethostbyname(host)
icmp_protocol = socket.getprotobyname("icmp")
while ttl <= max_ttl:
Socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp_protocol)
if send_ping(ttl, destination_address, Socket):
Socket.close()
break
ttl += 1
Socket.close()
sys.exit()
if __name__ == "__main__":
tracert("www.baidu.com")
(2)Ping.py
import os
import socket
import struct
import threading
import time
import tkinter
import tkinter.messagebox
import select
from concurrent.futures.thread import ThreadPoolExecutor
from tkinter import *
from tkinter import ttk
ICMP_ECHO_REQUEST = 8
DEFAULT_TIMEOUT = 2
DEFAULT_COUNT = 1
# 创建锁
lock = threading.Lock()
Running = True
def do_checksum(source_string):
sum = 0
max_count = (len(source_string) / 2) * 2
count = 0
while count < max_count:
val = source_string[count + 1] * 256 + source_string[count]
sum = sum + val
sum = sum & 0xffffffff
count = count + 2
if max_count < len(source_string):
sum = sum + ord(source_string[len(source_string) - 1])
sum = sum & 0xffffffff
sum = (sum >> 16) + (sum & 0xffff)
sum = sum + (sum >> 16)
answer = ~sum
answer = answer & 0xffff
answer = answer >> 8 | (answer << 8 & 0xff00)
return answer
def receive_ping(sk, ID, timeout):
time_remaining = timeout
while True:
start_time = time.time()
readable = select.select([sk], [], [], time_remaining)
time_spent = (time.time() - start_time)
if not readable[0]: # 超时
return
time_received = time.time()
recv_packet, addr = sk.recvfrom(1024)
icmp_header = recv_packet[20:28]
type, code, checksum, packet_ID, sequence = struct.unpack("bbHHh", icmp_header)
if packet_ID == ID:
bytes_In_double = struct.calcsize("d")
time_sent = struct.unpack("d", recv_packet[28:28 + bytes_In_double])[0]
return time_received - time_sent
time_remaining = time_remaining - time_spent
if time_remaining <= 0:
return
class Pinger(object):
def __init__(self, target_host, count=DEFAULT_COUNT, timeout=DEFAULT_TIMEOUT):
self.target_host = target_host
self.count = count
self.timeout = timeout
def send_ping(self, sk, ID):
target_addr = socket.gethostbyname(self.target_host)
my_checksum = 0
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1)
bytes_In_double = struct.calcsize("d")
data = (192 - bytes_In_double) * "R"
data = struct.pack("d", time.time()) + bytes(data.encode('utf-8'))
my_checksum = do_checksum(header + data)
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1)
packet = header + data
sk.sendto(packet, (target_addr, 1))
def ping_once(self):
"""在超时时返回延迟(s)或 none"""
global sock
icmp = socket.getprotobyname("icmp")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
except socket.error as e:
if e.errno == 1:
# Not superuser, so operation not permitted
e.msg += "ICMP 消息只能从根用户进程发送"
raise socket.error(e.msg)
except Exception as e:
print("Exception: %s" % e)
my_ID = os.getpid() & 0xFFFF
self.send_ping(sock, my_ID)
delay = receive_ping(sock, my_ID, self.timeout)
sock.close()
return delay
def ping(self):
for i in range(self.count):
# print("Ping to %s..." % self.target_host)
try:
delay = self.ping_once()
except socket.gaierror:
return -2
print("Ping failed. (socket error: '%s')" % e[1])
break
if delay is None:
# return self.timeout
return -2
print("Ping failed. (timeout within %ssec.)" % self.timeout)
else:
delay = delay * 1000
# print("Get ping in %0.4fms" % delay)
return delay
def closePool():
global Running
Running = False
print("OFF")
def clearTb():
x = tb.get_children()
for item in x:
tb.delete(item)
numlb['text'] = 0
def insertRes():
global Running
Running = True
print("ON")
clearTb()
timeout = int(t6.get()) / 1000
threadNum = int(t7.get())
# print(threadNum)
# print(timeout)
start = int(t4.get())
end = int(t5.get())
pool = ThreadPoolExecutor(threadNum)
for i in range(start, end):
ip = t1.get() + '.' + t2.get() + '.' + t3.get() + '.' + str(i)
pool.submit(run, ip, i, timeout)
def run(ipStart, i, timeout):
global Running
if not Running:
return
ping = Pinger(ipStart, timeout=timeout)
res1 = ping.ping()
res2 = ping.ping()
res3 = ping.ping()
if res1 == -2 or res2 == -2 or res3 == -2:
tb.insert("", i, value=(ipStart, "超时或错误"))
else:
res = abs(res1 + res2 + res3) / 3
res = str(round(res, 4)) + 'ms'
tb.insert("", i, value=(ipStart, res))
lock.acquire()
num = int(numlb['text'])
num += 1
numlb['text'] = num
lock.release()
if __name__ == '__main__':
root = Tk()
root.title("Ping")
root.geometry("720x320")
root.resizable(False, False)
frame1 = Frame(root)
frame1.pack()
lb1 = Label(frame1, text=' IP地址:', font=20)
lb1.pack(side=LEFT)
t1 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t1.pack(side=LEFT)
t1.insert(0, 192)
lb2 = Label(frame1, text='.', font=20)
lb2.pack(side=LEFT)
t2 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t2.pack(side=LEFT)
t2.insert(0, 168)
lb3 = Label(frame1, text='.', font=20)
lb3.pack(side=LEFT)
t3 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t3.pack(side=LEFT)
t3.insert(0, 31)
lb3 = Label(frame1, text='从', font=20)
lb3.pack(side=LEFT)
t4 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t4.pack(side=LEFT)
t4.insert(0, 0)
lb3 = Label(frame1, text='到', font=20)
lb3.pack(side=LEFT)
t5 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t5.pack(side=LEFT)
t5.insert(0, 255)
lb3 = Label(frame1, text=' 超时:', font=20)
lb3.pack(side=LEFT)
t6 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t6.pack(side=LEFT)
t6.insert(0, 2000)
lb3 = Label(frame1, text=' 线程:', font=20)
lb3.pack(side=LEFT)
t7 = Entry(frame1, font=20, textvariable=IntVar, width=6)
t7.pack(side=LEFT)
t7.insert(0, 50)
frame2 = Frame(root, width=720, height=200)
frame2.pack()
scroll1 = Scrollbar(frame2)
scroll1.pack(side=RIGHT, fill=Y)
tb = ttk.Treeview(frame2, yscrollcommand=scroll1.set, show="headings", height=12)
scroll1.config(command=tb.yview)
tb['columns'] = ("ip", "time")
tb.column("ip", width=320, anchor='center')
tb.column("time", width=320, anchor='center')
tb.heading("ip", text='ip')
tb.heading('time', text='状态')
tb.pack()
frame3 = Frame(root, bg='gray')
frame3.pack(fill=X)
btn1 = ttk.Button(frame3, text="开始", command=insertRes)
btn1.pack(side=LEFT)
btn2 = ttk.Button(frame3, text='结束', command=closePool)
btn2.pack(side=LEFT)
btn3 = ttk.Button(frame3, text="清空", command=clearTb)
btn3.pack(side=LEFT)
numlb = Label(frame3, text='0')
numlb.pack(side=RIGHT)
lb = Label(frame3, text="响应数:")
lb.pack(side=RIGHT)
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
tkinter.messagebox.showinfo("提示", "退出成功!")
root.destroy()
exit()
root.protocol("WM_DELETE_WINDOW", Exit)
root.mainloop()
三、Wireshark的使用与网络分析
1、实验目的
- 熟悉并掌握Wireshark的基本使用;
- 了解网络协议实体间进行交互以及报文交换的情况;
- 分析以太网帧、ARP协议、IP协议、UDP和TCP的结构。
2、总体设计
(1)背景知识
① Wireshark简介
Wireshark是一种可以运行在Windows, UNIX, Linux等操作系统上的分组嗅探器,是一个开源免费软件。
运行Wireshark程序时,最初各窗口中并无数据显示。Wireshark的界面主要有五个组成部分:
- 命令菜单(command menus):命令菜单位于窗口的最顶部,是标准的下拉式菜单。
- 协议筛选框(display filter specification):在该处填写某种协议的名称,Wireshark据此对分组列表窗口中的分组进行过滤,只显示你需要的分组。
- 捕获分组列表(listing of captured packets):按行显示已被捕获的分组内容,其中包括:分组序号、捕获时间、源地址和目的地址、协议类型、协议信息说明。单击某一列的列名,可以使分组列表按指定列排序。其中,协议类型是发送或接收分组的最高层协议的类型。
- 分组首部明细(details of selected packet header):显示捕获分组列表窗口中被选中分组的首部详细信息。包括该分组的各个层次的首部信息,需要查看哪层信息,双击对应层次或单击该层最前面的“+”即可。
- 分组内容窗口(packet content):分别以十六进制和ASCII码两种格式显示被捕获帧的完整内容。
② 以太网的MAC帧结构
在以太网链路上的数据包称作以太帧。以太帧起始部分由前导码和帧开始符组成。后面紧跟着一个以太网报头,以MAC地址说明目的地址和源地址。帧的中部是该帧负载的包含其他协议报头的数据包(例如IP协议)。以太帧由一个32位冗余校验码结尾。它用于检验数据传输是否出现损坏。
各字段的含义与作用如下:
- 前同步码:第一个字段是7个字节的前同步码,1和0交替,作用是用来使接收端的适配器在接收MAC帧时能够迅速调整时钟频率,使它和发送端的频率相同。
- 帧开始定界符:第二个字段是1个字节的帧开始定界符,前六位1和0交替,最后的两个连续1表示告诉接收端适配器帧信息要来了,准备接收。
- MAC 目的地址:第三个字段是6字节(占48位),发送方的网卡(MAC)地址,当网卡接收到数据帧时,首先会检查该帧的目的地址是否与当前适配器的物理地址相同,如果相同则进一步处理,不同则直接丢弃。
- 源MAC地址:发送端的MAC地址同样占6个字节。
- 类型:该字段在网络协议栈分解中及其重要,考虑当PDU(协议数据单元)来到某一层时,它需要将PDU交付给上层,而上层协议众多,所以在处理数据的时候,必须要一个字段标识这个交付给谁。
- 数据:数据也叫有效载荷,除过当前层协议需要使用的字段外,即需要交付给上层的数据,以太网帧数据长度规定最小为46字节,最大为1500字节,如果有不到46字节时,会用填充字节填充到最小长度。最大值也叫最大传输单元(MTU)。
- 帧检验序列FCS(使用CRC校验法):检测该帧是否出现差错。
③ ARP协议
各字段的含义与作用如下:
- 硬件类型:16位字段,用来定义运行ARP的网络类型。每个局域网基于其类型被指派一个整数;
- 协议类型:16位字段,用来定义使用的协议。可用于任何高层协议;
- 硬件长度:8位字段,用来定义物理地址的长度,以字节为单位;
- 协议长度:8位字段,用来定义逻辑地址的长度,以字节为单位;
- 操作码:16位字段,用来定义报文的类型。已定义的分组类型有两种:ARP请求(1),ARP响应(2);
- 源硬件地址:这是一个可变长度字段,用来定义发送方的物理地址;
- 源逻辑地址:这是一个可变长度字段,用来定义发送方的IP地址;
- 目的硬件地址:这是一个可变长度字段,用来定义目标的物理地址;
- 目的逻辑地址:这是一个可变长度字段,用来定义目标的IP地址。
④ IP协议
- 版本号:指定IP协议的版本(IPv4/IPv6);
- 首部长度:表示IP报头的长度,以4字节为单位;
- 服务类型:3位优先权字段,4位TOS字段,和1位保留字段。4位TOS分别表示:最小延时,最大吞吐量,最高可靠性,最小成本。四者相互冲突,只能选择一个;
- 总长度:IP报文的总长度,用于将各个IP报文进行分离;
- 标识:唯一的标识主机发送的报文,如果数据在IP层进行了分片,那么每一个分片对应的id都相同;
- 标志字段:第一位保留,表示暂时没有规定该字段的意义。第二位表示禁止分片,表示如果报文长度超过MTU,IP模块就会丢弃该报文。第三位表示更多分片,如果报文没有进行分片,则该字段设置为0,如果报文进行了分片,则除了最后一个分片报文设置为0以外,其余分片报文均设置为1;
- 片偏移:分片相对原始数据开始处的偏移,表示当前分片在原数据中的偏移位置,实际偏移的字节数是这个值× 8 \times 8×8得到的。除了最后一个报文,其他报文长度必须是8的整数倍,否则报文会不连续;
- 生存时间:数据报到达目的地的最大报文跳数,一般是64,每经过一个路由,TTL 减1,减到0还没到达就丢弃,主要是用来防止出现路由循环;
- 协议:表示上层协议的类型;
- 首部检验和:使用CRC进行校验,来鉴别数据报的首部是否损坏,但不检验数据部分;
- 源IP地址和目的IP地址:表示发送端和接收端所对应的IP地址;
- 选项字段:不定长,最多40字节;
⑤ UDP和TCP协议
UDP首部格式各字段意义如下:
- 源端口:源端口号。在需要对方回信时选用。不需要时可用全0;
- 目的端口:目的端口号。这在终点交付报文时必须使用;
- 长度:UDP用户数据报的长度,其最小值是8(仅有首部);
- 检验和:检测UDP用户数据报在传输中是否有错。有错就丢弃。
TCP首部格式各字段意义如下:
- 源端口和目的端口:各占2个字节,分别写入源端口号和目的端口号。和UDP的分用相似,TCP的分用功能也是通过端口实现的;
- 序号:占4字节。序号范围是[0,232-1],共232个序号。序号增加到232-1后,下一个序号就又回到0。也就是说,序号使用mod 232运算。TCP是面向字节流的。在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号;
- 确认号:占4字节,是期望收到对方下个报文段的首个数据字节的序号;
- 数据偏移:占4位,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远,实际上是指出TCP报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。注意,“数据偏移”的单位是32位字(即以4字节长的字为计算单位)。由于4位二进制数能够表示的最大十进制数字是15,因此数据偏移的最大值是60字节,这也是TCP首部的最大长度(即选项长度不能超过40字节);
- 保留:占6位,保留为今后使用,但目前应置为0;
- 窗口:占2字节。窗口值是[0,216-1]之间的整数。窗口指的是发送本报文段的一方的接收窗口(而不是自己的发送窗口)。窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。之所以要有这个限制,是因为接收方的数据缓存空间是有限的;
- 检验和:占2字节。检验和字段检验的范围包括首部和数据这两部分。在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用IPv6,则相应的伪首部也要改变;
- 紧急指针:占2字节。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据;
- 选项:长度可变,最长可达40字节。当没有使用“选项”时,TCP的首部长度是20字节。
(2)设计步骤
- Wireshark工具的基本使用方法
- 以太网的帧结构
- ARP协议分组结构
- IP协议分组结构
- UDP与TCP的报头结构
3、详细设计
(1)Wireshark工具的基本使用方法
- 下载Wireshark抓包分析工具并安装;
- 查看本机的IP地址和MAC地址,在命令行窗口输入"ipconfig -all",查看本机MAC地址和IP地址;
- 启动Wireshark软件,选择准备捕获数据包的网卡接口;
- 等待片刻后停止捕获,观察捕获到的数据包;
- 设置过滤条件,查看本机IP地址发出的数据包,使用"ip.src"为过滤规则,在数据包详情窗口查看源MAC地址是否与本机的MAC地址;
- 重新设置过滤条件,选择查看由本机MAC地址接收的数据包,使用"eth.dst"为过滤条件,在数据包详情窗口查看目的IP地址是否是本机IP地址;
(2)以太网的帧结构
- 查看本机的IP地址和MAC地址,在命令行窗口输入"ipconfig -all",查看本机MAC地址和IP地址,之后输入"arp-d"清理ARP缓存;
- 启动Wireshark软件,选择准备捕获数据包的网卡接口,捕获过程中访问学校网站"www.hnust.edu.cn",一段时间后停止捕获,观察捕获到的数据包;
- 使用"ip.dst"为过滤条件设置过滤数据包,查看捕获数据包的数据链路层帧结构以及网络层报头IP地址并记录;
- 使用"eth.dst"为过滤条件设置过滤广播帧,查看捕获数据包的数据链路层帧结构以及网络层报头IP地址并记录;
(3)ARP协议分组结构
- 查看本机的IP地址和MAC地址,在命令行窗口输入"ipconfig -all",查看本机MAC地址和IP地址,并记录本机网关IP地址;
- 查看ARP工具的参数,在命令行窗口输入"arp -a",查看本地的ARP缓存表并记录;
- 在命令行窗口输入"arp -d",删除本地的ARP缓存表,之后再输入"arp -a",查看ARP缓存表的变化情况,然后输入"ping 网关地址",再查看ARP缓存表的变化情况,据此分析ARP缓存表的工作模式;
- 启动Wireshark软件,开始进行捕获,捕获前或捕获中在命令行窗口输入"arp -d",删除本地的ARP缓存表,之后再输入"ping 网关地址",一段时间后停止捕获,观察捕获到的数据包;
- 设置过滤条件过滤从本机MAC地址发出的ARP数据包,查看捕获数据包的数据链路层帧结构及ARP协议分组结构并记录。
4、实验结果与分析
(1)Wireshark工具的基本使用方法
① 查看本机的I地址和MAC地址
② 启动Wireshark软件,选择准备捕获数据包的网卡接口
③ 等待片刻后停止捕获,观察捕获到的数据包
④ 查看本机IP地址发出的数据包
⑤ 查看本机MAC地址接收的数据包
※ 思考题:
① 每次发出或接收的数据包,本地IP地址和MAC地址是否总是对应的?
答:不是,以下图为例:
数据包在发送或接收的过程,源IP地址与目的IP地址始终保持不变,而MAC地址在经由路由器于局域网上转发时一直变化。
② 尝试写一条规则,查看所有的HTTP协议的数据包。
答:"http",如下图所示。
(2)以太网的帧结构
① 查看本机的IP地址和MAC地址并清理ARP缓存
② 启动Wireshark软件,选择准备捕获数据包的网卡接口,捕获过程中访问学校网站"www.hnust.edu.cn",一段时间后停止捕获,观察捕获到的数据包
③ 使用"ip.dst"为过滤条件设置过滤数据包,查看捕获数据包的数据链路层帧结构以及网络层报头IP地址并记录
※ 思考题:
(i)从IP地址来看,这个数据包是从哪一台主机发往哪一台主机的?
答:从IP-192.168.31.12发往IP-218.75.230.30。
(ii)试分析目的MAC地址和目的IP地址是否对应同一主机?
答:数据包发送过程中目的IP地址始终保持不变,而数据包经过路由器时,MAC帧首部中目的地址发送变化,在数据链路层要丢弃原MAC帧的首部和尾部,转发时重新添加上MAC帧的首部和尾部。因此有在目的IP地址与源IP地址处于同一网段时,目的MAC地址和目的IP地址对应同一主机;而目的IP地址与源IP地址处于不同网段时,目的MAC地址实际上是网关的MAC地址。
(iii)试分析源MAC地址和源IP地址是否对应同一主机?
答:分析同上一题,只在目的IP地址与源IP地址处于同一网段时,源MAC地址和源IP地址对应同一主机;而目的IP地址与源IP地址处于不同网段时,源MAC地址实际上是网关的MAC地址。
④ 使用"eth.dst"为过滤条件设置过滤广播帧,查看捕获数据包的数据链路层帧结构以及网络层报头IP地址并记录
※ 思考题:
① 从IP地址来看,这个数据包是从哪一台主机发往哪一台主机的?
答:IP-192.168.31.12→IP-192.168.31.255。
② 从MAC地址来看,这个数据包是从哪一台主机发往哪一台主机的?
答:MAC-54:05:db:6d:fd:c1→MAC- ff:ff:ff:ff:ff:ff。
③ 试分析广播帧所起的作用是什么?
答:广播帧是发送给本局域网上所有站点的全1地址的帧,其作用是让所有收到该广播帧的主机都接收并处理这个帧,提高了通信效率。但发生广播帧会产生大量流量,降低带宽利用率,影响网络性能。
④ 为什么在捕获的数据包中看不到以太网的帧尾?帧尾在何时被处理了?
答:Wireshark捕获的数据包是由网卡接收到的,首先对帧检验序列FCS进行计算,并与帧尾FCS进行对比,若一致则接收,反之则丢弃。Wireshark捕获到的是FCS校验通过的帧,帧尾FCS被硬件删去,并且Wireshark不会捕获FCS校验失败的帧,因此看不到以太网帧尾。
(3)ARP协议分组结构
① 查看本机MAC地址和IP地址,并记录本机网关IP地址
② 查看本地的ARP缓存表
③ 删除本地的ARP缓存表后查看ARP缓存表的变化情况,然后输入"ping 网关地址"后查看ARP缓存表变化情况,据此分析ARP缓存表的工作模式
删除本地的ARP缓存表后,ARP变少了;"ping 网关地址"后ARP增加。
ARP缓存表工作模式如下:
- 主机A发送数据给主机B,主机A首先会检查自己的ARP缓存表,查看是否有主机B的IP地址和MAC地址的对应关系,如果有,则会将主机B的MAC地址作为源MAC地址封装到数据帧中;如果没有,主机A则会发送一个ARP请求信息,请求的目标IP地址是主机B的IP地址,目标MAC地址是MAC地址的广播帧(即FF:FF:FF:FF:FF:FF),源IP地址和MAC地址是主机A的IP地址和MAC地址;
- 当交换机接受到此数据帧之后,发现此数据帧是广播帧,因此,会将此数据帧从非接收的所有接口发送出去;
- 当主机B接受到此数据帧后,会校对IP地址是否是自己的,并将主机A的IP地址和MAC地址的对应关系记录到自己的ARP缓存表中,同时会发送一个ARP应答,其中包括自己的MAC地址;
- 主机A在收到这个回应的数据帧之后,在自己的ARP缓存表中记录主机B的IP地址和MAC地址的对应关系。而此时交换机已经学习到了主机A和主机B的MAC地址。
④ 启动Wireshark软件,捕获前或捕获中删除本地的ARP缓存表,之后再输入"ping 网关地址",一段时间后停止捕获,观察捕获到的数据包
⑤ 设置过滤条件过滤从本机MAC地址发出的ARP数据包,查看捕获数据包的数据链路层帧结构及ARP协议分组结构并记录
※ 思考题:
① 从帧头中的MAC地址来看这个数据帧是谁发给谁的?
答: 由本机MAC-54:05:db:6d:fd:c1发给局域网上的所有主机。
② ARP分组结构中的硬件类型、上层协议类型、操作类型分别有什么作用?
答: ARP分组结构中的三种类型(均为2个字节)如下:
- 硬件类型:表明ARP分组是跑在什么类型的网络上的;
- 协议类型:表明使用ARP分组的上层协议是什么类型;
- 操作类型:表明该ARP分组的类型。
③ ARP分组结构中的目的MAC地址是多少?为什么是这个取值?
答:以太网首部的目的MAC地址为ff:ff:ff:ff:ff:ff,表示广播,而ARP分组结构中的目的MAC地址为00:00:00:00:00:00,起到填充的作用。
④ 试分析这个ARP分组的作用是什么?
答: 该ARP请求包含目标主机的IP地址,当前局域网内所有主机都会收到,在数据链路层都会收到然后处理交给上层,目的主机收到广播的ARP请求,若发现包含的IP地址与本机IP地址相符合,则向源主机发一个ARP应答,将自己的MAC地址写到应答包中,而其他主机接收后发现与自己的IP地址不符合后就会直接丢弃。
5、小结与心得体会
通过学习Wireshark的使用与网络分析,对Wireshark工具的基本使用方法、以太网的帧结构、ARP协议分组结构、IP协议分组结构、UDP与TCP的报头结构有了基本的了解,掌握了通过控制台使用各种网络分析指令,如"ping"、"ipconfig"、"arp"等,对各类数据包的分组转发过程有了一定的了解。
四、网络嗅探器的设计与实现
1、实验目的
通过学习原始套接字的工作原理和规则,了解各层报文的首部结构,据此设计一个可以实时监视网络状态、数据流动情况以及网络上传输的信息的网络嗅探器。
2、总体设计
(1)背景知识
① 原始套接字
raw socket是一种不同于SOCK_STREAM和SOCK_DGRAM的套接字,实现于系统核心,创建方式与TCP/UDP的创建方法类似,可以接收本机网卡上的数据帧或者数据包,对于监听网络的流量和分析有作用。
raw socket的功能与TCP或者UDP类型socket的功能有很大的不同:TCP/UDP类型的套接字只能够访问传输层以及传输层以上的数据,因为当IP层把数据传递给传输层时,下层的数据包首部已经被丢掉了。而原始套接字却可以访问传输层以下的数据,所以使用raw socket可以实现上至应用层的数据操作,也可以实现下至链路层的数据操作。
Python中有如下几种方式创建raw socket:
- socket(AF_INET,SOCK_RAW,IPPROTO)
IPPROTO=IPPROTO_TCP or IPPROTO_UDP or IPPROTO_ICMP,该套接字可以接收协议类型为TCP、UDP、ICMP等发往本机的IP数据包、不能收到非发往本地IP的数据包(IP软过滤会丢弃这些不是发往本机IP的数据包)、不能收到从本机发送出去的数据包。发送时需要自己组织TCP、UDP、ICMP等首部、可以调用setsockopt()方法来包装IP首部,适用于ping程序
- socket(AF_PACKET, SOCK_RAW, htons(ETH_P))
ETH_P=ETH_P_IP or ETH_P_ARP or ETH_P_ALL,创建这种套接字可以监听网卡上的所有数据帧。其中有:
ETH_P_IP 0x800 只接收发往本机MAC的IP类型的数据帧;
ETH_P_ARP 0x806 只接受发往本机MAC的ARP类型的数据帧;
ETH_P_RARP 0x8035 只接受发往本机MAC的RARP类型的数据帧;
ETH_P_ALL 0x3 接收发往本机MAC的所有类型IP、ARP、RARP的数据帧,接收从本机发出的所有类型的数据帧(混杂模式打开的情况下,会接收到非发往本地MAC的数据帧)。
- socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P))
ETH_P=ETH_P_IP or ETH_P_ARP or ETH_P_ALL,功能与上一种功能类似,但是不包括以太网首部,可以接收非IP协议的数据包。
- socket(AF_INET, SOCK_PACKET, htons(ETH_P))
ETH_P=ETH_P_IP or ETH_P_ARP or ETH_P_ALL,一般用于抓包程序。
raw socket是直接使用IP协议的非面向连接的socket,可以调用bind()和connect()方法进行地址绑定:
- socket.bind(address)
将套接字绑定到地址。在AF_INET下,以元组(HOST, POST)形式表示地址。调用bind()方法后,发送数据包的源IP地址将是bind函数指定的地址。如果不调用bind()方法,则内核将以发送接口的主IP地址填充IP头。如果使用setsockopt()方法设置了IP_HDRINCL(headerincluding)选项,就必须手工填充每个要发送的数据包的源IP地址,否则,内核将自动创建IP首部。
- socket.connect(address)
连接到address的套接字。一般address的格式为元组(HOST, POST),如果连接出错,返回socket.error错误。调用connect()方法后,就可以使用write()方法和send()方法来发送数据包,而且内核将会用这个绑定的地址填充IP数据包的目的IP地址,否则应使用sendto()方法或sendmsg()方法来发送数据包,并且要在函数参数中指定对方的IP地址。
② 网络嗅探器
(i)原理
网络嗅探器(Sniffer)利用的是共享式的网络传输介质。共享即意味着网络中的一台机器可以嗅探到传递给本网段(冲突域)中的所有机器的报文。例如最常见的以太网就是一种共享式的网络技术,以太网卡收到报文后,通过对目的地址进行检查,来判断是否是传递给自己的,若是则把报文传递给操作系统,否则将报文丢弃不进行处理。网络嗅探器通过将网卡设置为混杂模式来实现对网络的嗅探。
一个实际的主机系统中的数据收发是由网卡来完成的,当网卡接收到传输来的数据包时,网卡内的单片程序首先解析数据包的目的网卡物理地址,然后根据网卡驱动程序设置的接收模式判断该不该接收,认为该接收就产生中断信号通知CPU,认为不该接收就丢掉数据包,所以不该接收的数据包就被网卡截断了,上层应用根本就不知道这个过程。如果CPU得到了网卡的中断信号,则根据网卡的驱动程序设置的网卡中断程序地址调用驱动程序接收数据,并将接收的数据交给上层协议软件外理。
(ii)网卡的四种接收模式
- 广播模式:该模式下的网卡能够接收网络中的广播信息;
- 组播模式:设置在该模式下的网卡能够接收组播数据;
- 直接模式:该模式下只有目的网卡才能接收该数据;
- 混杂模式:该模式下的网卡能够接收一切通过它的数据,而不管该数据是否是传给它的。
(2)模块介绍
程序总共分为五个部分,分别是:
- 解析MAC首部;
- 解析TCP首部;
- 解析UDP首部;
- 解析IP首部;
- 解析ICMP首部。
(3)设计步骤
- 创建raw socket套接字;
- 通过调用socket.bind()方法将raw socket绑定到对应的网卡上;
- 通过调用socket.ioctl()方法将网卡设置为混杂模式;
- 通过调用socket.recvfrom()方法接受数据;
- 解析数据,并通过调用tkinter库的方法绘制UI并输出。
3、详细设计
(1)程序流程图
(2)关键代码
def main(count):
……
try:
……
num = 1
while True:
data, addr = s.recvfrom(65535)
Sourse_IP = socket.inet_ntoa(struct.unpack('!BBHHHBBH4s4s', data[0:20])[8])
Destination_IP = socket.inet_ntoa(struct.unpack('!BBHHHBBH4s4s', data[0:20])[9])
p = struct.unpack('!BBHHHBBH4s4s', data[0:20])[6]
Protocol = "TCP" if p == 6 else "ICMP" if p == 1 else "UDP" if p == 17 else ""
if not Protocol:
continue
if num > int(count):
print()
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
s.close()
return
elif Sourse_IP == src_IP or Destination_IP == dst_IP or Protocol == PROTOCOL or flag:
tree.insert("", num, text="",
values=(str(num).rjust(10), Sourse_IP.rjust(15), Destination_IP.
rjust(15), Protocol))
print("[{}]".format(num))
mac_len = parse_mac(data)
ip_len, pro = parse_ip(data)
if pro == 6:
parse_tcp(data, ip_len)
elif pro == 1:
parse_icmp(data, ip_len)
elif pro == 17:
parse_udp(data, mac_len + ip_len)
host = addr[0]
activeDegree[host] = activeDegree.get(host, 0) + 1
num += 1
else:
num += 1
except Exception as e:
print(e)
def parse_mac(raw_buffer):
eth_length = 14
eth_header = raw_buffer[:eth_length]
eth = struct.unpack('!6s6sH', eth_header)
eth_protocol = socket.ntohs(eth[2])
print(……)
return eth_length
def parse_tcp(raw_buffer, iph_length):
tcp_header = raw_buffer[iph_length: iph_length + 20]
tcph = struct.unpack('!HHLLBBHHH', tcp_header)
source_port = tcph[0]
dest_port = tcph[1]
sequence = tcph[2]
acknowledgement = tcph[3]
doff_reserved = tcph[4]
tcph_length = doff_reserved >> 4
print(……)
def parse_udp(raw_buffer, idx):
udph_length = 8
udp_header = raw_buffer[idx: idx + udph_length]
udph = struct.unpack('!HHHH', udp_header)
source_port = udph[0]
dest_port = udph[1]
length = udph[2]
checksum = udph[3]
print(……)
def parse_ip(raw_buffer):
ip_header = raw_buffer[0:20]
iph = struct.unpack('!BBHHHBBH4s4s', ip_header)
version_ihl = iph[0]
version = version_ihl >> 4
ihl = version_ihl & 0xF
iph_length = ihl * 4
ttl = iph[5]
protocol = iph[6]
s_addr = socket.inet_ntoa(iph[8])
d_addr = socket.inet_ntoa(iph[9])
print(……)
return iph_length, protocol
def parse_icmp(raw_buffer, iph_length):
buf = raw_buffer[iph_length: iph_length + ctypes.sizeof(ICMP)]
icmp_header = ICMP(buf)
print(……)
4、实验结果与分析
(1)运行结果
① 不给出任何信息,弹出提示框
② 只给出监听次数(100),默认监听所有数据报
③ 给出源IP地址(192.168.31.12)和监听次数(100)
④ 给出目的IP地址(192.168.31.12)和监听次数(100)
⑤ 给出数据包类型(TCP)和监听次数(100)
⑥ 演示视频
网络嗅探器
(2)实验分析
首先调用tkinter库绘制UI,然后获取过滤条件,通过变量num进行计数,然后Sourse_IP== src_IP or Destination_IP==dst_IP or Protocol==PROTOCOL后调用parse_mac()、parse_tcp()、parse_udp()、parse_ip()、parse_icmp()方法获取数据报的各项信息并回显在UI上。
5、小结与心得体会
通过学习编写网络嗅探器程序,学习了raw socket的工作原理和规则,掌握了网络嗅探器的基本原理,对socket相关方法实现网络嗅探的流程有了一定的了解,同时参考了raw socket编程的示例,设计了一个可以监视网络状态、数据流动情况以及网络上传输的信息的网络嗅探器,并能支持过滤条件操作。
6、完整代码
import ctypes
import socket
import struct
import threading
import tkinter.filedialog
import tkinter.messagebox
from tkinter import ttk
activeDegree = dict()
HOST = "192.168.31.12"
flag_thread = False
body = tkinter.Tk()
body.geometry("720x480")
body.title("Sniffer")
body.resizable(0, 0)
one = tkinter.Label(body, width=640, height=480, bg="LightBlue")
one.pack()
Sourse_IP_address = tkinter.StringVar()
Sourse_IP_address.set("")
Destination_IP_address = tkinter.StringVar()
Destination_IP_address.set("")
Protocol = tkinter.StringVar()
Protocol.set("")
COUNT_number = tkinter.IntVar()
COUNT_number.set("")
label_Sourse_IP_address = tkinter.Label(body, text='源IP地址', background='LightBlue')
label_Sourse_IP_address.place(x=20, y=10, width=100, height=40)
entry_Sourse_IP_address = tkinter.Entry(body, width=60, textvariable=Sourse_IP_address)
entry_Sourse_IP_address.place(x=110, y=15, width=120, height=30)
label_Destination_IP_address = tkinter.Label(body, text='目的IP地址', background='LightBlue')
label_Destination_IP_address.place(x=20, y=50, width=100, height=40)
entry_Destination_IP_address = tkinter.Entry(body, width=60, textvariable=Destination_IP_address)
entry_Destination_IP_address.place(x=110, y=55, width=120, height=30)
label_Protocol = tkinter.Label(body, text='数据报类型', background='LightBlue')
label_Protocol.place(x=250, y=10, width=100, height=40)
# entry_Protocol = tkinter.Entry(body, width=60, textvariable=Protocol)
# entry_Protocol.place(x=340, y=15, width=120, height=30)
combobox_Protocol = ttk.Combobox(body, textvariable=Protocol, values=("", "ICMP", "TCP", "UDP"))
combobox_Protocol.current(0)
combobox_Protocol.configure(state='readonly')
combobox_Protocol.place(x=340, y=15, width=120, height=30)
label_COUNT_number = tkinter.Label(body, text='监听次数', background='LightBlue')
label_COUNT_number.place(x=250, y=50, width=100, height=40)
entry_COUNT_number = tkinter.Entry(body, width=60, textvariable=COUNT_number)
entry_COUNT_number.place(x=340, y=55, width=120, height=30)
frame = tkinter.Frame(body)
frame.place(x=20, y=100, width=680, height=360)
scrollbar = ttk.Scrollbar(frame)
scrollbar.pack(side="right", fill="y")
columns = ["No.", "Sourse_IP", "Destination_IP", "Protocol"]
tree = ttk.Treeview(frame, show="headings", columns=columns, yscrollcommand=scrollbar.set)
scrollbar.config(command=tree.yview)
tree.column("No.", width=30, anchor="center")
tree.column("Sourse_IP", width=100, anchor="center")
tree.column("Destination_IP", width=100, anchor="center")
tree.column("Protocol", width=50, anchor="center")
tree.heading("No.", text="No.")
tree.heading("Sourse_IP", text="Sourse_IP")
tree.heading("Destination_IP", text="Destination_IP")
tree.heading("Protocol", text="Protocol")
tree.place(x=0, y=0, width=660, height=360)
def treeview_sort_column(treeview, column, reverse):
line = [(treeview.set(k, column), k) for k in treeview.get_children()]
line.sort(reverse=reverse)
for index, (val, k) in enumerate(line):
treeview.move(k, '', index)
treeview.heading(column, command=lambda: treeview_sort_column(treeview, column, not reverse))
for col in columns:
tree.heading(col, text=col, command=lambda _col=col: treeview_sort_column(tree, _col, False))
def main(count):
global activeDegree, HOST, flag_thread
Items = tree.get_children()
for item in Items:
tree.delete(item)
src_IP = entry_Sourse_IP_address.get()
dst_IP = entry_Destination_IP_address.get()
PROTOCOL = combobox_Protocol.get()
if not count:
tkinter.messagebox.showinfo("提示", "请输入监听次数!")
return
if not src_IP and not dst_IP and not PROTOCOL:
flag = True
else:
flag = False
try:
print("HOST: ", HOST)
print("COUNT: ", count)
# 创建原始套接字
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
# 服务端套接字地址绑定
s.bind((HOST, 0))
# 设置在捕获数据报中含有IP报头
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
# 启用混杂模式,捕获所有数据报
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
flag_thread = True
# 开始捕获数据报
num = 1
cnt = 0
while True:
if not flag_thread:
print()
break
data, addr = s.recvfrom(65535)
# Sourse_MAC = eth_addr(data[0:6])
# Destination_MAC = eth_addr(data[6:12])
Sourse_IP = socket.inet_ntoa(struct.unpack('!BBHHHBBH4s4s', data[0:20])[8])
Destination_IP = socket.inet_ntoa(struct.unpack('!BBHHHBBH4s4s', data[0:20])[9])
p = struct.unpack('!BBHHHBBH4s4s', data[0:20])[6]
Protocol = "TCP" if p == 6 else "ICMP" if p == 1 else "UDP" if p == 17 else ""
if not Protocol:
continue
if num > int(count):
# 关闭混杂模式
print()
flag_thread = False
if cnt:
tkinter.messagebox.showinfo("提示", "捕获完成!")
else:
tkinter.messagebox.showinfo("提示", "未捕获到数据报!")
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
s.close()
return
elif Sourse_IP == src_IP or Destination_IP == dst_IP or Protocol == PROTOCOL or flag:
tree.insert("", num, text="",
values=(str(num).rjust(10), Sourse_IP.rjust(15), Destination_IP.rjust(15), Protocol))
cnt += 1
print("[{}]".format(num))
mac_len = parse_mac(data)
ip_len, pro = parse_ip(data)
if pro == 6:
parse_tcp(data, ip_len)
elif pro == 1:
parse_icmp(data, ip_len)
# if len(data) - mac_len - ip_len >= 8:
elif pro == 17:
parse_udp(data, mac_len + ip_len)
# print('mac: ', mac)
# print('get addr', addr)
host = addr[0]
activeDegree[host] = activeDegree.get(host, 0) + 1
# if addr[0] != HOST:
# print(addr[0])
num += 1
else:
num += 1
except Exception as e:
print(e)
def parse_mac(raw_buffer):
eth_length = 14
eth_header = raw_buffer[:eth_length]
eth = struct.unpack('!6s6sH', eth_header)
eth_protocol = socket.ntohs(eth[2])
print('Ethernet II => Source MAC : ' + eth_addr(raw_buffer[6:12]) +
' Destination MAC : ' + eth_addr(raw_buffer[0:6]) + ' Protocol : ' + str(eth_protocol))
# print('P->13/14: ' + str(eth_protocol))
return eth_length
def eth_addr(a):
b = "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" % (a[0], a[1], a[2], a[3], a[4], a[5])
return b
def parse_tcp(raw_buffer, iph_length):
tcp_header = raw_buffer[iph_length: iph_length + 20]
tcph = struct.unpack('!HHLLBBHHH', tcp_header)
source_port = tcph[0]
dest_port = tcph[1]
sequence = tcph[2]
acknowledgement = tcph[3]
doff_reserved = tcph[4]
tcph_length = doff_reserved >> 4
print(('TCP => Source Port: {source_port}, Dest Port: {dest_port} '
'Sequence Number: {sequence} Acknowledgement: {acknowledgement} '
'TCP header length: {tcph_length}').format(
source_port=source_port, dest_port=dest_port,
sequence=sequence, acknowledgement=acknowledgement,
tcph_length=tcph_length
))
def parse_udp(raw_buffer, idx):
udph_length = 8
udp_header = raw_buffer[idx: idx + udph_length]
udph = struct.unpack('!HHHH', udp_header)
source_port = udph[0]
dest_port = udph[1]
length = udph[2]
checksum = udph[3]
print(('UDP => Source Port: {source_port}, Dest Port: {dest_port} '
'Length: {length} CheckSum: {checksum}').format(
source_port=source_port, dest_port=dest_port,
length=length, checksum=checksum
))
def parse_ip(raw_buffer):
ip_header = raw_buffer[0:20]
iph = struct.unpack('!BBHHHBBH4s4s', ip_header)
version_ihl = iph[0]
version = version_ihl >> 4
ihl = version_ihl & 0xF
iph_length = ihl * 4
ttl = iph[5]
protocol = iph[6]
s_addr = socket.inet_ntoa(iph[8])
d_addr = socket.inet_ntoa(iph[9])
print(('IP => Version: {version}, Header Length: {header}, '
'TTL: {ttl}, Protocol: {protocol}, Source IP: {source}, '
'Destination IP: {destination}').format(
version=version, header=iph_length,
ttl=ttl, protocol=protocol, source=s_addr,
destination=d_addr
))
return iph_length, protocol
def parse_icmp(raw_buffer, iph_length):
buf = raw_buffer[iph_length: iph_length + ctypes.sizeof(ICMP)]
icmp_header = ICMP(buf)
print(('ICMP => Type:%d, Code: %d, CheckSum: %d'
% (icmp_header.type, icmp_header.code, icmp_header.checksum)))
class ICMP(ctypes.Structure):
"""ICMP 结构体"""
_fields_ = [
('type', ctypes.c_ubyte),
('code', ctypes.c_ubyte),
('checksum', ctypes.c_ushort),
('unused', ctypes.c_ushort),
('next_hop_mtu', ctypes.c_ushort)
]
def __new__(cls, socket_buffer):
return cls.from_buffer_copy(socket_buffer)
# noinspection PyMissingConstructor
def __init__(self, socket_buffer):
self.socket_buffer = socket_buffer
def Sniffer():
if flag_thread:
tkinter.messagebox.showinfo("提示", "当前正在捕获中!")
return
t = threading.Thread(target=main(count=entry_COUNT_number.get()))
t.start()
t.join()
# for item in activeDegree.items():
# print(item)
def thread_it(func, *args):
t = threading.Thread(target=func, args=args)
t.setDaemon(True)
t.start()
snifferButton = tkinter.Button(body, text="开始", command=lambda: thread_it(Sniffer), background="yellow")
snifferButton.place(x=520, y=15, width=70, height=30)
body.bind('<Return>', lambda: thread_it(Sniffer))
def Stop():
global flag_thread
if flag_thread:
flag_thread = False
tkinter.messagebox.showinfo("提示", "停止捕获!")
stopButton = tkinter.Button(body, text="终止", command=Stop, background="orange")
stopButton.place(x=620, y=15, width=70, height=30)
body.bind('<Return>', Stop)
def Reset():
global flag_thread
if flag_thread:
tkinter.messagebox.showinfo("提示", "当前正在捕获中!")
return
Items = tree.get_children()
for item in Items:
tree.delete(item)
entry_Sourse_IP_address.delete("0", "end")
entry_Destination_IP_address.delete("0", "end")
entry_COUNT_number.delete("0", "end")
combobox_Protocol.current(0)
resetButton = tkinter.Button(body, text="重置", command=Reset, background="white")
resetButton.place(x=520, y=55, width=70, height=30)
body.bind('<Return>', Reset)
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
tkinter.messagebox.showinfo("提示", "退出成功!")
body.destroy()
exit()
exitButton = tkinter.Button(body, text="退出", command=Exit, background="red")
exitButton.place(x=620, y=55, width=70, height=30)
body.bind('<Return>', Exit)
body.protocol("WM_DELETE_WINDOW", Exit)
body.mainloop()
五、网络报文分析程序的设计与实现
1、实验目的
通过学习网络嗅探器(Sniffer)程序的实现,了解各层报文的首部结构,并结合使用Wireshark软件观察网络各层报文捕获、解析和分析的过程,尝试编写出网络报文的解析程序。
2、总体设计
(1)背景知识(详见‘实验三→背景知识’的说明)
① 以太网MAC帧格式
② IP数据报格式
③ ICMP数据报格式
④ UDP数据报格式
⑤ TCP数据报格式
(2)模块介绍
程序总共分为三大部分,分别是:
- 设置捕获过滤条件;
- 解析各类数据报首部;
- 数据报信息显示UI。
(3)设计步骤
- 创建raw socket套接字;
- 通过调用socket.bind()方法将raw socket绑定到对应的网卡上;
- 通过调用socket.ioctl()方法将网卡设置为混杂模式;
- 通过调用socket.recvfrom()方法接受数据;
- 通过调用parse_mac()、parse_tcp()、parse_udp()、parse_ip()、parse_icmp()方法解析数据报;
- 通过调用tkinter库的方法绘制UI并控制输出。
3、详细设计
(1)程序流程图
(2)关键代码
# 获取表格中对应行的信息
def treeviewClick(_):
# noinspection PyBroadException
try:
item_text = []
for item in tree.selection():
item_text = tree.item(item, "values")
for m in Massage:
if m[0] == item_text[0]:
Ethernet_II.set(m[1])
IP.set(m[2])
Protocol_down.set(m[3])
except:
pass
tree.bind('<ButtonRelease-1>', treeviewClick)
# 显示/隐藏 Ethernet II
def Ethernet_II_Button():
global flag_Ethernet_II
if flag_Ethernet_II:
entry_Ethernet_II_null.place_forget()
entry_Ethernet_II.place(x=110, y=480, width=760, height=30)
else:
entry_Ethernet_II_null.place(x=110, y=480, width=760, height=30)
entry_Ethernet_II.place_forget()
flag_Ethernet_II = bool(1 - flag_Ethernet_II)
# 显示/隐藏 IP
def IP_Button():
global flag_IP
if flag_IP:
entry_IP_null.place_forget()
entry_IP.place(x=110, y=520, width=760, height=30)
else:
entry_IP_null.place(x=110, y=520, width=760, height=30)
entry_IP.place_forget()
flag_IP = bool(1 - flag_IP)
# 显示/隐藏 Protocol
def Protocol_down_Button():
global flag_Protocol
if flag_Protocol:
entry_Protocol_down_null.place_forget()
entry_Protocol_down.place(x=110, y=560, width=760, height=30)
else:
entry_Protocol_down_null.place(x=110, y=560, width=760, height=30)
entry_Protocol_down.place_forget()
flag_Protocol = bool(1 - flag_Protocol)
# 清空当前数据报信息
def MessageClean():
global flag_Ethernet_II, flag_IP, flag_Protocol
Ethernet_II.set("")
IP.set("")
Protocol_down.set("")
entry_Ethernet_II.delete("0", "end")
entry_IP.delete("0", "end")
entry_Protocol_down.delete("0", "end")
flag_Ethernet_II = False
flag_IP = False
flag_Protocol = False
Ethernet_II_Button()
IP_Button()
Protocol_down_Button()
4、实验结果与分析
(1)运行结果
① 不给出任何信息,弹出提示框
② 只给出监听次数(100),默认监听所有数据报
③ 给出源IP地址(192.168.31.12)和监听次数(100)
④ 给出目的IP地址(192.168.31.12)和监听次数(100)
⑤ 给出数据包类型(UDP)和监听次数(100)
⑥ 演示视频
网络报文分析程序
(2)实验分析
首先调用tkinter库绘制UI,然后获取过滤条件,通过变量num进行计数,然后Sourse_IP== src_IP or Destination_IP==dst_IP or Protocol==PROTOCOL后调用parse_mac()、parse_tcp()、parse_udp()、parse_ip()、parse_icmp()方法获取数据报的各项信息,并保存在Message二维列表中。点击选中表格上某行时,通过调用treeviewClick()方法,首先获取该行的序号,并与Message逐一对比,直到找到对应数据报信息,依次提取Message列表对应项的数据报信息,赋值给Ethernet_II、IP和Protocol_down("Protocol_down"为报文信息显示变量,而"combobox_Protocol_up"为过滤条件变量),之后便可通过点击Ethernet_II、IP和Protocol按钮,然后通过依次调用Ethernet_II_Button()、IP_Button()和Protocol_Button()方法将报文信息显示在UI上。当下一次执行开始捕获操作或重置UI操作时,程序会调用MessageClean()方法清除上一次捕获的所有信息,并准备开始接收下一次捕获的报文信息。
5、小结与心得体会
通过回顾网络嗅探器(Sniffer)程序的实现,同时了解各报文的首部结构,并结合使用Wireshark软件观察、捕获、解析和分析网络各层报文,同时通过大量搜索tkinter库的相关控件的使用注意事项,反复进行UI设计与排版,提高业务逻辑处理能力,并据此完成了网络报文分析程序的编程,实现了设置过滤条件捕获报文、显示报文详细信息、表格列排序等多个功能。
6、完整代码
import ctypes
import socket
import struct
import threading
import tkinter.messagebox
import tkinter.filedialog
from tkinter import ttk
activeDegree = dict()
# 本机IP地址
HOST = "192.168.31.12"
# 显示信息
Massage = []
msg = []
# 进度条
progressbar_p = 0
progressbar_max = 0
# 条件标志
flag_thread = False
flag_Ethernet_II = True
flag_IP = True
flag_Protocol = True
# UI界面
body = tkinter.Tk()
body.geometry("900x620")
body.title("Network_Message_Analyzer")
body.resizable(False, False)
one = tkinter.Label(body, width=640, height=480, background="LightBlue")
one.pack()
# 源IP地址
Sourse_IP_address = tkinter.StringVar()
Sourse_IP_address.set("")
# 目的IP地址
Destination_IP_address = tkinter.StringVar()
Destination_IP_address.set("")
# 数据报类型
Protocol_up = tkinter.StringVar()
Protocol_up.set("")
# 监听次数
COUNT_number = tkinter.IntVar()
COUNT_number.set("")
# 以太网帧信息
Ethernet_II = tkinter.StringVar()
Ethernet_II.set("")
# IP数据报信息
IP = tkinter.StringVar()
IP.set("")
# Protocol信息
Protocol_down = tkinter.StringVar()
Protocol_down.set("")
# 源IP地址标签
label_Sourse_IP_address = tkinter.Label(body, text='源IP地址', background='LightBlue')
label_Sourse_IP_address.place(x=20, y=10, width=100, height=40)
entry_Sourse_IP_address = tkinter.Entry(body, width=60, textvariable=Sourse_IP_address)
entry_Sourse_IP_address.place(x=110, y=15, width=220, height=30)
# 目的IP地址标签
label_Destination_IP_address = tkinter.Label(body, text='目的IP地址', background='LightBlue')
label_Destination_IP_address.place(x=20, y=50, width=100, height=40)
entry_Destination_IP_address = tkinter.Entry(body, width=60, textvariable=Destination_IP_address)
entry_Destination_IP_address.place(x=110, y=55, width=220, height=30)
# 数据报类型标签
label_Protocol_up = tkinter.Label(body, text='数据报类型', background='LightBlue')
label_Protocol_up.place(x=360, y=10, width=100, height=40)
combobox_Protocol_up = ttk.Combobox(body, textvariable=Protocol_up, values=("", "ICMP", "TCP", "UDP"))
combobox_Protocol_up.current(0)
combobox_Protocol_up.configure(state='readonly')
combobox_Protocol_up.place(x=450, y=15, width=220, height=30)
# 监听次数标签
label_COUNT_number = tkinter.Label(body, text='监听次数', background='LightBlue')
label_COUNT_number.place(x=360, y=50, width=100, height=40)
entry_COUNT_number = tkinter.Entry(body, width=60, textvariable=COUNT_number)
entry_COUNT_number.place(x=450, y=55, width=220, height=30)
# 表格界面
frame = tkinter.Frame(body)
frame.place(x=20, y=100, width=850, height=360)
# 滚动条
scrollbar = ttk.Scrollbar(frame)
scrollbar.pack(side="right", fill="y")
columns = ["No.", "Sourse_IP", "Destination_IP", "Protocol"]
tree = ttk.Treeview(frame, show="headings", columns=columns, yscrollcommand=scrollbar.set)
scrollbar.config(command=tree.yview)
# 设置表格列属性
tree.column("No.", width=30, anchor="center")
tree.column("Sourse_IP", width=100, anchor="center")
tree.column("Destination_IP", width=100, anchor="center")
tree.column("Protocol", width=50, anchor="center")
# 显示表格列属性
tree.heading("No.", text="No.")
tree.heading("Sourse_IP", text="Sourse_IP")
tree.heading("Destination_IP", text="Destination_IP")
tree.heading("Protocol", text="Protocol")
tree.place(x=0, y=0, width=830, height=360)
# 表格列排序
def treeview_sort_column(treeview, column, reverse):
line = [(treeview.set(k, column), k) for k in treeview.get_children()]
line.sort(reverse=reverse)
for index, (val, k) in enumerate(line):
treeview.move(k, '', index)
treeview.heading(column, command=lambda: treeview_sort_column(treeview, column, not reverse))
for col in columns:
tree.heading(col, text=col, command=lambda _col=col: treeview_sort_column(tree, _col, False))
# 获取表格中对应行的信息
def treeviewClick(_):
# noinspection PyBroadException
try:
item_text = []
for item in tree.selection():
item_text = tree.item(item, "values")
for m in Massage:
if m[0] == item_text[0]:
Ethernet_II.set(m[1])
IP.set(m[2])
Protocol_down.set(m[3])
except:
pass
tree.bind('<ButtonRelease-1>', treeviewClick)
# 解析进度条
def progressbar_loading():
global progressbar_p, progressbar_max
progressbar['value'] = progressbar_p
progressbar['maximum'] = progressbar_max
# 解析进度条标签
label_progressbar = tkinter.Label(body, text="解析进度条", background="lightBlue")
label_progressbar.place(x=20, y=465, width=80, height=30)
progressbar = ttk.Progressbar(body)
progressbar.place(x=110, y=470, width=760, height=20)
# 以太网帧信息(清空)
entry_Ethernet_II_null = tkinter.Entry(body, width=60, textvariable="")
entry_Ethernet_II_null.place(x=110, y=500, width=760, height=30)
entry_Ethernet_II_null.configure(state='readonly')
# IP数据报信息(清空)
entry_IP_null = tkinter.Entry(body, width=60, textvariable="")
entry_IP_null.place(x=110, y=540, width=760, height=30)
entry_IP_null.configure(state='readonly')
# Protocol信息(清空)
entry_Protocol_down_null = tkinter.Entry(body, width=60, textvariable="")
entry_Protocol_down_null.place(x=110, y=580, width=760, height=30)
entry_Protocol_down_null.configure(state='readonly')
# 以太网帧信息
entry_Ethernet_II = tkinter.Entry(body, width=60, textvariable=Ethernet_II)
entry_Ethernet_II.place(x=110, y=500, width=760, height=30)
entry_Ethernet_II.configure(state='readonly')
entry_Ethernet_II.place_forget()
# IP数据报信息
entry_IP = tkinter.Entry(body, width=60, textvariable=IP)
entry_IP.place(x=110, y=540, width=760, height=30)
entry_IP.configure(state='readonly')
entry_IP.place_forget()
# Protocol信息
entry_Protocol_down = tkinter.Entry(body, width=60, textvariable=Protocol_down)
entry_Protocol_down.place(x=110, y=580, width=760, height=30)
entry_Protocol_down.configure(state='readonly')
entry_Protocol_down.place_forget()
# 显示/隐藏Ethernet II
def Ethernet_II_Button():
global flag_Ethernet_II
if flag_Ethernet_II:
entry_Ethernet_II_null.place_forget()
entry_Ethernet_II.place(x=110, y=500, width=760, height=30)
else:
entry_Ethernet_II_null.place(x=110, y=500, width=760, height=30)
entry_Ethernet_II.place_forget()
flag_Ethernet_II = bool(1 - flag_Ethernet_II)
# 显示/隐藏Ethernet II按钮
Ethernet_II_button = tkinter.Button(body, text="Ethernet_II", command=Ethernet_II_Button, background="white")
Ethernet_II_button.place(x=20, y=500, width=80, height=30)
Ethernet_II_button.bind('<Return>', Ethernet_II_Button)
# 显示/隐藏IP
def IP_Button():
global flag_IP
if flag_IP:
entry_IP_null.place_forget()
entry_IP.place(x=110, y=540, width=760, height=30)
else:
entry_IP_null.place(x=110, y=540, width=760, height=30)
entry_IP.place_forget()
flag_IP = bool(1 - flag_IP)
# 显示/隐藏IP按钮
IP_button = tkinter.Button(body, text="IP", command=IP_Button, background="white")
IP_button.place(x=20, y=540, width=80, height=30)
IP_button.bind('<Return>', IP_Button)
# 显示/隐藏Protocol
def Protocol_down_Button():
global flag_Protocol
if flag_Protocol:
entry_Protocol_down_null.place_forget()
entry_Protocol_down.place(x=110, y=580, width=760, height=30)
else:
entry_Protocol_down_null.place(x=110, y=580, width=760, height=30)
entry_Protocol_down.place_forget()
flag_Protocol = bool(1 - flag_Protocol)
# 显示/隐藏Protocol按钮
Protocol_down_button = tkinter.Button(body, text="Protocol", command=Protocol_down_Button, background="white")
Protocol_down_button.place(x=20, y=580, width=80, height=30)
Protocol_down_button.bind('<Return>', Protocol_down_Button)
# 清空当前数据报信息
def MessageClean():
global flag_Ethernet_II, flag_IP, flag_Protocol
Ethernet_II.set("")
IP.set("")
Protocol_down.set("")
entry_Ethernet_II.delete("0", "end")
entry_IP.delete("0", "end")
entry_Protocol_down.delete("0", "end")
flag_Ethernet_II = False
flag_IP = False
flag_Protocol = False
Ethernet_II_Button()
IP_Button()
Protocol_down_Button()
# 解析数据报
def main(count):
global activeDegree, HOST, Massage, msg, progressbar_p, progressbar_max, flag_thread
# 获取过滤条件
Items = tree.get_children()
for item in Items:
tree.delete(item)
src_IP = entry_Sourse_IP_address.get()
dst_IP = entry_Destination_IP_address.get()
PROTOCOL = combobox_Protocol_up.get()
# 未输入监听次数
if not count:
tkinter.messagebox.showinfo("提示", "请输入监听次数!")
return
# 默认情况下接收所有数据报
if not src_IP and not dst_IP and not PROTOCOL:
flag = True
else:
flag = False
try:
print("HOST: ", HOST)
print("COUNT: ", count)
# 创建原始套接字
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
# 绑定原始套接字
s.bind((HOST, 0))
# 设置捕获含有IP报头的数据报
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)
# 设置混杂模式
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
# 解析开始
flag_thread = True
# 初始化进度条
progressbar_p = 0
progressbar_max = int(count)
Massage = []
# 设定初始序号
num = 1
while True:
# 加载进度条
progressbar_loading()
# 检测是否终止程序
if not flag_thread:
print()
break
data, addr = s.recvfrom(65535)
# 获取填入表格的信息
Sourse_IP = socket.inet_ntoa(struct.unpack('!BBHHHBBH4s4s', data[0:20])[8])
Destination_IP = socket.inet_ntoa(struct.unpack('!BBHHHBBH4s4s', data[0:20])[9])
p = struct.unpack('!BBHHHBBH4s4s', data[0:20])[6]
Protocol = "ICMP" if p == 1 else "TCP" if p == 6 else "UDP" if p == 17 else ""
if not Protocol:
continue
# 达到监听次数
if num > int(count):
print()
flag_thread = False
if len(Massage):
tkinter.messagebox.showinfo("提示", "解析完成!")
else:
tkinter.messagebox.showinfo("提示", "未解析到数据报!")
s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
s.close()
return
# 过滤
elif Sourse_IP == src_IP or Destination_IP == dst_IP or Protocol == PROTOCOL or flag:
msg = []
tree.insert("", num, text="",
values=(str(num).rjust(10), Sourse_IP.rjust(15), Destination_IP.rjust(15), Protocol))
msg.append(str(num).rjust(10))
print("[{}]".format(num))
mac_len = parse_mac(data)
ip_len, pro = parse_ip(data)
if pro == 6:
parse_tcp(data, ip_len)
elif pro == 1:
parse_icmp(data, ip_len)
elif pro == 17:
parse_udp(data, mac_len + ip_len)
Massage.append(msg)
host = addr[0]
activeDegree[host] = activeDegree.get(host, 0) + 1
num += 1
else:
num += 1
# 进度自增
progressbar_p = num - 1
except Exception as e:
print(e)
# 解析MAC帧
def parse_mac(raw_buffer):
global msg
eth_length = 14
eth_header = raw_buffer[:eth_length]
eth = struct.unpack('!6s6sH', eth_header)
eth_protocol = socket.ntohs(eth[2])
msg.append('Source MAC : ' + eth_addr(raw_buffer[6:12]) +
' Destination MAC : ' + eth_addr(raw_buffer[0:6]) + ' Protocol : ' + str(eth_protocol))
print('Ethernet II => Source MAC : ' + eth_addr(raw_buffer[6:12]) +
' Destination MAC : ' + eth_addr(raw_buffer[0:6]) + ' Protocol : ' + str(eth_protocol))
return eth_length
# 解码
def eth_addr(a):
b = "%.2x:%.2x:%.2x:%.2x:%.2x:%.2x" % (a[0], a[1], a[2], a[3], a[4], a[5])
return b
# 解析IP数据报
def parse_ip(raw_buffer):
global msg
ip_header = raw_buffer[0:20]
iph = struct.unpack('!BBHHHBBH4s4s', ip_header)
version_ihl = iph[0]
version = version_ihl >> 4
ihl = version_ihl & 0xF
iph_length = ihl * 4
ttl = iph[5]
protocol = iph[6]
s_addr = socket.inet_ntoa(iph[8])
d_addr = socket.inet_ntoa(iph[9])
msg.append(('Version: {version}, Header Length: {header}, '
'TTL: {ttl}, Protocol: {protocol}, Source IP: {source}, '
'Destination IP: {destination}').format(
version=version, header=iph_length,
ttl=ttl, protocol=protocol, source=s_addr,
destination=d_addr
))
print(('IP => Version: {version}, Header Length: {header}, '
'TTL: {ttl}, Protocol: {protocol}, Source IP: {source}, '
'Destination IP: {destination}').format(
version=version, header=iph_length,
ttl=ttl, protocol=protocol, source=s_addr,
destination=d_addr
))
return iph_length, protocol
# ICMP结构体
class ICMP(ctypes.Structure):
_fields_ = [
('type', ctypes.c_ubyte),
('code', ctypes.c_ubyte),
('checksum', ctypes.c_ushort),
('unused', ctypes.c_ushort),
('next_hop_mtu', ctypes.c_ushort)
]
def __new__(cls, socket_buffer):
return cls.from_buffer_copy(socket_buffer)
# noinspection PyMissingConstructor
def __init__(self, socket_buffer):
self.socket_buffer = socket_buffer
# 解析ICMP数据报
def parse_icmp(raw_buffer, iph_length):
global msg
buf = raw_buffer[iph_length: iph_length + ctypes.sizeof(ICMP)]
icmp_header = ICMP(buf)
msg.append(('ICMP => Type:%d, Code: %d, CheckSum: %d'
% (icmp_header.type, icmp_header.code, icmp_header.checksum)))
print(('ICMP => Type:%d, Code: %d, CheckSum: %d'
% (icmp_header.type, icmp_header.code, icmp_header.checksum)))
# 解析TCP数据报
def parse_tcp(raw_buffer, iph_length):
global msg
tcp_header = raw_buffer[iph_length: iph_length + 20]
tcph = struct.unpack('!HHLLBBHHH', tcp_header)
source_port = tcph[0]
dest_port = tcph[1]
sequence = tcph[2]
acknowledgement = tcph[3]
doff_reserved = tcph[4]
tcph_length = doff_reserved >> 4
msg.append(('TCP => Source Port: {source_port}, Dest Port: {dest_port}'
' Sequence Number: {sequence} Acknowledgement: {acknowledgement}'
' TCP header length: {tcph_length}').format(
source_port=source_port, dest_port=dest_port,
sequence=sequence, acknowledgement=acknowledgement,
tcph_length=tcph_length
))
print(('TCP => Source Port: {source_port}, Dest Port: {dest_port}'
' Sequence Number: {sequence} Acknowledgement: {acknowledgement}'
' TCP header length: {tcph_length}').format(
source_port=source_port, dest_port=dest_port,
sequence=sequence, acknowledgement=acknowledgement,
tcph_length=tcph_length
))
# 解析UDP数据报
def parse_udp(raw_buffer, idx):
global msg
udph_length = 8
udp_header = raw_buffer[idx: idx + udph_length]
udph = struct.unpack('!HHHH', udp_header)
source_port = udph[0]
dest_port = udph[1]
length = udph[2]
checksum = udph[3]
msg.append(('UDP => Source Port: {source_port}, Dest Port: {dest_port} '
'Length: {length} CheckSum: {checksum}').format(
source_port=source_port, dest_port=dest_port,
length=length, checksum=checksum
))
print(('UDP => Source Port: {source_port}, Dest Port: {dest_port} '
'Length: {length} CheckSum: {checksum}').format(
source_port=source_port, dest_port=dest_port,
length=length, checksum=checksum
))
# 入口
def Sniffer():
MessageClean()
t = threading.Thread(target=main(count=entry_COUNT_number.get()))
t.start()
t.join()
# 创建子线程以解决界面未响应问题
def thread_it(func, *args):
if flag_thread:
tkinter.messagebox.showinfo("提示", "当前正在解析中!")
return
t = threading.Thread(target=func, args=args)
t.setDaemon(True)
t.start()
# 开始按钮
startButton = tkinter.Button(body, text="开始", command=lambda: thread_it(Sniffer), background="yellow")
startButton.place(x=710, y=15, width=70, height=30)
startButton.bind('<Return>', lambda: thread_it(Sniffer))
# 程序终止
def Stop():
global flag_thread
if flag_thread:
flag_thread = False
tkinter.messagebox.showinfo("提示", "停止解析!")
# 终止按钮
stopButton = tkinter.Button(body, text="终止", command=Stop, background="orange")
stopButton.place(x=800, y=15, width=70, height=30)
stopButton.bind('<Return>', Stop)
# 重置界面
def Reset():
global progressbar_p, flag_thread
if flag_thread:
tkinter.messagebox.showinfo("提示", "当前正在解析中!")
return
Items = tree.get_children()
for item in Items:
tree.delete(item)
entry_Sourse_IP_address.delete("0", "end")
entry_Destination_IP_address.delete("0", "end")
entry_COUNT_number.delete("0", "end")
combobox_Protocol_up.current(0)
progressbar_p = 0
progressbar_loading()
MessageClean()
# 重置按钮
resetButton = tkinter.Button(body, text="重置", command=Reset, background="white")
resetButton.place(x=710, y=55, width=70, height=30)
resetButton.bind('<Return>', Reset)
# 退出程序
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
tkinter.messagebox.showinfo("提示", "退出成功!")
body.destroy()
exit()
# 退出按钮
exitButton = tkinter.Button(body, text="退出", command=Exit, background="red")
exitButton.place(x=800, y=55, width=70, height=30)
exitButton.bind('<Return>', Exit)
# 绑定主界面右上角退出
body.protocol("WM_DELETE_WINDOW", Exit)
# 加载UI界面
body.mainloop()
六、电子邮件客户端程序的设计与实现
1、实验目的
了解简单邮件传输协议SMTP和互联网文本报文格式,理解电子邮件组成和电子邮件的信息格式,掌握SMTP、MIME及POP3等对邮件的发送与读取,并在此基础上设计一个电子邮件客户端程序,指定发信人、收信人、主题及内容,并能查看发送邮件的情况。
2、总体设计
(1)背景知识
① 简单邮件传输协议SMTP
Simple Mail Transfer Protocol,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。SMTP协议属于TCP/IP协议簇,它帮助每台计算机在发送或中转信件时找到下一个目的地。通过SMTP协议所指定的服务器,就可以把Email寄到收信人的服务器上了,整个过程只要几分钟。SMTP服务器则是遵循SMTP协议的发送邮件服务器,用来发送或中转发出的电子邮件。
SMTP是一种TCP协议支持的提供可靠且有效电子邮件传输的应用层协议。以下为发送方和接收方的邮件服务器之间的SMTP通信的三个阶段:
(i)连接建立
发件人的邮件送到发送方邮件服务器的邮件缓存后,SMTP客户就每隔一定时间对邮件缓存扫描一次。如发现有邮件,就使用SMTP的熟知端口号码25与接收方邮件服务器的SMTP服务器建立TCP连接。在连接建立后,接收方SMTP服务器要发出“220 Service ready”,然后SMTP客户向SMTP服务器发送HELO命令,附上发送方的主机名。SMTP服务器若有能力接收邮件,则回答:“250 OK”,表示已经准备好进行接收。若SMTP服务器不可用,则回答“421 Service not available”。如在一定时间内发送不了邮件,邮件服务器会把这个情况通知发件人。SMTP不使用中间的邮件服务器。不管发送方和接收方的邮件服务器相隔有多远,不管在邮件传送过程中要经过多少个路由器,TCP连接总是在发送方和接收方这两个邮件服务器之间直接建立。当接收方邮件服务器出故障而不能工作时,发送方邮件服务器只能等待一段时间后再尝试和该邮件服务器建立TCP连接,不能先找一个中间的邮件服务器建立TCP连接。
(ii)邮件发送
邮件的传送从MAIL命令开始。MAIL命令后面有发件人的地址。若SMTP服务器已准备好接收邮件,则回答“250 OK”。否则,返回一个代码,指出原因。
下面跟着一个或多个RCPT命令,取决于把同一个邮件发送给一个或多个收件人。每发送一个RCPT命令,都应当有相应的信息从SMTP服务器返回。RCPT命令的作用就是:先弄清接收方系统是否已做好接收邮件的准备,然后才发送邮件。这样做是为了避免浪费通信资源,不至于发送了很长的邮件以后才知道地址错误。
再下面就是DATA命令,表示要开始传送邮件的内容。SMTP服务器返回的信息是:“354 Start mail input; end with <CRLF>.<CRLF>”。若不能接收邮件,则返回421(服务器不可用),500(命令无法识别)等。接着SMTP客户就发送邮件的内容。发送完毕后,再发送<CRLF>.<CRLF>表示邮件内容结束,若邮件收到了,则SMTP服务器返回信息“250 OK”,或返回差错代码。
(iii)连接释放
邮件发送完毕后,SMTP客户应发送QUIT命令。服务器返回的信息是“221(服务关闭)”,表示SMTP同意释放TCP连接。邮件传送的全部过程即结束。使用电子邮件的用户看不见以上这些过程,所有这些复杂过程都被电子邮件的用户代理屏蔽。
② 通用互联网邮件扩充MIME
Multipurpose Internet Mail Extensions,在未改动或取代SMTP的情况下继续使用原来的邮件格式,增加了邮件主体的结构,并定义了传送非ASCII码的编码规则,借此填补SMTP协议存在的一些缺点。也就是说,邮件可在现有的电子邮件程序和协议下传送。
MIME主要包括三部分内容:
- 5个新的邮件首部字段,它们可包含在原来的邮件首部中。这些字段提供了有关邮件主体的信息。
- 定义了许多邮件内容格式,对多媒体电子邮件的表示方法进行了标准化。
- 定义了传送编码,可对任何内容格式进行转换,而不会被邮件系统改变。
为适应于任意数据类型和表示,每个MIME报文包含告知收件人数据类型和使用编码的信息。MIME把增加的信息加入到原来的邮件首部中。
③ 邮件读取协议POP3
Post Office Protocol-Version 3,是TCP/IP协议族中的一员。POP3协议主要用于支持使用客户端远程管理在服务器上的电子邮件。POP3 协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。要把POP3收取的文本变成可以阅读的邮件,还需要解析原始文本,将其变成可阅读的邮件对象。
④ 电子邮件核心组成
(2)模块介绍
程序总共分为两大部分,分别是邮件发送和邮件读取:
① 邮件发送
通过tkinter组件编写UI,用户可以输入发信人(From)、收信人(To)、主题(Subject)和内容(Message),通过调用MIME相关库,获取输入的各项信息,确认邮件类型(本实验为文本类型)后调用SMTP相关库,设置SMTP服务器(本实验选取QQ邮箱演示,则SMTP服务器的地址就是smtp.qq.com,而端口号是465或587),最后是通过Email相关库来设置邮件内容,包括主题、正文等,然后用设置好的服务器发送设置好的邮件内容。
② 邮件读取
通过tkinter组件编写UI,用户点击“获取”按钮,首先需要获取邮件原始文本,通过调用POP3相关库连接到POP3服务器,取编号最大的为最新的邮件,退出连接后解码字符串并设置字符集,随后依次解析邮件头和邮件正文,还原为原始的邮件对象。
(3)设计步骤
- 开启邮箱SMTP服务;
- 设置好SMTP服务器地址;
- 设置服务器邮箱地址和密码( QQ邮箱为授权码);
- 设置要发送的邮件内容,例如发信人,收信人,主题和正文;
- 将设置好的邮件内容传给服务器并发送;
- 获取邮件的原始文本;
- 解析原始文本,还原为邮件对象;
其中需要使用QQ邮箱的SMTP协议,开启SMTP的路径是:
邮箱首页→设置→账户→POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务→开启
开启后QQ邮箱会提供一个授权码,用于连接SMTP和POP3服务器
3、详细设计
(1)程序流程图
(2)关键代码
def Send():
global From, To, Subject, Message
……
if not (From and To and Subject and Message):
tkinter.messagebox.showwarning('warning', message='请填写缺失信息!')
else:
msg = MIMEMultipart()
msg['From'] = From # 发信人
msg['To'] = To # 收信人
msg['Subject'] = Subject # 主题
msg.attach(MIMEText(Message, 'plain'))
server = smtplib.SMTP(SMTP_address) # 连接到SMTP服务器
server.starttls()
server.login(From, email_code) # 邮箱授权码
text = msg.as_string() # 内容
server.sendmail(From, To, text)
server.quit()
tkinter.messagebox.showwarning('warning', message='发送成功!')
def get_origin_text(): # 获取邮件原始文本
pop_server = poplib.POP3(POP3_address) # 连接到POP3服务器
pop_server.user(user_address) # 邮箱号
pop_server.pass_(email_code) # 邮箱授权码
print('邮件数: %s\n邮件尺寸: %s(byte)' % pop_server.stat())
resp, mails, octets = pop_server.list()
index = len(mails)
resp, lines, octets = pop_server.retr(index)
msg_content = b'\r\n'.join(lines).decode('utf-8')
msg = Parser().parsestr(msg_content)
pop_server.quit() # 退出连接
return msg
def parse_msg(msg):
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '') # 获取邮件头的内容
if value:
if header == 'Subject': # 获取主题的信息,并解码
value = decode_str(value) # 解码字符串
else:
hdr, addr = parseaddr(value) # 解析字符串中的邮件地址
name = decode_str(hdr) # 解码字符串
value = '%s <%s>' % (name, addr)
print('%s: %s' % (header, value))
if msg.is_multipart(): # 如果消息由多个部分组成,则返回True
parts = msg.get_payload() # 返回一个包含邮件所有的子对象的列表
for n, part in enumerate(parts): # 枚举,遍历各个对象
print('part %s' % (n + 1))
parse_msg(part)
else:
content_type = msg.get_content_type() # 获取邮件信息的内容类型
if content_type == 'text/plain' or content_type == 'text/html':
content = msg.get_payload(decode=True)
charset = set_charset(msg) # 设置字符集
if charset: # 字符集不为空
content = content.decode(charset) # 解码
print('Text: %s' % content)
else:
print('Attachment: %s' % content_type) # 附件
4、实验结果与分析
(1)运行结果
① 邮件发送
② 邮件读取
③ 演示视频
电子邮件客户端程序
(2)实验分析
① 邮件发送
首先tkinter生成客户端页面,通过get()方法获取用户输入的发信人、收信人、主题和内容,同时创建MIMEMultipart类型的变量msg,存入From、To、Subject和Message,通过smtplib.SMTP(SMTP_address, 587)方法以TLS加密的方式连接至SMTP服务器,用starttls()方法建立安全连接,然后再将msg上传至SMTP服务器并发送,之后通过quit()方法退出SMTP服务器。
② 邮件读取
首先通过poplib.POP3(POP3_address)方法连接至POP3服务器,获取用户邮箱和邮箱授权码,同时获取邮箱内的首条邮件的原始文本,之后通过quit()方法退出POP3服务器。再依次解析邮件头和邮件正文,通过decode_str()方法解码字符串和set_charset()方法设置字符集,并对主题信息、邮件地址和邮件信息依次进行解码,还原为邮件对象并在终端以指定格式输出。
5、小结与心得体会
通过学习编写电子邮件客户端程序,了解简单邮件传输协议SMTP和互联网文本报文格式,理解电子邮件组成和电子邮件的信息格式,掌握SMTP、MIME及POP3等对邮件的发送与读取,并借此学习了Python中SMTP、MIME、POP3库中的常用方法调用,以及tkinter库提供的界面,通过学习基本的邮件传输方法,实现了指定发信人、收信人、主题及内容的邮件发送,并能查看30天内接收邮件的数量和大小,以及首个发送邮件的情况。
6、完整代码
import smtplib
import poplib
import tkinter
import tkinter.messagebox
import tkinter.filedialog
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
From = ''
To = ''
Subject = ''
Message = ''
# SMTP服务器地址
SMTP_address = "smtp.qq.com"
# POP3服务器地址
POP3_address = "pop.qq.com"
# 用户邮箱
user_address = "xxx@xxx.com"
# 邮箱授权码
email_code = "xxxxxx"
body = tkinter.Tk()
body.geometry("640x480")
body.title("Email")
body.resizable(0, 0)
one = tkinter.Label(body, width=640, height=480, bg="LightBlue")
one.pack()
from_address = tkinter.StringVar()
from_address.set('')
to_address = tkinter.StringVar()
to_address.set('')
subject = tkinter.StringVar()
subject.set('')
message = tkinter.StringVar()
message.set('')
label_from_address = tkinter.Label(body, text='发信人', bg="LightBlue")
label_from_address.place(x=20, y=20, width=100, height=40)
entry_from_address = tkinter.Entry(body, width=60, textvariable=from_address)
entry_from_address.place(x=120, y=25, width=450, height=30)
label_to_address = tkinter.Label(body, text='收信人', bg="LightBlue")
label_to_address.place(x=20, y=70, width=100, height=40)
entry_to_address = tkinter.Entry(body, width=60, textvariable=to_address)
entry_to_address.place(x=120, y=75, width=450, height=30)
label_subject = tkinter.Label(body, text='主题', bg="LightBlue")
label_subject.place(x=20, y=120, width=100, height=40)
entry_subject = tkinter.Entry(body, width=60, textvariable=subject)
entry_subject.place(x=120, y=125, width=450, height=30)
label_message = tkinter.Label(body, text='内容', bg="LightBlue")
label_message.place(x=20, y=170, width=100, height=40)
entry_message = tkinter.Entry(body, width=60, textvariable=message)
entry_message.place(x=120, y=175, width=450, height=200)
def Send():
global From, To, Subject, Message
From = entry_from_address.get()
To = entry_to_address.get()
Subject = entry_subject.get()
Message = entry_message.get()
if not (From and To and Subject and Message):
tkinter.messagebox.showwarning('warning', message='请填写缺失信息!')
else:
msg = MIMEMultipart()
msg['From'] = From # 发信人
msg['To'] = To # 收信人
msg['Subject'] = Subject # 主题
msg.attach(MIMEText(Message, 'plain'))
server = smtplib.SMTP(SMTP_address, 587) # 连接到SMTP服务器
server.starttls()
server.login(From, email_code) # 邮箱授权码
text = msg.as_string() # 内容
server.sendmail(From, To, text)
server.quit()
tkinter.messagebox.showwarning('warning', message='发送成功!')
sendButton = tkinter.Button(body, text="发\t送", command=Send, bg="Yellow")
sendButton.place(x=120, y=400, width=120, height=30)
sendButton.bind('<Return>', Send)
def Clean():
entry_from_address.delete("0", "end")
entry_to_address.delete("0", "end")
entry_subject.delete("0", "end")
entry_message.delete("0", "end")
cleanButton = tkinter.Button(body, text="清\t空", command=Clean, bg="white")
cleanButton.place(x=285, y=400, width=120, height=30)
cleanButton.bind('<Return>', Clean)
def get_origin_text(): # 获取邮件原始文本
pop_server = poplib.POP3(POP3_address) # 连接到POP3服务器
pop_server.user(user_address) # 邮箱号
pop_server.pass_(email_code) # 邮箱授权码
print('邮件数: %s\n邮件尺寸: %s(byte)' % pop_server.stat()) # stat()返回(邮件数,邮件尺寸)
resp, mails, octets = pop_server.list() # list()返回所有邮件的编号列表,默认返回20个元素
index = len(mails) # 获取最新的一封邮件(索引号从1开始),编号最大的为最新的一封
resp, lines, octets = pop_server.retr(index) # lines存储了邮件的原始文本的每一行,可以获得整个邮件的原始文本
msg_content = b'\r\n'.join(lines).decode('utf-8') # b表示:后面字符串是bytes类型
msg = Parser().parsestr(msg_content)
pop_server.quit() # 退出连接
return msg
def decode_str(s): # 解码字符串
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
def set_charset(msg): # 设置字符集
charset = msg.get_charset() # 获取字符集
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset
def parse_msg(msg):
# 解析邮件头
for header in ['From', 'To', 'Subject']: # 遍历获取发件人,收件人,主题的相关信息
value = msg.get(header, '') # 获取邮件头的内容
if value:
if header == 'Subject': # 获取主题的信息,并解码
value = decode_str(value) # 解码字符串
else:
hdr, addr = parseaddr(value) # 解析字符串中的邮件地址
name = decode_str(hdr) # 解码字符串
value = '%s <%s>' % (name, addr)
print('%s: %s' % (header, value))
# 解析邮件正文
if msg.is_multipart(): # 如果消息由多个部分组成,则返回True
parts = msg.get_payload() # 返回一个包含邮件所有的子对象的列表
for n, part in enumerate(parts): # 枚举,遍历各个对象
print('part %s' % (n + 1))
parse_msg(part)
else:
content_type = msg.get_content_type() # 获取邮件信息的内容类型
if content_type == 'text/plain' or content_type == 'text/html': # 如果是纯文本或者html类型
content = msg.get_payload(decode=True) # 返回一个包含邮件所有的子对象(已解码)的列表
charset = set_charset(msg) # 设置字符集
if charset: # 字符集不为空
content = content.decode(charset) # 解码
print('Text: %s' % content)
else:
print('Attachment: %s' % content_type) # 附件
def Get():
msg = get_origin_text() # 第一步:用 poplib 获取邮件的原始文本。
parse_msg(msg) # 第二步:用 email 解析原始文本,还原为邮件对象。
getButton = tkinter.Button(body, text="获\t取", command=Get, bg="orangered")
getButton.place(x=450, y=400, width=120, height=30)
getButton.bind('<Return>', Get)
def Exit():
response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?")
if response:
tkinter.messagebox.showinfo("提示", "退出成功!")
body.destroy()
exit()
body.protocol("WM_DELETE_WINDOW", Exit)
body.mainloop()