拖拽鼠标画矩形

经常看到有同学问:如何实现用鼠标拖拽出一个矩形区域的效果,类似 QQ 截图的那个矩形区域?

很简单的一个问题,主要是处理鼠标的按下、移动、松开三个事件:

  • 按下鼠标:用一个变量标记要开始拖拽出矩形了,并记录按下的位置作为矩形的左上角顶点
  • 移动鼠标:按下时移动鼠标,鼠标的当前位置作为矩形新的右下角的顶点,每次移动事件发生时都要重新画一次矩形,因为矩形变了
  • 松开鼠标:清除拖拽矩形的标志,松开鼠标后,移动鼠标时就不要再改变矩形了

Qt Creator 简介

工欲善其事,必先利其器,顺手的开发工具,能够让我们事半功倍,Qt 开发,推荐使用 Qt Creator,因为在 Qt Creator 中集成了很多实用的功能,不需要切换到其他软件就能使用

  • 编写代码(废话)
  • 使用 UI Designer 进行可视化的布局界面
  • 搜索帮助文档
  • 学习自带的例子
  • Ctrl + K 快速搜索、定位

接下来就简单的介绍下我个人觉得 Qt Creator 使用中比较重要的地方。

线程池 QThreadPool

创建线程需要向系统申请资源,线程切换时操作系统会切换线程上下文,可能会从用户态切换到内核态,当有很多线程时,频繁地切换线程会导致消耗大量的 CPU 以及内核资源,真正用于计算的资源就减少了,反而会降低程序的效率。线程并不是越多越好,线程池的作用是管理、复用、回收一组线程,控制线程的数量,避免频繁的创建和销毁线程而浪费资源。

Qt 中的线程池类为 QThreadPool,每一个 Qt 程序都有一个全局的线程池,调用 QThreadPool::globalInstance() 得到,它默认最多创建 8 个线程,如果想改变最大线程数则调用 setMaxThreadCount() 进行修改,调用 activeThreadCount() 查看线程池中当前活跃的线程数。

使用线程池挺简单的,定一个任务类例如叫 Task,继承 QRunnable 并实现虚函数 run(),Task 的对象作为 QThreadPool::start() 的参数就可以了,线程池会自动的在线程中调用 Task 的 run() 函数,异步执行。线程池中的 QRunnable 对象太多时并不会为立即为每一个 QRunnable 对象创建一个线程,而是让它们排队执行,同时最多有 maxThreadCount() 个线程并行执行。

提交给线程池的 QRunnable 对象在它的 run() 函数执行完后会被自动 delete 掉,如果不想线程池删除它,在调用线程池的 start() 前调用 setAutoDelete(false) 即可。

创建使用动态链接库

想一想大多数时候我们的项目是不是所有代码都会放在同一个工程中?人少的时候都不是事,但当项目越来越大,开发人员越来越多,会发觉开发、管理能让人窒息,大家都绞在一起,出问题时互相推诿责任,各自有理,这时如果按照功能模块进行分组各自开发,以库的形式提供给其他人使用,就能够最大限度的并行开发,提高工作效率,而且项目的模块也很清晰,责任一目了然,此外使用动态链接库后还能够按模块升级,编译的速度也更快。下面就介绍怎么在工程中创建和使用动态链接库。

Windows 中叫动态链接库(Dynamic Link Library: .dll),Linux 中叫共享库(Shared Library: .so),Mac 下后缀为 .dylib,实际指的是一种类型的库,这里都统称为动态链接库吧。

动态链接库需要理解符号的概念,符号包含函数、变量或者类,分为公有符号和私有符号:

  • 公有符号: 在其他程序或者库使用的符号,需要根据用途进行特殊标记:

    • Q_DECL_EXPORT: 编译为动态链接库时符号要标记为 Q_DECL_EXPORT,表明是导出符号
    • Q_DECL_IMPORT: 在调用动态链接库时符号要标记为 Q_DECL_IMPORT,表明是使用符号
  • 私有符号: 除了公有符号外的其他符号,在此库外不应该能够访问,不需要进行标记

    建议: 不要在头文件中声明私有符号。

符号上的标记 Q_DECL_EXPORTQ_DECL_IMPORT 不能同时存在,为了在导出和导入时使用同一个头文件, 头文件中包含下面的宏,编译时根据条件使用不同的宏就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <QtCore/qglobal.h>
// 根据条件定义 MYLIBRARY_SHARED_SYMBOL 为不同的宏
#if defined(MYLIBRARY_LIBRARY)
# define MYLIBRARY_SHARED_SYMBOL Q_DECL_EXPORT
#else
# define MYLIBRARY_SHARED_SYMBOL Q_DECL_IMPORT
#endif
// 使用 MYLIBRARY_SHARED_SYMBOL 声明符号,编译时会根据条件替换为 Q_DECL_EXPORT 或者 Q_DECL_IMPORT
class MYLIBRARY_SHARED_SYMBOL Calculator {
...
};

为了达到了在导出和导入时使用同一个头文件的目的:

  • 生成动态链接库工程的 pro 文件中添加 DEFINES += MYLIBRARY_LIBRARY,在编译的时候 MYLIBRARY_SHARED_SYMBOL 就会被替换为 Q_DECL_EXPORT
  • 使用动态链接库工程的 pro 文件中千万不要加 DEFINES += MYLIBRARY_LIBRARY,编译的时候 MYLIBRARY_SHARED_SYMBOL 就会被替换为 Q_DECL_IMPORT

测试 Widget 的效率

Widget 的效率怎么样,来进行一个简单的测试,添加 1千,1万,2万个,……,10万个 QPushButton(修改程序中的 buttonsCount 即可),看看程序的创建好按钮,点击按钮执行槽函数,程序退出效果怎么样。

