网络调试助手 程序设计

点此在我的博客中查看原文,显示效果更佳

NetAssist_PyQt 项目已开源分享至GitHub,如果这个项目和这篇博客对你有帮助的话,希望你能给我的GitHub仓库一颗小星星✨

0.序

寒假学习了计算机网络方面的知识,把之前稍有了解的socket编程进一步学习,加之从夏天学到冬天一直在学一直没学完的PyQt5终于学到70%入门了,于是萌生了给自己做一个好看又好用的网络调试助手小工具的想法,把socket编程、面向对象编程、PyQt编程、逻辑与界面分离、git多分支等新知识运用在实践中。也便于未来写自己的应用层程序时调试。

1.基本设计与项目结构,逻辑界面分离

实现一个“网络调试助手”程序,要求可以作为TCP服务端、TCP客户端、UDP服务端、UDP客户端接收发送信息,还具有重复发送、16进制发送接收、保存接收信息到txt文件等功能。

尽可能实现逻辑与界面分离:网络功能的逻辑界面功能的逻辑纯UI代码分离。

即,网络模块只需略微修改一两行事件机制的代码即可移植到其他任何程序、在界面功能逻辑增加功能不会对其他部分造成影响、通过QtDesigner修改UI布局不会对其他部分产生影响。

UI控件与布局

纯UI界面由Qt Designer设计生成MainWindowUI.ui文件后用pyuic5转换为Python代码(MainWindowUI.py),只负责控件的显示与布局;

Qt Designer工具可以可视化实时编辑控件与布局,也可以实现比较细致的调整

界面逻辑

界面逻辑由MainWindowLogic.py实现,包括用户输入的检查、计数器的实现、重复发送、16进制发送、保存数据到txt等等。

例如,当用户点击“连接网络”按钮,先由这部分代码对用户输入的IP地址端口号等进行获取、检查,再结合协议类型判断连接类型,最后把确认无误的连接信息发送到网络部分进行真正的连接。这样就简化了网络部分的代码。

也有部分高级控件的功能是通过对Qt原生控件的重写实现的,保存在UI.MyWidgets.py中,方便其他项目复用。比如带有IP地址输入验证功能的LineEdit、复位计数按钮是一个可以点击的Label

网络功能逻辑

在Network包的三个模块下实现网络连接功能。Tcp.py包括TCP服务端、TCP客户端的连接建立、发送数据、断开连接等;Udp.py除了UDP服务端客户端,还有一个获得本机IP地址的函数get_host_ip;WebServer实现了一个简易的Web服务器。

# Network.__init__.py
from Network.Udp import get_host_ip
from Network.Tcp import TcpLogic
from Network.Udp import UdpLogic
from Network.WebServer import WebLogicclass NetworkLogic(TcpLogic, UdpLogic, WebLogic):pass

网络模块只有信息反馈(事件处理)使用了PyQt5中的信号pyqtSignal,也就是说,如果用其他GUI甚至Flask实现了界面,只需要改动几行代码即可把Network全部功能完美移植过去

界面与功能连接

在main.py中进行界面与网络功能的连接。通过类的多继承获得具有完整逻辑功能的界面和网络功能,再通过信号与槽的连接实现界面与网络功能的连接。

class MainWindow(WidgetLogic, NetworkLogic):# 使用多继承,获得具有逻辑功能的界面WidgetLogic和NetworkLogic的网络功能def __init__(self, parent=None):super().__init__(parent)# 进行了许多界面逻辑信号与网络逻辑功能槽函数的连接self.link_signal.connect(self.link_signal_handler)self.disconnect_signal.connect(self.disconnect_signal_handler)self.send_signal.connect(self.send_signal_handler)self.tcp_signal_write_msg.connect(self.msg_write)self.tcp_signal_write_info.connect(self.info_write)self.udp_signal_write_msg.connect(self.msg_write)self.udp_signal_write_info.connect(self.info_write)self.signal_write_msg.connect(self.msg_write)

