CentOS7 安装 LibreOffice

使用 CentOS7 无界面版本作为服务器的操作系统,在里面安装 LibreOffice 用于转换各种文档到 PDF,LibreOffice 的安装步骤如下:

  1. 下载:

    1. 访问 https://www.libreoffice.org/download/download/
    2. 选择 Linux x86_64(rpm) 的版本
    3. 下载得到 LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz (目前最新版为 6.0.5)
  2. 安装:

    1. 删除: 在安装之前,先删除已经安装的 LibreOffice: yum remove libreoffice*
    2. 解压: tar -xvf LibreOffice_6.0.5_Linux_x86-64_rpm.tar.gz
    3. 安装:
      1. cd LibreOffice_6.0.5.2_Linux_x86-64_rpm/RPMS
      2. yum localinstall *.rpm
    4. 查看:
      1. which libreoffice6.0 看到路径为 /usr/bin/libreoffice6.0
      2. ll /usr/bin/libreoffice6.0 得到 /opt/libreoffice6.0/program/soffice,说明安装到了 /opt/libreoffice6.0
  3. 依赖:

    执行 libreoffice6.0 可能会提示库文件找不到,如 libcairo.so.2,libcups.so.2,libSM.so.6 等,执行下面几条命令安装需要的库:

    • yum install cairo -y
    • yum install cups-libs -y
    • yum install libSM -y

安装 LibreOffice 可参考 https://www.tecmint.com/install-libreoffice-on-rhel-centos-fedora-debian-ubuntu-linux-mint/

Java 中可使用 JODConverter 调用 LibreOffice 进行文件格式转换,可参考 Office 文档转为 PDF 和 HTML

QLineEdit 中增加按钮

下图为 Safari 的地址栏,在输入框右边有一个刷新按钮:

在输入框中增加按钮的设计是比较常见的,例如 Chrome 的地址栏、Firefox 的搜索框、Tim 的搜索框等,这种控件 Qt 没有提供,那应该怎么实现呢?下面提供 2 种思路:

  • QLineEdit + QPushButton 使用 QHBoxLayout 布局到一个 QWidget 中,去掉 QLineEdit 得到焦点时的高亮效果,此时应该高亮它的父控件,失去焦点时取消它的父控件的高亮效果
  • QLineEdit 作为一个普通的 QWidget,也就是它能够使用 QLayout 把 QPushButton 作为子控件布局到它里面

思路有了,第一种方式需要写很多代码进行控制,实现比较麻烦,下面只介绍第二种思路的实现。

模型视图编程

模型视图控制器 (MVC) 编程哪家讲的好,莫非是 Qt 帮助文档里的 Model/View Programming 了,可惜没有中文的,看过不少书里这方面的内容都是从这篇文档里抄的,我们就不要再花心思去创新了,直接翻译吧。

模型视图编程简介

Qt contains a set of item view classes that use a model/view architecture to manage the relationship between data and the way it is presented to the user. The separation of functionality introduced by this architecture gives developers greater flexibility to customize the presentation of items, and provides a standard model interface to allow a wide range of data sources to be used with existing item views. In this document, we give a brief introduction to the model/view paradigm, outline the concepts involved, and describe the architecture of the item view system. Each of the components in the architecture is explained, and examples are given that show how to use the classes provided.

Qt 中有几个 item view 的类 (视图),使用模型/视图架构来管理和显示数据。这种分离的设计给开发者很高的灵活性,可以自定义数据的显示方式,视图通过模型提供的标准接口可以支持各种各样的数据源。本文涉及到模型/视图编程范例、简要的概念介绍以及描述视图系统的架构。每一个部分都会进行解释以及给出相关代码展示怎么使用。

模型视图架构

Model-View-Controller (MVC) is a design pattern originating from Smalltalk that is often used when building user interfaces. In Design Patterns, Gamma et al. write:

MVC consists of three kinds of objects. The Model is the application object, the View is its screen presentation, and the Controller defines the way the user interface reacts to user input. Before MVC, user interface designs tended to lump these objects together. MVC decouples them to increase flexibility and reuse.

Smalltalk 经常用来构建用户界面,模型-视图-控制器 (MVC) 这种设计模式就是从 Smalltalk 借鉴而来的。Gamma et al. 在设计模式中写到:

MVC 由 3 种对象组成:Model 是应用的数据,View 显示数据,Controller 定义了用户界面对用户输入的响应。在使用 MVC 之前,用户界面的设计常常把这些对象耦合在一起。MVC 能够解耦它们从而提供更高的灵活性和重用性。

The model/view architecture
The model communicates with a source of data, providing an interface for the other components in the architecture. The nature of the communication depends on the type of data source, and the way the model is implemented.

The view obtains model indexes from the model; these are references to items of data. By supplying model indexes to the model, the view can retrieve items of data from the data source.
In standard views, a delegate renders the items of data. When an item is edited, the delegate communicates with the model directly using model indexes.

在模型/视图架构中,模型访问数据源中的数据,视图等控件通过模型提供的接口访问数据。通信的方式取决于数据源的类型和模型实现的方式。
数据由数据项组成,index 是数据项的引用,视图从模型中获得 index。视图通过访问模型中的 index 就能够访问数据源中的数据了。
在标准的视图中,代理用来渲染数据项。当数据项被编辑时,代理使用模型的 index 和模型交互。

Generally, the model/view classes can be separated into the three groups described above: models, views, and delegates. Each of these components is defined by abstract classes that provide common interfaces and, in some cases, default implementations of features. Abstract classes are meant to be subclassed in order to provide the full set of functionality expected by other components; this also allows specialized components to be written.

Models, views, and delegates communicate with each other using signals and slots:

  • Signals from the model inform the view about changes to the data held by the data source.
  • Signals from the view provide information about the user’s interaction with the items being displayed.
  • Signals from the delegate are used during editing to tell the model and view about the state of the editor.

根据上面的介绍,模型/视图的类可以分为三类:模型、视图和代理,它们都有相应的抽象类提供通用的接口和某些功能的默认实现。抽象类就意味着要被其他类继承,根据需求提供对应的实现。

模型、视图和代理之间使用信号槽进行通信:

  • 数据源中的数据发生变化时,模型发射信号通知视图
  • 用户和视图交互时视图会发射信号,例如点击 view item,信号的参数包含了被交互的 view item 的信息
  • 编辑数据项的时候代理会把编辑器的状态通过信号通知模型和视图

模型

All item models are based on the QAbstractItemModel class. This class defines an interface that is used by views and delegates to access data. The data itself does not have to be stored in the model; it can be held in a data structure or repository provided by a separate class, a file, a database, or some other application component.

The basic concepts surrounding models are presented in the section on Model Classes.

QAbstractItemModel provides an interface to data that is flexible enough to handle views that represent data in the form of tables, lists, and trees. However, when implementing new models for list and table-like data structures, the QAbstractListModel and QAbstractTableModel classes are better starting points because they provide appropriate default implementations of common functions. Each of these classes can be subclassed to provide models that support specialized kinds of lists and tables.

The process of subclassing models is discussed in the section on Creating New Models.

所有的 item 模型都是基于类 QAbstractItemModel 实现的。视图和代理使用 QAbstractItemModel 定义的接口访问数据。数据不一定是保存在模型中,也可以保存在其他类、文件、数据库或者其他应用中。

模型相关的概念在模型的类一节中进行介绍。

模型类 QAbstractItemModel 定义的访问数据接口 (函数) 是很灵活的,能够满足表格、列表和树用来显示模型的数据。然而,当给列表和表格自定义新的模型时,继承 QAbstractListModel 或者 QAbstractTableModel 是个很好的选择,因为他们提供了很多通用操作的默认实现,就不需要我们再重复实现。

实现自定义模型在创建新的模型类一节中进行介绍。

Qt provides some ready-made models that can be used to handle items of data:

  • QStringListModel is used to store a simple list of QString items.
  • QStandardItemModel manages more complex tree structures of items, each of which can contain arbitrary data.
  • QFileSystemModel provides information about files and directories in the local filing system.
  • QSqlQueryModel, QSqlTableModel, and QSqlRelationalTableModel are used to access databases using model/view conventions.

If these standard models do not meet your requirements, you can subclass QAbstractItemModel, QAbstractListModel, or QAbstractTableModel to create your own custom models.