添加 1千个,1万个按钮的时候窗口显示的速度非常快。
添加 2万个的时候就需要几秒窗口才显示出来。
添加的越多窗口显示需要的时间越长,添加 10万个需要等很久。
按钮越多,程序退出的时间就越长,不过即使是 10万个按钮,退出也就是多了几秒,因为释放的内存多,这倒是没什么。
当窗口显示出来后,不管添加了多少个按钮,点击按钮,它的槽函数都是瞬间就被执行。
在实际应用中,添加上百个 widgets 在窗口上的见过,但有谁会添加上万个 widgets 到窗口上?不担心被产品经理揍的可以试试!

测试 Graphics View 的效率

Qt 说 Graphics View Framework 效率很高,到底有多高呢?来进行一个简单的测试,向 scene 中添加 10万,50万,100万个 items(修改程序中的 rowCount 和 colCount 即可),进行缩放、旋转看看效率怎么样。

在可视区域内的 items 少的话,不管 scene 里有多少个 items,10万个和 100万个的区别不大,效率都是非常高的,但可视区内 items 越多的话,越多效率越低。Qt 使用 Binary-Space-Partitioning 算法管理 items,能够快速的找出可视区内的 items 进行绘制(100万个 items 中可能只需要绘制 100 个 items),不会绘制所有的 items,绘制操作是非常耗时的,这也就是为什么影响效率最大的因素是可视区内的 items。

实际项目中添加 10万个 items 的效率和此处测试的 10万个 items 的效率是有些微区别的,绘制的消耗由其 paint() 函数决定。

Qt 全局快捷键

全局快捷键: 按下快捷键后,不管程序是否当前正在使用的程序,它都能得到此快捷键的事件通知,例如 Windows 里按下 Ctrl + Shift + A 就可以使用 QQ 截图一样。

Qt SDK 没有自带设置全局快捷键的功能,需要自己实现,在 Github 上也有人开源了全局快捷键的库,例如 QHotKey,可以直接在项目中使用。

QHotkey 有很多特点,能够满足绝大多数的需求:

  • Works on Windows, Mac and X11
  • Easy to use, can use QKeySequence for easy shortcut input
  • Supports almost all common keys (Depends on OS & Keyboard-Layout)
  • Allows direct input of Key/Modifier-Combinations
  • Supports multiple QHotkey-instances for the same shortcut (with optimisations)
  • Thread-Safe - Can be used on all threads (See section Thread safety)
  • Allows usage of native keycodes and modifiers, if needed

下面就写个 HelloWorld 级别的程序,定义全局快捷键 Alt+P 唤出程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <QHotkey>
#include <QApplication>
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
QHotkey *hotkey = new QHotkey(QKeySequence("Alt+P"), true); // Alt 和 P 之间不能有空格
qDebug() << "Is Registered: " << hotkey->isRegistered();
connect(hotkey, &QHotkey::activated, [this](){
this->show();
this->raise();
this->activateWindow();
this->raise();
QApplication::setActiveWindow(this);
this->raise();
});
}

继承 QThread 实现多线程

Qt 中使用多线程,最简单直观的方法就是继承 QThread,重写 run() 方法,需要使用多线程执行的代码放在 run() 函数中,调用 start() 函数启动线程,线程正在运行时 isRunning() 返回 true,结束运行后发出信号 finished()

实现线程

仍以读取文本显示到 QTextEdit 为例,类 ReadingThread 继承 QThread,在 run() 方法中读取文件并添加到 QTextEdit。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文件名: ReadingThread.h
#ifndef READINGTHREAD_H
#define READINGTHREAD_H
#include <QThread>
class QTextEdit;
class ReadingThread : public QThread {
public:
ReadingThread(QTextEdit *textEdit, QObject *parent = NULL);
protected:
void run() Q_DECL_OVERRIDE;
private:
QTextEdit *textEdit;
};
#endif // READINGTHREAD_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 文件名: ReadingThread.cpp
#include "ReadingThread.h"
#include <QFile>
#include <QTextStream>
#include <QTextEdit>
#include <QMetaObject>
ReadingThread::ReadingThread(QTextEdit *textEdit, QObject *parent) : QThread(parent), textEdit(textEdit) {
}
void ReadingThread::run() {
QFile file("/Users/Biao/Desktop/data.txt");
if (!file.open(QIODevice::Text | QIODevice::ReadOnly)) {
return;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
textEdit->append(line);
}
}

多线程编程

读取文件显示到 text edit 中,一个非常简单的需求,啥也不说了,撸起袖子,打开 Qt Creator 开干。

先设计 UI 如下,中间是 QTextEdit,底部是按钮 QPushButton:

点击按钮,按行读取文件,然后添加到 QTextEdit,对我们来说也是分分钟的事(Lambda Lambda Lambda):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReadingWidget::ReadingWidget(QWidget *parent) : QWidget(parent), ui(new Ui::ReadingWidget) {
ui->setupUi(this);
// 点击按钮,按行读取文件,添加到 text edit 中显示出来
connect(ui->pushButton, &QPushButton::clicked, [this] {
QFile file("/Users/Biao/Desktop/data.txt"); // 文件路径自己修改一下啊
if (!file.open(QIODevice::Text | QIODevice::ReadOnly)) {
return;
}
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine(); // 读取一行
ui->textEdit->append(line); // 添加到 text edit
}
});
}

分布式 ID 生成算法 Snowflake

分布式系统中,有一些需要使用全局唯一 ID 的场景,这种时候为了防止 ID 冲突可以使用 36 位的 UUID,但是 UUID 有一些缺点,首先他相对比较长,另外 UUID 一般是无序的字符串。

有些时候我们希望能使用简单一些的 ID,并且希望 ID 能够按照时间有序生成,为了解决这个问题,Twitter 发明了 SnowFlake 算法,不依赖第三方介质例如 Redis、数据库,本地生成程序生成分布式自增 ID,这个 ID 只能保证在工作组中的机器生成的 ID 唯一,不能像 UUID 那样保证时空唯一。

Snowflake 把时间戳、工作组 ID、工作机器 ID、自增序列号组合在一起,生成一个 64bits 的整数 ID,能够使用 70 年,每台机器每秒可产生约 25 万个 ID。