2.代码解读

对部分我认为很好玩的代码做个简单说明

获取本机IP地址

最初的设想是在Ubuntu上用ifconfig 加一些管道来截取仅含本机IPv4地址的字符串,在Windows用ipconfig如法炮制。经过一番努力,完美的失败了。换一个思路,打开搜索引擎搜索“Python 获取本机IP地址”,于是我得到了下面这段精巧的代码

# Network.UdpLogic.py
import socket
def get_host_ip() -> str:"""获取本机IP地址"""try:s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)s.connect(('8.8.8.8', 80))ip = s.getsockname()[0]finally:s.close()return ip

如果直接用socket.gethostbyname(socket.gethostname())获取地址,很有可能是错误的(Vmware虚拟机的地址、127.0.0.1等)

通过UDP尝试连接’8.8.8.8:80’,不管是否连接成功,获得的本机IP一定是正确的。在Ubuntu和Windows上都可用,还省去了判断操作系统的大段代码。

TCP连接中的shutdown

Python官方文档中socket.close()方法下面还有一个小Note

Note

close() releases the resource associated with a connection but does not necessarily close the connection immediately. If you want to close the connection in a timely fashion, call shutdown() before close().

注解

close() 释放与连接相关联的资源,但不一定立即关闭连接。如果需要及时关闭连接,请在调用 close() 之前调用 shutdown()

在close()前显式调用shutdown()方法,以实现立即关闭连接,这可以解决我之前遇到的问题:明明TCP客户端已经关闭,但服务端仍尝试与其发送消息

下面是文档中shutdown方法的部分:

socket. shutdown(how)

Shut down one or both halves of the connection. If how is SHUT_RD, further receives are disallowed. If how is SHUT_WR, further sends are disallowed. If how is SHUT_RDWR, further sends and receives are disallowed.

socket. shutdown(how)

关闭一半或全部的连接。如果 howSHUT_RD,则后续不再允许接收。如果 howSHUT_WR,则后续不再允许发送。如果 howSHUT_RDWR,则后续的发送和接收都不允许。

所以在我的代码中,在socket.close()之前加上一行socket.shutdown(socket.SHUT_RDWR) 即可

# Network.Tcp.py
class TcpLogic:def tcp_close(self) -> None:"""功能函数,关闭网络连接的方法"""if self.link_flag == self.ServerTCP:for client, address in self.client_socket_list:client.shutdown(socket.SHUT_RDWR)  # 显式调用shutdown方法client.close()self.client_socket_list = list()self.tcp_socket.close()msg = '已断开网络\n'self.tcp_signal_write_msg.emit(msg)elif self.link_flag == self.ClientTCP:self.tcp_socket.shutdown(socket.SHUT_RDWR)  # 显式调用shutdown方法self.tcp_socket.close()msg = '已断开网络\n'self.tcp_signal_write_msg.emit(msg)

强制关闭线程的代码

# Network.StopThreading.pyimport ctypes
import inspect# 强制关闭线程的方法
def _async_raise(tid, exc_type):tid = ctypes.c_long(tid)if not inspect.isclass(exc_type):exc_type = type(exc_type)res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exc_type))if res == 0:raise ValueError("invalid thread id")elif res != 1:ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)raise SystemError("PyThreadState_SetAsyncExc failed")def stop_thread(thread):_async_raise(thread.ident, SystemExit)

UI控件布局Form类的继承

通过Qt Designer生成的MainWindowUI.py中只有一个Ui_Form类,下有setupUiretranslateUi两个方法,前者记录了所有控件布局信息,后者记录界面上的所有文字内容(方便实现中文英语等多语言翻译切换)。

