Py 的魔力转圈圈:让我们一起用 Python3 & Tkinter 实现一个报文模拟器(附简单服务端代码)

一、引言

在工作中,我们总会遇到这样的需求:

我们需要向服务端程序发送指定格式的报文,然后服务端进行一定的处理之后,再向我们发回处理后的报文。

或者说,我们总会需要:

在编写服务端运行的程序的时候,总想要有一个能够模拟接收报文、监控报文并且还能够回复报文的一个客户端程序,这样就可以方便我们调试编写我们的服务端运行的程序逻辑。

前者是编写客户端程序的视角,后者是编写服务端程序的视角。

不管怎么说,我们总会想要一个报文模拟器,能够接收、监控、发送报文,可以方便我们客户端与服务端报文交互相关的开发测试工作。

我也一直有这么一个想法,正好最近遇到了一个报文格式相对比较复杂的项目,不自己编写一个报文模拟器的话,服务端程序的逻辑调试将会非常麻烦。

因此,我对这个报文模拟器提出了自己的需求:

  1. 可以接收服务端发来的报文信息
  2. 可以向服务端发送指定的报文信息
  3. 可以滚动监控报文的接收与发送
  4. 可以编辑报文保存到本地,也可以从本地读取报文
  5. 为了让这个项目自成一体,最好加上一个自行编写的简单的服务端程序

于是,经过了几天的鏖战,我终于算是非常简陋的实现了上述的需求:
1

那么,现在,就让我们一起来实现这个报文模拟器吧:)

ps: 对这个项目的代码感兴趣的同学,可以来我的 GitHub
wangying2016/Packet_Simulator

二、需求分析:选择 Python3 & Tkinter

既然要做一个报文模拟器,也给自己提出了引言中提到的 5 个需求,那么我们就需要思考如果去实现它。

1. 技术选择

技术上,这里我选择了 Python3 & Tkinter,至于为什么。。。

因为我喜欢 Python3 & Tkiner 呀 :)

不过话说回来,对于我们程序员来说,一些工作中的辅助工具,当然是编写难度越简单越好,编写代码量越少越好,选择的语言当是库越强大越好,其语言原生支持的 UI 库也能基本达到我们的需求即可。

于是乎,我选择了 Python3,另外又因为 Tkinter 是 Python3 原生支持的跨平台的 UI 库,其强大而又简单易学,因此实现的工具就这么决定下来了。

2. 业务选择

有些人可能会觉得奇怪,我们编写一个小工具,为什么还涉及到业务的选择呢?

这是当然的,尽管我们只是想要编写一个报文模拟器,但是我们需要构建一个简单的场景去让它跑起来。这个场景中,包括我们服务端的程序,包括我们需要去定义的服务端与客户端交互的报文格式,这些都是需要定义好的。

另外,我们将自己的业务定义的越简单越具有代表性,那么后续我们将这个报文模拟器移植到具体的项目中使用的时候,也好进行扩展,比如说服务端与客户端交互的报文格式,涉及到报文的加解密方式、报文的读取与写入等等逻辑,这些都是需要根据具体的项目进行个性化扩展的。

这里,我们简单定义以下的业务规则:

  1. 场景范围
    报文的发送使用短连接的方式进行,也就是每次发送完报文即关闭 socket 连接。另外,不论是服务端还是客户端,都需要保持一个长时间监听的 socket 连接,主要负责对对方发送的报文的监听。
  2. 报文格式
    报文的格式因项目而异,这里我们作为实验项目,定义最简单的就好了,这里我定义如下:
    2

我们做好了技术和业务上的选择,剩下来就是进行代码的编写了,从哪部分开始呢?

当然是 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)

        self.txt = Text(self.frame2, height=10)
        self.txt.pack(fill=X, pady=5, padx=5, expand=True)

        self.frame3 = Frame(self)
        self.frame3.pack(fill=X, expand=True)

        button1 = Button(self.frame3, text="打开本地", command=self.open_local)
        button1.pack(side=LEFT, padx=5, pady=5, fill=X, expand=True)

        button2 = Button(self.frame3, text="保存本地", command=self.save_local)
        button2.pack(side=LEFT, padx=5, pady=5, fill=X, expand=True)

        button3 = Button(self.frame3, text="发送报文", command=self.send_msg)
        button3.pack(side=LEFT, padx=5, pady=5, fill=X, expand=True)

        self.frame4 = Frame(self)
        self.frame4.pack(fill=X, expand=True)

        lbl3 = Label(self.frame4, text='日志监控')
        lbl3.pack(side=LEFT, padx=5, pady=5)

        self.frame5 = Frame(self)
        self.frame5.pack(fill=BOTH, expand=True)

        self.log = scrolledtext.ScrolledText(self.frame5, height=55, width=150)
        self.log.pack(side=LEFT, pady=5, padx=5, expand=True)
        self.log.focus_set()

        self.add_log('packet simulator', 'start listen')