Qt 已经提供了一些可用于处理数据项的模型:

  • QStringListModel 用于存储简单的列表数据,数据项为 QString
  • QStandardItemModel 用于管理有树结构关系的数据项,每个数据项可以包含任意的数据
  • QFileSystemModel 访问本地文件系统的文件和文件夹
  • QSqlQueryModel, QSqlTableModel 和 QSqlRelationalTableModel 使用模型视图的方式访问数据库

如果这些标准的模型还不能满足我们的需求,可以继承 QAbstractItemModel, QAbstractListModel, or QAbstractTableModel 实现自定义的模型类。

分组布局

在进行界面布局的时候,常把控件根据功能分组放在一起,最常用的就是使用 QGroupBox 来放置一组控件。QGroupBox 虽然使用起来很方便,但就是有点丑,在要求较高的设计中,还得使用控件组合加自定义绘图或者 QSS 等才能实现,例如下面这个软件界面,直接使用 Qt 提供的控件是满足不了的:

上图中分组的布局没有使用 QGroupBox,而是用几个控件组合起来实现的,设计如下:

异形按钮组

不少软件里看到过如下的按钮组,有 5 个按钮,中间 1 个,上下左右各一个:

可以通过绘图的方式实现:计算每一个按钮的位置、大小、图片、点击的时候判断点击到了哪个按钮然后刷新绘制它的样式,并调用相应的函数执行点击操作,难度还是相当大的。

操作图像像素,实现各种效果

Qt 中图像相关的类主要是 QPixmap 和 QImage,QPixmap 没有提供访问图像像素数据的接口,访问图像的像素数据需要使用 QImage,主要的函数有 (相关重载函数没有列出来):

1
2
3
4
5
6
7
8
// 获取图像的像素数据
QRgb pixel(int x, int y) const
QColor pixelColor(int x, int y) const
uchar* scanLine(int i)

// 设置图像的像素数据
void setPixel(int x, int y, uint index_or_rgb)
void setPixelColor(int x, int y, const QColor &color)

下面把一个图像转为灰度图为例介绍怎么操作图像的像素:

  1. 取得图像的宽、高
  2. 根据宽、高遍历每一个像素
  3. 得到每一个像素的 RGBA 颜色分量
  4. 对得到的颜色分量 RGBA 进行灰度计算得到新的颜色
  5. 使用计算得到的颜色设置对应像素

自定义按钮组

如下的按钮组相信大家都看到过,最左和最右的按钮是圆角的,中间的按钮是矩形的,同时只能有一个按钮是选中状态:

按钮的样式使用 QSS 实现,使用 setProperty 设置按钮的 class 属性为 GroupButton,就可以利用类选择器 .GroupButton 选择按钮组的按钮,避免它们的样式影响到普通按钮。为了单独设置最左和最右按钮的样式,使用 setProperty 为其设置一个属性 position,最左按钮的为 first,最右按钮的为 last,然后就能使用属性选择器 .GroupButton[position="first"].GroupButton[position="last"] 选择它们了。

SpringMVC 使用 @PathVariable 获取有 . 的 URL 中的变量

1
2
3
4
5
@GetMapping("/api/file/{filename}")
@ResponseBody
public String foo(@PathVariable String filename) {
return filename;
}

默认配置时,不同的 URL ,获取到的 filename 为:

  • /api/file/foo,filename 为 foo
  • /api/file/foo.pdf,filename 为 foo
  • /api/file/foo.pdf.png,filename 为 foo.pdf
  • /api/file/foo.pdf.png.doc,filename 为 foo.pdf.png

最后一个 . 被截断了,解决这个问题有 2 中方法:

  • 使用正则表达式进行路径匹配,映射为:@GetMapping("/api/file/{filename:.+}")

    • 缺点:每个路径的映射都要写一遍,不方便
    • 优点:缺点也是优点,只影响需要的路径
  • 配置 annotation-driven,映射为:@GetMapping("/api/file/{filename}")

    • 优点:只需要配置一次,整个应用都生效,方便
    • 缺点:优点也是缺点,影响了整个系统,不过还没有发现对整个系统有什么副作用
    1
    2
    3
    4
    <mvc:annotation-driven>
    <mvc:path-matching registered-suffixes-only="true"/>
    ...
    </mvc:annotation-driven>

