项目总结 (2)

很累的一个月,为了赶项目天天码代码,第一次在看到代码的时候有想吐的感觉。

用 lua 作为配置文件

因为在之前的一个项目中师兄采用了网络作为前后端交互的方式,这样两部分分开调试比较方便,从此以后实验室的项目不管是否需要联网都采用这种方法,结果在项目中除了根据甲方的需求把控件拖来拖去之外剩下的就是解析各种各样的报文格式。

前段时间另一个项目需要写一个报文测试的小工具,就是有一堆自定义的报文格式,界面上有下拉条选择或者让用户自己输入某个字段的值,然后把报文发出去,收到返回信息并且把内容解析一下。我已经不怎么碰这个项目了,但是这次时间有点紧,于是哥又“被”当仁不让地充当临时工把活接了下来。在前一个项目中师兄使用了 xml 作为报文格式配置文件,这个小工具我打算用 lua 来试一下,也算是一个实际应用的机会。界面用了 qt,因为现在 qt 俨然成为本人实验室项目界面开发的默认工具了。

先说说使用 lua 作为配置文件吧。报文格式是常见的 TLV 格式(“T”表示 type,“L”表示长度,“V”表示数据),有些字段会有限定的范围:

packet = {
    "报文名称",
    {"字段名称1", "字段类型", 个数, {可选值1, "说明1"}, {可选值2}, {起始值, 结束值, 步长}, ...},
    ......
}

其中字段类型定义了一些常见的类型并规定了长度,例如“int”就是 4 字节,“double”就是 8 个字节等。个数表示该字段中包含了多少个这样的类型。例如

time = {
    "时间",
    {"报文标识",        "char", 1},
    {"报文长度",        "uint", 1},
    {"回应时间:年",    "uint", 1},
    {"回应时间:月",    "uint", 1, {1, 12}},
    {"回应时间:日",    "uint", 1, {1, 31}},
    {"回应时间:时",    "uint", 1, {0, 23}},
    {"回应时间:分",    "uint", 1, {0, 60}},
    {"回应时间:秒",    "uint", 1, {0, 60}},
}

根据配置文件可以自动打包和解析数据,比手动一个个地写要省事多了,也提高了准确性。通过可选值和说明可以用来制作下拉菜单,而且在收到报文的时候可以显示对应值的说明,比简单地显示一个数值要人性化一点。这里用 lua 写显得复杂了,如果只是静态配置的话 xml 是最适合的。

后来发现有些小问题没解决,例如上面报文中的"可选时间:日",如果使用下拉菜单的话,下拉菜单的选项应该根据用户选择的月份来确定,因为并不是每个月的天数都是一样的。但是配置文件中并不知道每个域的含义,它只是按照说明把内容显示在界面上,并且发送和解析的部分只按照类型长度和个数来解析,简单地通过静态配置并不能达到这个效果。可以考虑为有需要的域增加一个函数,该函数接收一些参数(可能是已经收到的报文中的某个域),然后根据这个参数返回相应的范围交给界面显示。由于对 lua 的了解不深,目前也只能先想想了。网上有很多类似功能的网络调试助手,可以作为 tcp 服务端或服务端,只是发送数据和接收数据都是以十六进制或二进制显示,可读性比较差,而且如果要修改报文中的某个域的值还得一个个字节地数。希望后面有时间会把这个东西完善一下,做成一个可配置的报文发送和接收工具。

由于第一次使用经验不足,把 lua 用得像汇编,每次调用完 lua 的函数都要调用 lua_pop() 清空栈的内容,其实在一个函数完全执行完后再恢复为调用前的状态也是可以的。不过这样的用法倒是为我找出了不少程序的问题:每次进入函数和退出函数前都打印一下栈的大小,如果不一致则说明了程序有问题了。不管哪种方法都要时刻注意栈的使用情况,了解每个函数都会往栈里放什么东西,哪些函数会从栈的哪个位置取东西,然后在调用函数前准备好所有参数,在执行完后清理干净。否则要是忘了清理时间长了会内存泄漏,乱清理又会崩溃,总之要很小心就是了。