Tkinter 的布局并不难学,只需要能够自行去寻找源代码中的信息,能够上网搜索相关教程即可,这里就不再赘述了。

2. 滚动文本框

在界面中,最难的控件莫过于最下面的日志监控的滚动文本框了。
3
这个是使用的 Tkinter 的 scrolledtext 控件,其中最重要的加入日志信息的函数实现如下:

    def add_log(self, title, msg):
       tm = time.localtime(time.time())
       fmt_msg = '%s-%s-%s %s:%s:%s %s\n%s\n\n' % (tm.tm_year,
                                                   '{:0>2}'.format(tm.tm_mon),
                                                   tm.tm_mday,
                                                   '{:0>2}'.format(tm.tm_hour),
                                                   '{:0>2}'.format(tm.tm_min),
                                                   '{:0>2}'.format(tm.tm_sec),
                                                   title,
                                                   msg)
       self.log.insert(INSERT, fmt_msg)
       self.log.focus_set()
       self.log.yview_moveto(1)

其中,前面都是匹配时间格式,后面的 self.log.yview_moveto(1) 函数可以保持当前显示永远在最后一行(实现滚动跟踪的功能)。

3. 还有其他疑惑…

如果还有其他疑惑,我推荐网上的一份 Tkinter 教程,非常利于新手的学习:
Python Programming with Tkinter

现在,我们实现了界面的布局,也能够模拟滚动的日志信息显示了,那么接下来我们做什么呢?

Socket 编程:)

四、Socket 编程:服务端与客户端的交互

归根结底,这到底还是一个服务端与客户端交互的项目。因此除了报文模拟器,我们还要编写一个服务端的程序。

当然了,为了尽量简化这个项目的结构,我们使用了发起者短连接的方式。也就是,当报文发起者发送报文的时候,自行创建一个 socket 连接,发送完毕后立即关闭。对于报文监听者来说,就需要一直挂着监听。

1. 服务端程序

服务单程序还算比较简单,只需要一直挂着监听报文即可,只是在接收到报文之后,需要另起一个 socket 连接进行报文的回复。

下列是监听报文的代码:

class Server:
    """
    Listen for client.
    """
    def __init__(self):
        # Create socket
        tcp_server_socekt = socket(AF_INET, SOCK_STREAM)

        # Bind ip & port
        server_address = (server_ip, server_port)
        tcp_server_socekt.bind(server_address)

        # Begin listen
        tcp_server_socekt.listen(1)
        print('start listen\n')

        # Server long connection, client short connection
        while True:
            # Listen for client's connection
            new_socket, client_address = tcp_server_socekt.accept()

            # Receive client data
            recv_data = str(new_socket.recv(1024), encoding='utf-8')

            # if data length is 0, because client close the connect
            if len(recv_data) > 0:
                print('recv data = [%s]\n' % recv_data)
            else:
                print('recv data = 0\n')

            # Processing message
            processor = Processor(recv_data)
            send_data = processor.get_msg()
            client = Client(send_data)
            print('send data = [%s]\n' % send_data)

            # Close the client socket
            new_socket.close()

        # Stop listen
        tcp_server_socekt.close()

可以看到,我们在挂起监听之后,每次接收到报文信息之后,都是调用了 Client() 类进行报文的回复,在这个类中,我们新起了一个连接自发送者的 socket 连接:

class Client:
    """
    Send message to client.
    """
    def __init__(self, data):
        # Create socket
        self.tcp_client_socket = socket(AF_INET, SOCK_STREAM)

        # Bind ip & port
        self.server_address = (client_ip, client_port)

        # Connect to client
        self.tcp_client_socket.connect(self.server_address)

        # Send data
        self.tcp_client_socket.send(bytes(data.encode("utf-8")))

        # Close connect
        self.tcp_client_socket.close()

所以,服务端程序的逻辑是非常简单的,就是一直监听报文模拟器的报文,然后接收到报文之后进行处理,最后新起一个连接进行返回。

2. 客户端程序

实际上客户端程序同服务端程序很类似,也是挂着监听服务端的报文,然后新起一个 socket 连接发送报文。

但是,客户端程序又有不一样的地方,那就是需要考虑到多线程去处理 UI 和监听线程。

这里,在 GUI 类的初始化中,我新起了一个线程进行报文的监听:

class GUI(Frame):
    """
    GUI with tkinter.
    """
    def __init__(self):
        super().__init__()
        # Instance variable
        ...
        
        # Init ui
        self.init_ui()

        # Begin listen
        t = threading.Thread(target=network)
        t.setDaemon(True)
        t.start()

def network():
    server = Server()

class Server:
    """
    Listen message from server.
    """
    def __init__(self):
    	...

上述代码应该能够让你看清楚这个项目的结构,是非常清晰的。