QTreeView 小集

树形控件是非常常用的,例如组织结构、目录树、省市县的地区结构等都是典型的树形结构,Qt 里可以使用 QTreeView 和 QTreeWidget 来展示树形结构,这里我们只介绍 QTreeView 的使用,QTreeView 本身只用于树的显示,树的数据由 QStandardItemModel 来存储。

创建单列树

创建单列树的节点分两种情况:

  • 创建第一级节点调用函数 QStandardItemModel::appendRow(QStandardItem *item)
  • 创建第二级、第三级等非第一级节点调用函数 QStandardItem::(QStandardItem *item)

下面的例子创建省市县的树形结构展示如何创建只有一列的树,为了更好的从变量名上看出地区的关系,使用数字和层级的方式进行命名,程序运行结果如下:

去掉 png 图片的 iCCP 警告

Qt 中使用 png 图片有时候会给出警告 libpng warning: iCCP: known incorrect sRGB profile:

Libpng-1.6 is more stringent about checking ICC profiles than previous versions. You can ignore the warning. To get rid of it, remove the iCCP chunk from the PNG image.

Some applications treat warnings as errors; if you are using such an application you do have to remove the chunk.

解决办法:

  1. 安装 ImageMagick (Mac: brew install ImageMagick)
  2. 到图片文件夹,执行命令 mogrify *.png 去掉此文件夹下 png 图片的 iCCP 警告

要想找出有 iCCP 问题的 png 图片,可以使用工具 pngcrush:

  1. 安装 pngcrush (Mac: brew install pngcrush)
  2. 到图片文件夹,执行命令 pngcrush -n -q *.png 找出有 iCCP 警告的图片

更多细节请参考 libpng warning: iCCP: known incorrect sRGB profile

一次 HTTP 被运营商劫持的血泪史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en" dir="ltr">

<head>
<script src="/js/jquery.js" charset="utf-8"></script>
<script src="/js/paper.js" charset="utf-8"></script>
</head>

<body>
<script>
$(document).ready(function() {
__Exam_PaperInit();
});
</script>
</body>

</html>

很简单的页面,jquery.js 和 paper.js 加载完,然后调用 __Exam_PaperInit() 进行初始化。在办公室、家里都没出现过问题,但是在学校的机房里访问这个页面,时不时的出错,提示如 __Exam_PaperInit() 不存在,这么简单直接的逻辑,咋会出错呢,想不明白,猜测例如是不是机房的环境网络设置有问题等,但花了很久仍然找不到原因,更要命的是,系统过几天就有几千人要用来考试了,问题解决不了的话,可以想象影响会有多大。

iTerm 设置

高亮输出

不同的输出显示为不同的颜色,例如下面的 [DEBUG] 的信息暗一些

Preferences > Profiles > Advanced > Triggers > Edit: 使用正则表达式进行设置,Action 选择为 Highlight Text...

GitBook 使用 Coding.net 的 Pages 访问

GitBook 生成的静态网页文件在 _book 目录中,下面介绍怎么把它发布到 coding.net 的 Pages 服务中,这样就能够通过网络访问了。

  1. https://coding.net 创建一个账号 xtuer(下面请换为自己的账号)
  2. 创建仓库 2 个仓库 fox 和 fox-doc (仓库名字随意取):
    • fox-doc: GitBook 源文件
    • fox: GitBook 生成的静态文件
  3. 克隆这 2 个仓库到本地的同一个文件夹下
    • git clone git@git.coding.net:xtuer/fox.git
    • git clone git@git.coding.net:xtuer/fox-doc.git

右键菜单

右键菜单有多种实现方式:

  • 设置 contextMenuPolicy 为:
    • Qt::ActionsContextMenu
    • Qt::CustomContextMenu
  • 重写 contextMenuEvent 函数

下面就分别介绍这几种右键菜单的实现。

iView 的 Table 中插入按钮

很多时候需要在 Table 的单元格中使用按钮,iView 的官方例子使用函数 createElement (简写 h) 来创建,但是代码很繁杂、不直观、难以实现复杂的 DOM 结构。还好除此之外可以使用 JSX 来实现,能够方便的增加 class、wrapper、图标、任意的 DOM 等。