mysql 的使用

项目中数据库貌似是必不可少的。我对数据库并不熟悉,而且无论是 oracle 还是 db2 还是 sqlserver 给我的印象都是又大又慢配置又复杂,虽然有一些号称是轻量级的数据库如 mysql(这个算轻量么),progresql 和 sqlite,但是总的来说数据库给我的印象都是一个字:慢。

这次的收获是知道了插入的时候一次 insert 多条记录比每条记录都写一个 insert 语句要快多了,顺便复习了一下简单的 sql 语句。另外 mysql_init(NULL) 之后,如果 mysql_real_connect() 不成功的话也要调用 mysql_close() 释放内存。没了。

用 qt 画简单的曲线图

上一个项目也有画曲线的需求,师兄用的是 qwt,我觉得为了画一个曲线装这么一大坨东西太麻烦,而且万一学会了老板以后天天让我画曲线,所以决定不用 qwt。在网上搜了下找到 这个,但是说的并不详细,于是仔细看了下 QPainter 的用法,自己写了个简单的。思路很简单,就是固定一个 QWidget 的长宽,选择几个固定的 x 坐标,y 坐标是个队列,每次来新数据的时候删除队头的 y 坐标后重绘所有点,如果 y 值超出坐标范围就把所有 y 坐标按比例缩小后再绘。不说废话,贴代码:

#ifndef CURVE_H
#define CURVE_H

#include <QtGui/QWidget>
#include <QTimer>

class Curve : public QWidget {

    Q_OBJECT

    public:

        Curve(QWidget *parent = 0, Qt::WFlags flags = 0);
        ~Curve();
        void setup();

    protected:

        void paintEvent(QPaintEvent *);

    private slots:

        void timerUpdate();

    private:

        QTimer timer;

        int maxNrPoints;
        QVector<int> x;
        QList<int> y;
        QList<int> dy;
};

#endif // CURVE_H
#include <QPainter>
#include "curve.h"

Curve::Curve(QWidget *parent, Qt::WFlags flags)
: QWidget(parent, flags) {}

Curve::~Curve() {}

void Curve::setup()
{
    maxNrPoints = (width() / 16) + 2;

    for (int i = 0, tmp = 0; i < maxNrPoints; ++i, tmp += 16)
        x.push_back(tmp);

    connect(&timer, SIGNAL(timeout()), this, SLOT(timerUpdate()));
    timer.start(500);
}

void Curve::timerUpdate()
{
    int h = height(), max = 0;

    y.push_back(counter);

    if (y.size() >= maxNrPoints)
        y.pop_front();

    for (int i = 0; i < y.size(); ++i)
        if (y[i] > max)
            max = y[i];

    if (max <= h)
        dy = y;
    else {
        dy.clear();
        for (int i = 0; i < y.size(); ++i)
            dy.push_back(((double)(y[i]) / max) * h);
    }

    update();
}

void Curve::paintEvent(QPaintEvent *)
{
    int h = height();
    QPainter painter(this);

    for (int i = 1; i < y.size(); ++i) {
        register int ty = h - dy[i];
        painter.setPen(QPen(Qt::red, 2));
        painter.drawLine(x[i - 1], h - dy[i - 1], x[i], ty);
        painter.setPen(QPen(Qt::blue, 2));
        if (y[i] > 0)
            painter.drawText(x[i], ty, QString::number(y[i]));
    }
}

