计算机网络课程设计【Python实现】

您所在的位置:网站首页 简单网络嗅探器的设计与实现 计算机网络课程设计【Python实现】

计算机网络课程设计【Python实现】

2024-01-27 23:58| 来源: 网络整理| 查看: 265

一、网络聊天程序的设计与实现 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('', 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('', 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('', 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('', 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] > 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 > 16) + (sum & 0xffff) sum = sum + (sum >> 16) answer = ~sum answer = answer & 0xffff answer = answer >> 8 | (answer 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 > 16) + (sum & 0xffff) sum = sum + (sum >> 16) answer = ~sum answer = answer & 0xffff answer = answer >> 8 | (answer 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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('', 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 .”。若不能接收邮件,则返回421(服务器不可用),500(命令无法识别)等。接着SMTP客户就发送邮件的内容。发送完毕后,再发送.表示邮件内容结束,若邮件收到了,则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 ' % (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 = "[email protected]" # 邮箱授权码 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('', 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('', 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 ' % (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('', Get) def Exit(): response = tkinter.messagebox.askyesno("退出", "你确定要退出程序吗?") if response: tkinter.messagebox.showinfo("提示", "退出成功!") body.destroy() exit() body.protocol("WM_DELETE_WINDOW", Exit) body.mainloop()



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3