一、引言
在工作中,我们总会遇到这样的需求:
我们需要向服务端程序发送指定格式的报文,然后服务端进行一定的处理之后,再向我们发回处理后的报文。
或者说,我们总会需要:
在编写服务端运行的程序的时候,总想要有一个能够模拟接收报文、监控报文并且还能够回复报文的一个客户端程序,这样就可以方便我们调试编写我们的服务端运行的程序逻辑。
前者是编写客户端程序的视角,后者是编写服务端程序的视角。
不管怎么说,我们总会想要一个报文模拟器,能够接收、监控、发送报文,可以方便我们客户端与服务端报文交互相关的开发测试工作。
我也一直有这么一个想法,正好最近遇到了一个报文格式相对比较复杂的项目,不自己编写一个报文模拟器的话,服务端程序的逻辑调试将会非常麻烦。
因此,我对这个报文模拟器提出了自己的需求:
- 可以接收服务端发来的报文信息
- 可以向服务端发送指定的报文信息
- 可以滚动监控报文的接收与发送
- 可以编辑报文保存到本地,也可以从本地读取报文
- 为了让这个项目自成一体,最好加上一个自行编写的简单的服务端程序
于是,经过了几天的鏖战,我终于算是非常简陋的实现了上述的需求:
那么,现在,就让我们一起来实现这个报文模拟器吧:)
ps: 对这个项目的代码感兴趣的同学,可以来我的 GitHub
wangying2016/Packet_Simulator
二、需求分析:选择 Python3 & Tkinter
既然要做一个报文模拟器,也给自己提出了引言中提到的 5 个需求,那么我们就需要思考如果去实现它。
1. 技术选择
技术上,这里我选择了 Python3 & Tkinter,至于为什么。。。
因为我喜欢 Python3 & Tkiner 呀 :)
不过话说回来,对于我们程序员来说,一些工作中的辅助工具,当然是编写难度越简单越好,编写代码量越少越好,选择的语言当是库越强大越好,其语言原生支持的 UI 库也能基本达到我们的需求即可。
于是乎,我选择了 Python3,另外又因为 Tkinter 是 Python3 原生支持的跨平台的 UI 库,其强大而又简单易学,因此实现的工具就这么决定下来了。
2. 业务选择
有些人可能会觉得奇怪,我们编写一个小工具,为什么还涉及到业务的选择呢?
这是当然的,尽管我们只是想要编写一个报文模拟器,但是我们需要构建一个简单的场景去让它跑起来。这个场景中,包括我们服务端的程序,包括我们需要去定义的服务端与客户端交互的报文格式,这些都是需要定义好的。
另外,我们将自己的业务定义的越简单越具有代表性,那么后续我们将这个报文模拟器移植到具体的项目中使用的时候,也好进行扩展,比如说服务端与客户端交互的报文格式,涉及到报文的加解密方式、报文的读取与写入等等逻辑,这些都是需要根据具体的项目进行个性化扩展的。
这里,我们简单定义以下的业务规则:
- 场景范围
报文的发送使用短连接的方式进行,也就是每次发送完报文即关闭 socket 连接。另外,不论是服务端还是客户端,都需要保持一个长时间监听的 socket 连接,主要负责对对方发送的报文的监听。 - 报文格式
报文的格式因项目而异,这里我们作为实验项目,定义最简单的就好了,这里我定义如下:
我们做好了技术和业务上的选择,剩下来就是进行代码的编写了,从哪部分开始呢?
当然是 UI。
三、界面设计:强大而又简单的 Tkinter
在我的代码文件 Sim_Sender.py 中,类 GUI 是用来实现界面设计的关键的类。
其中的 __init__
方法用来初始化界面布局,布局包括一个输入框、一个文本框、三个按钮以及一个滚动文本框。
另外,控件所绑定的触发函数也定义在 GUI 类中,还包括本地报文文件的读和写,以及按钮的触发函数、滚动文本框的内容的写入等等。
这里,我挑几个重药的地方解释一下吧。
1. 窗口建立与初始化
Tkinter 中的窗口建立是通过 Tk() 函数进行的,其返回的值类似于窗口句柄,可以进行窗口大小和位置的设定。
继承于 Frame(框架类)的 GUI 类是窗口布局的控制类,其中初始化了我们界面上显示的这种种的控件。
if __name__ == '__main__':
...
# Gui
root = Tk()
app = GUI()
...
root.geometry("350x600+300+300")
root.mainloop()
代码中,在定义了窗口位置和大小之后,root.mainloop()
就是我们 Windows 编程中最熟悉的窗口消息循环啦:)
窗口的布局使用的是 pack 布局方式(Tkinter 支持 pack/grid/place 三种布局)。布局最核心的代码当然是 GUI 类中的 init_ui() 函数了:
def init_ui(self):
self.master.title("报文模拟器")
self.pack(fill=BOTH, expand=True)
self.frame1 = Frame(self)
self.frame1.pack(fill=X, expand=True)
lbl1 = Label(self.frame1, text="识别代码", width=10)
lbl1.pack(side=LEFT, padx=5, pady=5)
self.entry = Entry(self.frame1)
self.entry.pack(fill=X, padx=5, expand=True)
self.frame2 = Frame(self)
self.frame2.pack(fill=X, expand=True)
lbl2 = Label(self.frame2, text="报文内容", width=10)
lbl2.pack(side=LEFT, anchor=N, padx=5, pady=5