JSX 实现

1
2
3
4
5
6
7
8
9
10
11
{ title: '操作', key: 'action', width: 160, align: 'center',
// 编辑和删除按钮
render: (h, params) => {
return (
<div class="cell-button-container">
<i-button type="primary" size="small" onClick={()=>{this.editSchool(params.index)}} icon="edit">编辑</i-button>
<i-button type="error" size="small" onClick={()=>{this.deleteSchool(params.index)}} icon="android-delete">删除</i-button>
</div>
);
}
}

提示:

  • 按钮的标签使用 <i-button>,不能使用 <Button>
  • 按钮的事件处理 vue 中为 on-click,但在 JSX 中为 onClick

MyBatis 传递多个参数

MyBatis 传递多个参数一般有以下几种方法:

  • 使用 Map
  • 把参数封装成 Bean,传递 Bean 的对象
  • 使用 @Param
  • 编译时使用 -parameters 参数 (推荐使用)

下面以用户名和密码作为参数查询用户为例进行介绍。

Layout 秘录

布局管理器 QHBoxLayout、QVBoxLayout、QGridLayout 相信大家都很熟悉了,对于常用的功能就不一一列举,这里将介绍一下几个不常用,在复杂的自定义界面时又可能会用到的功能:

  • QGridLayout 中多个 Widget 放在同一个位置
  • 把一个 Widget 替换为另一个 Widget
  • QHBoxLayout、QVBoxLayout 中插入 Widget
  • 从 Layout 中删除 Widget

自定义标题栏无边框阴影窗口

Qt 的默认窗口使用系统风格,不能修改标题栏和边框,满足不了高度自定义的窗口设计,这时只能把窗口的默认标题栏和边框隐藏起来,替换上我们自定义的标题栏和边框,下面就以实现自定义标题栏无边框阴影窗口为例进行介绍。

技术要点:

  • 隐藏系统标题栏和边框: QWidget::setWindowFlags(Qt::FramelessWindowHint)
  • 窗口透明隐藏默认背景: QWidget::setAttribute(Qt::WA_TranslucentBackground)
  • QWidget::paintEvent(QPaintEvent *event) 里绘制任意形状的自定义背景
  • 拖拽移动窗口
  • 缩放窗口

带阴影的圆形 Label

圆形头像大家应该都见过不少软件里用过吧,例如 QQ 的好友列表,网页里的人物头像,有没有想过在 Qt 里怎么做到呢?

这一节中就来介绍怎么实现下图中的圆形 QLabel,然后扩展到给 QLabel 添加阴影效果、模糊效果以及加上边框:

圆形 Label

最核心的就是圆形 QLabel 的实现,有很多种方法能够做到,这里使用 QSS 来实现: Border Image + Border Radius,也就是几行代码的事:

  • 圆形: 先设置 QLabel 的大小为固定大小,这样当窗口大小变化时不会影响 QLabel 的大小,并且设置 border-radius 为 QLabel 高度的一半
    • 必须正好是一半出来的效果才能是正圆
    • 大于一半 border-radius 就失去了效果,出来的是矩形,这应该是 QSS 的 Bug,CSS 里就不这样
    • 小于一半的效果是圆角矩形
  • 背景: 为了让背景图缩放填满 QLabel,需要使用 border-image 并且设置 QLabel 边框的宽度为 0
1
2
3
4
5
6
7
8
9
10
QQLabel {
min-width: 100px;
max-width: 100px;
min-height: 100px;
max-height: 100px;

border-radius: 50px;
border-width: 0 0 0 0;
border-image: url(/Users/Biao/Desktop/estas.jpg) 0 0 0 0 stretch strectch;
}

上面的 QSS 就能得到左边第一个圆形 QLabel 的效果。

MongoDB 初接触

MongoDB 的结构是:数据库 > 集合 (collection) > 文档 (document) > 属性 (field)

MySQL 的结构是: 数据库 > 表 (table) > 记录 (record or row) > 属性 (field or column)

下载安装

不同的系统安装 MongoDB 差别挺大的:

启动访问

  • 启动 MongoDB:

    • mongod

    • mongod --config C:/etc/mongod.conf

  • 访问 MongoDB:

    • mongo
    • mongo --host IP
    • 使用 IDEA 的插件 Mongo Plugin
    • 漂亮的免费客户端 dbKoda
    • 智能的免费客户端 NoSQLBooster for MongoDB (推荐使用)

使用百度 OCR 服务识别图片中的文本

访问 https://cloud.baidu.com/product/ocr/general 可以先体验一下百度的 OCR 文字识别,在功能演示处上传一个含有文字的图片就可以看到识别效果,还是挺不错的,接下来就介绍使用 OCR 服务的编程实现:

  1. 点击立即使用

  2. 点击创建应用 (需要登陆)

  3. 得到应用 API KeySecret Key (在程序中需要使用,对应程序中的 APP_IDAPP_KEY)

  4. 使用 API KeySecret Key 换取 access_token,请参考鉴权认证机制

  5. 使用 OCR 服务识别图片中的文字,请参考通用文字识别

    • 把图片进行 Base64 编码成为字符串

      文档中说所有图片均需要 Base64 编码后再进行 urlencode,这里容易造成困扰,其实 Base64 后就够了,因为 Base64 包含的 64 个字符为 a-z, A-Z, 0-9, /, + 以及填充字符 = 都包含在了 urlencode 不需要进行编码的字符内。

    • 去掉图片头,如 data:image/jpg;base64,

    • 传给百度,然后就能得到识别的 JSON 结果

Qt 项目中使用 OpenCV

相信很多人在 Qt 项目中使用 OpenCV 都遇到过麻烦,Windows 开发者软件推荐 一文中介绍过使用 Vcpkg 来管理第三方库,这里就使用 Vcpkg 安装 OpenCV 然后在 MSVC 的 Qt 项目中使用(因为 Vcpkg 使用的是 MSVC 编译器,OpenCV 是 C++ 库,不能够跨编译器,所以 MinGW 的项目不能使用):

  1. 安装 Vcpkg 就不用多说了,安装到 C 盘根目录下吧

  2. 安装 OpenCV: vcpkg install opencv

  3. Qt Creator 中 创建 Qt 项目

  4. 修改项目的 .pro 文件,主要是下面 2 句引入 OpenCV

    1
    2
    INCLUDEPATH += C:/vcpkg/installed/x86-windows/include
    LIBS += C:/vcpkg/installed/x86-windows/lib/opencv_*.lib

    使用 opencv_*.lib 引入所有 opencv_ 开头的 lib 文件,这样就不需要一个一个的引入 lib 了。

    引入 dll 的时候也可以使用 * 来匹配一次引入多个,例如 tiff*.dll

  5. main.cpp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <opencv2/opencv.hpp>
    using namespace cv;

    int main() {
    Mat img = imread("D:/Wallpaper/desktop.jpg");
    imshow("TEST", img);
    waitKey(6000);

    return 0;
    }
  6. 把 OpenCV 相关的 DLL 从 C:/vcpkg/installed/x86-windows/bin 目录复制到编译出的 exe 所在目录

  7. 运行程序,然后就看到打开一个窗口,图片显示在窗口中

Windows 开发者软件推荐

包管理器 Chocolatey

Chocolatey 是一款专为 Windows 系统开发的、基于 NuGet 的包管理器工具,类似于

  • Node 的 npm
  • MacOS 的 brew
  • Ubuntu 的 apt-get
  • CentOS 的 yml

Chocolatey 的设计目标是成为一个去中心化的框架,便于开发者按需快速安装应用程序和工具,官网为 https://chocolatey.org,安装很简单,根据说明安装即可。

常用命令

  • 搜索: choco search something

  • 列出: choco list -lo

  • 安装: choco install cmake

    可访问 https://chocolatey.org/packages 查看已有的包和说明

  • 卸载: choco uninstall cmake

  • 升级: choco upgrade cmake

  • 固定包的版本,防止包被升级: choco pin windirstat

MyBatis Collecton

MyBatis 中一对多的关系使用 collection 进行映射,但是怎么确定哪些行是同一个对象的数据呢?关键是使用 <id> 来归类数据(MyBatis 的文档只提到是为了提高效率),下面介绍使用 <id><result> 的区别。

