目录：准备代码运行演示准备安装库：PyQt5，poplib，smtplib邮件客户端主要功能包括接收邮件、发送邮件、邮件分类.........，时间有限，只做出了发送邮件和接收邮件。接收邮件主要依赖pop服务器，发送邮件依赖smtp服务器，例如网易163邮箱的pop服务器为pop.163.com，smtp服务器为smtp.163.com，发送端口为465。目前测试网易邮箱没有问题，但是使用前确保打开了163的 POP3/SMTP/IMAP 服务，及开启 客户端授权密码。代码包含三个界面，可使用QTdesigner设计GUI，也可直接使用我生成的界面py文件。三个界面如下：ui生成的代码分别是：主界面ui --> pop3.py# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'pop3.ui' # # Created by: PyQt5 UI code generator 5.13.0 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(551, 716) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.tableWidget = QtWidgets.QTableWidget(self.centralwidget) self.tableWidget.setGeometry(QtCore.QRect(30, 70, 501, 231)) self.tableWidget.setObjectName("tableWidget") self.tableWidget.setColumnCount(5) self.tableWidget.setRowCount(0) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(0, item) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(1, item) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(2, item) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(3, item) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(4, item) self.tableWidget.horizontalHeader().setDefaultSectionSize(150) self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(30, 30, 61, 21)) self.label.setObjectName("label") self.pushButton = QtWidgets.QPushButton(self.centralwidget) self.pushButton.setGeometry(QtCore.QRect(360, 20, 81, 31)) self.pushButton.setObjectName("pushButton") self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget) self.pushButton_2.setGeometry(QtCore.QRect(110, 20, 81, 31)) self.pushButton_2.setObjectName("pushButton_2") self.pushButton_3 = QtWidgets.QPushButton(self.centralwidget) self.pushButton_3.setGeometry(QtCore.QRect(454, 20, 81, 31)) self.pushButton_3.setObjectName("pushButton_3") self.label_2 = QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(50, 320, 41, 21)) self.label_2.setObjectName("label_2") self.lineEdit = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit.setGeometry(QtCore.QRect(90, 320, 441, 21)) self.lineEdit.setObjectName("lineEdit") self.label_3 = QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(40, 360, 51, 21)) self.label_3.setObjectName("label_3") self.lineEdit_2 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_2.setGeometry(QtCore.QRect(90, 360, 441, 21)) self.lineEdit_2.setObjectName("lineEdit_2") self.label_4 = QtWidgets.QLabel(self.centralwidget) self.label_4.setGeometry(QtCore.QRect(30, 440, 61, 21)) self.label_4.setObjectName("label_4") self.lineEdit_3 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_3.setGeometry(QtCore.QRect(90, 440, 441, 21)) self.lineEdit_3.setObjectName("lineEdit_3") self.label_5 = QtWidgets.QLabel(self.centralwidget) self.label_5.setGeometry(QtCore.QRect(50, 480, 41, 21)) self.label_5.setObjectName("label_5") self.textEdit = QtWidgets.QTextEdit(self.centralwidget) self.textEdit.setGeometry(QtCore.QRect(90, 480, 441, 131)) self.textEdit.setObjectName("textEdit") self.pushButton_4 = QtWidgets.QPushButton(self.centralwidget) self.pushButton_4.setGeometry(QtCore.QRect(230, 20, 81, 31)) self.pushButton_4.setObjectName("pushButton_4") self.label_6 = QtWidgets.QLabel(self.centralwidget) self.label_6.setGeometry(QtCore.QRect(50, 400, 41, 21)) self.label_6.setObjectName("label_6") self.lineEdit_4 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_4.setGeometry(QtCore.QRect(90, 400, 441, 21)) self.lineEdit_4.setObjectName("lineEdit_4") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 551, 23)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) item = self.tableWidget.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "标题")) item = self.tableWidget.horizontalHeaderItem(1) item.setText(_translate("MainWindow", "发送者")) item = self.tableWidget.horizontalHeaderItem(2) item.setText(_translate("MainWindow", "时间")) item = self.tableWidget.horizontalHeaderItem(3) item.setText(_translate("MainWindow", "发送邮箱")) item = self.tableWidget.horizontalHeaderItem(4) item.setText(_translate("MainWindow", "内容")) self.label.setText(_translate("MainWindow", "收信箱：")) self.pushButton.setText(_translate("MainWindow", "登录")) self.pushButton_2.setText(_translate("MainWindow", "发送邮件")) self.pushButton_3.setText(_translate("MainWindow", "退出")) self.label_2.setText(_translate("MainWindow", "标题：")) self.label_3.setText(_translate("MainWindow", "发送者：")) self.label_4.setText(_translate("MainWindow", "发送邮箱：")) self.label_5.setText(_translate("MainWindow", "正文：")) self.pushButton_4.setText(_translate("MainWindow", "刷新信箱")) self.label_6.setText(_translate("MainWindow", "时间："))发送邮件界面ui --> smtp.py# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'smtp.ui' # # Created by: PyQt5 UI code generator 5.13.0 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(938, 405) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(70, 50, 81, 41)) self.label.setObjectName("label") self.lineEdit = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit.setGeometry(QtCore.QRect(150, 60, 321, 21)) self.lineEdit.setObjectName("lineEdit") self.label_2 = QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(90, 100, 61, 41)) self.label_2.setObjectName("label_2") self.lineEdit_2 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_2.setGeometry(QtCore.QRect(150, 110, 321, 21)) self.lineEdit_2.setObjectName("lineEdit_2") self.label_3 = QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(70, 10, 81, 41)) self.label_3.setObjectName("label_3") self.label_4 = QtWidgets.QLabel(self.centralwidget) self.label_4.setGeometry(QtCore.QRect(70, 150, 81, 41)) self.label_4.setObjectName("label_4") self.lineEdit_4 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_4.setGeometry(QtCore.QRect(150, 160, 321, 21)) self.lineEdit_4.setObjectName("lineEdit_4") self.label_5 = QtWidgets.QLabel(self.centralwidget) self.label_5.setGeometry(QtCore.QRect(70, 200, 81, 41)) self.label_5.setObjectName("label_5") self.lineEdit_5 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_5.setGeometry(QtCore.QRect(150, 210, 321, 21)) self.lineEdit_5.setObjectName("lineEdit_5") self.label_6 = QtWidgets.QLabel(self.centralwidget) self.label_6.setGeometry(QtCore.QRect(500, 0, 71, 41)) self.label_6.setObjectName("label_6") self.lineEdit_6 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_6.setGeometry(QtCore.QRect(570, 10, 321, 20)) self.lineEdit_6.setObjectName("lineEdit_6") self.label_7 = QtWidgets.QLabel(self.centralwidget) self.label_7.setGeometry(QtCore.QRect(500, 40, 61, 41)) self.label_7.setObjectName("label_7") self.textEdit = QtWidgets.QTextEdit(self.centralwidget) self.textEdit.setGeometry(QtCore.QRect(570, 50, 321, 191)) self.textEdit.setObjectName("textEdit") self.pushButton = QtWidgets.QPushButton(self.centralwidget) self.pushButton.setGeometry(QtCore.QRect(650, 270, 81, 31)) self.pushButton.setObjectName("pushButton") self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget) self.pushButton_2.setGeometry(QtCore.QRect(780, 270, 81, 31)) self.pushButton_2.setObjectName("pushButton_2") self.label_8 = QtWidgets.QLabel(self.centralwidget) self.label_8.setGeometry(QtCore.QRect(150, 10, 211, 41)) self.label_8.setObjectName("label_8") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 938, 23)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) self.label.setText(_translate("MainWindow", "SMTP服务器：")) self.lineEdit.setText(_translate("MainWindow", "smtp.163.com")) self.label_2.setText(_translate("MainWindow", "端口号：")) self.lineEdit_2.setText(_translate("MainWindow", "465")) self.label_3.setText(_translate("MainWindow", "发件人账号：")) self.label_4.setText(_translate("MainWindow", "发件人昵称：")) self.label_5.setText(_translate("MainWindow", "收件人邮箱：")) self.label_6.setText(_translate("MainWindow", "邮件主题：")) self.label_7.setText(_translate("MainWindow", "邮件正文：")) self.pushButton.setText(_translate("MainWindow", "发送")) self.pushButton_2.setText(_translate("MainWindow", "取消")) self.label_8.setText(_translate("MainWindow", "TextLabel"))登录界面ui-->idenfy.py# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'idenfy.ui' # # Created by: PyQt5 UI code generator 5.13.0 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(384, 263) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(70, 50, 61, 41)) self.label.setObjectName("label") self.label_2 = QtWidgets.QLabel(self.centralwidget) self.label_2.setGeometry(QtCore.QRect(70, 90, 61, 41)) self.label_2.setObjectName("label_2") self.lineEdit = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit.setGeometry(QtCore.QRect(140, 60, 171, 21)) self.lineEdit.setText("") self.lineEdit.setObjectName("lineEdit") self.lineEdit_2 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_2.setGeometry(QtCore.QRect(140, 100, 171, 21)) self.lineEdit_2.setText("") self.lineEdit_2.setObjectName("lineEdit_2") self.label_3 = QtWidgets.QLabel(self.centralwidget) self.label_3.setGeometry(QtCore.QRect(60, 10, 71, 41)) self.label_3.setObjectName("label_3") self.lineEdit_3 = QtWidgets.QLineEdit(self.centralwidget) self.lineEdit_3.setGeometry(QtCore.QRect(140, 20, 171, 21)) self.lineEdit_3.setObjectName("lineEdit_3") self.pushButton = QtWidgets.QPushButton(self.centralwidget) self.pushButton.setGeometry(QtCore.QRect(100, 150, 71, 31)) self.pushButton.setObjectName("pushButton") self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget) self.pushButton_2.setGeometry(QtCore.QRect(230, 150, 71, 31)) self.pushButton_2.setObjectName("pushButton_2") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 384, 23)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) self.label.setText(_translate("MainWindow", "邮箱账号：")) self.label_2.setText(_translate("MainWindow", "邮箱密码：")) self.label_3.setText(_translate("MainWindow", "pop3服务器：")) self.lineEdit_3.setText(_translate("MainWindow", "pop.163.com")) self.pushButton.setText(_translate("MainWindow", "登录")) self.pushButton_2.setText(_translate("MainWindow", "取消"))主函数 emailServer.pyimport idenfy import pop3 import smtp import sys from PyQt5.QtWidgets import QMessageBox,QLineEdit,QApplication,QMainWindow,QAbstractItemView,QTableWidgetItem import _thread from PyQt5.QtCore import QCoreApplication import poplib import base64 from email.parser import Parser from email.header import decode_header from email.utils import parseaddr import datetime import smtplib from email.mime.text import MIMEText from email.utils import formataddr app = QApplication(sys.argv) is_Idenfy = False user = { 'pop3':None, 'user':None, 'pass':None } class Email_Server(object): def __init__(self,user_dic): # print(user_dic) self.user_mail = user_dic['user'] self.password = user_dic['pass'] self.pop_server = user_dic['pop3'] # pop.163.com self.Connect_Server() def Connect_Server(self): # 连接服务 self.server = poplib.POP3(self.pop_server) self.server.user(self.user_mail) self.server.pass_(self.password) def _close_(self): # 关闭服务器资源 self.server.close() def Get_Email_Count(self): # 获取邮件数目 email_num, email_size = self.server.stat() # print("邮件数量: {0}, 消息大小：{1}Byte".format(email_num,email_size)) return email_num def Get_Email_Data(self,email_row = None): rsp, msglines, msgsiz = self.server.retr(email_row) # print("服务器的响应: {0},\n原始邮件内容： {1},\n该封邮件所占字节大小： {2}".format(rsp, msglines, msgsiz)) # 解析 msg_content = b'\r\n'.join(msglines).decode('gbk') # print(msg_content) msg = Parser().parsestr(text=msg_content) # print(msg) self.msg = msg # 标题 subject = self.msg['Subject'] value, charset = decode_header(subject)[0] if charset: value = value.decode(charset) self.email_title = value print("标题： ",self.email_title) # 发送方 hdr, addr = parseaddr(self.msg['From']) name, charset = decode_header(hdr)[0] if charset: name = name.decode(charset) self.email_name = name self.email_addr = addr print("发送方: ",name,addr) # 时间 date = decode_header(self.msg.get('date')) utcstr = date[0][0].replace('+00:00', '') self.email_time = str(utcstr) # print(utcdatetime) # 内容 try: content = self.msg.get_payload() content_charset = content[0].get_content_charset() # 获取编码格式 print(content_charset) text = content[0].as_string().split('base64')[-1] try: text_content = base64.b64decode(text).decode(content_charset) # base64解码 except: text_content = base64.b64decode(text).decode('gbk','ignore') # base64解码 # print(text_content) self.content = text_content except: self.content = " " mail_Server = None class Send_email(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.send_ui = smtp.Ui_MainWindow() self.send_ui.setupUi(self) self.send_ui.pushButton_2.clicked.connect(self.close) self.send_ui.pushButton.clicked.connect(self.Send) def Send(self): global mail_Server try: smtp_server = self.send_ui.lineEdit.text() smtp_port = self.send_ui.lineEdit_2.text() send_name = self.send_ui.lineEdit_4.text() recv_mail = self.send_ui.lineEdit_5.text() send_title = self.send_ui.lineEdit_6.text() send_text = self.send_ui.textEdit.toPlainText() except: QMessageBox.about(self, "Message", "填写完整信息") _thread.start_new_thread(self.Send_Email, (smtp_server,smtp_port,send_name,recv_mail,send_title,send_text,)) def Send_Email(self,smtp_server,smtp_port,send_name,recv_mail,send_title,send_text): global mail_Server try: msg = MIMEText(send_text,'plain','utf-8') msg['From'] = formataddr([send_name,mail_Server.user_mail])#发件人邮箱昵称、发件人邮箱 msg['To'] = formataddr(["star", recv_mail]) # 收件人邮箱昵称、收件人邮箱账号 msg['Subject'] = send_title # 邮件主题（标题 server = smtplib.SMTP_SSL(smtp_server, int(smtp_port)) # 发送人邮箱SMTP服务器，端口号 server.login(mail_Server.user_mail, mail_Server.password) # 发件人邮箱账号、密码 server.sendmail(mail_Server.user_mail, [recv_mail, ], msg.as_string()) server.quit() QMessageBox.about(self, "Message", "发送成功！") self.close() except: QMessageBox.critical(self, "失败", "pop验证失败", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) sendUi = None class mainWin(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.main_ui = pop3.Ui_MainWindow() self.main_ui.setupUi(self) self.main_ui.pushButton_2.clicked.connect(self.Send) self.main_ui.pushButton_4.clicked.connect(self.Update) self.main_ui.tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) self.main_ui.tableWidget.itemClicked.connect(self.itemclick) self.main_ui.textEdit.document().setMaximumBlockCount(100) def itemclick(self): # print("嘤嘤嘤") now_current_row = self.main_ui.tableWidget.currentIndex().row() print(now_current_row) rowtitle = self.main_ui.tableWidget.item(now_current_row,0).text() # print(rowdata) rowsender = self.main_ui.tableWidget.item(now_current_row,1).text() etime = self.main_ui.tableWidget.item(now_current_row,2).text() rowaddr = self.main_ui.tableWidget.item(now_current_row,3).text() contents = self.main_ui.tableWidget.item(now_current_row,4).text() self.main_ui.textEdit.clear() self.main_ui.lineEdit.clear() self.main_ui.lineEdit_2.clear() self.main_ui.lineEdit_3.clear() self.main_ui.lineEdit_4.clear() _thread.start_new_thread(self.Dis_mail_data, (rowtitle,rowsender,rowaddr,contents,etime)) def Dis_mail_data(self, title, sender, addr, cont,etime): self.main_ui.lineEdit.setText(title) self.main_ui.lineEdit_2.setText(sender) self.main_ui.lineEdit_3.setText(addr) self.main_ui.lineEdit_4.setText(etime) # print(len(cont)) if len(cont) > 5000: self.main_ui.textEdit.append(" ") else: self.main_ui.textEdit.append(cont) def Update(self): global is_Idenfy,mail_Server if not is_Idenfy: QMessageBox.about(self, "Message", "请先登录") else: _thread.start_new_thread(self.Upthread, ()) def Upthread(self): global mail_Server #清空列表 allrownum = self.main_ui.tableWidget.rowCount() for i in range(allrownum): self.main_ui.tableWidget.removeRow(0) # 获取邮箱邮件数 mail_count = mail_Server.Get_Email_Count() # 获取最新10条邮件 for i in range(mail_count,mail_count-10,-1): mail_Server.Get_Email_Data(i) _thread.start_new_thread(self.Display, (mail_Server.email_title,mail_Server.email_name,mail_Server.email_addr,mail_Server.content,mail_Server.email_time,)) def Display(self,title,name,addr,content,etime): rrow = self.main_ui.tableWidget.rowCount() self.main_ui.tableWidget.insertRow(rrow) self.main_ui.tableWidget.setItem(rrow, 0, QTableWidgetItem(title)) self.main_ui.tableWidget.setItem(rrow, 1, QTableWidgetItem(name)) self.main_ui.tableWidget.setItem(rrow, 2, QTableWidgetItem(str(etime))) self.main_ui.tableWidget.setItem(rrow, 3, QTableWidgetItem(addr)) self.main_ui.tableWidget.setItem(rrow, 4, QTableWidgetItem(content)) def Send(self): global is_Idenfy,user,sendUi if not is_Idenfy: QMessageBox.about(self, "Message", "请先登录") else: sendUi = Send_email() sendUi.send_ui.label_8.setText(user['user']) sendUi.show() class loginWin(QMainWindow): def __init__(self): QMainWindow.__init__(self) self.login_ui = idenfy.Ui_MainWindow() self.login_ui.setupUi(self) self.login_ui.pushButton_2.clicked.connect(self.close) self.login_ui.pushButton.clicked.connect(self.Login) self.login_ui.lineEdit_2.setEchoMode(QLineEdit.Password) def Login(self): print("login....") global user,mail_Server,is_Idenfy user['user'] = str(self.login_ui.lineEdit.text()) user['pass'] = str(self.login_ui.lineEdit_2.text()) user['pop3'] = str(self.login_ui.lineEdit_3.text()) # print(user) try: mail_Server = Email_Server(user) print("连接成功.......") QMessageBox.about(self, "连接成功", "验证成功！") self.close() is_Idenfy = True except: print("pop验证失败！") QMessageBox.critical(self, "失败", "pop验证失败", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if __name__ == '__main__': ui = mainWin() loginui = loginWin() ui.main_ui.pushButton.clicked.connect(loginui.show) ui.main_ui.pushButton_3.clicked.connect(QApplication.quit) ui.show() sys.exit(app.exec_())运行演示运行emailServer.py后出现主界面，在进行操作前请先登录。登录后点击刷新信箱将会显出最新的10封邮件，点击表格任意行将会在下方显示邮件详情点击发送邮件可弹出发送邮件窗口，填写必要信息后点击发送，稍作等待会有弹窗提示发送成功或失败：发送成功后收件人会收到邮件。目前客户端存在一些问题：1)无法添加图片或附件。2)发送邮件成功后客户端可能会被关闭。
目录：准备代码运行演示准备基于python3.6，PyQt5，Scapy.windows 下使用Scapy可能出现Error:Sniffing and sending packets is not available at layer 2:......，安装 winPcap 可有效解决。PyQt5 的安装及环境配置：PyQt5中文文档： http://code.py40.com/pyqt5/安装请参考： https://blog.csdn.net/m0_37606112/article/details/78419892环境配置请参考： https://blog.csdn.net/lyzwjaa/article/details/79429901第一个Demo请参考： https://blog.csdn.net/px41834/article/details/79383985Scapy官方文档：https://scapy.readthedocs.io/en/latest/installation.htmlscapy.sniff()函数解析：Sniff方法定义：sniff(filter="",iface="any", prn=function, count=N)filter的规则使用 Berkeley Packet Filter (BPF)语法。iface用来指定要在哪个网络接口上进行抓包（通常不指定即所有网络接口）prn指定回调函数，每当一个符合filter的报文被探测到时，就会执行回调函数，通常使用lambda表达式来写回调函数count指定最多嗅探多少个报文（是指符合filter条件的报文，而非所有报文）GUI设计设计了之后请使用 pyuic5 将ui转为py文件。代码注意：import demo->demo为你的GUI的py文件import sys import demo from PyQt5.QtWidgets import QApplication,QMainWindow,QAbstractItemView,QTableWidgetItem from scapy.all import * import _thread from PyQt5.QtCore import QCoreApplication app = QApplication(sys.argv) MainWindow = QMainWindow() ui = demo.Ui_MainWindow() ui.setupUi(MainWindow) ui.tableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) msg = {1:"ICMP", 6:"TCP", 17:"UDP", 40:"Others", 2054:"ARP"} # 报文种类 def display(sniff_data): rrow = ui.tableWidget.rowCount() ui.tableWidget.insertRow(rrow) ui.tableWidget.setItem(rrow, 0, QTableWidgetItem(sniff_data['from'])) ui.tableWidget.setItem(rrow, 1, QTableWidgetItem(sniff_data['to'])) ui.tableWidget.setItem(rrow, 2, QTableWidgetItem(sniff_data['msg_type'])) ui.tableWidget.setItem(rrow, 3, QTableWidgetItem(sniff_data['data'])) def getoptData(pkt): pkt.show() print(pkt[0].type) sniff_data = {} if int(pkt[0].type) == 2048: sniff_data['from'] = str(pkt[1].src) sniff_data['to'] = str(pkt[1].dst) try: sniff_data['msg_type'] = str(msg[int(pkt[1].proto)]) except: sniff_data['msg_type'] = str(msg[40]) try: sniff_data['data'] = str(pkt[3]) except: sniff_data['data'] = str("") _thread.start_new_thread(display, (sniff_data,)) elif int(pkt[0].type) == 2054: sniff_data['from'] = str(pkt[1].psrc) sniff_data['to'] = str(pkt[1].pdst) sniff_data['msg_type'] = str(msg[2054]) try: sniff_data['data'] = str(pkt[2]) except: sniff_data['data'] = str("") _thread.start_new_thread(display, (sniff_data,)) def sniff_thread(ty,num): sniff(filter=ty, prn=getoptData, count=num) def ccc(): ty = ui.comboBox.currentText() if ty == "all": ty ="" num = int(ui.lineEdit.text()) _thread.start_new_thread(sniff_thread, (ty, num,)) def clearThread(): allrownum = ui.tableWidget.rowCount() for i in range(allrownum): ui.tableWidget.removeRow(0) def clearTable(): _thread.start_new_thread(clearThread, ()) if __name__ == "__main__": MainWindow.show() ui.pushButton.clicked.connect(ccc) ui.pushButton_2.clicked.connect(QCoreApplication.quit) ui.pushButton_3.clicked.connect(clearTable) sys.exit(app.exec_())运行演示监听报文种类选项：ICMP,TCP,UDP and ALL.
目录：知识储备代码运行效果知识储备接收端重点：(1)、创建一个 SOCK_DGRAM 类型的 Socket。(2)、将此 Socket 绑定到本地的一个端口上，为了接收服务器端发送的多播数据。(3)、加入多播组。(4)、接收多播数据。发送端重点：(1)、创建一个 SOCK_DGRAM 类型的 Socket。(2)、加入多播组。(3)、发送多播数据。当一台主机想要同时接收或者发送时，必须绑定相同的端口号。在网络中，线程的控制是非常重要的，在此实验中我采用了双线程，主线程用于监听发送端，副线程用于监听接收端。我编不下去了，自己看代码吧。采用QT creator平台，项目文件目录如下：首先添加库：在untitled.pro文件中任意空白行插入 LIBS += -lWs2_32GUI布局为：代码mainwindow.h 用于声明一下全局变量。#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include "thread.h" #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> class TestThread; //全局变量声明 extern struct sockaddr_in remote,local,from; extern SOCKET sock,sockM; namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void on_pushButton_clicked(); void receiver(char*,char*); void on_pushButton_2_clicked(); private: Ui::MainWindow *ui; TestThread *Thread; }; #endif // MAINWINDOW_Hthread.h 用于创建线程类：#ifndef TESTTHREAD_H #define TESTTHREAD_H #include <QThread> #include "mainwindow.h" class TestThread : public QThread { Q_OBJECT public: explicit TestThread(QObject *parent = 0); protected: void run(); signals: void TestSignal(char*,char*); private: int tnc=4; }; #endif // TESTTHREAD_H main.cpp 无需改动mainwindow.cpp 中绑定按钮监听事件和发送消息#include "mainwindow.h" #include "ui_mainwindow.h" #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #define MCASTADDR "233.0.0.1" //本例使用的多播组地址。 #define MCASTPORT 5150 //本地端口号。 #define BUFSIZE 1024 //发送数据缓冲大小。 //#pragma comment(lib,"ws2_32") using namespace std; SOCKET sock,sockM; struct sockaddr_in remote,local; void MainWindow::receiver(char* recvbuf,char* ipadd){ cout<<ipadd << "+"<<recvbuf <<endl; QString recvip = QString::fromLocal8Bit(ipadd); QString recvtext = QString::fromLocal8Bit(recvbuf); ui->textEdit->textCursor().insertText("<"+recvip+"> : \n "+recvtext+"\n"); } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); WSADATA wsd; if( WSAStartup( MAKEWORD(2,2),&wsd) != 0 ){ cout << "WSAStartup() failed\n" << endl; } if((sock=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,WSA_FLAG_MULTIPOINT_C_LEAF|WSA_FLAG_MULTIPOINT_D_LEAF|WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) { cout<< "socket failed with:" << WSAGetLastError() <<endl; WSACleanup(); } //将 sock 绑定到本机某端口上。 local.sin_family = AF_INET; local.sin_port = htons(MCASTPORT); local.sin_addr.s_addr = INADDR_ANY; if( bind(sock,(struct sockaddr*)&local,sizeof(local)) == SOCKET_ERROR ) { cout<< "bind failed with:" << WSAGetLastError() <<endl; closesocket(sock); WSACleanup(); } remote.sin_family = AF_INET; remote.sin_port = htons(MCASTPORT); remote.sin_addr.s_addr = inet_addr( MCASTADDR ); if(( sockM = WSAJoinLeaf(sock,(SOCKADDR*)&remote,sizeof(remote),NULL,NULL,NULL,NULL,JL_BOTH)) == INVALID_SOCKET) { cout<< "WSAJoinLeaf() failed: " << WSAGetLastError() <<endl; closesocket(sock); WSACleanup(); } Thread = new TestThread();//新建线程 connect(Thread, SIGNAL(TestSignal(char*,char*)), this, SLOT(receiver(char*,char*))); Thread->start(); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_pushButton_clicked() { QString sendtext = ui->lineEdit->text(); char sendbuf[BUFSIZE]; strcpy(sendbuf,sendtext.toLatin1().data()); cout <<"sendbuf: "<< sendbuf <<endl; if( sendto(sockM,(char*)sendbuf,strlen(sendbuf),0,(struct sockaddr*)&remote,sizeof(remote))==SOCKET_ERROR) { printf("sendto failed with: %d\n",WSAGetLastError()); closesocket(sockM); closesocket(sock); WSACleanup(); } cout <<"send sucessfully."<<endl; ui->lineEdit->clear(); } void MainWindow::on_pushButton_2_clicked() { exit(0); closesocket(sockM); closesocket(sock); WSACleanup(); }thread.cpp 用于副线程接收消息：#include "thread.h" #include "mainwindow.h" #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #include <stdlib.h> #define MCASTADDR "233.0.0.1" //本例使用的多播组地址。 #define MCASTPORT 5150 //本地端口号。 #define BUFSIZE 1024 //发送数据缓冲大小。 using namespace std; struct sockaddr_in from; TestThread::TestThread(QObject *parent) : QThread(parent) { } void TestThread::run() { //触发信号 char recvbuf[BUFSIZE]; /*struct ip_mreq mcast; // Winsock1.0 */ int len = sizeof( struct sockaddr_in); int ret; while(1){ if(( ret = recvfrom(sock,recvbuf,BUFSIZE,0,(struct sockaddr*)&from,&len)) == SOCKET_ERROR){ printf("recvfrom failed with:%d\n",WSAGetLastError()); closesocket(sockM); closesocket(sock); WSACleanup(); } recvbuf[ret] = '\0'; emit TestSignal(recvbuf,inet_ntoa(from.sin_addr)); } }运行效果仅适用于局域网，可将改程序打包在局域网的其他PC上用于通信。
2019/12/24update：已解决线程并发导致主界面假死问题。 今天是冬至，祝大家有个愉快的的冬天。o(*￣▽￣*)o花了一天的时间给程序做了一个GUI，在调试多线程的时候遇到了困难，昨天晚上2点多才弄完，但是感觉这个多线程有点奇怪（雾目录：提示代码运行演示提示我用的GUI开发环境是Qt Creator。Qt的下载地址和安装步骤看 这里 。Qt creator的语法和C++一样，只不过在C++的基础上增加了专门用于图像界面的库，C++代码也可在Creator上流畅运行。开始之前先新创一个项目，命名为Tracert。然后在Tracert.pro文件中添加我们需要用到的库，在任意行添加 "LIBS += -lWs2_32" 即可。然后新建一个头文件threadtest.h和一个资源文件threadtest.cpp，用于创建多线程。在Forms目录下有一个布局文件mainwindow.ui，点击这个ui文件，我们可快捷编辑GUI布局。现在我们的资源目录为：代码mainwindow.h中是我们主要的类，用于GUI和声明一些全局变量：#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include "threadtest.h" class TestThread; extern int ipnumber; extern int nowrow; extern QString nowip; extern int outtime; namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: void DisplayMsg(QString,bool,QString,QString); void on_pushButton_clicked(); void on_pushButton_2_clicked(); private: Ui::MainWindow *ui; TestThread* t[255]; QString ip1; QString ip2; }; #endif // MAINWINDOW_H threadtest.h 头文件用于创建线程类：#ifndef TESTTHREAD_H #define TESTTHREAD_H #include <QThread> #include "mainwindow.h" class TestThread : public QThread { Q_OBJECT public: explicit TestThread(QObject *parent = 0); protected: void run(); signals: void TestSignal(QString,bool,QString,QString); private: int tnc=4; }; #endif // TESTTHREAD_H main.cpp不需要做任何改动：#include "mainwindow.h" #include <QApplication> #include <iostream> int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }mainwindow.cpp 就是我们需要重点关注的地方了，GUI的绑定事件及GUI主界面的更新都在此文件内操作，所以代码有点长：#include "mainwindow.h" #include "ui_mainwindow.h" #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <cstring> #include <stdlib.h> #include <stdio.h> #include <string.h> using namespace std; QString nowip; int nowrow; int ipnumber; int outtime; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); WSADATA wsa; WSAStartup(MAKEWORD(2,2),&wsa); ui->tableWidget->setColumnCount(4); //ui->tableWidget->setRowCount(255); ui->tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); } MainWindow::~MainWindow() { delete ui; } //跳转到下一个IP地址 QString toNextIp(QString str1) { //QString2char char *tempIp = (char *)malloc(17); QByteArray ba = str1.toLatin1(); strcpy(tempIp,ba.data()); char temp[4][4]; int ip1=0; int ip2=0; for(int i=0; i < strlen(tempIp); i++) { if(tempIp[i]=='.') { temp[ip1][ip2] = '\0'; ip1++; ip2=0; continue; } temp[ip1][ip2] = tempIp[i]; ip2++; } temp[ip1][ip2] ='\0'; if(atoi(temp[3])==254) { strcpy(temp[3],"1"); } else { int t1 = atoi(temp[3])+1; itoa(t1,temp[3],10); } //cout <<temp[3]; char tip[17]; strcpy(tip,temp[0]); for(int i=1; i<4; i++) { strcat(tip,"."); strcat(tip,temp[i]); } QString qs = QString::fromLocal8Bit(tip); return qs; } //更新主界面 void MainWindow::DisplayMsg(QString aip,bool is_online,QString qsmac,QString qsname){ int row_c = ui->tableWidget->rowCount(); ui->tableWidget->insertRow(row_c); ui->tableWidget->setItem(row_c,0,new QTableWidgetItem(aip)); if(is_online){ ui->tableWidget->setItem(row_c,3,new QTableWidgetItem(QString("Online"))); }else{ ui->tableWidget->setItem(row_c,3,new QTableWidgetItem(QString("Away"))); } ui->tableWidget->setItem(row_c,1,new QTableWidgetItem(qsname)); ui->tableWidget->setItem(row_c,2,new QTableWidgetItem(qsmac)); } //按钮监听 void MainWindow::on_pushButton_clicked() { ip1 = ui->lineEdit->text(); ip2 = ui->lineEdit_2->text(); QString outtime2 = ui->lineEdit_3->text(); ui->label_6->setText("from:"+ip1); ui->label_7->setText("to:"+ip2); outtime = outtime2.toInt(); int number=0; while((QString::compare(ip1,ip2))<0){ number++; ip1 = toNextIp(ip1); } ipnumber = number; ip1 = ui->lineEdit->text(); int flag=0; while(flag<ipnumber){ nowrow = flag; nowip = ip1; ip1 = toNextIp(ip1); t[flag] = new TestThread();//新建线程 connect(t[flag], SIGNAL(TestSignal(QString,bool,QString,QString)), this, SLOT(DisplayMsg(QString,bool,QString,QString))); //执行子线程 t[flag]->start(); Sleep(100); flag++; } } void MainWindow::on_pushButton_2_clicked() { exit(0); } threadtest.cpp 文件中包含了线程函数的参数传递，以及一下耗时的并发运算：#include "threadtest.h" #include "mainwindow.h" #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <cstring> #include <stdlib.h> #include <stdio.h> #include <string.h> using namespace std; TestThread::TestThread(QObject *parent) : QThread(parent) { } //IP 报头 typedef struct { unsigned char hdr_len:4; //4 位头部长度 unsigned char version:4; //4 位版本号 unsigned char tos; //8 位服务类型 unsigned short total_len; //16 位总长度 unsigned short identifier; //16 位标识符 unsigned short frag_and_flags; //3 位标志加 13 位片偏移 unsigned char ttl; //8 位生存时间 unsigned char protocol; //8 位上层协议号 unsigned short checksum; //16 位校验和 unsigned long sourceIP; //32 位源 IP 地址 unsigned long destIP; //32 位目的 IP 地址 } IP_HEADER; //ICMP 报头 typedef struct { BYTE type; //8 位类型字段 BYTE code; //8 位代码字段 USHORT cksum; //16 位校验和 USHORT id; //16 位标识符 USHORT seq; //16 位序列号 } ICMP_HEADER; //报文解码结构 typedef struct { USHORT usSeqNo; //序列号 DWORD dwRoundTripTime; //往返时间 in_addr dwIPaddr; //返回报文的 IP 地址 } DECODE_RESULT; //计算网际校验和函数 USHORT checksum(USHORT *pBuf,int iSize) { unsigned long cksum=0; while(iSize>1) { cksum+=*pBuf++; iSize-=sizeof(USHORT); } if(iSize) { cksum+=*(UCHAR *)pBuf; } cksum=(cksum>>16)+(cksum&0xffff); cksum+=(cksum>>16); return (USHORT)(~cksum); } //对数据包进行解码，结果封装到Decode当中 /* buf:接收到的buf，字符串 ipacketsize:接收到的数据长度 解析结果封装到DecodeResult ICMP类型， echo_reply是一个常量，放到全局也行 ICMP类型 */ BOOL DecodeIcmpResponse(char * pBuf,int iPacketSize,DECODE_RESULT &DecodeResult,BYTE ICMP_ECHO_REPLY,BYTE ICMP_TIMEOUT) { //检查数据报大小的合法性 IP_HEADER* pIpHdr = (IP_HEADER*)pBuf; //将接收到的数据转换成ip抱头 int iIpHdrLen = pIpHdr->hdr_len * 4;//计算报文长度 //如果接收的报文长度小于IP报文长度，就退出 if (iPacketSize < (int)(iIpHdrLen+sizeof(ICMP_HEADER))) return FALSE; //根据 ICMP 报文类型提取 ID 字段和序列号字段 ICMP_HEADER *pIcmpHdr=(ICMP_HEADER *)(pBuf+iIpHdrLen); //根据字符串结构体指针自动匹配长度 USHORT usID,usSquNo; //如果返回的是回显应答报文 if(pIcmpHdr->type==ICMP_ECHO_REPLY) //ICMP 回显应答报文 { usID=pIcmpHdr->id; //报文 ID usSquNo=pIcmpHdr->seq; //报文序列号 } //如果返回的是超时差错报文 else if(pIcmpHdr->type==ICMP_TIMEOUT)//ICMP 超时差错报文 { char * pInnerIpHdr=pBuf+iIpHdrLen+sizeof(ICMP_HEADER); //载荷中的 IP 头 int iInnerIPHdrLen=((IP_HEADER *)pInnerIpHdr)->hdr_len*4; //载荷中的 IP 头长 ICMP_HEADER * pInnerIcmpHdr=(ICMP_HEADER *)(pInnerIpHdr+iInnerIPHdrLen);//载荷中的 ICMP 头 usID=pInnerIcmpHdr->id; //报文 ID usSquNo=pInnerIcmpHdr->seq; //序列号 } //如果都不是，则退出 else { return false; } //检查 ID 和序列号以确定收到期待数据报 //检查ID是否为当前进程ID，获取的序列号是否为指定序列号 if(usID!=(USHORT)GetCurrentProcessId()||usSquNo!=DecodeResult.usSeqNo) { return false; } //记录 IP 地址并计算往返时间 DecodeResult.dwIPaddr.s_addr=pIpHdr->sourceIP; DecodeResult.dwRoundTripTime=GetTickCount()-DecodeResult.dwRoundTripTime; //处理正确收到的 ICMP 数据报 if (pIcmpHdr->type == ICMP_ECHO_REPLY ||pIcmpHdr->type == ICMP_TIMEOUT) { //输出往返时间信息 if(DecodeResult.dwRoundTripTime) cout<<" "<<DecodeResult.dwRoundTripTime<<"ms"<<flush; else cout<<" "<<"<1ms"<<flush; } return true; } //判断目标IP是否可达 bool tracert(char* IpAddress,int outtime) { bool is_reach = false; //得到 IP 地址 u_long ulDestIP=inet_addr(IpAddress); //转换不成功时按域名解析 if(ulDestIP==INADDR_NONE) { hostent * pHostent=gethostbyname(IpAddress); if(pHostent) { ulDestIP=(*(in_addr*)pHostent->h_addr).s_addr; } else { cout<<"输入的 IP 地址或域名无效!"<<endl; WSACleanup(); return 0; } } cout<<"\nTracing route to "<<IpAddress<<" with a maximum of 30 hops.\n"<<endl; //填充目地端 socket 地址 sockaddr_in destSockAddr; ZeroMemory(&destSockAddr,sizeof(sockaddr_in)); destSockAddr.sin_family=AF_INET;//协议IPV4 destSockAddr.sin_addr.s_addr=ulDestIP;//目的IP地址 //创建原始套接字 SOCKET sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED); //超时时间 int iTimeout=outtime; //接收超时 setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char *)&iTimeout,sizeof(iTimeout)); //发送超时 setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char *)&iTimeout,sizeof(iTimeout)); //构造 ICMP 回显请求消息，并以 TTL 递增的顺序发送报文 //ICMP 类型字段 const BYTE ICMP_ECHO_REQUEST=8; //请求回显 const BYTE ICMP_ECHO_REPLY=0; //回显应答 const BYTE ICMP_TIMEOUT=11; //传输超时 //其他常量定义 const int DEF_ICMP_DATA_SIZE=32; //ICMP 报文默认数据字段长度 const int MAX_ICMP_PACKET_SIZE=1024;//ICMP 报文最大长度（包括报头） const DWORD DEF_ICMP_TIMEOUT=3000; //回显应答超时时间 const int DEF_MAX_HOP=30; //最大跳站数 //填充 ICMP 报文中每次发送时不变的字段 //发送缓冲区 char IcmpSendBuf[sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE];//发送缓冲区 memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf)); //初始化发送缓冲区，用0填充 //接收缓冲区 char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; //接收缓冲区 memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区，用0填充 ICMP_HEADER * pIcmpHeader=(ICMP_HEADER*)IcmpSendBuf; //发送报头 pIcmpHeader->type=ICMP_ECHO_REQUEST; //类型为请求回显,8 pIcmpHeader->code=0; //代码字段为 0 pIcmpHeader->id=(USHORT)GetCurrentProcessId(); //ID 字段为当前进程号 memset(IcmpSendBuf+sizeof(ICMP_HEADER),'E',DEF_ICMP_DATA_SIZE);//数据字段，用'E'填充 USHORT usSeqNo=0; //ICMP 报文序列号 int iTTL=1; //TTL 初始值为 1 BOOL bReachDestHost=FALSE; //循环退出标志 int iMaxHot=DEF_MAX_HOP; //循环的最大次数 30 DECODE_RESULT DecodeResult; //传递给报文解码函数的结构化参数 while(!bReachDestHost&&iMaxHot--) { //设置 IP 报头的 TTL 字段 setsockopt(sockRaw,IPPROTO_IP,IP_TTL,(char *)&iTTL,sizeof(iTTL)); cout<<iTTL<<flush; //输出当前序号 //填充 ICMP 报文中每次发送变化的字段 ((ICMP_HEADER *)IcmpSendBuf)->cksum=0; //校验和先置为 0 ((ICMP_HEADER *)IcmpSendBuf)->seq=htons(usSeqNo++); //填充序列号 ((ICMP_HEADER *)IcmpSendBuf)->cksum=checksum((USHORT *)IcmpSendBuf,sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE); //计算校验和 //记录序列号和当前时间 DecodeResult.usSeqNo=((ICMP_HEADER*)IcmpSendBuf)->seq; //当前序号 DecodeResult.dwRoundTripTime=GetTickCount(); //当前时间 //发送 TCP 回显请求信息 sendto(sockRaw,IcmpSendBuf,sizeof(IcmpSendBuf),0,(sockaddr*)&destSockAddr,sizeof(destSockAddr)); //接收 ICMP 差错报文并进行解析处理 sockaddr_in from; //对端 socket 地址 int iFromLen=sizeof(from); //地址结构大小 int iReadDataLen; //接收数据长度 while(1) { //接收数据 iReadDataLen=recvfrom(sockRaw,IcmpRecvBuf,MAX_ICMP_PACKET_SIZE,0,(sockaddr*)&from,&iFromLen); if(iReadDataLen!=SOCKET_ERROR)//有数据到达 { //对数据包进行解码 if(DecodeIcmpResponse(IcmpRecvBuf,iReadDataLen,DecodeResult,ICMP_ECHO_REPLY,ICMP_TIMEOUT)) { //到达目的地，退出循环 if(DecodeResult.dwIPaddr.s_addr==destSockAddr.sin_addr.s_addr)bReachDestHost=true; //输出 IP 地址 cout<<'\t'<<inet_ntoa(DecodeResult.dwIPaddr)<<endl; if(DecodeResult.dwIPaddr.s_addr == destSockAddr.sin_addr.s_addr) { //cout << inet_ntoa(destSockAddr.sin_addr)<<"可达！"<<endl; is_reach = true; } break; } } else if(WSAGetLastError()==WSAETIMEDOUT) //接收超时，输出*号 { cout<<" *"<<'\t'<<"Request timed out."<<endl; break; } else { break; } } iTTL++; //递增 TTL 值 } return is_reach; } // get mac and pcName int _System(const char * cmd, char *pRetMsg, int msg_len,char *mac,char*name) { FILE * fp; char * p = NULL; int res = -1; char mac1[128]; char name1[128]; if (cmd == NULL || pRetMsg == NULL || msg_len < 0) { printf("Param Error!\n"); return -1; } if ((fp = _popen(cmd, "r")) == NULL) { printf("Popen Error!\n"); return -2; } else { memset(pRetMsg, 0, msg_len); //get lastest result int index = 0; while (fgets(pRetMsg, msg_len, fp) != NULL) { if(index==8){ //printf("Msg:%s", pRetMsg); //print all info // printf(strlen(pRetMsg)); strcpy(mac1,pRetMsg); int m1=0; for(int i=4;mac1[i]!=' ';i++){ mac[m1++] = mac1[i]; } //printf("%s\n",mac); }else if(index==13){ //printf("Msg:%s", pRetMsg); //print all info strcpy(name1,pRetMsg); int n1=0; for(int i=15;name1[i]!=' ';i++){ name[n1++] = name1[i]; } //printf("%s\n",name); } index++; } if ((res = _pclose(fp)) == -1) { printf("close popenerror!\n"); return -3; } pRetMsg[strlen(pRetMsg) - 1] = '\0'; return 0; } } void TestThread::run() { //触发信号 //tnc = 5; //emit TestSignal(nowip,outtime); char *tempIp = (char *)malloc(17); QByteArray ba = nowip.toLatin1(); strcpy(tempIp,ba.data()); cout << tempIp <<endl; bool is_online=false; char mac[50]={0}; char name[50] = {0}; QString qsname; QString qsmac; if(tracert(tempIp,outtime)){ is_online = true; //ui->tableWidget->setItem(row_c,3,new QTableWidgetItem(QString("Online"))); char cmd[50] = "nbtstat -a "; strcat(cmd,tempIp); char a8Result[128] = { 0 }; int ret = 0; ret = _System(cmd, a8Result, sizeof(a8Result),mac,name); if(!ret){ // printf("%s\n",mac); // printf("%s\n",name); qsname = QString::fromLocal8Bit(name); qsmac = QString::fromLocal8Bit(mac); cout << name<<" "<<mac<<endl; //ui->tableWidget->setItem(row_c,1,new QTableWidgetItem(qsname)); //ui->tableWidget->setItem(row_c,2,new QTableWidgetItem(qsmac)); } } TestSignal(nowip,is_online,qsmac,qsname); } 以上便是全部代码了。运行演示程序能正常运行。 ? ? ?(上午更新了获取MAC地址。注意！！！获取MAC地址的方法是代替cmd执行 nbtstat -a [ip]，但是命令只能在Windows 7/XP & Linux上才能运行，也就是说如果在Windows7/XP上执行程序能获取Mac&PcName，在Windows 8/10系统运行不会获取 Mac&PcName ，但不影响程序运行。示例图有个乱码不用介意)
目录：知识储备代码运行演示知识储备此题为我计算机网络期末课程设计的第二题。由于我对GUI的掌握度不高，先暂时不做图形界面。关于ICMP报文和IP报文的详解请阅读教材。以下为需要强调学习的知识：ICMP数据报包括ICMP数据头及ICMP数据部分，IP数据报包括IP数据头及IP数据部分，ICMP数据报将会作为IP数据报的数据部分。ICMP接收的报文种类中其中有两种为：回显应答报文及超时差错报文。利用这两种报文可判断出路由途中的某主机是可达的还是超时的。Tracert的重点是对接收到的数据进行解码，与发送的数据进行对比，判断是否为当前进程发送的数据，以及是否为目的主机返回的数据。代码#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <string> #include <cstring> #include <iostream> #include <stdlib.h> #include <stdio.h> using namespace std; #pragma comment(lib, "Ws2_32.lib") //IP 报头 typedef struct { unsigned char hdr_len:4; //4 位头部长度 unsigned char version:4; //4 位版本号 unsigned char tos; //8 位服务类型 unsigned short total_len; //16 位总长度 unsigned short identifier; //16 位标识符 unsigned short frag_and_flags; //3 位标志加 13 位片偏移 unsigned char ttl; //8 位生存时间 unsigned char protocol; //8 位上层协议号 unsigned short checksum; //16 位校验和 unsigned long sourceIP; //32 位源 IP 地址 unsigned long destIP; //32 位目的 IP 地址 } IP_HEADER; //ICMP 报头 typedef struct { BYTE type; //8 位类型字段 BYTE code; //8 位代码字段 USHORT cksum; //16 位校验和 USHORT id; //16 位标识符 USHORT seq; //16 位序列号 } ICMP_HEADER; //报文解码结构 typedef struct { USHORT usSeqNo; //序列号 DWORD dwRoundTripTime; //往返时间 in_addr dwIPaddr; //返回报文的 IP 地址 } DECODE_RESULT; //计算网际校验和函数 USHORT checksum(USHORT *pBuf,int iSize) { unsigned long cksum=0; while(iSize>1) { cksum+=*pBuf++; iSize-=sizeof(USHORT); } if(iSize) { cksum+=*(UCHAR *)pBuf; } cksum=(cksum>>16)+(cksum&0xffff); cksum+=(cksum>>16); return (USHORT)(~cksum); } //对数据包进行解码，结果封装到Decode当中 /* buf:接收到的buf，字符串 ipacketsize:接收到的数据长度 解析结果封装到DecodeResult ICMP类型， echo_reply是一个常量，放到全局也行 ICMP类型 */ BOOL DecodeIcmpResponse(char * pBuf,int iPacketSize,DECODE_RESULT &DecodeResult,BYTE ICMP_ECHO_REPLY,BYTE ICMP_TIMEOUT) { //检查数据报大小的合法性 IP_HEADER* pIpHdr = (IP_HEADER*)pBuf; //将接收到的数据转换成ip抱头 int iIpHdrLen = pIpHdr->hdr_len * 4;//计算报文长度 //如果接收的报文长度小于IP报文长度，就退出 if (iPacketSize < (int)(iIpHdrLen+sizeof(ICMP_HEADER))) return FALSE; //根据 ICMP 报文类型提取 ID 字段和序列号字段 ICMP_HEADER *pIcmpHdr=(ICMP_HEADER *)(pBuf+iIpHdrLen); //根据字符串结构体指针自动匹配长度 USHORT usID,usSquNo; //如果返回的是回显应答报文 if(pIcmpHdr->type==ICMP_ECHO_REPLY) //ICMP 回显应答报文 { usID=pIcmpHdr->id; //报文 ID usSquNo=pIcmpHdr->seq; //报文序列号 } //如果返回的是超时差错报文 else if(pIcmpHdr->type==ICMP_TIMEOUT)//ICMP 超时差错报文 { char * pInnerIpHdr=pBuf+iIpHdrLen+sizeof(ICMP_HEADER); //载荷中的 IP 头 int iInnerIPHdrLen=((IP_HEADER *)pInnerIpHdr)->hdr_len*4; //载荷中的 IP 头长 ICMP_HEADER * pInnerIcmpHdr=(ICMP_HEADER *)(pInnerIpHdr+iInnerIPHdrLen);//载荷中的 ICMP 头 usID=pInnerIcmpHdr->id; //报文 ID usSquNo=pInnerIcmpHdr->seq; //序列号 } //如果都不是，则退出 else { return false; } //检查 ID 和序列号以确定收到期待数据报 //检查ID是否为当前进程ID，获取的序列号是否为指定序列号 if(usID!=(USHORT)GetCurrentProcessId()||usSquNo!=DecodeResult.usSeqNo) { return false; } //记录 IP 地址并计算往返时间 DecodeResult.dwIPaddr.s_addr=pIpHdr->sourceIP; DecodeResult.dwRoundTripTime=GetTickCount()-DecodeResult.dwRoundTripTime; //处理正确收到的 ICMP 数据报 if (pIcmpHdr->type == ICMP_ECHO_REPLY ||pIcmpHdr->type == ICMP_TIMEOUT) { //输出往返时间信息 if(DecodeResult.dwRoundTripTime) cout<<" "<<DecodeResult.dwRoundTripTime<<"ms"<<flush; else cout<<" "<<"<1ms"<<flush; } return true; } //跳转到下一个IP地址 char *toNextIp(char *tempIp) { char temp[4][4]; int ip1=0; int ip2=0; for(int i=0; i< strlen(tempIp); i++) { if(tempIp[i]=='.') { temp[ip1][ip2] = '\0'; ip1++; ip2=0; continue; } temp[ip1][ip2] = tempIp[i]; ip2++; } temp[ip1][ip2] ='\0'; if(atoi(temp[3])==254) { strcpy(temp[3],"1"); } else { int t1 = atoi(temp[3])+1; itoa(t1,temp[3],10); } char tip[17]; strcpy(tip,temp[0]); for(int i=1; i<4; i++) { strcat(tip,"."); strcat(tip,temp[i]); } //puts(tip); return tip; } int main() { //初始化 Windows sockets 网络环境 WSADATA wsa; WSAStartup(MAKEWORD(2,2),&wsa); char begin_ip[17]; char IpAddress[17]; char end_ip[17]; cout<<"请输入起始IP地址 ："; cin>>begin_ip; cout<<"请输入终止IP地址："; cin>>end_ip; strcpy(IpAddress,begin_ip); while(strcmp(IpAddress,end_ip)!=0) { bool is_reach = false; //得到 IP 地址 u_long ulDestIP=inet_addr(IpAddress); //转换不成功时按域名解析 if(ulDestIP==INADDR_NONE) { hostent * pHostent=gethostbyname(IpAddress); if(pHostent) { ulDestIP=(*(in_addr*)pHostent->h_addr).s_addr; } else { cout<<"输入的 IP 地址或域名无效!"<<endl; WSACleanup(); return 0; } } cout<<"\nTracing route to "<<IpAddress<<" with a maximum of 30 hops.\n"<<endl; //填充目地端 socket 地址 sockaddr_in destSockAddr; ZeroMemory(&destSockAddr,sizeof(sockaddr_in)); destSockAddr.sin_family=AF_INET;//协议IPV4 destSockAddr.sin_addr.s_addr=ulDestIP;//目的IP地址 //创建原始套接字 SOCKET sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED); //超时时间 int iTimeout=3000; //接收超时 setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char *)&iTimeout,sizeof(iTimeout)); //发送超时 setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char *)&iTimeout,sizeof(iTimeout)); //构造 ICMP 回显请求消息，并以 TTL 递增的顺序发送报文 //ICMP 类型字段 const BYTE ICMP_ECHO_REQUEST=8; //请求回显 const BYTE ICMP_ECHO_REPLY=0; //回显应答 const BYTE ICMP_TIMEOUT=11; //传输超时 //其他常量定义 const int DEF_ICMP_DATA_SIZE=32; //ICMP 报文默认数据字段长度 const int MAX_ICMP_PACKET_SIZE=1024;//ICMP 报文最大长度（包括报头） const DWORD DEF_ICMP_TIMEOUT=3000; //回显应答超时时间 const int DEF_MAX_HOP=30; //最大跳站数 //填充 ICMP 报文中每次发送时不变的字段 //发送缓冲区 char IcmpSendBuf[sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE];//发送缓冲区 memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf)); //初始化发送缓冲区，用0填充 //接收缓冲区 char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; //接收缓冲区 memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区，用0填充 ICMP_HEADER * pIcmpHeader=(ICMP_HEADER*)IcmpSendBuf; //发送报头 pIcmpHeader->type=ICMP_ECHO_REQUEST; //类型为请求回显,8 pIcmpHeader->code=0; //代码字段为 0 pIcmpHeader->id=(USHORT)GetCurrentProcessId(); //ID 字段为当前进程号 memset(IcmpSendBuf+sizeof(ICMP_HEADER),'E',DEF_ICMP_DATA_SIZE);//数据字段，用'E'填充 USHORT usSeqNo=0; //ICMP 报文序列号 int iTTL=1; //TTL 初始值为 1 BOOL bReachDestHost=FALSE; //循环退出标志 int iMaxHot=DEF_MAX_HOP; //循环的最大次数 30 DECODE_RESULT DecodeResult; //传递给报文解码函数的结构化参数 while(!bReachDestHost&&iMaxHot--) { //设置 IP 报头的 TTL 字段 setsockopt(sockRaw,IPPROTO_IP,IP_TTL,(char *)&iTTL,sizeof(iTTL)); cout<<iTTL<<flush; //输出当前序号 //填充 ICMP 报文中每次发送变化的字段 ((ICMP_HEADER *)IcmpSendBuf)->cksum=0; //校验和先置为 0 ((ICMP_HEADER *)IcmpSendBuf)->seq=htons(usSeqNo++); //填充序列号 ((ICMP_HEADER *)IcmpSendBuf)->cksum=checksum((USHORT *)IcmpSendBuf,sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE); //计算校验和 //记录序列号和当前时间 DecodeResult.usSeqNo=((ICMP_HEADER*)IcmpSendBuf)->seq; //当前序号 DecodeResult.dwRoundTripTime=GetTickCount(); //当前时间 //发送 TCP 回显请求信息 sendto(sockRaw,IcmpSendBuf,sizeof(IcmpSendBuf),0,(sockaddr*)&destSockAddr,sizeof(destSockAddr)); //接收 ICMP 差错报文并进行解析处理 sockaddr_in from; //对端 socket 地址 int iFromLen=sizeof(from); //地址结构大小 int iReadDataLen; //接收数据长度 while(1) { //接收数据 iReadDataLen=recvfrom(sockRaw,IcmpRecvBuf,MAX_ICMP_PACKET_SIZE,0,(sockaddr*)&from,&iFromLen); if(iReadDataLen!=SOCKET_ERROR)//有数据到达 { //对数据包进行解码 if(DecodeIcmpResponse(IcmpRecvBuf,iReadDataLen,DecodeResult,ICMP_ECHO_REPLY,ICMP_TIMEOUT)) { //到达目的地，退出循环 if(DecodeResult.dwIPaddr.s_addr==destSockAddr.sin_addr.s_addr)bReachDestHost=true; //输出 IP 地址 cout<<'\t'<<inet_ntoa(DecodeResult.dwIPaddr)<<endl; if(DecodeResult.dwIPaddr.s_addr == destSockAddr.sin_addr.s_addr) { //cout << inet_ntoa(destSockAddr.sin_addr)<<"可达！"<<endl; is_reach = true; } break; } } else if(WSAGetLastError()==WSAETIMEDOUT) //接收超时，输出*号 { cout<<" *"<<'\t'<<"Request timed out."<<endl; break; } else { break; } } iTTL++; //递增 TTL 值 } cout << "主机号："<<IpAddress << (is_reach? " 在线":" 不在线") <<endl; strcpy(IpAddress,toNextIp(IpAddress)); } }运行演示注意：范围测试只适用于局域网。输入ip范围为 *.*.*.1~*.*.*.254 。 255为广播地址。返回结果为对每个地址的tracert路径，及目的IP地址是否在线。
基于Socket通信原理。原理请点击 Socket目录:添加库代码运行结果1、添加库编辑环境为CodeBlocks。首先添加要用的库。具体操作为：Settings --> Compiler Settings --> Linker settings，然后点击Add，找到路径 E:\CodeBlocks\MinGW\lib\libws2_32.a （根据CB安装的路径不同自行改变），在右侧输入 -lws2_32，点击OK保存，重启CodeBlocks添加成功。2、代码Server.cpp#include <stdio.h> #include <Winsock2.h> #pragma comment(lib, "WS2_32") // 链接到WS2_32.lib using namespace std; #include <fstream> #include <string> #include <iostream> DWORD WINAPI RECVm(void *Param){ char sendBuf[50];//缓冲区 char recvBuf[50];//缓冲区 char username[50]={0}; // 用户名 SOCKET sockConn = *((SOCKET *)Param); strcpy(sendBuf,"Join in chatroom success!You can start chatting...\n"); send(sockConn,sendBuf,strlen(sendBuf)+1,0);//将数据原样返回 recv(sockConn,recvBuf,50,0); strcpy(username,recvBuf); printf("%s has join in chatting room.\n",username); while(1) { //接收 if(recv(sockConn,recvBuf,50,0)<=0){ printf("%s has exit!\n",username); break; } if(recvBuf!=""){ printf("%s: %s\n",username,recvBuf); } strcpy(recvBuf,""); } closesocket(sockConn); //closesocket(sockSrv); return 0; } int main() { printf("Welcome to chatting room.....\n"); WORD wVersionRequested;//wVersionRequested 是我们要求使用的 WinSock 的版本。 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ){ return 0;} if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ){ WSACleanup( ); return 0;} //创建套接字 SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0); //创建sockaddr_in结构体变量,绑定套接字 SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//通讯地址 addrSrv.sin_family=AF_INET; //sin_family必须为AF_INET,ipv4 addrSrv.sin_port=htons(6000); //端口号 //将套接字和 IP,端口 绑定 bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)); //进入监听状态，后台等待连接队列的最小限制值5 listen(sockSrv,5); //接收 SOCKADDR_IN addrClient; int len=sizeof(SOCKADDR); while(1){ SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len); CreateThread(NULL,0,RECVm,&sockConn,0,NULL); } }Client.cpp#include <iostream> #include <stdio.h> #include <Winsock2.h> #pragma comment(lib, "WS2_32") using namespace std; // int main() { //初始化 WORD wVersionRequested; //版本 WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 1, 1 ); //主版号为1，副版号为1 err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return 0; } if ( LOBYTE( wsaData.wVersion ) != 1 || HIBYTE( wsaData.wVersion ) != 1 ) { WSACleanup( ); return 0; } //创建套接字 SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(6000); //创建连接 connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)); //发送 char yourname[50]={0}; printf("Please enter your username: "); gets(yourname); send(sockClient,yourname,strlen(yourname)+1,0); char recvBuf[50]; recv(sockClient,recvBuf,50,0); printf("Response: %s\n",recvBuf); while(1){ printf("Enter Messages: "); gets(yourname); send(sockClient,yourname,strlen(yourname)+1,0); //recv(sockClient,recvBuf,50,0); //printf("Response: %s\n",recvBuf); } //char recvBuf[50]; //recv(sockClient,recvBuf,50,0); //printf("Response: %s\n",recvBuf); closesocket(sockClient); WSACleanup(); } 3、运行程序先运行Server程序，然后再运行Client程序，可运行多个Client程序，即一个服务端，多个客户端。效果图：