# UI.MainWindowUI.py# Created by: PyQt5 UI code generator 5.15.2class Ui_Form(object):def setupUi(self, Form):# Ui_Form类本身没有Widget控件,需要在调用setupUi方法时传入需要被Ui_Form类布局的窗口FormForm.setObjectName("Form")Form.resize(700, 570)Form.setMinimumSize(QtCore.QSize(600, 500))# ......def retranslateUi(self, Form):# 所有界面上的文字都是在这个方法中设置的,这是为了方便软件实现国际化多语言_translate = QtCore.QCoreApplication.translateForm.setWindowTitle(_translate("Form", "网络调试助手"))self.ProtocolTypeLabel.setText(_translate("Form", "协议类型"))self.ProtocolTypeComboBox.setItemText(0, _translate("Form", "TCP"))# ......

Ui_Form类并无QWidget窗口,需要在MainWindowLogic.py中创建一个继承自QWidget的QmyWidget类,把这个类实例传入Ui_Form.setupUi方法中。

# MainWindowLogic.py
from UI import MainWindowUIclass WidgetLogic(QWidget):def __init__(self, parent=None):super().__init__(parent)  # 调用父类构造函数,创建QWidget窗体self.__ui = MainWindowUI.Ui_Form()  # 把Ui_Form设置为QmyWidget的私有属性self.__ui.setupUi(self)  # 调用setupUi()函数创建UI窗体self.__ui.retranslateUi(self)  # 设置文字内容

显示创建MainWindowUI类的私有属性self.__ui,包含了可视化设计的窗体上的所有组件。只有通过 self.__ui才能访问窗体上的组件,外部无法访问,更符合面向对象封装隔离的设计思想

IP地址输入框的验证器

为IP地址输入框专门写了IPv4AddrLineEdit类,使得用户在输入IP地址时,只有键盘输入正确的格式才能真正输入,比如按下键盘上字母键是没有效果的。同时方便起见,把中文输入法输入的句号也自动转化成英文输入法的句点.

(可以参考前面的博文PyQt5 输入验证器-正则方式)

# UI.MyWidgets.pyclass IPv4AddrLineEdit(QLineEdit):"""带有验证输入IPv4地址功能的LineEdit"""class IPValidator(QRegExpValidator):def validate(self, inputs: str, pos: int) -> [QValidator.State, str, int]:# 重写validate方法以实现可以自动把中文句号转化为英文句点的功能inputs = inputs.replace('。', '.')return super().validate(inputs, pos)# 一串神秘的正则表达式,据说可以验证IPv4类型的地址reg_ex = QRegExp("((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)")def __init__(self, parent=None):super().__init__(parent)ip_input_validator = self.IPValidator(self.reg_ex, parent)  # 实例化一个验证器对象self.setValidator(ip_input_validator)  # 为LineEdit设置验证器

类似的,也为端口号的输入设置了验证器

# UI.MyWidgets.pyclass PortLineEdit(QLineEdit):"""带有验证器的端口号输入LineEdit"""class PortValidator(QIntValidator):# 重写整数型验证器来实现更精确的控制def fixup(self, inputs: str) -> str:if len(inputs) == 0:return ''  # 防止输入框为空时报错elif int(inputs) > 65535:return '7777'  # 如果用户输入的内容无效,则焦点离开后内容自动变成7777return inputsdef __init__(self, parent=None):super().__init__(parent)validator = self.PortValidator(0, 65535, parent)  # 确保端口号为int整数、范围合理self.setValidator(validator)

然后在Qt Designer中把对应的控件进行提升即可

连接状态的标识