数据表

name email country province street
Biao biao@gmail.com china 北京 天河街
Biao biao@icloud.com Deutschland Braunschweig Wiesenstrasse
Alice alice@gmail.com china 河北 鼓楼大街

Xml Mapper

查找所有用户

1
SELECT name, email, country, province, street FROM user

映射文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mapper namespace="com.xtuer.mapper.UserMapper">
<select id="users" resultMap="userResultMap">
SELECT name, email, country, province, street FROM user
</select>

<resultMap id="userResultMap" type="User">
<id property="name" column="name"/> <!-- 关注这里 -->
<result property="email" column="email"/>
<collection property="addresses" resultMap="addressResultMap"/>
</resultMap>

<resultMap id="addressResultMap" type="Address">
<result property="country" column="country"/>
<result property="province" column="province"/>
<result property="street" column="street"/>
</resultMap>
</mapper>

Java 按照拼音排序

Java 中按照拼音序对字符串排序,只需要使用 CHINA 的 Collator 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.text.Collator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

public class Test {
public static void main(String[] args) {
List<String> tokens = new LinkedList<>();
tokens.add("黄"); // h
tokens.add("慌"); // h
tokens.add("晃"); // h
tokens.add("欢"); // h
tokens.add("中"); // z
tokens.add("这"); // z
tokens.add("国"); // g
tokens.add("古"); // g
tokens.add("安"); // a

Collator collator = Collator.getInstance(Locale.CHINA);
tokens.sort((a, b) -> collator.compare(a, b));

System.out.println(tokens);
}
}

MySQL 数据类型

MySQL 中定义数据字段的类型对你数据库的优化是非常重要的,支持多种类型,大致可以分为三类:数值、日期/时间和字符串(字符)类型。

数值类型

MySQL支持所有标准 SQL 数值数据类型,这些类型包括严格数值数据类型(INTEGER、SMALLINT、DECIMAL 和 NUMERIC),以及近似数值数据类型(FLOAT、REAL 和 DOUBLE)。

关键字 INT 是 INTEGER 的同义词,关键字 DEC 是 DECIMAL 的同义词。
BIT 数据类型保存位字段值,并且支持 MyISAM、MEMORY、InnoDB 和 BDB 表。
作为 SQL 标准的扩展,MySQL 也支持整数类型 TINYINT、MEDIUMINT 和 BIGINT。

Windows 设置 JDK 的默认编码

现在 Java 开发项目编码一般都是 UTF-8,Windows 下 JDK 的默认编码是 GB2312,可把其默认值设置为 UTF-8:

  1. 右键点击计算机 > 属性 > 高级系统设置 > 高级 > 环境变量 > 系统变量: 设置 JAVA_TOOL_OPTIONS 的值为 -Dfile.encoding=UTF-8
  2. 命令行显示 UTF-8 字符: 执行 chcp 65001,设置命令行的属性,选择字体 Lucida Console(不要选择点阵字体)。如果要换回 GBK 执行 chcp 936 ,再把字体改成点阵字体即可。

SQL Server 导出 CSV 和 XML

导出 XML

1
bcp "select * from tableName FOR XML AUTO, ROOT('Root')" queryout C:/x.xml -S(local) -T -r -c
  • -w: 使用 UTF-16 编码,小端
  • -c: 使用 GBK 编码
  • -c -C6501: 使用 UTF-8 编码,但是有的计算机上不支持
  • -T: 本机使用 -T 表示可信连接,如果是访问其他机器使用 -U user -P pwd 输入用户名和密码

注意:

  • 如果不使用 -r,则导出的 XML 每行最多有 2033 个字符,会破坏 XML,用了 -r 后就没有换行符了,整个 XML 的内容在同一行。
  • 内容中的 & 等特殊字符不会被转义就直接放到属性值里了,此时用 XML 库解析会出错。

导出 CSV

1
sqlcmd -S localhost -d dbName -E -o "csvFile.csv" -Q "set nocount on; select * from tableName" -W -w 999 -s ","
  • -W: remove trailing spaces from each individual field
  • -s",": sets the column seperator to the comma (,)
  • -w 999: sets the row width to 999 chars(this will need to be as wide as the longest row or it will wrap to the next line)
  • -U: username
  • -P: password
  • -h-1: removes column name headers from the result
  • set nocount on: 输出时不显示 XXX 行受到影响的统计信息