对于客户端报文发送来说,实际上也是很雷同服务端的:

    def send_msg(self):
        # Get parameter
        argv1 = self.entry.get().strip()
        argv2 = self.txt.get('1.0', END).strip().replace('\n', '')
        if len(argv1) != 4:
            messagebox.showerror('错误', '请输入 4 位识别代码')
            return
        print('argv1 = [%s], argv2 = [%s]' % (argv1, argv2))

        # Make message
        maker = Maker()
        msg = maker.get_msg(argv1, argv2)
        print('send data: \n[%s]\n' % msg)

        # Send Data
        client = Client(msg)

class Client:
    """
    Send message to server.
    """
    def __init__(self, data):
        # Create socket
        self.tcp_client_socket = socket(AF_INET, SOCK_STREAM)

        # Bind ip & port
        self.server_address = (server_ip, server_port)

        # Connect to server
        self.tcp_client_socket.connect(self.server_address)

        # Send data
        self.tcp_client_socket.send(bytes(data.encode("utf-8")))

        # Close connect
        self.tcp_client_socket.close()

        # Add log
        data = 'data = [%s]' % data
        app.add_log('send data', data)

这里进行报文的组包,然后调用 Client 类新建一个 socket 连接进行报文的发送。

至此,这个项目最核心的网络编程相关的内容就编写完了。

那么,还剩下什么呢?

报文的读写:)

五、报文读写:生成和解读

我们之前在需求分析的时候就对报文格式进行了定义,那么对应的,我们需要一个按照报文格式生成报文的类,和一个按照报文格式解读报文的类。

生成报文的类:

class Maker:
    """
    Packet generate class.
    """
    def __init__(self):
        self.index = ''
        self.content = ''
        self.msg = ''

    def get_msg(self, index, content):
        self.index = index
        self.content = content
        length = 6 + 4 + 2 + len(content)
        self.msg = '{:0>6}'.format(length) + '|' + index + '|' + content
        return self.msg

    def get_index(self):
        return self.index

    def get_content(self):
        return self.content

解读报文的类:

class Reader:
    """
    Packet analysis class.
    """
    def __init__(self, msg):
        self.msg = msg
        self.index = msg[7:11]
        self.content = msg[12:]

    def get_index(self):
        return self.index

    def get_content(self):
        return self.content

这里因为我的报文格式是我自己定义的,就不再赘述了,值得一说的是,根据项目的复杂程度不同,只需要扩展这两个类,即可对不同项目进行支持了:)

至此,这个项目也就做完了 ^_^
完结撒花 :)

六、总结

作为一个消费者就是我自己的项目来说,只要我用着舒服也就可以了。不过这个报文模拟器当然还有着这样或那样的不足:

  1. 界面不够友好
  2. 功能还不够强大

等等,这些都是可以在后续的项目开发过程中,根据自己的想法进行添加和实现。

不过话说回来,我真的很喜欢 Python 的 Tkinter 界面库,虽然没有那么好看,但是只要能够跑 Python3 的地方,就能跑 Tkinter 不得不说真的是非常的方便,想想 IDLE 就是这玩意儿写的,除了界面不够那么炫酷,其他基本也就满足需求了,特别适合这种随手就来的小工具项目啊!

Python3 & Tkinter 的学习之路还有很长
To be Stronger:)

本软件可以模拟不同类型的交易报文,可以对交易测试案例进行统一管理,并可以进行简单时间统计和成功率统计。 使用本软件可以减轻传统测试过程中的修改-编译-测试-的循环等待时间,在测试过程中可以根据需要随时更改报文内容。 本软件支持任意格式的报文,可以模拟不同格式的报文,如定长,变长,XML,8583等报文。每个域的内容可以是常量,也可以支持约定的表达式。 本软件可以根据需要设置对应答相关域进行合法性检查,可以校验应答报文和请求报文的匹配关系,可以校验域的长度,校验域的内容等。 本软件支持MAC的生成、校验以及PIN加密处理,同时可以根据需要调整是否需要进行MAC和PIN加密。 支持服务端功能,根据不同的报文设置不同的应答报文。 本软件运行程序无需安装,只需将相关程序和测试案例文件拷贝到相应的文件夹下即可执行。 Version 1.7.0 521 修正一些BUG,增加服务端的设置。 Version 1.6.4.405 增加了应答服务的交易码解析,根据解析后的交易码匹配应答案例。 增加了再次接收长度,对于特殊报文,可先读取一定长度的内容,再根据此配置读取指定长度。 Version 1.6.4.317 增加了应答报文的处理,配置案例文件如:_resp.txt(以_开头),按该文本内容格式发出报文。 增加了服务配置的接收长度属性,指明长度(如96:按96长度位固定接收),或者(a4-按4位长度位接收,b2-按2位BCD码长度位接收)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值