Snowflake 生成的 ID 的 bit 结构如下:

VS2013 使用 dll

Qt 使用 curl 一文中介绍了怎么编译 curl 并且在 Qt 项目中使用,那么在 VS 项目中应该怎么使用 curl 的 dll 呢?

动态库的使用分为隐式链接和显示链接两种方式:

  • 显示链接: 只需要 .dll 动态库文件,代码中使用 LoadLibrary + GetProcAddress 加载函数后需要自己进行函数类型转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 函数类型定义
    typedef void (*DLLFunc)(int);
    // 加载 dll 中的函数
    HINSTANCE hInstLibrary = LoadLibrary("DLLSample.dll");
    DLLFunc dllFunc = (DLLFunc)GetProcAddress(hInstLibrary, "TestDLL");
    // 执行函数
    dllFunc(123);
  • 隐式链接: 需要 .h 头文件、.lib 库导入文件和 .dll 动态库文件,代码中直接使用库的函数即可

    推荐使用隐式链接,更省事,可参考 LIB 和 DLL 的区别与使用

VS2013 中隐式链接使用 dll 一般有两种方法:

  • 使用 #pragma 引入 lib

  • 设置 项目属性 引入 lib

Qt 使用 curl

Qt 已经提供了 QNetworkAccessManager 用于 Http 访问,Qt 访问网络的 HttpClient 对其进行了简单封装,如下就可以进行 GET 请求:

1
2
3
HttpClient("http://localhost:8080/device").get([](const QString &response) {
qDebug() << response;
});

但是,在非 Qt 项目中就不能使用 QNetworkAccessManager 了,还有就是因为 curl 成熟、强大、跨平台,可能有些项目更希望使用 curl,所以在此以 Windows MinGW 的 Qt 项目为例,介绍 curl 的集成使用。

Qt 安装

对于想学习 Qt 的同学来说,下载、安装 Qt 还真不是一件容易的事,但这也是学习 Qt 的基石,否则连个 Qt Hello World 都不能在电脑上编写,还玩什么呢?

下载

下载 Qt,首先想到的是到官网 https://www.qt.io 下载,悲催的是,别说对于新手,就算对于我这种业余爱好 Qt 好多年的伪骨灰,进去后都是懵逼状态,别说还要注册、登录,估计连找下载的地方都困难,大多数人在这估计就有放弃的想法了,不能从大门进,还不能直捣黄龙么,访问 http://download.qt.io/archive/qt/ 就可以无障碍的挑选想要的 Qt 版本了。

线段拟合曲线

QPainter 提供了绘制线段、矩形、椭圆、圆、圆弧、路径等的函数,如果想绘制正弦 (y=sin(x))、余弦 (y=cos(x)) 的曲线,QPainter 没有提供相应的绘制函数,应该怎么办呢?

李小龙的武术哲学: 以无法为有法,以无限为有限。

数学曲线是连续的,计算机的世界却是离散的,离散的世界使用极限的方式就可以模拟出连续的效果。可以把曲线想象成是一条一条线段连起来形成的图形,这些线段越短,连成的图形就越逼近曲线,这种方法就是线段拟合曲线,学过微积分的同学是不是感觉这个方法很熟悉?

下面以绘制正弦 (y=sin(x)) 曲线为例进行介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void FittingCurveWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.translate(10, 150);
// 绘制坐标轴
painter.setPen(QPen(Qt::gray, 1, Qt::DashLine));
painter.drawLine(0, 0, 700, 0);
painter.drawLine(0, -200, 0, 200);
painter.setPen(QPen(Qt::black, 1));
// 计算正弦的坐标点,绘制线段
qreal prex = 0, prey = 0;
// [0, 314] 归一为 [0, PI]
for (int i = 0; i <= 628; ++i) {
qreal x = i;
qreal y = qSin(i/314.0*M_PI) * 100;
painter.drawLine(prex, prey, x, y);
prex = x;
prey = y;
}
}

Nginx 验证 Token

为了提高效率,常把 Nginx 作为静态文件服务器,把视频文件,JS,CSS 等放到 Nginx 上。例如我们要开发一个视频网站,免费视频不需要访问权限验证,收费视频就需要对用户的权限进行验证,验证通过了才能够继续访问,Nginx 可以借助 Lua 来实现访问验证,用户信息使用 token 表示

Nginx 简单的验证代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location ~ /private/.+\.mp4$ {
root html;
access_by_lua '
-- 应用的 ID 和 key,和应用服务器上的一致
local appIdKeys = {["app_1"] = "key_1", ["app_2"] = "key_2"};
local args = ngx.req.get_uri_args();
local appId = args["appId"];
local appKey = appIdKeys[appId];
local token1 = args["token"]; -- 参数中 token
local token2 = ngx.md5(appId .. appKey); -- 用应用的 ID 找到对应的 key,然后根据算法计算 token
-- 如果参数中的 token 和计算得到的 token 不相等,则说明访问非法,禁止访问,否则放行访问
if token1 ~= token2 then
ngx.exit(ngx.HTTP_FORBIDDEN);
end
';
}

Nginx 和应用服务器上同时存储 appId 和 appKey,这样就能根据参数中的 appId 查找到对应的 appKey。至于使用 Lua 的变量存储,或者使用数据库,还是文件,根据具体的情况而定(Nginx 中 Lua 能够访问数据、Redis 等)。

上面的验证规则比较简单,如果其他人得到了 token,就可以无限制的访问了,为了增强安全性,可以使用更多的参数生成 token,例如用户 id,限制 URL 期限的时间戳等。

Nginx 默认没有安装 Lua 模块,需要自己安装,可参考 http://qtdebug.com/mac-nginx-lua

Nginx 安装 Lua 支持

Nginx 支持 Lua 需要安装 lua-nginx-module 模块,一般常用有 2 种方法:

  • 编译 Nginx 的时候带上 lua-nginx-module 模块一起编译

  • 使用 OpenResty: Nginx + 一些模块,默认启用了 Lua 支持(推荐使用此方式)

    OpenResty is just an enhanced version of Nginx by means of addon modules anyway. You can take advantage of all the exisitng goodies in the Nginx world.

    OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

    OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