注意:sqlcmd 导出为 CSV 文件时,如果列中有逗号,那么导出的 CSV 文件会被破坏,还没找到好办法。

参考 How to export data as CSV format from SQL Server using sqlcmd?

iTerm ssh 自动登录

使用 SSH 远程登录时:

  1. 输入 ssh root@host-ip
  2. 输入密码

每次都重复这样的操作,不仅麻烦,还要记忆好多东西,为了解决这个问题,借助 iTerm2 Profile 可以实现 SSH 自动登录:

  1. 编写 expect 脚本
  2. 使用此脚本创建 Profile
  3. 使用此 Profile 打开新标签页

MySQL 导入导出 SQL 文件

创建数据库

1
CREATE DATABASE IF NOT EXISTS databaseName DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

导入 SQL 文件

MySQL 可以使用 GUI 客户端导入 SQL 文件,此外在命令行下有常用下面 2 种方式导入 SQL 文件(先要创建好数据库)

  • 使用 mysql 导入
    • 命令中不带密码
      1. ./mysql -uroot -p 数据库名 < 导入的文件名.sql
      2. 输入密码
    • 命令中带密码: ./mysql -uroot -p$password 数据库名 < 导入的文件名.sql
  • 使用 source 导入
    1. ./mysql -uroot -p
    2. 输入密码
    3. use databaseName
    4. source 导入的文件名.sql

导出 SQL 文件

导出有 2 种: 导出数据库(包含建表语句和表中的数据),导出表结构(只有建表语句)

  • 导出数据库: mysqldump -uusername -p 数据库名 > 导出的文件名.sql
  • 导出表结构: mysqldump -uusername -p -d 数据库名 > 导出的文件名.sql

自定义 Widget 使用 QSS

相信很多同学继承如 QWidget,QPushButton 等实现自定义的控件后,发现在此控件上 QSS 不生效了,这不行啊,设置背景、边框、字体等如果没有 QSS 那就太麻烦了。其实在自定义控件上启用 QSS 非常简单,只要调用一下 setAttribute(Qt::WA_StyledBackground) 即可,就像下面这样

1
2
3
4
Widget::Widget(QWidget *parent) : QWidget(parent) {
this->setAttribute(Qt::WA_StyledBackground); // 启用 QSS
this->setStyleSheet("border: 2px solid red; background: pink; border-radius: 10px;"); // 设置 QSS
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <QApplication>
#include "Widget.h"

int main(int argc, char *argv[]) {
QApplication app(argc, argv);

QWidget window;

Widget customWidget(&window);
customWidget.setGeometry(20, 20, 100, 100);

window.resize(300, 300);
window.show();

return app.exec();
}

如上在 main() 函数中使用自定义控件 Widget,QSS 生效了,去掉 this->setAttribute(Qt::WA_StyledBackground) 后 QSS 就没效果了:

按下鼠标拖动窗口

QWidget 已经实现了在标题栏按下鼠标移动窗口的功能,但是当实现无边框窗口时,因为没有了标题栏,移动窗口的功能就需要我们自己实现了,不过也不复杂,主要是处理鼠标的按下、移动、松开三个事件:

  • 按下鼠标:记录此时鼠标的全局坐标和窗口左上角的坐标,并且设置鼠标为按下状态
  • 移动鼠标:鼠标按下时移动鼠标,计算此时鼠标和鼠标按下时的位移差,加上按下鼠标时窗口左上角的坐标得到窗口新的坐标,移动窗口到此坐标
  • 松开鼠标:设置鼠标为未按下状态

拖拽鼠标画矩形

经常看到有同学问:如何实现用鼠标拖拽出一个矩形区域的效果,类似 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 年,每台机器每秒可产生约 400 万个 ID (2^12*1000,每毫秒理论最多生成 2^12 个 ID)。

Snowflake 也有自己的缺点,虽然不同 workId 的机器生成的 ID 永远不会相同,但是同一台机器当把时间往后回拨后,生成的 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