刚开始的时候不了解 update() 中信号的传递机制,做法是这样的:先在 qt designer 中拖了个 QWidget 到界面上,然后把这个 QWidget 的指针传给 Curve 类中的 QPainter,结果就是画不出图来,后来终于明白为什么:Curve 类发了个 update() 的信号,调用了 Curve::paintEvent(),但是要画图的 QWidget 并没有收到重绘的信号,因此 painter 画出来的东西并没有显示出来;然而即使给 QWidget 发了重绘信号也无法改写 QWidget 的 paintEvent() 函数,根本就没有按需要画。后来的做法是:先在界面上搭好图,然后在自动生成的 ui_curve.h 中看看 QWidget 是的布局语句是怎样写的(为了把 Curve 显示在指定的地方),这样这个 QWidget 就不需要了,可以使用 Curve 代替(因为 Curve 继承了 QWidget)。

还是 qt

经过了上次的教训,现在写个简单的程序都写成至少两个类了,一个负责界面,另一个负责具体的事务处理,两者之间通过 signal/slot 通信。虽然复杂点,但是要加功能的话却是不用大改,反正程序跑得慢浪费的又不是我的时间……虽然整天挂在嘴边说功能与表现形式相分离,但是有些功能不知道该放在界面类还是功能类,或者说设计上还缺了什么。例如一个发送文件的程序,定时发送的定时器就不知道放在什么地方好。功能类应该只管发数据(写到文件里还是打到屏幕上,发到网络还是放到数据库里),而数据的发送方式(循环发还是只发一次,发一条还是发几条)都应该由信号发出者来指定;而界面类只需要负责显示,还有在用户点击发送按钮的时候取出数据交给发送线程,至于数据的发送方式似乎也不应该归它管。这么说来是不是该再增加一个专门管发送方式的控制类呢(到这里突然想起了传说中的 MVC 模式)?最后把定时器放在了界面类,一个小工具还是不要钻牛角尖了。

qt 是一个 c++ 库,因为本人有轻度的完美主义,类里容不下一个 public 变量,哪怕是个实数变量都要隐藏起来,然后提供 set() 和 get() 函数进行访问,要全是 struct 或者弄个全是 public 的 class 都不好意思把代码拿出去见人。为了把该隐藏的 80% 隐藏起来,但是又要让外部看见该暴露的 20%,就不得不对这 20% 再进行一次封装,结果就是类里面很多函数都只是一个对 private 变量的函数调用。虽然说使用起来其实也差不多,但是对于封装的过程,就像在大城市里,明明就在 100 米开外的地方,却不得不开车绕上几公里,等红绿灯……虽然我也很实用主义,这样做模块的确是独立了,但是总觉得封装过度的东西转来转去很麻烦,搞到头都晕了。当然有些地方非得搞这么复杂是自己的问题。

如果是以前的我肯定直接把报文格式都直接写死在程序里,一是程序要求简单;二是写个脚本还要解析,比直接写在程序里麻烦,至少就这个需求来说是麻烦了,程序也不如直接写的快。但是在几个项目中打杂了一段时间,发现需求经常变,而且变得毫无道理,因此灵活性比效率要重要得多(这个报文格式就变了好几回,庆幸自己用了配置文件)。对于那些什么都不懂的甲方来说,如果程序的性能不能满足要求,要换的是硬件而不是软件。而且光是画界面已经够烦的了,经常出现内存访问越界还有内存占用和程序运行时间成正比关系的曲线更让人抓狂。在无数次抓狂之后终于意识到在界面或测试小程序这样的一些对运行效率要求不高的地方还用这么“低级”的语言来写,而且还在为多一次少一次内存复制开销较劲一番简直就是自虐。

最后就是这次程序是在 windows 开发的,虽然 vs 的编辑功能差了点,但是作为一个调试环境很是方便。有一个叫 viemu 的软件可以集成到 vs 中,这样既可以在使用 vim 的同时也可以使用 vs 提供的代码补全,定义跳转等功能,否则就只能在 gvim 和 vs 间来回切换了。另外 vs2008 自带了 emacs 的简单模拟却没有 vim 的模拟,可能是因为模拟 emacs 比 vim 要简单吧,不过 qtcreator 倒是有 vim 的模拟。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注