赞
踩
使用Socket编程,了解Socket通信的原理,会使用Socket进行简单的网络编程,并在此基础上编写聊天程序,运行服务器端和客户端,实现多个客户端通过服务器端进行通信。
① 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编程方法
将套接字绑定到地址。address地址的格式取决于地址族。在AF_INET下,以元组(host,port)的形式表示地址;
开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。其中backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5,这个值不能无限大,因为要在内核中维护连接队列;
接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据。address是连接客户端的地址;
连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误;
关闭套接字;
接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略;
将string中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小,即可能未将指定内容全部发送。
程序总共分为两大部分,分别是服务器端和客户端:
① 服务器端
(i)统计在线人员模块
统计客户端登录的情况,获取当前在线人员的列表。若用户断开连接,将其从用户列表中删除并更新用户列表。
(ii)接收消息模块
接受来自客户端的用户名。如果用户名为空,则使用用户的IP与端口作为用户名;若用户名出现重复,则在用户名后依此加上后缀"2"、"3"、"4"等;获取用户名后便会不断地接受用户端发来的消息,结束后关闭连接;如果用户断开连接,将该用户从用户列表中删除,然后更新用户列表。
(iii)发送数据模块
服务端在接受到数据后,会对其进行一些处理然后发送给客户端:对于聊天内容,服务端直接发送给客户端;而对于用户列表,便由json.dumps处理后发送。
② 客户端
(i)登录模块
通过tkinter绘制UI,获取IP、PORT和用户名(可实现防重名),退出登录界面时会弹出确认提示,确定后则退出程序;
(ii)接收消息模块
保持连接状态,获取数据信息,并识别"message+username+chatwith"格式的消息,对聊天状态进行判断,进行相应的显示。
(iii)发送消息模块
消息从聊天框发送后,将以"message+username+chatwith"的格式送出,触发条件是sendButton()方法对应的“发送”按钮。
① 服务器端
② 客户端
① 服务器端
- # 接受来自客户端的用户名,如果用户名为空,使用用户的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
① 登录界面(输入IP地址和用户名)
② 聊天界面(登录后界面)
③ 群聊示例(输入格式:message)
④ 私聊示例(输入格式:message+username+chatwith)
⑤ 服务器的开启与退出
⑥ 演示视频
网络聊天程序
首先运行服务器端,创建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,并按对应的字体颜色分别显示在对应的聊天界面上。
通过学习编写网络聊天程序,对Socket编程有了初步的了解,熟悉了TCP服务的各个常用系统调用阶段,并借此学习了Python中Socket库中的常用方法调用以及tkinter库提供的界面,通过学习基本的服务器端和客户端的通信,进而扩展学习了多个客户端之间的通信,并通过编写客户端的条件判断结构实现了程序的私聊功能。
(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的工作原理
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回送请求和回送回答报文:
Tracert程序主要分为三个部分,分别是:
① 计算校验和
② 测试连接
设置超时时间,使用struct模块创建一个ICMP_ECHO_REQUEST数据报,将查验请求的数据发往目的地址。在未到达超时时间之前socket处于阻塞状态等待响应,当有数据传回时就接受响应,然后提取包含ID的ICMP报文首部和ICMP内容,根据请求响应的延时与超时时间对比和路由情况给定返回值。
③ 跟踪路由
设置TTL的初始值和最大值,获取远程主机的DNS主机名和数据包类型,Tracert 每次将送出的数据报的TTL 加1来发现下一个路由器,一直持续到某个数据报成功抵达目的地,退出程序。
Ping程序主要分为三个部分,分别是:
① 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
① Tracert.py
以tracert("www.baidu.com")为例
跟踪结果与控制台命令结果对比一致
② Ping.py
③ 演示视频
Tracert与Ping程序
① 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上。
通过学习Tracert程序原理,对路由追踪的过程有了初步了解, 同时通过学习Ping的基本原理,了解了ICMP报文的类型和发送过程,并借此学习了Python中Socket库中的常用方法调用以及tkinter库提供的界面,同时在此基础上编写了Ping程序,完成了测试不同主机之间的连通性的任务。
(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简介
Wireshark是一种可以运行在Windows, UNIX, Linux等操作系统上的分组嗅探器,是一个开源免费软件。
运行Wireshark程序时,最初各窗口中并无数据显示。Wireshark的界面主要有五个组成部分:
② 以太网的MAC帧结构
在以太网链路上的数据包称作以太帧。以太帧起始部分由前导码和帧开始符组成。后面紧跟着一个以太网报头,以MAC地址说明目的地址和源地址。帧的中部是该帧负载的包含其他协议报头的数据包(例如IP协议)。以太帧由一个32位冗余校验码结尾。它用于检验数据传输是否出现损坏。
各字段的含义与作用如下:
③ ARP协议
各字段的含义与作用如下:
④ IP协议
⑤ UDP和TCP协议
UDP首部格式各字段意义如下:
TCP首部格式各字段意义如下:
(1)Wireshark工具的基本使用方法
(2)以太网的帧结构
(3)ARP协议分组结构
(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地址不符合后就会直接丢弃。
通过学习Wireshark的使用与网络分析,对Wireshark工具的基本使用方法、以太网的帧结构、ARP协议分组结构、IP协议分组结构、UDP与TCP的报头结构有了基本的了解,掌握了通过控制台使用各种网络分析指令,如"ping"、"ipconfig"、"arp"等,对各类数据包的分组转发过程有了一定的了解。
通过学习原始套接字的工作原理和规则,了解各层报文的首部结构,据此设计一个可以实时监视网络状态、数据流动情况以及网络上传输的信息的网络嗅探器。
① 原始套接字
raw socket是一种不同于SOCK_STREAM和SOCK_DGRAM的套接字,实现于系统核心,创建方式与TCP/UDP的创建方法类似,可以接收本机网卡上的数据帧或者数据包,对于监听网络的流量和分析有作用。
raw socket的功能与TCP或者UDP类型socket的功能有很大的不同:TCP/UDP类型的套接字只能够访问传输层以及传输层以上的数据,因为当IP层把数据传递给传输层时,下层的数据包首部已经被丢掉了。而原始套接字却可以访问传输层以下的数据,所以使用raw socket可以实现上至应用层的数据操作,也可以实现下至链路层的数据操作。
Python中有如下几种方式创建raw socket:
IPPROTO=IPPROTO_TCP or IPPROTO_UDP or IPPROTO_ICMP,该套接字可以接收协议类型为TCP、UDP、ICMP等发往本机的IP数据包、不能收到非发往本地IP的数据包(IP软过滤会丢弃这些不是发往本机IP的数据包)、不能收到从本机发送出去的数据包。发送时需要自己组织TCP、UDP、ICMP等首部、可以调用setsockopt()方法来包装IP首部,适用于ping程序
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的数据帧)。
ETH_P=ETH_P_IP or ETH_P_ARP or ETH_P_ALL,功能与上一种功能类似,但是不包括以太网首部,可以接收非IP协议的数据包。
ETH_P=ETH_P_IP or ETH_P_ARP or ETH_P_ALL,一般用于抓包程序。
raw socket是直接使用IP协议的非面向连接的socket,可以调用bind()和connect()方法进行地址绑定:
将套接字绑定到地址。在AF_INET下,以元组(HOST, POST)形式表示地址。调用bind()方法后,发送数据包的源IP地址将是bind函数指定的地址。如果不调用bind()方法,则内核将以发送接口的主IP地址填充IP头。如果使用setsockopt()方法设置了IP_HDRINCL(headerincluding)选项,就必须手工填充每个要发送的数据包的源IP地址,否则,内核将自动创建IP首部。
连接到address的套接字。一般address的格式为元组(HOST, POST),如果连接出错,返回socket.error错误。调用connect()方法后,就可以使用write()方法和send()方法来发送数据包,而且内核将会用这个绑定的地址填充IP数据包的目的IP地址,否则应使用sendto()方法或sendmsg()方法来发送数据包,并且要在函数参数中指定对方的IP地址。
② 网络嗅探器
(i)原理
网络嗅探器(Sniffer)利用的是共享式的网络传输介质。共享即意味着网络中的一台机器可以嗅探到传递给本网段(冲突域)中的所有机器的报文。例如最常见的以太网就是一种共享式的网络技术,以太网卡收到报文后,通过对目的地址进行检查,来判断是否是传递给自己的,若是则把报文传递给操作系统,否则将报文丢弃不进行处理。网络嗅探器通过将网卡设置为混杂模式来实现对网络的嗅探。
一个实际的主机系统中的数据收发是由网卡来完成的,当网卡接收到传输来的数据包时,网卡内的单片程序首先解析数据包的目的网卡物理地址,然后根据网卡驱动程序设置的接收模式判断该不该接收,认为该接收就产生中断信号通知CPU,认为不该接收就丢掉数据包,所以不该接收的数据包就被网卡截断了,上层应用根本就不知道这个过程。如果CPU得到了网卡的中断信号,则根据网卡的驱动程序设置的网卡中断程序地址调用驱动程序接收数据,并将接收的数据交给上层协议软件外理。
(ii)网卡的四种接收模式
程序总共分为五个部分,分别是:
- 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(……)
① 不给出任何信息,弹出提示框
② 只给出监听次数(100),默认监听所有数据报
③ 给出源IP地址(192.168.31.12)和监听次数(100)
④ 给出目的IP地址(192.168.31.12)和监听次数(100)
⑤ 给出数据包类型(TCP)和监听次数(100)
⑥ 演示视频
网络嗅探器
首先调用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上。
通过学习编写网络嗅探器程序,学习了raw socket的工作原理和规则,掌握了网络嗅探器的基本原理,对socket相关方法实现网络嗅探的流程有了一定的了解,同时参考了raw socket编程的示例,设计了一个可以监视网络状态、数据流动情况以及网络上传输的信息的网络嗅探器,并能支持过滤条件操作。
- 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()
通过学习网络嗅探器(Sniffer)程序的实现,了解各层报文的首部结构,并结合使用Wireshark软件观察网络各层报文捕获、解析和分析的过程,尝试编写出网络报文的解析程序。
① 以太网MAC帧格式
② IP数据报格式
③ ICMP数据报格式
④ UDP数据报格式
⑤ TCP数据报格式
程序总共分为三大部分,分别是:
- # 获取表格中对应行的信息
- 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()
① 不给出任何信息,弹出提示框
② 只给出监听次数(100),默认监听所有数据报
③ 给出源IP地址(192.168.31.12)和监听次数(100)
④ 给出目的IP地址(192.168.31.12)和监听次数(100)
⑤ 给出数据包类型(UDP)和监听次数(100)
⑥ 演示视频
网络报文分析程序
首先调用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()方法清除上一次捕获的所有信息,并准备开始接收下一次捕获的报文信息。
通过回顾网络嗅探器(Sniffer)程序的实现,同时了解各报文的首部结构,并结合使用Wireshark软件观察、捕获、解析和分析网络各层报文,同时通过大量搜索tkinter库的相关控件的使用注意事项,反复进行UI设计与排版,提高业务逻辑处理能力,并据此完成了网络报文分析程序的编程,实现了设置过滤条件捕获报文、显示报文详细信息、表格列排序等多个功能。
- 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()
了解简单邮件传输协议SMTP和互联网文本报文格式,理解电子邮件组成和电子邮件的信息格式,掌握SMTP、MIME及POP3等对邮件的发送与读取,并在此基础上设计一个电子邮件客户端程序,指定发信人、收信人、主题及内容,并能查看发送邮件的情况。
① 简单邮件传输协议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主要包括三部分内容:
为适应于任意数据类型和表示,每个MIME报文包含告知收件人数据类型和使用编码的信息。MIME把增加的信息加入到原来的邮件首部中。
③ 邮件读取协议POP3
Post Office Protocol-Version 3,是TCP/IP协议族中的一员。POP3协议主要用于支持使用客户端远程管理在服务器上的电子邮件。POP3 协议收取的不是一个已经可以阅读的邮件本身,而是邮件的原始文本,这和SMTP协议很像,SMTP发送的也是经过编码后的一大段文本。要把POP3收取的文本变成可以阅读的邮件,还需要解析原始文本,将其变成可阅读的邮件对象。
④ 电子邮件核心组成
程序总共分为两大部分,分别是邮件发送和邮件读取:
① 邮件发送
通过tkinter组件编写UI,用户可以输入发信人(From)、收信人(To)、主题(Subject)和内容(Message),通过调用MIME相关库,获取输入的各项信息,确认邮件类型(本实验为文本类型)后调用SMTP相关库,设置SMTP服务器(本实验选取QQ邮箱演示,则SMTP服务器的地址就是smtp.qq.com,而端口号是465或587),最后是通过Email相关库来设置邮件内容,包括主题、正文等,然后用设置好的服务器发送设置好的邮件内容。
② 邮件读取
通过tkinter组件编写UI,用户点击“获取”按钮,首先需要获取邮件原始文本,通过调用POP3相关库连接到POP3服务器,取编号最大的为最新的邮件,退出连接后解码字符串并设置字符集,随后依次解析邮件头和邮件正文,还原为原始的邮件对象。
其中需要使用QQ邮箱的SMTP协议,开启SMTP的路径是:
邮箱首页→设置→账户→POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务→开启
开启后QQ邮箱会提供一个授权码,用于连接SMTP和POP3服务器
- 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) # 附件
① 邮件发送
② 邮件读取
③ 演示视频
电子邮件客户端程序
① 邮件发送
首先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()方法设置字符集,并对主题信息、邮件地址和邮件信息依次进行解码,还原为邮件对象并在终端以指定格式输出。
通过学习编写电子邮件客户端程序,了解简单邮件传输协议SMTP和互联网文本报文格式,理解电子邮件组成和电子邮件的信息格式,掌握SMTP、MIME及POP3等对邮件的发送与读取,并借此学习了Python中SMTP、MIME、POP3库中的常用方法调用,以及tkinter库提供的界面,通过学习基本的邮件传输方法,实现了指定发信人、收信人、主题及内容的邮件发送,并能查看30天内接收邮件的数量和大小,以及首个发送邮件的情况。
- 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()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。