HTML5 使用 MQTT

HTML5 中也能使用 MQTT:

  1. ActiveMQ 启用 MQTT,可参考 http://qtdebug.com/misc-activemq/

  2. 启动 ActiveMQ: activemq start

  3. 使用 MQTT 的 HTML 如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <html>
    <head>
    <title>Test Ws mqtt.js</title>
    </head>
    <body>
    <script src="./browserMqtt.js"></script>
    <script>
    // 虽然使用的是 MQTT,但底层还是使用 WebSocket 实现的,所以这里的端口需要使用 ActiveMQ 里 WS 的端口 61614,而不是 MQTT 的端口 1883
    var client = mqtt.connect('ws://127.0.0.1:61614'); // you add a ws:// url here
    client.subscribe('foo'); // 订阅 Topic
    client.on('message', function(topic, payload) {
    console.log([payload].join('')); // 提取消息需要使用 [].join()
    })
    client.publish('foo', 'Hello World!'); // 发送消息
    // 不停的发送消息进行测试
    setInterval(function() {
    client.publish('foo', 'Time: ' + new Date().getTime());
    }, 1000);
    </script>
    </body>
    </html>
  4. 写一个 Java 的 MQTT 发布和订阅的程序一起测试,可参考 http://qtdebug.com/misc-mqtt/

下载 browserMqtt.js,也可以自己编译(新版本好像有问题,只能发消息,不能订阅消息),详细文档请参考 https://github.com/mqttjs/MQTT.js

简单的 Mock 工具 RestServerMock

前后端分离,如果前端需要等到服务器端接口开发完成后才能继续的话,效率太低,使用 Mock 工具能够更好的使得前后端分离各自开发,RestServerMock 一个是简单的静态 Web 服务器的 Mock 工具:

A Simple REST HTTP server that serves the configured JSON responses rest-server-mock.

Currently it servers JSON responses with status code 200. Future versions will support more options including : status codes, headers, encodings, etc.

Enum 注入

注入 Enum 需要借助 org.springframework.beans.factory.config.FieldRetrievingFactoryBean,下面以注入 FastJson 的 SerializerFeature 为例.

1
2
3
4
5
6
7
8
9
10
package com.alibaba.fastjson.serializer;
public enum SerializerFeature {
QuoteFieldNames,
UseSingleQuotes,
WriteMapNullValue,
WriteEnumUsingToString,
WriteEnumUsingName,
...
}

Spring Boot 热更新

热更新在开发中对于提高效率是非常重要的,SpringBoot 带了一个 org.springframework.boot:spring-boot-devtools,但是在 SpringBoot + Gradle + IDEA 的配合中不怎么好用,下面介绍另一种热更新的方式:

  • 引入 springloaded(不需要配置其他的):

    1
    2
    3
    4
    5
    6
    7
    8
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'org.springframework:springloaded:1.2.8.RELEASE'
    }
    }
  • 终端进入项目目录,执行 gradle -t classes 启动一个监听任务,当发现项目中的 Java 类发生变化时进行自动编译,模版文件变化时自动复制到 build 对应的目录中

  • 终端进入项目目录,执行 gradle bootRun 启动项目

    修改 Java 文件和模版文件等看看效果

Spring Boot Thymeleaf

SpringBoot 默认使用 Thymeleaf 2,为了使用 Thymeleaf 3,需要引入下面的依赖:

1
2
3
4
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.thymeleaf:thymeleaf:3.0.7.RELEASE')
compile('org.thymeleaf:thymeleaf-spring4:3.0.7.RELEASE')
compile('nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.2.2')

application.properties 中配置:

1
2
3
4
5
spring.thymeleaf.mode=HTML5
spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html

Controller:

1
2
3
4
@GetMapping("/hello")
public String hello() {
return "hello"; // View 的名字为 hello.html
}

hello.html:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
Thymeleaf
</body>
</html>

SpringBoot Redis

SpringBoot 中使用 Redis 非常简单:

  1. 引入依赖

    1
    compile('org.springframework.boot:spring-boot-starter-data-redis')
  2. 在 application.properties 中配置 Redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.password=
    spring.redis.database=0
    spring.redis.pool.max-active=8
    spring.redis.pool.max-wait=-1
    spring.redis.pool.max-idle=8
    spring.redis.pool.min-idle=0
    spring.redis.timeout=0
  3. 使用 StringRedisTemplate 访问 Redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class HelloController {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @GetMapping("/redis")
    public String redis() {
    return redisTemplate.opsForValue().get("user");
    }
    }

SpringBoot MyBatis

SpringBoot 使用 MyBatis 主要为以下 4 步:

  1. 引入依赖

    1
    2
    compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1')
    runtime('mysql:mysql-connector-java')
  2. 配置数据源: 配置 application.properties

    1
    2
    3
    4
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.url=jdbc:mysql://localhost:3306/test
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  3. 编写 Mapper: 使用注解 @Mapper 自动生成 Mapper 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.xtuer.mapper;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    import java.util.Map;
    @Mapper
    public interface UserMapper {
    @Select("SELECT * FROM user WHERE username=#{username}")
    public Map findUserByUsername(String username);
    }
  4. 使用 Mapper: 使用 @Autowired 装配 mapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.xtuer.controller;
    import com.xtuer.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import java.util.Map;
    @RestController
    public class HelloController {
    @Autowired
    private UserMapper userMapper;
    @GetMapping("/hello")
    public Map hello(@RequestParam String username) {
    return userMapper.findUserByUsername(username);
    }
    }

Hexo 跳过指定文件的渲染

Hexo 博客中所见文章都是经由渲染的静态网页,而静态网页的样式都直接由 Hexo 的主题控制,所以 Hexo 博客大部分都呈现出一种高度的统一化与规范化。不过 Hexo 提供了跳过渲染功能,使得我们可以直接在博客中放入自定义网页: 在 _config.yml 配置中配置 skip_render:

如果要跳过 source 文件夹下的test.html,可以这样配置:

1
skip_render: test.html

注意,千万不要加上个/写成/test.html,这里只能填相对于 source 文件夹的相对路径

如果要忽略 source 下的 test 文件夹下所有文件,可以这样配置:

1
skip_render: test/*

如果要忽略 source 下的 test 文件夹下.html文件,可以这样配置:

1
skip_render: test/*.html

如果要忽略 source 下的 test 文件夹内所有文件包括子文件夹以及子文件夹内的文件,可以这样配置:

1
skip_render: test/**

如果要忽略多个路径的文件或目录,可以这样配置:

1
2
3
skip_render:
- test.html
- test/*

参考:

九宫格绘图

很多时候都会使用图片作为 widget 的背景,如果图片和 widget 一样大的话那就没什么好说的,背景效果和图片的效果看上去一样,可更多的时候我们会面临图片和 widget 不一样大,如果把图片简单的缩放到和 widget 一样大作为背景的话,背景常常会变形、有锯齿等,如下面的背景图大小为 128 x 108,要作为 300 x 200 大小的背景,直接缩放绘制的效果很不好,如若使用接下来将要介绍的九宫格绘图技术来绘制背景的话,效果正是我们期望的:

  • 左边是直接缩放绘制的效果,背景发虚,有锯齿,圆角被放大
  • 右边是九宫格技术绘制的效果,圆角和背景的圆角一样

Spring Security QQ 登陆

Spring Security 中实现 QQ 登陆,可以在 FORM_LOGIN_FILTER 前插入一个 filter 用于拦截 QQ 登陆成功后的回调,进行身份认证。

开发前需要准备一个 QQ 互联账号和修改 hosts,按照下面的说明操作即可。

要点: Spring Security 中身份认证成功的标志很简单,只要用用户信息创建一个 Authentication 对象,保存到 SecurityContextHolder 就可以了。

Spring Security 发现 SecurityContextHolder 中有 Authentication 后,就认为用户已经通过了身份认证,对访问的资源进行权限验证时调用 Authentication.getAuthorities() 获取用户的权限进行验证。

注册 QQ 互联账号

  1. 在开发前,需要在 QQ 互联 注册一个开发者账号: https://connect.qq.com
  2. 然后点击 应用管理: https://connect.qq.com/manage.html
  3. 创建 网站应用,里面有开发需要的 APP IDAPP Key

修改 hosts

例如我们在 QQ 互联中填写的回调 URL 为 http://open.qtdebug.com:8080/oauth/qq/callback,很显然 QQ 服务器是不能访问这个地址的,因为这个地址不存在,为了在 QQ 登陆成功后 QQ 服务器能访问这个地址,需要在系统的 hosts 文件里添加 127.0.0.1 open.qtdebug.com

还有另一种方式是使用如 Ngrok 把本地映射为外网可访问。

SpringBoot Start

Spring Boot 创建入门级 RESTful Web 项目简单到令人发指,下面就来看看怎么用吧(如果想知道 Spring Boot 是啥,搜索即可):

  1. 创建项目的骨架
  2. 添加 RestController
  3. 启动项目: gradle bootRun
  4. 打包项目: gradle build

创建项目的骨架

  1. 访问 http://start.spring.io
  2. 选择 Gradle: Generate a Gradle Project with Java Spring Boot 1.5.6
  3. 填写 Group(项目的包名,例如 com.xtuer) 和 Artifact(可不填)
  4. Search for dependencies 输入 web
  5. 点击 Generate Project,会自动下载项目骨架的 zip 文件
  6. 解压,如果不需要里面的 gradlew,删除即可

Spring Security JWT + Token 认证

Spring Security Session + Token 认证 中介绍了 Token 相关的身份验证,但是怎么验证 token 和使用 token 获取用户信息没有进行介绍,可以把 token 存储到 Redis、数据库等,下面介绍另一种 token 实现方法 JWT(Json Web Token),这种 token 不需要存储到服务器,自身就能进行验证。

JWT 中存储了 token 的签名,用户信息,还可以存储 token 的签发时间用于服务器验证 token 的有效期,并且这些信息如果被篡改了的话就会导致 token 失效,JWT 的理论请参考 http://www.jianshu.com/p/576dbf44b2ae

为了在 Spring Security 中使用 JWT,需要修改下面 3 个类:

  • TokenAuthenticationFilter
  • TokenService
  • JwtUtils

Spring Security 权限继承

如下面层级结构的权限:

1
2
3
ROLE_ADMIN > ROLE_STAFF
ROLE_STAFF > ROLE_USER
ROLE_USER > ROLE_GUEST

A user who is authenticated with ROLE_ADMIN, will behave as if they have all four roles, as well the user will have the authorities ROLE_USER and ROLE_GUEST who is authenticated with ROLE_USER请参考帮助文档

Spring Security 里通过 RoleHierarchyVoter 实现权限继承(只需要配置,不需要写代码)。

注意:

<http> 需要设置 use-expressions 为 false,禁用 SpEL 表达式进行权限判断,因为它为 true 时 Spring Security 使用 WebExpressionConfigAttribute,它的 getAttribute() 总是返回 null,导致 RoleVoter.supports() 总是返回 false,于是权限校验失败,为 false 时使用的是 SecurityConfig,这时就没问题了,可以在 RoleVoter.vote() 中打断点进行验证。

坑爹的是,Spring Security 的帮助文档里没说这个,走了不少弯路。

微信企业号开发

微信企业号主要是为了链接组织和内部员工的,通过提供通讯录、组织新闻公告、活动、投票、调研、论坛 BBS、意见建议墙、招聘、考勤、流程审批、任务管理等一系列功能和服务,提高组织运作效率。微信企业号是微信为企业客户提供的移动服务,旨在提供企业移动应用入口。

微信企业号,有以下一些特点:

  • 关注更安全

    只有企业通讯录的成员才能关注企业号,分级管理员、保密消息等各种特性确保企业内部信息的安全

  • 应用可配置

    企业可自行在企业号中配置多个服务号,可以连接不同的企业应用系统,只有授权的企业成员才能使用相应的服务号

  • 消息无限制

    发送消息无限制,并提供完善的管理接口及微信原生能力,以适应企业复杂、个性化的应用场景

  • 使用更便捷

    企业号在微信中有统一的消息入口,用户可以更方便地管理企业号消息。微信通讯录也可以直接访问企业号中的应用

Spring Security Session + Token 认证

前面通过表单进行登陆,会为用户创建一个 session 保存在服务器端,session id 保存在 cookie 中,每次访问服务器的时候服务器端从 cookie 中读取 session id 然后找到用户的 session,就能知道当前用户的信息。但是对于移动端来说,传递 cookie 不是很方便,一般都会使用 token 来进行验证。

Token 就是一个字符串(可以使用 uuid),验证时使用的 token 可以理解为和 session id 的功能差不多:

  1. 用户申请 token 时,可以把 token 作为 key,用户信息的对象作为 value 保存到 Redis 中,把 token 返回给移动端
  2. 移动端保存 token,有很多种方式,例如保存到文件中,sqlite 里都可以
  3. 每次访问的时候把 token 放到请求的 header 中
  4. 服务器端从 header 中读取 token,然后用 token 作为 key 去 Redis 中去读用户数据
  5. 如果读取到的用户数据有效,则说明用户是合法的,认证通过,继续访问,否则返回错误,终止请求

使用纯 token 验证,不支持 session,这样的应用一般都是用来提供纯数据服务(应用中没有网页,很多微服务就是这样的),以下叫 DSA(Data Service Application),但是数据也是需要后台功能来管理的,大多都会使用 Web 应用,叫 DMA(Data Management Application),Web 应用需要使用 session,也就是说 DSA 和 DMA 是独立的 2 个应用,不能共存,因为 DSA 中不支持 session,而 DMA 中需要 session。这种设计的好处是 DSA 很轻量级,只关心数据服务,能够降低开发的复杂度,还有其它比如每个服务都很简单,只关注于一个业务功能,每个微服务可以由不同的团队独立开发,微服务是松散耦合的等等。但是也有缺点,比如有可能对资源的访问需要重复实现,例如一个电子图书馆程序,读取图书信息的 API /api/books/{bookId} 在 DSA 中需要实现,在 DMA 中也要提供实现,因为 DMA 中也需要读取图书信息进行管理,就算用分布式服务使用 dubbo 负责服务治理,由 DMA 提供访问数据的逻辑,但是 DSA 和 DMA 里都至少也要各自有个 Controller 来处理这个 URL 吧。

本文的目的,是要实现一个 Web 应用即支持 session,同时又能支持使用 token 进行身份验证时不生成 session:

  • 浏览器访问 /api/books/{bookId} 时,从 cookie 中读取 session id 找到对应的 session,获取当前用户,如果没有登陆则跳转到登陆页面进行登陆,登陆成功会创建 session
  • 移动端访问 /api/books/{bookId} 时,从 header 中读取 token 找到对应的用户,如果没有 token 或者 token 过期、用户信息无效则返回错误提示未登陆认证(token 可以事先请求保存起来),整个过程不会产生 session

Qt 程序简单打包

程序在开发工具例如 QtCreator 中运行没有问题,不少同学开发好后就直接把 xxx.exe 给用户使用,用户双击 xxx.exe 后提示错误,打开程序失败。很是奇怪: 程序在我的电脑里打开好好的,为什么到其他电脑上就不行了呢,是不是他的电脑有问题?不知道此同学有没有在自己电脑上双击过这个程序!

例如双击下面的 Gui.exe,提示找不到 libgcc_s_dw2-1.dll,那是因为 Qt 的程序运行的时候除了需要可执行程序本身外,还需要依赖一些其他的 dll,需要把这些 dll 一起打包给用户才行:

Qt 程序打包一般有 2 种方式,纯手动打包和半自动打包,下面以 Windows 中打包 Qt 程序 Gui.exe 为例进行介绍,环境如下:

  • 安装 MinGW 的 Qt 5.9.1 到 F
  • DLL 目录: F:\Qt\Qt5.9.1\5.9.1\mingw53_32\bin
  • Qt 的插件目录: F:\Qt\Qt5.9.1\5.9.1\mingw53_32\plugins

GitBook 入门

GitBook 使用 Markdown 来写文档,官网已经有 5 万多本使用 GitBook 写的书了。现在不少公司都开始用 GitBook 来写项目文档、使用手册等。下面就简要的介绍怎么使用 GitBook:

  1. 安装 Git
  2. 安装 Nodejs
  3. 安装 GitBook: npm install gitbook -g
  4. 安装 GitBook-Cli: npm install -g gitbook-cli
  5. 下载 GitBook Editor, GitBook 官方提供的编辑器: https://www.gitbook.com/editor

HTML5 播放器 Video.js

Video.js 是一个简洁、漂亮的 HTML5 播放器,支持字幕,还可支持 Flash(不支持 HTML5 时自动切换到 Flash),使用很简单,也能自定义插件:

Video.js is a JavaScript and CSS library that makes it easier to work with and build on HTML5 video. This is also known as an HTML5 Video Player. Video.js provides a common controls skin built in HTML/CSS, fixes cross-browser inconsistencies, adds additional features like fullscreen and subtitles, manages the fallback to Flash or other playback technologies when HTML5 video isn’t supported, and also provides a consistent JavaScript API for interacting with the video.

事件的坐标

JS 的事件有几个重要的坐标:

  • (offsetX, offsetY): 事件触发点在事件源元素的坐标系统中的坐标(相对于元素的左上角,左上角坐标为 (0, 0))
  • (pageX, pageY): 事件触发点相对于整个页面左上角的坐标,包括了滚动条隐藏的部分
  • (clientX, clientY): 事件触发点相对于页面可视部分(客户区)左上角的坐标,不包括滚动条隐藏的部分
  • (screenX, screenY): 事件触发点相对于屏幕左上角的坐标

限制同一个账号的登陆用户

有时希望限制同一个账号同时只能有 1 个用户登陆,通常为后一次登录将使前一次登录失效。Spring Security 的 session-management为我们提供了这种限制:

  1. 在 web.xml 中定义监听器 HttpSessionEventPublisher

    1
    2
    3
    <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
  2. 通过 concurrency-control 来限制账号登陆数

    1
    2
    3
    4
    5
    6
    <http auto-config="true">
    ...
    <session-management>
    <concurrency-control max-sessions="1"/>
    </session-management>
    </http>

测试 ThreadLocal

每个 Thread 都有一个 ThreadLocalMap 的对象,存储时以 ThreadLocal 变量为 key,set() 的参数作为 value,这样同一个 ThreadLocal 变量在不同的线程中就可以存储不同的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ThreadLocalTest {
private static ThreadLocal<String> foo = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
new Thread(() -> {
await(300);
System.out.println(Thread.currentThread().getName() + ": " + foo.get()); // [2] 输出: Thread-1: null
foo.set("1"); // [3]
await(1000);
System.out.println(Thread.currentThread().getName() + ": " + foo.get()); // [5] 输出: Thread-1: 1
}, "Thread-1").start();
new Thread(() -> {
foo.set("2"); // [1]
await(600);
System.out.println(Thread.currentThread().getName() + ": " + foo.get()); //[4] 输出: Thread-2: 2
}, "Thread-2").start();
}
public static void await(long timeout) {
try { Thread.sleep(timeout); } catch (InterruptedException e) {}
}
}

输出:

1
2
3
Thread-1: null
Thread-2: 2
Thread-1: 1

ThreadLocal 的变量一般定义为 private static 的。

截取 Canvas 绘制的图形

使用 canvas 绘图时,很多时候在 canvas 上绘制的图像只占 canvas 的一部分,如果把整个 canvas 的图像发送到服务器就比较浪费空间和带宽,所以保存真正绘制的图像部分是有必要的。

下图的上部分为一个的 canvas,绘制的图像大概只占它的四分之一,我们的目的是把 canvas 中多余的部分去掉,得到真正绘制的图像,如下部分显示:

MySQL 命令行客户端 MyCLI

MyCLI 是一个 MySQL 的命令行客户端,可以实现自动补全(auto-completion)和语法高亮,具体特性如下:

  • 智能补全
  • SQL 语法高亮显示
  • 自动完成输入 SQL关键字以及数据库列表
  • SELECT * FROM <tab> 只显示表名
  • SELECT * FROM users WHERE <tab> 只显示列名
  • 支持 tab 自动补全
  • MySQL 的输出会通过 less 命令进行格式化输出
  • 支持 ssl 连接

Qt 调用摄像头

可以使用 OpenCV 来操作摄像头,不过 Qt5 已经自带了调用系统摄像头的功能,使用起来很方便,主要是使用下面 3 个类:

  • QCamera
  • QCameraViewfinder
  • QCameraImageCapture

下面代码的效果为

Thymeleaf 语法

Thymeleaf 使用 HTML 元素的属性获取 model 中的数据,属性的前缀是 th:,例如 th:text, th:src

变量访问

1
2
3
<span th:text="${name}">Thymeleaf 解析后会被覆盖</span>
<span th:text="|Welcome ${name}|">Thymeleaf 解析后会被覆盖</span>
<span th:text="'Welcome ' + ${name}">Thymeleaf 解析后会被覆盖</span>

字符串拼接时 |...| 的方式更简洁,但是里面不能包含表达式,第三种方式功能强大,可以包含表达式。

变量访问也可以使用级联的方式: ${user.name}

使用 URL

1
<a th:href="@{/login}" th:if="${session.user == null}">Login</a>

使用 th:href="@{/uri}" 引入 URL,/ 开头时会在 URI 前面加上项目的 context path

Thymeleaf 集成

有了 Freemarker,Velocity 等模版后,为什么要选择 Thymeleaf?

  • Freemarker 的模版还是有一些非 HTML 的标签在里面,对于前端来说需要学习相关语法
  • Velocity 虽然也很好,但是已经很久不更新了,Spring 5 已经官方宣布不支持 Velocity 了
  • Thymeleaf 的语法就是 HTML 的语法,动态内容部分使用 HTML 的属性来实现,属性部分不会影响 HTML 的设计,前后端可以很好的分离

软件开发流程

第一步: 我们要确定一个可行的设计方案: 第二步: 我们要开始把框架搭好 第三步: 我们开始一个模块一个模块的完成
第四步: 可以拿去给测试们看了(白盒黑盒都有) 第五步: 产品经过测试通过可以拿去给安全组检查了 最后: 我们的产品就可以上线了

I make things.

Gradle Deploy

项目打包后一般可以按照以下几个步骤进行部署:

  1. 选择正确的环境打包: 测试环境、线上环境等
  2. 把 war 包上传到服务器(使用 FTP、scp 等)
  3. 停止 tomcat: <tomcat>/bin/shutdown.sh
  4. 删除服务器上的项目文件: rm -rf <project_path>
  5. 解压 war 包到项目路径下: unzip project.war -d <project_path>
  6. 启动 tomcat: <tomcat>/bin/startup.sh
  7. 删除上传的 war 包
  8. 如果有 N 个服务器,就需要重复 2 到 7 共 N 次

每次部署都要重复这么多步骤,效率不高,而且容易疏忽出错,为了解决这些问题,借助 Gradle 的 deploy 插件,一条命令 gradle deploy 就完成上面的这些事了。

CentOS 7 简单使用

下载 CentOS

访问 https://www.centos.org/download/ 下载 Minimal ISO 即可,其他的虽然功能齐全,但是太大了。

必要工具

  • yum install zip unzip:

    1
    2
    3
    4
    5
    # 把文件夹 H5 和文件 x.html 压缩成 result.zip
    zip -r result.zip H5 x.html
    # 解压 filename.zip, 如无 -d 则解压到当前目录,有则解压到目录 dest-directory
    unzip filename.zip [-d dest-directory]
  • yum install net-tools (安装后才能使用 ifconfig 等)

  • tar 解压 tar.gz: tar xf filename.tar.gz

  • 安装 tree: yum install tree

实现 Steps 路径样式

如下图使用多个 步骤 表示一个过程:

这样的组件 Qt 没有提供,需要我们自己实现,可以用下面几种方式实现:

  • 使用 QPainter 绘图:计算每一个步骤的图形(可以使用 QPainterPath)和位置,然后在 QPainterPath 上填充背景和文字

  • QPushButton + QSS Border-Image + 绝对坐标定位:因为 QPushButton 之间有重叠,而不是一个紧挨着一个的排列,所以需要计算每个步骤的坐标进行定位,使用 PS 设计步骤在不同状态时的背景图,需要 6 张图片:

    • 当前步骤:第一个位置的图、中间的图、最后一个位置的图

    • 非当前步骤:第一个位置的图、中间的图、最后一个位置的图

    • 每个图是步骤的完整背景图,例如

      优点:直观

      缺点:需要手动计算坐标

  • QPushButton + QSS Border-Image + QHBoxLayout:使用 Layout 把 QPushButton 一个紧挨着一个的排列,使用 PS 设计步骤在不同状态时的背景图,需要 5 张图片:

    • 当前步骤:当前步骤前一个步骤的图、最后一个位置的图

    • 非当前步骤:第一个位置的图、中间的图、最后一个位置的图

    • 每个图都不是步骤的完整背景图,例如

      优点:能够使用 Layout 进行布局,不需要手动计算坐标

      缺点:不够直观,不过,在步骤之间加一点空隙,估计大家都明白怎么做了:每一个步骤的背景都有一部分在它的前一个步骤上:

Nginx + Tomcat 使用 Https

Nginx 作为前端反向代理或者负载均衡,Tomcat 不需要自己处理 https,https 由 Nginx 处理:

  • 用户首先和 Nginx 建立连接,完成 SSL 握手
  • 而后 Nginx 作为代理以 http 协议将请求转发给 Tomcat 处理
  • Nginx 再把 Tomcat 的输出通过 SSL 加密发回给用户

这中间是透明的,Tomcat 只是在处理 http 请求而已(默认监听 8080 端口)。因此,这种情况下不需要配置 Tomcat 的 SSL,只需要配置 Nginx 的 SSL,Tomcat 和 Nginx 需要配置以下几项:

  • Nginx 中启用 https:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    http {
    include mime.types;
    default_type text/html;
    gzip on;
    gzip_types text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/richtext image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;
    sendfile on;
    # Tomcat 服务器集群
    upstream app_server {
    server 127.0.0.1:8080 weight=4;
    server 127.0.0.1:8081 weight=2;
    server 127.0.0.1:8082 weight=1;
    }
    server {
    listen 443; # https 的默认端口是 443
    charset utf-8;
    server_name www.xtuer.com; # host_name of URL
    # 启用 https
    ssl on;
    ssl_certificate /Users/Biao/Desktop/cert/server.crt;
    ssl_certificate_key /Users/Biao/Desktop/cert/server.key;
    location / {
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # 把 https 的协议告知 Tomcat,否则 Tomcat 可能认为是 http 的请求
    proxy_set_header X-Forwarded-Proto $scheme;
    # 请求转发给 Tomcat 集群处理
    proxy_pass http://app_server;
    }
    }
    }

    关键是以下几项:

    • ssl on
    • ssl_certificate
    • ssl_certificate_key
    • X-Forwarded-Proto
  • Tomcat 的 server.xml 的 Host 中配置 Valve:

    1
    2
    3
    <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
    <Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" protocolHeader="X-Forwarded-Proto" protocolHeaderHttpsValue="https"/>
    </Host>
    • X-Forwarded-Proto 是为了正确地识别实际用户发出的协议是 http 还是 https。
    • X-Forwarded-For 是为了获得实际用户的 IP。

Vue 自定义组件

Vue 提供了自定义组件的功能,可以定义全局组件,也可以定义局部组件:

  • 全局组件: 使用 Vue.component() 来注册
  • 局部组件: 使用 Vue 对象的 components 属性来注册

下面先介绍全局组件的自定义,然后再简要的介绍局部组件的自定义。

QtCreator 中重构 Widget 的名字

QtCreator 中创建的 Qt Designer Form Class 包含三个文件: .h, .cpp, .ui,例如我们创建了一个 Form Class Widget,则包含下面三个文件:Widget.h, Widget.cpp, Widget.ui,其中的类名为 Widget,如果想要把其重命名为 MyWidget,则可以按照下面几步进行:

  • 文件重命名为 MyWidget.h, MyWidget.cpp, MyWidget.ui
  • 修改 MyWidget.ui 中的 objectName
  • 重构 MyWidget.h 中的类名 Ui::Widget 和 Widget,同时也可修改 #ifndef 的名字
  • 修改 MyWidget.cpp 中的 #include

签名验证

签名验证

签名验证涉及到客户端(比如一个 Web 应用)和服务器端,每个客户端在服务器上有一个对应的 app_idapp_key,大致步骤如下:

  1. 客户端使用 app_id + app_key + 其他参数生成签名字符串 sign

  2. 把 app_id、其他参数 和 sign 一起发送给服务器(app_key 不发送)

  3. 服务器接收到请求后,根据参数中的 app_id 查找到对应的 app_key,然后根据签名算法生成签名字符串 sign2

    客户端和服务器端使用同样的签名算法生成签名字符串。

  4. 字符串比较参数中的 sign 和服务器生成的 sign2,如果相等则签名没问题,放行访问,否则签名无效,拒绝访问