通过self.link_flag属性保存当前连接状态。界面逻辑的类WidgetLogic和网络功能的类NetworkLogic中都有这个属性:前者根据用户操作变化其值,后者根据其值实现对应网络功能。
self.link_flag的值主要由WidgetLogic下的方法来设置:
# MainWindowLogic.pyclass WidgetLogic(QWidget):def __init__(self, parent=None):# ......self.link_flag = self.NoLink  # 初始化连接状态为未连接self.protocol_type = 'TCP'# ......def click_link_handler(self):"""连接按钮连接时的槽函数"""# 一些获取用户输入的代码,在此省略# ......if self.protocol_type == "TCP" and server_flag:self.link_flag = self.ServerTCP  # 把连接状态置为TCP服务端elif self.protocol_type == "TCP" and not server_flag:self.link_flag = self.ClientTCP  # 把连接状态置为TCP客户端elif self.protocol_type == "UDP" and server_flag:self.link_flag = self.ServerUDP  # 把连接状态置为UDP服务端elif self.protocol_type == "UDP" and not server_flag:self.link_flag = self.ClientUDP  # 把连接状态置为UDP客户端elif self.protocol_type == "Web Server" and server_flag and self.dir:self.link_flag = self.WebServer  # 连接状态置为WebServerdef click_disconnect(self):"""实现断开连接的功能函数"""# ......self.link_flag = self.NoLink  # 断开连接后把连接状态重置为未连接# 把int类型的标识位保存在类属性中,用self.NoLink替换-1,增强代码可读性NoLink = -1ServerTCP = 0ClientTCP = 1ServerUDP = 2ClientUDP = 3WebServer = 4

有了连接状态标识,就可以把目前的连接状态作为许多操作的判断依据,比如:

# Network.Tcp.pyclass TcpLogic:def __init__(self):# ......self.link_flag = self.NoLink  # 用于标记是否开启了连接def tcp_send(self, send_msg):"""功能函数,用于TCP服务端和TCP客户端发送消息"""# ......if self.link_flag == self.ServerTCP:# 向所有连接的客户端发送消息if self.client_socket_list:for client, address in self.client_socket_list:client.send(send_info_encoded)msg = 'TCP服务端已发送'self.tcp_signal_write_msg.emit(msg)self.tcp_signal_write_info.emit(send_info, self.InfoSend)if self.link_flag == self.ClientTCP:self.tcp_socket.send(send_info_encoded)msg = 'TCP客户端已发送'self.tcp_signal_write_msg.emit(msg)self.tcp_signal_write_info.emit(send_info, self.InfoSend)def tcp_close(self):"""功能函数,关闭网络连接的方法"""if self.link_flag == self.ServerTCP:# 断开TCP服务端连接的代码for client, address in self.client_socket_list:# 先关闭所有已连接的客户端client.shutdown(2)client.close()self.client_socket_list = list()  # 把已连接的客户端列表重新置为空列表# 再关闭服务端self.tcp_socket.close()msg = '已断开网络\n'self.tcp_signal_write_msg.emit(msg)# ...停止线程的代码...elif self.link_flag == self.ClientTCP:# 断开TCP客户端连接的代码self.tcp_socket.shutdown(2)self.tcp_socket.close()msg = '已断开网络\n'self.tcp_signal_write_msg.emit(msg)# ...停止线程的代码...NoLink = -1ServerTCP = 0ClientTCP = 1

通过self.link_flag实现了分用,不管Server还是Client,发送消息断开连接时调用的函数都是同一个。同理,main.py中,通过标识实现断开连接分用。

# main.pyclass MainWindow(WidgetLogic, NetworkLogic):def disconnect_signal_handler(self):"""断开连接的槽函数"""if self.link_flag == self.ServerTCP or self.link_flag == self.ClientTCP:self.tcp_close()elif self.link_flag == self.ServerUDP or self.link_flag == self.ClientUDP:self.udp_close()elif self.link_flag == self.WebServer:self.web_close()

该值除了在网络部分有应用,也用在一些界面逻辑的控制上,比如:

# MainWindowLogic.pyclass WidgetLogic(QWidget):def open_file_handler(self):"""打开文件按钮的槽函数"""if self.link_flag in [self.ServerTCP, self.ClientTCP, self.ClientUDP]:# 如果连接状态为TCP服务端/客户端、UDP客户端,则“打开文件”按钮功能为打开文本文件# ......  打开文本文件加载到发送输入框的代码 ......elif self.link_flag == self.NoLink and self.protocol_type == 'Web Server':# 如果连接状态为WebServer,则按钮功能为选择工作目录self.dir = QFileDialog.getExistingDirectory(self, "选择index.html所在路径", './')self.__ui.SendPlainTextEdit.clear()self.__ui.SendPlainTextEdit.appendPlainText(str(self.dir))self.__ui.SendPlainTextEdit.setEnabled(False)# 如果连接状态为未连接或UDP服务端等,则按钮无作用

用户输入检查

我的软件思路是,如果只输入本机端口号,则作为Server启动,绑定这个端口;如果只输入目标IP和目标端口,则作为Client启动,向该IP端口发送数据。所以必须对用户的异常输入(如只输入目标端口)进行处理:

未输入任何信息

# MainWindowLogic.py
def click_link_handler(self):"""连接按钮连接时的槽函数"""if my_port == -1 and target_port == -1 and target_ip == '':mb = QMessageBox(QMessageBox.Critical, '错误', '请输入信息', QMessageBox.Ok, self)mb.open()self.editable(True)  # 恢复可编辑状态self.__ui.ConnectButton.setChecked(False)  # 恢复连接按钮状态# 提前终止槽函数return None

仅输入目标IP

    elif target_port == -1 and target_ip != '':input_d = PortInputDialog(self)  # 在UI.MyWidgets中定义,具有端口号检查功能input_d.setWindowTitle("服务启动失败")input_d.setLabelText("请输入目标端口号作为Client启动,或取消")input_d.intValueSelected.connect(lambda val: self.__ui.TargetPortLineEdit.setText(str(val)))input_d.open()self.__ui.ConnectButton.setChecked(False)# 提前终止槽函数return None

仅输入目标端口

    elif target_port != -1 and target_ip == '':mb = QMessageBox(QMessageBox.Critical, 'Client启动错误', '请输入目标IP地址', QMessageBox.Ok, self)mb.open()self.__ui.ConnectButton.setChecked(False)# 提前终止槽函数return None

同时输入了本机端口、目标IP、目标端口

WebServer未选择工作目录

如果连接之前没有使用“选择路径”按钮选择工作目录,会在按下“连接网络”按钮时弹出文件夹选择对话框

def click_link_handler(self):"""连接按钮连接时的槽函数"""if self.protocol_type == "Web Server" and not self.dir:# 处理用户未选择工作路径情况下连接网络self.dir = QFileDialog.getExistingDirectory(self, "选择index.html所在路径", './')if self.dir:self.__ui.SendPlainTextEdit.clear()self.__ui.SendPlainTextEdit.appendPlainText(str(self.dir))self.__ui.SendPlainTextEdit.setEnabled(False)else:# 如果用户在弹出的文件夹选择对话框中选择了取消,则重置状态self.__ui.ConnectButton.setChecked(False)return None

3.版本计划

希望在下一版本加入以下功能:

  • 【重要】优化网络模块,不能对抛出的异常视而不见

  • HEX 16进制收发信息

  • 最小化到托盘

  • 保存上一次的状态

如果有小伙伴对此感兴趣,欢迎提交PR

4.致谢

Network包下的模块借鉴了Wangler2333 的开源项目 tcp_udp_web_tools-pyqt5

QSS美化的代码来自飞扬青云 的 QWidgetDemo 项目

在此表示感谢

网络调试助手-程序设计-PyQt5实战 (Python socket GUI)相关推荐

  1. UE4 TCP通信 (UE客户端与网络调试助手服务端、python服务端通信)

    目录 一.使用UE4建立TCP客户端 二.使用网络调试助手建立服务端 三.基于网络调试助手的服务端与UE客户端通信 四.基于python的TCP服务端与UE客户端通信 一.使用UE4建立TCP客户端 ...

  2. Python网络编程(1.利用socket(udp)+网络调试助手,发送数据)

    1.socket(简称 套接字)  是进制间通信的一种方式,它与其他进程间通信的一个主要不同是: 它能实现不同主机间的进程间通信,我们网络上各种各样的服务器大多数都是基于Socket 来完成通信的 2 ...

  3. python写网络调试助手_Qt开源作品4-网络调试助手

    ## 一.前言 网络调试助手和串口调试助手是一对的,用Qt开发项目与硬件通信绝大部分都是要么串口通信(RS232 RS485 Modbus等),要么就是网络通信(TCP UDP HTTP等),所以一旦 ...

  4. Android tcp与网络调试助手初入了解

    项目需要,用到Android作为客户端,电脑作为服务端,进行文件传输.记录一下自己第一次使用tcp建立通信的测试例子. 仅供第一次接触tcp/udp的初学者,参考,注意电脑和手机必须在同一个局域网下, ...

  5. 【Zynq UltraScale+ MPSoC】基于LWIP模板的udp通信与测试(一):网络调试助手和PS端的简单通信

    文章目录 一.前言 二.PL端的配置 三.PS端的程序设计 1.LWIP的UDP服务器模板介绍 readme main.c udp_perf_server platform_zynqmp.c 2.具体 ...

  6. Windows下使用C语言创建定时器并周期和网络调试助手通信

    在Windows C下采用timeSetEvent函数来设置定时器 关于timeSetEvent的函数原型及注释如下所示: MMRESULT timeSetEvent(UINT uDelay, // ...

  7. Windows下使用C语言的UDP编程接收网络调试助手发送的数据

    代码 #include <stdio.h> #include <winsock2.h> #pragma comment (lib, "ws2_32.lib" ...

  8. Windows下使用C语言的周期UDP编程同时发送和接收网络调试助手数据

    代码: #include <Windows.h> //需要包含该头文件 #include <stdio.h>#pragma comment(lib,"Winmm.li ...

  9. 【上位机】通过QTCreator编写WIFI上位机与网络调试助手通信绘制曲线

    文章目录 前言 一.使用QT Creator编写上位机 二.上位机与网络调试助手联调 三.总结 前言 17年电赛H题中要求编写WIFI上位机实现远程幅频特性曲线显示, 以下是本人在近期摸索出来的一些心 ...

最新文章

  1. 如何kill同一个应用的所有进程
  2. java json 去重_js操作两个json数组合并、去重,以及删除某一项元素
  3. GridView列值绑定
  4. NOIP2017TG D1T2 时间复杂度
  5. 计算机学院 年度工作计划,计算机教研组年度工作计划
  6. 设计模式学习(五):行为型模式
  7. win11鼠标怎么在轮滑时只滚动一个屏幕 Windows11鼠标设置轮滑只滚动一个屏幕的步骤方法
  8. PHP两种redirect
  9. linux 系统清理工具下载,五款最佳Linux文件系统清理工具
  10. Amplify Shader Editor手册
  11. 【附白皮书下载】专家黄正杰:从微笑曲线出发,思考制造业数字化转型方向
  12. Flutter 倒计时
  13. javaweb-day03-7(基础加强-泛型)
  14. QT 自定义加载等待(Loading)提示框
  15. 有没有好用的证件照生成器?教你一键生成证件照
  16. RTSP基础之RTSP/RTP推流协议流程
  17. 使用openssl生成https证书
  18. matlab gain使用,matlabgain模块
  19. 孤立词语音识别(1)——利用HMM-GMM模型实现数字识别(完整收发系统)
  20. NtripShare EdgeEngine GNSS边缘解算盒子/模块/软件用户手册

热门文章

  1. Windows explorer.exe是啥?
  2. Swift2.2 学习笔记(十二) ___控制流
  3. Tomcat debug 配置
  4. LSM(Log-Structured Merge Tree)
  5. 程序员必读的30本书单--超级推荐
  6. 东南大学信息学院考研经验
  7. 【借鉴/转载】WSI的处理
  8. 一个完整的机器学习项目需要哪些步骤
  9. java none怎么用tomcat_关于如何在Tomcat中使用JavaBean
  10. 计蒜客一月入门赛:《三个火枪手》题解