Chemmy's Blog

chengming0916@outlook.com

Windows Azure 是微软基于云计算的操作系统,能够为开发者提供一个平台,帮助开发可运行在云服务器、数据中心、Web 和 PC 上的应用程序。

Azure 是一种灵活和支持互操作的平台,能够将处于云端的开发者个人能力,同微软全球数据中心网络托管的服务,比如存储、计算和网络基础设施服务,紧密结合起来。帮助开发者在“云端”和“客户端”同时部署应用,使得企业与用户都能共享资源。

本文整理了丰富的 Windows Azure 学习资源,帮助开发者能全面地学习 Windows Azure 知识,并将 Windows Azure 运用在项目和实际工作中。

通过本系列博客,先来了解一下 Windows Azure 平台的基本知识。Windows Azure,正如同桌面操作系统 Windows 和服务器操作系统 Windows Server 一样,是一个云端的操作系统。开发人员可以使用同一套技术:.NET(包括 Silverlight),或者 Win32,同时针对桌面,服务器,以及云,开发程序,而不需要针对某个平台学习专门的技术。Visual Studio 和 Expression Studio 为开发人员提供了强大的工具支持。

Windows Azure平台简介(一):定位与产品结构

Windows Azure平台简介(二):Windows Azure

Windows Azure平台简介(三):AppFabric

Windows Azure平台简介(四):SQL Azure以及其他服务

在开始本教学之前,请确保你从 Windows Azure 平台下载下载并安装了最新的 Windows Azure 开发工具。本教学使用 Visual Studio 2010 作为开发工具。

Windows Azure入门教学系列 (一):创建第一个WebRole程序

Windows Azure入门教学系列 (二):部署第一个Web Role程序

Windows Azure入门教学系列 (三):创建第一个Worker Role程序

Windows Azure入门教学系列 (四):使用Blob Storage

Windows Azure入门教学系列 (五):使用Queue Storage

Windows Azure入门教学系列 (六):使用Table Storage

Windows Azure入门教学系列 (七):使用REST API访问Storage Service

Windows Azure入门教学系列 (八):使用Windows Azure Drive

Azure学习笔记:Web Site(1)

Azure学习笔记:Service Bus(2)

Azure学习笔记:Storage(3)

Azure学习笔记:Cloud Service(4)

Azure Storage 是微软 Azure 云提供的云端存储解决方案,当前支持的存储类型有 Blob、Queue、File 和 Table。

Azure Blob Storage 基本用法 – Azure Storage 之 Blob

Azure Queue Storage 基本用法 – Azure Storage 之 Queue

Azure File Storage 基本用法 – Azure Storage 之 File

Azure Table storage 基本用法 – Azure Storage 之 Table

Windows Azure Storage 支持三重冗余的。保存在 Azure Storage 的内容,会在同一个数据中心保留有3个副本。这样的好处显而易见:当数据中心发生一般性故障的时候,比如磁盘损坏,机架服务器损坏等,用户保存在 Azure Storage 的数据不会丢失。每次对于 Storage 的写操作,都会对三个副本进行同步写操作,等到在副本操作完毕之后,才会返回执行成功给客户端。

Windows Azure 提供了三种不同类型的存储服务(这里的存储是非关系型数据,比如图片、文档等文件),用来提供给 Windows Azure 上运行的应用程序存储数据使用。依据不同的存储格式会有不同的限制,因为这些存储服务都是以分散式巨量存储(Distributed Mass Storage)为核心概念所设计出来的,为了要达成快速在分散式存储空间中存储与管理数据(还包含高可用度的赘余存储管理),微软有在数据的存储上做一些限制。

微软还提供了 REST API 来方便用户操作 Storage Service。

(1)Windows Azure Storage Service存储服务

(2)Windows Azure Storage Service存储服务之Blob详解(上)

(3)Windows Azure Storage Service存储服务之Blob详解(中)

(4)Windows Azure Storage Service存储服务之Blob Share Access Signature

(5)Windows Azure Drive

(6)Windows Azure Storage之Table

(7)使用工具管理Windows Azure Storage

(8)Windows Azure 上的托管服务CDN (上)

(9)Windows Azure 上的托管服务CDN (中) Blob Service

(10)Windows Azure 上的托管服务CDN (下) Hosted Service

(11)计算你存储的Blob的大小

(12)本地冗余存储 vs 地理冗余存储 (上)

(13)本地冗余存储 vs 地理冗余存储 (下)

(14)使用Azure Blob的PutBlock方法,实现文件的分块、离线上传

(15)使用WCF服务,将本地图片上传至Azure Storage (上) 服务器端代码

(16)使用WCF服务,将本地图片上传至Azure Storage (上) 客户端代码

(17)Azure Storage读取访问地域冗余(Read Access – Geo Redundant Storage, RA-GRS)

(18)使用HTML5 Portal的Azure CDN服务

(19)再谈Azure Block Blob和Page Blob

(20)使用Azure File实现共享文件夹

(21)使用AzCopy工具,加快Azure Storage传输速度

(22)Azure Storage如何支持多级目录

(23)计算Azure VHD实际使用容量

PowerShell 是管理 Azure 的最好方式之一,通过使用 PowerShell 脚本可以把很多的工作自动化。比如对于 Azure 上的虚拟机,可以设置定时关机操作,并在适当的时间把它开机,这样就能减少虚拟机的运行时间,同时也能为节能减排做出贡献。

(1)PowerShell入门

(2)修改Azure订阅名称

(3)上传证书

(4)使用PowerShell管理多个订阅

(5)使用Azure PowerShell创建简单的Azure虚拟机和Linux虚拟机

(6)设置单个Virtual Machine Endpoint

(7)使用CSV文件批量设置Virtual Machine Endpoint

(8)使用PowerShell设置Azure负载均衡器规则

(9)使用PowerShell导出订阅下所有的Azure VM的Public IP和Private IP

(10)使用PowerShell导出订阅下所有的Azure VM和Cloud Service的高可用情况

(11)使用自定义虚拟机镜像模板,创建Azure虚拟机并绑定公网IP(VIP)和内网IP(DIP)

(12)通过Azure PowerShell创建SSH登录的Linux VM

SQL Azure 是微软基于 Microsoft SQL Server Denali,也就是 SQL Server 2012 构建的云端关系型数据库服务。SQL Azure 是 SQL Server 的一个大子集,能够实现 SQL Server 的绝大部分功能,并且将它们作为云端的服务来扩展。SQL Azure Database 提供内置的高精准、可用性、功效与其他功能。

(1)入门

(2)SQL Azure vs SQL Server

(3)创建一个SQL Azure 服务器

(4)创建一个SQL Azure数据库

(5)使用SQL Server Management Studio连接SQL Azure

(6)使用Project Houston管理SQL Azure

(7)在SQL Azure Database中执行的T-SQL

(8)使用Visual Studio 2010开发应用连接SQL Azure云端数据库

(9)把本地的SQL Server数据库迁移到SQL Azure云数据库上

(10)SQL Azure Data Sync数据同步功能(上)

(11)SQL Azure Data Sync数据同步功能(下)

(12)使用新Portal 创建 SQL Azure Database

(13)Azure的两种关系型数据库服务:SQL Azure与SQL Server VM的不同

(14)将云端SQL Azure中的数据库备份到本地SQL Server

(15)SQL Azure 新的规格

(16)创建PaaS SQL Azure V12数据库

(17)SQL Azure V12 - 跨数据中心标准地域复制(Standard Geo-Replication)

(18)使用External Table实现垮库查询

(19)Stretch Database 概览

(20)使用SQL Server 2016 Upgrade Advisor

(21)将整张表都迁移到Azure Stretch Database里

(22)迁移部分数据到Azure Stretch Database

1. 《Windows Azure 实战》全面深入,完整覆盖 Windows Azure 所有关键技术和理论,详细讲解云计算开发流程、云服务架构(可用性、可靠性和高性能)、云设备整合、系统整合,以及云计算项目的管理。
注重实战,68个精心策划的针对特定实际应用场景的真实案例,详细呈现案例的设计思路和完整实现步骤。

2. 《Windows Azure 从入门到精通》介绍了如何构建和管理云端的可扩展应用,一次一个知识点,同时辅之以适当的练习,可帮助读者轻松掌握基本的编程技能,掌握 Windows Azure 云计算平台的核心服务和特性,是一本理想的入门教程。

3. 《云计算与Azure平台实战》解决了从本地转移到基于云的应用程序时,可能面临的各种问题;展示了如何将 ASP.NET 身份验证和角色管理用应用于 Azure Web 角色;揭示了迁移到 Windows Azure 时把计算服务卸载到一个或多个 WorkerWeb 角色的益处;讲解如何为共享 Azure 表选择最合适的 PartionKey 和 RowKey 值的组合;探讨了改善 Azure 表的可扩展性和性能的方法。

4. 《走进云计算:Windows Azure实战手记》介绍了你必须学会的微软云开发技术,介绍目前最火爆的云计算,深入剖析微软最新的云开发平台,涵盖 Windows Azure 环境、存储服务、SQL Azure 数据库与 App Fabric 服务平台 Step by Step 递进教学,初学者可按部就班地学习云应用的开发技术。

相关阅读:

Azure Blob Storage 基本用法 – Azure Storage 之 Blob

Azure Queue Storage 基本用法 – Azure Storage 之 Queue

Azure File Storage 基本用法 – Azure Storage 之 File

Azure Table storage 基本用法 – Azure Storage 之 Table

一、需求缘起

几乎所有的业务系统,都有生成一个记录标识的需求,例如:

(1)消息标识:message-id

(2)订单标识:order-id

(3)帖子标识:tiezi-id

这个记录标识往往就是数据库中的唯一主键,数据库上会建立聚集索引(cluster index),即在物理存储上以这个字段排序。

这个记录标识上的查询,往往又有分页或者排序的业务需求,例如:

(1)拉取最新的一页消息:selectmessage-id/ order by time/ limit 100

(2)拉取最新的一页订单:selectorder-id/ order by time/ limit 100

(3)拉取最新的一页帖子:selecttiezi-id/ order by time/ limit 100

所以往往要有一个time字段,并且在time字段上建立普通索引(non-cluster index)。

我们都知道普通索引存储的是实际记录的指针,其访问效率会比聚集索引慢,如果记录标识在生成时能够基本按照时间有序,则可以省去这个time字段的索引查询:

select message-id/ (order by message-id)/limit 100

再次强调,能这么做的前提是,message-id的生成基本是趋势时间递增的

这就引出了记录标识生成(也就是上文提到的三个XXX-id)的两大核心需求:

(1)全局唯一

(2)趋势有序

这也是本文要讨论的核心问题:如何高效生成趋势有序的全局唯一ID。

二、常见方法、不足与优化

【常见方法一:使用数据库的 auto_increment 来生成全局唯一递增ID】

优点:

(1)简单,使用数据库已有的功能

(2)能够保证唯一性

(3)能够保证递增性

(4)步长固定

缺点:

(1)可用性难以保证:数据库常见架构是一主多从+读写分离,生成自增ID是写请求,主库挂了就玩不转了

(2)扩展性差,性能有上限:因为写入是单点,数据库主库的写性能决定ID的生成性能上限,并且难以扩展

改进方法:

(1)增加主库,避免写入单点

(2)数据水平切分,保证各主库生成的ID不重复


如上图所述,由1个写库变成3个写库,每个写库设置不同的auto_increment初始值,以及相同的增长步长,以保证每个数据库生成的ID是不同的(上图中库0生成0,3,6,9…,库1生成1,4,7,10,库2生成2,5,8,11…)

改进后的架构保证了可用性,但缺点是:

(1)丧失了ID生成的“绝对递增性”:先访问库0生成0,3,再访问库1生成1,可能导致在非常短的时间内,ID生成不是绝对递增的(这个问题不大,我们的目标是趋势递增,不是绝对递增)

(2)数据库的写压力依然很大,每次生成ID都要访问数据库

为了解决上述两个问题,引出了第二个常见的方案

【常见方法二:单点批量ID生成服务】

分布式系统之所以难,很重要的原因之一是“没有一个全局时钟,难以保证绝对的时序”,要想保证绝对的时序,还是只能使用单点服务,用本地时钟保证“绝对时序”。数据库写压力大,是因为每次生成ID都访问了数据库,可以使用批量的方式降低数据库写压力。


如上图所述,数据库使用双master保证可用性,数据库中只存储当前ID的最大值,例如0。ID生成服务假设每次批量拉取6个ID,服务访问数据库,将当前ID的最大值修改为5,这样应用访问ID生成服务索要ID,ID生成服务不需要每次访问数据库,就能依次派发0,1,2,3,4,5这些ID了,当ID发完后,再将ID的最大值修改为11,就能再次派发6,7,8,9,10,11这些ID了,于是数据库的压力就降低到原来的1/6了。

优点

(1)保证了ID生成的绝对递增有序

(2)大大的降低了数据库的压力,ID生成可以做到每秒生成几万几十万个

缺点

(1)服务仍然是单点

(2)如果服务挂了,服务重启起来之后,继续生成ID可能会不连续,中间出现空洞(服务内存是保存着0,1,2,3,4,5,数据库中max-id是5,分配到3时,服务重启了,下次会从6开始分配,4和5就成了空洞,不过这个问题也不大)

(3)虽然每秒可以生成几万几十万个ID,但毕竟还是有性能上限,无法进行水平扩展

改进方法

单点服务的常用高可用优化方案是“备用服务”,也叫“影子服务”,所以我们能用以下方法优化上述缺点(1):

如上图,对外提供的服务是主服务,有一个影子服务时刻处于备用状态,当主服务挂了的时候影子服务顶上。这个切换的过程对调用方是透明的,可以自动完成,常用的技术是vip+keepalived,具体就不在这里展开。

 

【常见方法三:uuid】

上述方案来生成ID,虽然性能大增,但由于是单点系统,总还是存在性能上限的。同时,上述两种方案,不管是数据库还是服务来生成ID,业务方Application都需要进行一次远程调用,比较耗时。有没有一种本地生成ID的方法,即高性能,又时延低呢?

uuid是一种常见的方案:string ID =GenUUID();

优点

(1)本地生成ID,不需要进行远程调用,时延低

(2)扩展性好,基本可以认为没有性能上限

缺点

(1)无法保证趋势递增

(2)uuid过长,往往用字符串表示,作为主键建立索引查询效率低,常见优化方案为“转化为两个uint64整数存储”或者“折半存储”(折半后不能保证唯一性)

【常见方法四:取当前毫秒数】

uuid是一个本地算法,生成性能高,但无法保证趋势递增,且作为字符串ID检索效率低,有没有一种能保证递增的本地算法呢?

取当前毫秒数是一种常见方案:uint64 ID = GenTimeMS();

优点

(1)本地生成ID,不需要进行远程调用,时延低

(2)生成的ID趋势递增

(3)生成的ID是整数,建立索引后查询效率高

缺点

(1)如果并发量超过1000,会生成重复的ID

我去,这个缺点要了命了,不能保证ID的唯一性。当然,使用微秒可以降低冲突概率,但每秒最多只能生成1000000个ID,再多的话就一定会冲突了,所以使用微秒并不从根本上解决问题。

【常见方法五:类snowflake算法】

snowflake是twitter开源的分布式ID生成算法,其核心思想是:一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成1000*(2^12),也就是400W的ID,完全能满足业务的需求。

借鉴snowflake的思想,结合各公司的业务逻辑和并发量,可以实现自己的分布式ID生成算法。

举例,假设某公司ID生成器服务的需求如下:

(1)单机高峰并发量小于1W,预计未来5年单机高峰并发量小于10W

(2)有2个机房,预计未来5年机房数量小于4个

(3)每个机房机器数小于100台

(4)目前有5个业务线有ID生成需求,预计未来业务线数量小于10个

(5)…

分析过程如下:

(1)高位取从2016年1月1日到现在的毫秒数(假设系统ID生成器服务在这个时间之后上线),假设系统至少运行10年,那至少需要10年*365天*24小时*3600秒*1000毫秒=320*10^9,差不多预留39bit给毫秒数

(2)每秒的单机高峰并发量小于10W,即平均每毫秒的单机高峰并发量小于100,差不多预留7bit给每毫秒内序列号

(3)5年内机房数小于4个,预留2bit给机房标识

(4)每个机房小于100台机器,预留7bit给每个机房内的服务器标识

(5)业务线小于10个,预留4bit给业务线标识


这样设计的64bit标识,可以保证:

(1)每个业务线、每个机房、每个机器生成的ID都是不同的

(2)同一个机器,每个毫秒内生成的ID都是不同的

(3)同一个机器,同一个毫秒内,以序列号区区分保证生成的ID是不同的

(4)将毫秒数放在最高位,保证生成的ID是趋势递增的

缺点

(1)由于“没有一个全局时钟”,每台服务器分配的ID是绝对递增的,但从全局看,生成的ID只是趋势递增的(有些服务器的时间早,有些服务器的时间晚)

最后一个容易忽略的问题

生成的ID,例如message-id/ order-id/ tiezi-id,在数据量大时往往需要分库分表,这些ID经常作为取模分库分表的依据,为了分库分表后数据均匀,ID生成往往有“取模随机性”的需求,所以我们通常把每秒内的序列号放在ID的最末位,保证生成的ID是随机的。

又如果,我们在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀。解决方法是,序列号不是每次都归0,而是归一个0到9的随机数,这个地方。

下面附上C#.Net 实现snowflake算法实现

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CommonTools
{
public class SnowFlake
{
//机器ID
private static long workerId;
private static long twepoch = 687888001020L; //唯一时间,这是一个避免重复的随机量,自行设定不要大于当前时间戳
private static long sequence = 0L;
private static int workerIdBits = 4; //机器码字节数。4个字节用来保存机器码
public static long maxWorkerId = -1L ^ -1L << workerIdBits; //最大机器ID
private static int sequenceBits = 10; //计数器字节数,10个字节用来保存计数码
private static int workerIdShift = sequenceBits; //机器码数据左移位数,就是后面计数器占用的位数
private static int timestampLeftShift = sequenceBits + workerIdBits; //时间戳左移动位数就是机器码和计数器总字节数
public static long sequenceMask = -1L ^ -1L << sequenceBits; //一微秒内可以产生计数,如果达到该值则等到下一微妙在进行生成
private long lastTimestamp = -1L;

private static SnowFlake sigle = null;

private SnowFlake(long workerId)
{
if (workerId > maxWorkerId || workerId < 0)
throw new Exception(string.Format("worker Id can't be greater than {0} or less than 0 ", workerId));
SnowFlake.workerId = workerId;
}

public static long NewID()
{
if (sigle == null)
{
sigle = new SnowFlake(4L); //此处4L应该从配置文件里读取当前机器配置

}

return sigle.nextId();


}



private long nextId()
{
lock (this)
{
long timestamp = timeGen();
if(this.lastTimestamp == timestamp){ //同一微妙中生成ID
//用&运算计算该微秒内产生的计数是否已经到达上限
SnowFlake.sequence = (SnowFlake.sequence + 1) & SnowFlake.sequenceMask;
if (SnowFlake.sequence == 0)
{
//一微妙内产生的ID计数已达上限,等待下一微妙
timestamp = tillNextMillis(this.lastTimestamp);
}
}
else{ //不同微秒生成ID
SnowFlake.sequence = 0; //计数清0
}
if(timestamp < lastTimestamp)
{ //如果当前时间戳比上一次生成ID时时间戳还小,抛出异常,因为不能保证现在生成的ID之前没有生成过
throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} milliseconds",
this.lastTimestamp - timestamp));
}
this.lastTimestamp = timestamp; //把当前时间戳保存为最后生成ID的时间戳
long nextId = (timestamp - twepoch << timestampLeftShift)
| SnowFlake.workerId << SnowFlake.workerIdShift | SnowFlake.sequence;
return nextId;
}
}

/// <summary>
/// 获取下一微秒时间戳
/// </summary>
/// <param name="lastTimestamp"></param>
/// <returns></returns>
private long tillNextMillis(long lastTimestamp)
{
long timestamp = timeGen();
while(timestamp <= lastTimestamp)
{
timestamp = timeGen();
}
return timestamp;
}

/// <summary>
/// 生成当前时间戳
/// </summary>
/// <returns></returns>
private long timeGen()
{
return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
}

}
}

译自:Long Names Are Long,作者:Bob Nystrom

引言:来自 Code Review 的观察

Google 最明智的规定之一,是严格执行代码审查(Code Review)。每个改动上线前,都需要经过两种审查:

  1. 功能审查:确保代码完成了既定功能。
  2. 可读性审查:确保代码易于理解、维护,并符合语言惯例和文档规范。

作为 Dart 语言的设计者,我有幸参与了大量此类审查。这让我能像人类学家一样,观察开发者如何使用这门语言,并发现一些普遍的模式。其中最令我困扰的模式之一,就是过长的变量和函数命名

长命名的问题

在早期编程时代,外部标识符可能只靠前六个字符区分,且没有自动补全,长命名确实是个负担。但如今,我们似乎走向了另一个极端——命名变得过于冗长。

过长的命名会带来以下问题:

  • 损害代码清晰度:命名过于臃肿,反而让核心意图变得模糊。
  • 破坏代码结构:长命名常导致不必要的换行,破坏代码的视觉流。
  • 增加使用负担:长的类名使变量声明变得繁琐;长的方法名使其调用变得晦涩;长的变量名导致方法链过长。

我曾见过超过60个字符的命名,这足以写一首短诗。那么,如何解决这个问题呢?

命名的核心目标与原则

一个好的命名应达成两个目标:

  1. 清晰:能让人立刻知道它关联的是什么。
  2. 精确:能让人知道它关联的是什么。

一旦达成这两个目标,多余的字符就是噪音。以下是一些实用的命名原则。

原则一:省略类型信息

在静态类型语言中,类型声明已经提供了足够的信息,无需在命名中重复。

1
2
3
4
5
6
7
8
9
// 不佳的命名
String nameString;
List<DateTime> holidayDateList;
Map<Employee, Role> employeeRoleHashMap;

// 改进的命名
String name;
List<DateTime> holidays;
Map<Employee, Role> employeeRoles;

对于集合,使用复数名词来描述其内容,比使用“List”、“Map”等类型词汇更直观。方法名同样无需描述参数类型。

1
2
3
4
5
6
7
// 不佳的命名
mergeTableCells(List<TableCell> cells)
sortEventsUsingComparator(List<Event> events, Comparator<Event> comparator)

// 改进的命名
merge(List<TableCell> cells)
sort(List<Event> events, Comparator<Event> comparator)

原则二:省略无助于消除歧义的词

命名是一个标识符,用于定位定义,而非承载对象的所有细节。不要将你知道的所有信息都塞进命名里。

例如,看到一个名为 recentlyUpdatedAnnualSalesBid 的变量,读者会困惑:

  • 是否存在“非最近更新”的年度销售投标?
  • 是否存在“非年度”的销售投标?
  • 如果答案都是“否”,那么 recentlyUpdatedAnnual 就是冗余信息。
1
2
3
4
5
6
7
// 不佳的命名
finalBattleMostDangerousBossMonster;
weaklingFirstEncounterMonster;

// 改进的命名
boss;
firstMonster;

大胆地从简洁的命名开始。如果后续发现它会引起歧义,再添加修饰词也不迟。反之,一个冗长的命名很难再被简化。

原则三:省略可从上下文中推断的词

类中的方法和属性、方法中的局部变量,都存在于一个明确的上下文中,无需重复上下文信息。

1
2
3
4
5
6
7
8
9
10
11
// 不佳的命名
class AnnualHolidaySale {
int _annualSaleRebate;
void promoteHolidaySale() { ... }
}

// 改进的命名
class AnnualHolidaySale {
int _rebate;
void promote() { ... }
}

一般来说,变量的作用域越小,其命名就可以越短

原则四:省略无实际意义的“空泛词”

有些词如 datastatemanagerengineobjectentity 等,本身不传达具体信息,只是让命名听起来更“正式”或“重要”,应尽量避免。

一个好的命名能在读者脑海中勾勒出一幅画面。manager 这个词能让你想到什么?是做绩效评估还是管理预算?它什么具体画面都没提供。

实战演练:重构“华夫饼”代码

让我们通过一个违反所有原则的例子,看看如何应用上述规则进行重构。

原始代码(问题重重):

1
2
3
4
class DeliciousBelgianWaffleObject {
void garnishDeliciousBelgianWaffleWithStrawberryList(
List<Strawberry> strawberryList) { ... }
}

第一步:去掉参数类型信息
方法参数已经声明了 List<Strawberry>,方法名中无需重复。

1
2
3
class DeliciousBelgianWaffleObject {
void garnishDeliciousBelgianWaffle(List<Strawberry> strawberries) { ... }
}

第二步:去掉无助于消除歧义的形容词
除非系统中存在“难吃的比利时华夫饼”或“法国华夫饼”,否则 DeliciousBelgian 是多余的。

1
2
3
class WaffleObject {
void garnishWaffle(List<Strawberry> strawberries) { ... }
}

第三步:去掉可从上下文推断的词
garnishWaffleWaffleObject 类的方法,因此 Waffle 是多余的。

1
2
3
class WaffleObject {
void garnish(List<Strawberry> strawberries) { ... }
}

第四步:去掉无意义的“空泛词”
在面向对象语言中,所有类都是对象,Object 这个词没有提供任何额外信息。

1
2
3
class Waffle {
void garnish(List<Strawberry> strawberries) { ... }
}

最终结果:清晰、简洁、意图明确。

总结

命名是编程中最基础也最重要的任务之一。追求清晰的命名,并非鼓励使用晦涩的缩写,而是倡导在提供足够信息的前提下力求简洁。记住命名的两个核心目标:清晰与精确。让你的代码像海明威的小说一样简洁有力,而不是像法律条文一样冗长繁琐。

本文深入探讨了使用随机GUID作为数据库主键的性能瓶颈,并详细介绍了有序GUID(Sequential GUID / COMB GUID)的解决方案。通过将时间戳嵌入GUID的特定位置,使其在插入时保持大致递增的顺序,从而在多种数据库(SQL Server, MySQL, Oracle, PostgreSQL)中获得接近整型自增主键的插入性能。

一、 问题背景:为什么随机GUID性能差?

1.1 传统方案:整型自增ID
  • 优点:简单高效,由数据库自动生成,插入时新行总是追加到表末尾。
  • 缺点
    • 在ORM框架(如NHibernate, Entity Framework)中可能引发并发问题。
    • 在数据库复制等分布式场景下,依赖“单一权威源”生成ID会成为瓶颈。
1.2 诱人但危险的替代方案:随机GUID
  • 优点:全局唯一,可在客户端生成,无需与数据库交互,完美解决分布式ID生成问题。
  • 致命缺点插入性能低下
    • 原因:大多数数据库使用聚集索引(Clustered Index)组织表数据,数据行在磁盘上的物理顺序与主键顺序一致。随机GUID会导致新插入的行位于索引中间位置,引发大量的页分裂(Page Split)数据移动,随着数据量增长,插入操作会变得极其缓慢。

简单示例
向一个已按ID排序的表中插入新行。

  • 插入 ID=8(最大值):追加到末尾,效率高。
  • 插入 ID=5(中间值):需要移动 ID=7ID=8 的行以腾出空间,效率低。

随机GUID的插入类似于总是在中间插入。

二、 解决方案:有序GUID(COMB GUID)的核心思想

核心思路:改造GUID的结构,将其一部分(通常是6个字节)替换为一个随时间递增(或至少不减少)的值(如时间戳),剩余部分保持随机性以保证全局唯一。

  • 随机GUID示例(无序):
    1
    2
    fda437b5-6edd-42dc-9bbd-c09d10460ad0
    2cb56c59-ef3d-4d24-90e7-835ed5968cdc
  • 有序GUID示例(时间戳部分递增):
    1
    2
    3
    00000001-a411-491d-969a-77bf40f55175
    00000002-d97d-4bb9-a493-cad277999363
    00000003-916c-4986-a363-0a9b9c95ca52
    这样,新生成的GUID在插入时大概率会排在现有数据的后面,避免了中间插入带来的性能损耗。

三、 核心挑战与实现:适配不同数据库

不同的数据库对GUID的存储、排序方式存在差异,因此没有“一刀切”的有序GUID生成算法。

3.1 GUID结构与数据库差异

一个GUID(128位)通常表示为:11111111-2222-3333-4444-444444444444

  • Data1: 4字节
  • Data2: 2字节
  • Data3: 2字节
  • Data4: 8字节
数据库 原生类型 排序依据 有序部分应放置的位置
Microsoft SQL Server uniqueidentifier Data4的最后6个字节 GUID末尾 (Data4的最后6字节)
MySQL CHAR(36) (字符串) 字符串的字典序 GUID开头 (作为字符串比较时)
Oracle RAW(16) (二进制) 二进制字节序 GUID开头 (作为二进制比较时)
PostgreSQL UUID 有多种比较方式,通常按字符串或二进制 GUID开头 (作为字符串比较时)

关键难点:.NET Framework中Guid结构的字节序(Endianness)与字符串表示之间存在不一致,需要针对“按字符串存储”的情况进行特殊处理。

3.2 算法定义与实现

定义三种生成模式以适配不同数据库:

1
2
3
4
5
6
public enum SequentialGuidType
{
SequentialAsString, // 用于按字符串排序的DB (如MySQL)
SequentialAsBinary, // 用于按二进制排序的DB (如Oracle)
SequentialAtEnd // 用于SQL Server
}

生成步骤

  1. 生成随机部分:使用强随机数生成器(RNGCryptoServiceProvider)生成10个随机字节。
  2. 生成时间戳部分:获取当前UTC时间的Ticks(100纳秒间隔数),除以10000转换为毫秒精度,取其后6个字节(48位)。这足以保证约5800年内不重复。
  3. 组装GUID字节数组:根据SequentialGuidType,将时间戳字节和随机字节按规则拼接成16字节数组。
  4. 处理字节序:针对SequentialAsString模式且在Little-Endian系统上,需要调整Data1Data2区块的字节顺序,以确保其ToString()后的字符串保持有序。
  5. 构造Guid对象:使用字节数组构造函数返回最终的Guid

注意:使用6字节时间戳意味着在极端情况下(如每秒生成超过281万亿个GUID)可能重复,但结合10字节强随机数,实际碰撞概率极低,可安全用于绝大多数场景。

四、 适配指南与性能结论

4.1 各数据库适配类型推荐
数据库 推荐使用的 SequentialGuidType 说明
Microsoft SQL Server SequentialAtEnd 匹配其按Data4末尾排序的特性。
MySQL SequentialAsString GUID通常存为CHAR(36),按字符串比较。
Oracle SequentialAsBinary GUID存为RAW(16),按二进制比较。
PostgreSQL SequentialAsString UUID类型通常按字符串处理。
SQLite 视情况而定 无原生GUID类型,取决于使用的扩展如何存储。
4.2 性能对比结论(基于原文测试)
  • SQL Server:使用SequentialAtEnd时,插入性能接近整型自增ID,比随机GUID提升约**75%**。
  • MySQL:使用SequentialAsString时,性能接近整型自增ID。随机GUID在大量插入时性能会急剧下降。
  • Oracle & PostgreSQL:有序GUID也有显著提升(PostgreSQL节省近一半时间),但优势不如前两者明显,因为其存储引擎对随机插入的优化更好。

代价:生成有序GUID比生成随机GUID(Guid.NewGuid())慢,主要耗时在强随机数生成。但这对于大多数应用来说,与数据库插入性能的巨大提升相比,是微不足道的开销。

五、 完整实现代码与注意事项

5.1 完整C#实现
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
38
39
40
41
42
43
44
45
46
47
48
49
50
using System;
using System.Security.Cryptography;

public enum SequentialGuidType
{
SequentialAsString,
SequentialAsBinary,
SequentialAtEnd
}

public static class SequentialGuidGenerator
{
private static readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider();

public static Guid NewSequentialGuid(SequentialGuidType guidType)
{
byte[] randomBytes = new byte[10];
_rng.GetBytes(randomBytes);

long timestamp = DateTime.UtcNow.Ticks / 10000L;
byte[] timestampBytes = BitConverter.GetBytes(timestamp);

if (BitConverter.IsLittleEndian)
{
Array.Reverse(timestampBytes);
}

byte[] guidBytes = new byte[16];

switch (guidType)
{
case SequentialGuidType.SequentialAsString:
case SequentialGuidType.SequentialAsBinary:
Buffer.BlockCopy(timestampBytes, 2, guidBytes, 0, 6);
Buffer.BlockCopy(randomBytes, 0, guidBytes, 6, 10);
if (guidType == SequentialGuidType.SequentialAsString && BitConverter.IsLittleEndian)
{
Array.Reverse(guidBytes, 0, 4);
Array.Reverse(guidBytes, 4, 2);
}
break;

case SequentialGuidType.SequentialAtEnd:
Buffer.BlockCopy(randomBytes, 0, guidBytes, 0, 10);
Buffer.BlockCopy(timestampBytes, 2, guidBytes, 10, 6);
break;
}
return new Guid(guidBytes);
}
}
5.2 重要注意事项
  1. 非标准格式:此方法生成的GUID不符合RFC 4122规范,因为它没有包含版本号。但几乎所有数据库都不校验此格式。
  2. 时间戳来源:使用DateTime.UtcNow,其精度在Windows上约为10-15毫秒。这意味着在极短时间内生成大量GUID时,时间戳可能相同,但随机部分保证了唯一性。
  3. 随机性质量:使用RNGCryptoServiceProvider保证了随机部分的密码学强度,但性能较低。如果追求极致生成速度且对随机性要求可放宽,可考虑其他快速随机源,但需评估唯一性风险。
  4. 集成:此方案已集成在NHibernate、ABP等框架中。在实际项目中,应通过DbConnection或配置自动判断数据库类型并选择正确的生成模式。

总结:有序GUID是一种巧妙权衡,通过牺牲少量的客户端生成性能,换取了数据库插入性能的巨大提升,并保留了GUID的全局唯一、分布式生成的优点。理解不同数据库的GUID排序特性是实现此方案的关键。

一、环境与准备文件

1.1 开发环境

  • 开发环境:Windows
  • 开发工具:ffmpeg、nginx 1.7.11.3 Gryphon、nginx-rtmp-module、VLC media player

1.2 官方下载地址

工具 下载地址
ffmpeg http://www.ffmpeg.org
nginx http://nginx.org/en/download.html
nginx 1.7.11.3 Gryphon http://nginx-win.ecsds.eu/download/nginx
nginx-rtmp-module https://github.com/arut/nginx-rtmp-module
VLC media player https://www.videolan.org/vlc

二、直播协议概述

2.1 RTMP

实时消息传输协议(Real Time Messaging Protocol),是 Adobe Systems 公司为 Flash 播放器和服务器之间音频、视频和数据传输开发的开放协议。

  • 协议基于 TCP,是一个协议族,包括 RTMP 基本协议及 RTMPT/RTMPS/RTMPE 等多种变种
  • 实时性比较强,基本能保证延迟在 1-2s 内,是现在国内直播主要采用的方式之一
  • 需要安装 Flash,H5、iOS、Android 并不能原生支持

2.2 HLS

Apple 推出的直播协议,通过视频流切片成文件片段来直播。

  • 客户端首先会请求一个 m3u8 文件,里面会有不同码率的流,或者直接是 ts 文件列表
  • 实时性较差,但 H5、iOS、Android 都原生支持

2.3 HTTP-FLV

对 RTMP 协议的封装,相比于 RTMP 是一个开放的协议。

  • 具备了 RTMP 的实时性和 RTMP 不具备的开发性
  • flv.js 使得浏览器在不依赖 flash 的情况下播放 flv 视频,兼容移动端

三、安装 nginx、ffmpeg 与启动

3.1 安装目录结构

将所有组件放置在同一目录下:

1
2
3
4
5
E:\technology\
├── ffmpeg-4.2.1-win64-static\
├── nginx-1.17.9\
├── nginx-1.7.11.3-Gryphon\
└── nginx-rtmp-module\

3.2 配置环境变量

右键我的电脑 > 属性 > 高级系统设置 > 高级 > 环境变量 > 双击 Path,添加以下路径:

1
2
E:\technology\ffmpeg-4.2.1-win64-static\bin
E:\technology\nginx-1.17.9

3.3 启动 nginx

(1)直接双击 nginx.exe,双击后一个黑色的弹窗一闪而过

(2)打开 cmd 命令窗口,切换到 nginx 解压目录下,输入命令 nginx.exe 或者 start nginx,回车即可


四、检查 nginx 是否启动成功

4.1 浏览器访问

直接在浏览器地址栏输入网址 http://127.0.0.1:80http://localhost:80,回车,出现以下页面说明启动成功:

4.2 命令行检查

在 cmd 命令窗口输入命令 tasklist /fi "imagename eq nginx.exe",出现如下结果说明启动成功:

4.3 相关说明

  • nginx 的配置文件是 conf 目录下的 nginx.conf
  • 默认配置的 nginx 监听的端口为 80,如果 80 端口被占用可以修改为未被占用的端口
  • 检查 80 端口是否被占用的命令是:netstat -ano | findstr 0.0.0.0:80
  • 当我们修改了 nginx 的配置文件 nginx.conf 时,不需要关闭 nginx 后重新启动,只需要执行命令 nginx -s reload 即可让改动生效

五、配置 nginx.conf 文件

默认的 nginx.conf 文件并没有配置 RTMP 服务器,需要我们手动添加配置。具体步骤为:在 conf 目录中,复制粘贴 nginx.conf 文件并将其重命名为 nginx-win.conf,修改其内容为:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#user  nobody;
worker_processes 2;

events {
worker_connections 8192;
}

rtmp {
server {
listen 1935;
chunk_size 4000;
application live {
live on;
}
}
}

http {
include mime.types;
default_type application/octet-stream;

sendfile off;
server_names_hash_bucket_size 128;

client_body_timeout 10;
client_header_timeout 10;
keepalive_timeout 30;
send_timeout 10;
keepalive_requests 10;

server {
listen 80;
server_name localhost;

location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root nginx-rtmp-module/;
}
location /control {
rtmp_control all;
}

location / {
root html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

这里监听端口未改动,仍然为 80,重点添加了 RTMP 服务器的相关配置,以搭建 RTMP 服务器,rtmp 协议的默认端口号是 1935。


六、标准 nginx 方式(失败)

按住 Windows 键 +R,输入 cmd,进入 cmd 命令窗口,进入 nginx 目录:cd E:\technology\nginx-1.17.9,然后启动 nginx rtmp 服务器:

1
nginx.exe -c conf\nginx-win.conf

6.1 错误信息

1
nginx: [emerg] unknown directive "rtmp" in E:\technology\nginx-1.17.9/conf\nginx-win.conf:19

6.2 原因

nginx 的 windows 版本可能在编译的时候没有对 rtmp 模块进行编译导致使用不了。

6.3 解决方案

方法1:下载源码重新进行编译并把 rtmp 模块进行编译进去

过程较为繁琐,不推荐。有兴趣的可以参考:win7下nginx-rtmp-module的编译方法

方法2:下载带 rtmp 模块的 nginx 版本,如 nginx 1.7.11.3 Gryphon

后续亲测可用。


七、nginx Gryphon 方式(成功)

7.1 下载 nginx 1.7.11.3 Gryphon

下载带 rtmp 模块的 nginx 版本,下载地址为:http://nginx-win.ecsds.eu/download/nginx

下载完成后解压,将解压后的目录名 nginx 1.7.11.3 Gryphon 改成 nginx-1.7.11.3-Gryphon

7.2 下载服务器状态检查程序

下载 nginx-rtmp-module,下载地址为:https://github.com/arut/nginx-rtmp-module

将 nginx-rtmp-module-master.zip 解压后复制到目录 nginx-1.7.11.3-Gryphon 下,保证 stat.xls 的目录为:nginx-1.7.11.3-Gryphon\nginx-rtmp-module\stat.xsl

7.3 配置文件

配置文件 conf\nginx-win.conf 与上面第 5 节一致

7.4 启动服务器

按住 Windows 键 +R,输入 cmd,进入 cmd 命令窗口,进入 nginx 目录:cd E:\technology\nginx-1.7.11.3-Gryphon,然后启动 nginx rtmp 服务器:

1
nginx.exe -c conf\nginx-win.conf

八、RTMP 推流测试

8.1 FFmpeg 本地视频推流

1
ffmpeg.exe -re -i .\test.mp4 -vcodec libx264 -acodec aac -f flv rtmp://127.0.0.1:1935/live/home

8.2 VLC 播放器拉流

使用 VLC 播放器测试,输入网络 URL:rtmp://127.0.0.1:1935/live/home

上面 IP 地址均可换成本地 IP

8.3 摄像头推流测试

1
ffmpeg -f dshow -i video="FHD Camera" -vcodec libx264 -preset:v ultrafast -tune:v zerolatency -f flv rtmp://127.0.0.1:1935/live/home

在设备管理器的”照相机”处,获得摄像头设备名称摄像头为”FHD Camera”。更多推流命令请参考:Windows 搭建 nginx rtmp服务器


九、关闭 nginx

如果使用 cmd 命令窗口启动 nginx,关闭 cmd 窗口是不能结束 nginx 进程的,可使用两种方法关闭 nginx:

(1)输入 nginx 命令:nginx -s stop(快速停止 nginx)或 nginx -s quit(完整有序的停止 nginx)

(2)使用 taskkill 命令:taskkill /f /t /im nginx.exe


十、Linux 平台搭建

10.1 安装 nginx(带 rtmp 模块)

1. 下载稳定版本的 nginx

下载地址:http://nginx.org

2. 下载 rtmp 模块

下载地址:https://github.com/arut/nginx-rtmp-module

1
git clone https://github.com/arut/nginx-rtmp-module.git

3. 解压并编译 nginx

解压 nginx 的 tar 包,确保 nginx 和 rtmp 模块在同一目录

1
nginx-1.12.2  nginx-1.12.2.tar.gz  nginx-rtmp-module

进入 nginx 解压目录配置编译参数:

1
./configure --prefix=/usr/local/nginx --add-module=../nginx-rtmp-module --with-http_ssl_module

4. 编译安装

1
make && make install

如果已安装 nginx 可以在已有 nginx 上面增加模块,参考:https://www.cnblogs.com/zhangmingda/p/12622590.html

10.2 nginx.conf 配置

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

events {
worker_connections 1024;
}

http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/mime.types;
default_type application/octet-stream;

server {
listen 80 default_server;
server_name _;
root /usr/share/nginx/html;

# RTMP 状态页面
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
root /opt/rtmp/nginx-rtmp-module/;
}

# HLS 视频流
location /hls {
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
autoindex on;
alias /usr/share/nginx/html/hls;
expires -1;
add_header Cache-Control no-cache;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
}
}

server {
listen 443 ssl http2 default_server;
ssl_certificate "/etc/pki/nginx/server.crt";
ssl_certificate_key "/etc/pki/nginx/private/server.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
}
}

# RTMP 直播配置
rtmp {
server {
listen 1935;
chunk_size 4000;

# 点播
application vod {
play /usr/share/nginx/html/vod/flvs/;
}

# 直播
application live {
live on;
}

# HLS 直播
application hls {
live on;
hls on;
hls_path /usr/share/nginx/html/hls;
hls_fragment 5s;
hls_playlist_length 15s;
hls_continuous on;
hls_cleanup on;
hls_nested on;
}
}
}

十一、点播与直播功能

11.1 点播功能

1. 创建点播目录

1
mkdir /usr/share/nginx/html/vod/flvs/

将 test.flv 视频文件存放到该目录

2. 使用 VLC 播放器播放

播放按钮可以输入 rtmp:// 的连接,步骤为:播放 > 播放 > 网络媒体

3. 移动端播放

安卓手机万能播放器:我的 > 连接播放 > 输入 url 即可播放

11.2 直播功能

1. 推流

电脑端 OBS 可以输入推流地址 rtmp://IP/live/test 进行推流,其中 live 为配置文件中定义的直播应用,test 为自定义流名称

2. 拉流

使用 VLC 播放器输入:rtmp://127.0.0.1/live/test

3. HLS 协议

  • 推流:使用 rtmp://IP/应用名/流名称 进行推流
  • 拉流:支持 http 协议访问 hls 应用下流名称.m3u8 文件
1
2
3
4
5
6
7
8
<html>
<head>welcome test nginx-rtmp-module <br></head>
<body>
<video>
<source src="http://127.0.0.1/hls/test/index.m3u8"/>
</video>
</body>
</html>

十二、直播录制

12.1 flv.js 播放直播录制视频

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<title>flv.js demo</title>
<style>
.mainContainer { display: block; width: 1024px; margin-left: auto; margin-right: auto; }
.urlInput { display: block; width: 100%; margin-left: auto; margin-right: auto; margin-top: 8px; margin-bottom: 8px; }
.centeredVideo { display: block; width: 100%; height: 576px; margin-left: auto; margin-right: auto; margin-bottom: auto; }
.controls { display: block; width: 100%; text-align: left; margin-left: auto; margin-right: auto; }
</style>
</head>
<body>
<div class="mainContainer">
<video id="videoElement" class="centeredVideo" controls autoplay width="1024" height="576">Your browser is too old which doesn't support HTML5 video.</video>
</div>
<br>
<div>
<input style="width:500px" type="text" placeholder="请输入flv链接" id="flvUrl" />
<button onclick="load_flv()">加载</button>
</div>
<br>
<div class="controls">
<button onclick="flv_start()">开始</button>
<button onclick="flv_pause()">暂停</button>
<button onclick="flv_destroy()">停止</button>
<input style="width:100px" type="text" name="seekpoint" />
<button onclick="flv_seekto()">跳转</button>
</div>
<script src="flv.min.js"></script>
<script>
var player = document.getElementById('videoElement');
function load_flv() {
var flvurl = document.getElementById('flvUrl').value;
console.log(flvurl);
if (flvjs.isSupported()) {
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: flvurl
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
}
}
function flv_start() { player.play(); }
function flv_pause() { player.pause(); }
function flv_destroy() {
player.pause();
player.unload();
player.detachMediaElement();
player.destroy();
player = null;
}
function flv_seekto() {
player.currentTime = parseFloat(document.getElementsByName('seekpoint')[0].value);
}
</script>
</body>
</html>

十三、nginx-rtmp-module 指令详解

13.1 核心指令

指令 语法 上下文 说明
rtmp rtmp { ... } 保存所有 RTMP 配置的块
server server { ... } rtmp 声明一个 RTMP 实例
listen listen (addr[:port]|port|unix:path) [bind] server 给 NGINX 添加一个监听端口以接收 RTMP 连接
application application name { ... } server 创建一个 RTMP 应用
timeout timeout value rtmp, server Socket 超时,默认值为 1 分钟
ping ping value rtmp, server RTMP ping 间隔,默认值为一分钟
chunk_size chunk_size value rtmp, server, application 流整合的最大块大小,默认值为 4096

13.2 直播相关指令

指令 说明
live on 开启直播模式
meta on/off 切换发送元数据到客户端,默认为 on
interleave on/off 切换交叉模式,音频和视频数据会在同一个 RTMP chunk 流中传输
wait_key on/off 使视频流从一个关键帧开始
wait_video on/off 在第一个视频帧发送之前禁用音频
publish_notify on/off 发送 NetStream.Publish.Start 和 NetStream.Publish.Stop 给用户
sync timeout 同步音频和视频流,默认值为 300 ms

13.3 录制相关指令

指令 说明
record [off|all|audio|video|keyframes|manual] 切换录制模式
record_path path 指定录制的 flv 文件存放目录
record_suffix value 设置录制文件后缀名,默认为 ‘.flv’
record_max_size size 设置录制文件的最大值
record_max_frames nframes 设置每个录制文件的视频帧的最大数量
record_interval time 录制间隔
record_notify on/off 录制状态通知

示例:

1
2
3
4
5
recorder myrec {
record all manual;
record_path /var/rec;
record_notify on;
}

13.4 HLS 相关指令

指令 说明
hls on/off 在 application 切换 HLS
hls_path path 设置 HLS 播放列表和分段目录
hls_fragment time 设置 HLS 分段长度,默认为 5 秒钟
hls_playlist_length time 设置 HLS 播放列表长度,默认为 30 秒钟
hls_continuous on/off 切换 HLS 连续模式
hls_nested on/off 切换 HLS 嵌套模式
hls_cleanup on/off 切换 HLS 清理

13.5 访问控制

1
2
3
4
5
6
7
# 允许/禁止发布
allow publish 127.0.0.1;
deny publish all;

# 允许/禁止播放
allow play 192.168.0.0/24;
deny play all;

13.6 外部命令执行

定义每个流发布时要执行的带有参数的外部命令:

1
exec ffmpeg -i rtmp://localhost/src/$name -vcodec libx264 -acodec aac -f flv rtmp://localhost/hls/$name;

可用变量:

  • $name - 流的名字
  • $app - 应用名
  • $addr - 客户端地址
  • $flashver - 客户端 flash 版本
  • $swfurl - 客户端 swf url
  • $tcurl - 客户端 tc url
  • $pageurl - 客户端页面 url

13.7 回调函数

指令 说明
on_connect url 设置 HTTP 连接回调
on_play url 设置 HTTP 播放回调
on_publish url 设置 HTTP 发布回调
on_done url 设置播放/发布禁止回调
on_record_done url 设置录制完成回调
on_update url 设置 update 回调

13.8 Relay 相关指令

指令 说明
pull url [key=value]* 创建 pull 中继,流将从远程服务器上拉下来
push url [key=value]* 推送发布流到远程服务器
push_reconnect time push 重新连接前等待的时间,默认为 3 秒
session_relay on/off 切换会话 relay 模式

13.9 其他指令

指令 说明
max_connections number 为 rtmp 引擎设置最大连接数
rtmp_auto_push on/off 切换自动推送(多 worker 直播流)模式
rtmp_control all 设置 RTMP 控制程序

参考链接

一、安装方式选择

自动安装

使用 basicbench 包中的预置脚本:

1
2
# 来源:basicbench/GCC(by 侯宇)
# 将目录打包到目标机器执行自动安装脚本

手动安装

适用于需要自定义配置的场景,步骤如下:


二、手动安装步骤

1. 检查当前版本

1
gcc --version

2. 下载依赖包

GCC 4.8.2 需要以下依赖(版本要求):

  • GMP ≥ 4.2
  • MPFR ≥ 2.3.1
  • MPC ≥ 0.8.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cd /tmp
# 下载GCC源码
wget https://ftp.gnu.org/gnu/gcc/gcc-4.8.2/gcc-4.8.2.tar.bz2
tar xvjf gcc-4.8.2.tar.bz2

# 下载依赖库
wget http://10.88.162.17/software/gcc62/gmp-4.3.2.tar.bz2
tar xvjf gmp-4.3.2.tar.bz2

wget http://10.88.162.17/software/gcc62/mpc-0.8.1.tar.gz
tar xvzf mpc-0.8.1.tar.gz

wget http://10.88.162.17/software/gcc62/mpfr-2.4.2.tar.bz2
tar xvjf mpfr-2.4.2.tar.bz2

wget http://10.88.162.17/software/gcc62/binutils-2.27.tar.bz2
tar xvjf binutils-2.27.tar.bz2

3. 编译安装依赖库

按顺序安装(必须严格遵循此顺序):

3.1 GMP(GNU Multiple Precision Arithmetic Library)

1
2
3
4
5
cd /tmp/gmp-4.3.2
mkdir build && cd build
../configure --prefix=/opt/compiler/gcc-4.8.2
make -j20
make install

3.2 MPFR(Multiple Precision Floating-Point Reliable Library)

1
2
3
4
5
cd /tmp/mpfr-2.4.2
mkdir build && cd build
../configure --prefix=/opt/compiler/gcc-4.8.2 --with-gmp=/opt/compiler/gcc-4.8.2
make -j20
make install

3.3 MPC(Multiple Precision Complex Library)

1
2
3
4
5
6
7
cd /tmp/mpc-0.8.1
mkdir build && cd build
../configure --prefix=/opt/compiler/gcc-4.8.2 \
--with-gmp=/opt/compiler/gcc-4.8.2 \
--with-mpfr=/opt/compiler/gcc-4.8.2
make -j20
make install

4. 编译安装GCC

关键配置选项说明:

  • --prefix:安装路径
  • --disable-multilib:关闭跨平台支持(32/64位兼容)
  • --enable-languages:启用语言(默认all,可选c,c++,fortran等)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 设置临时库路径
export LD_LIBRARY_PATH=/opt/compiler/gcc-4.8.2/lib/:$LD_LIBRARY_PATH

cd /tmp/gcc-4.8.2
mkdir build && cd build
../configure --prefix=/opt/compiler/gcc-4.8.2 \
--with-gmp=/opt/compiler/gcc-4.8.2 \
--with-mpc=/opt/compiler/gcc-4.8.2 \
--with-mpfr=/opt/compiler/gcc-4.8.2 \
--enable-checking=release \
--enable-ld=yes \
--enable-gold=yes \
--disable-multilib
make -j20
make install

5. 编译Binutils(可选)

1
2
3
4
5
6
7
8
9
10
cd /tmp/binutils-2.27  # 注意原文拼写错误:bintuils → binutils
mkdir build && cd build
../configure --prefix=/opt/compiler/gcc-4.8.2 \
--enable-ld=yes \
--enable-gold=yes \
--with-gmp=/opt/compiler/gcc-4.8.2 \
--with-mpfr=/opt/compiler/gcc-4.8.2 \ # 注意原文拼写错误:mprf → mpfr
--with-mpc=/opt/compiler/gcc-4.8.2
make -j20
make install

三、环境配置

1. 永久生效配置

1
2
3
4
# 添加到用户profile
echo 'export PATH=/opt/compiler/gcc-4.8.2/bin/:$PATH' >> ~/.bash_profile
echo 'export LD_LIBRARY_PATH=/opt/compiler/gcc-4.8.2/lib/:/opt/compiler/gcc-4.8.2/lib64/:$LD_LIBRARY_PATH' >> ~/.bash_profile
source ~/.bash_profile

2. 软链接方案(推荐)

保留系统原GCC,通过版本化命令调用新版本:

1
2
3
ln -s /opt/compiler/gcc-4.8.2/bin/gcc /usr/local/bin/gcc482
ln -s /opt/compiler/gcc-4.8.2/bin/g++ /usr/local/bin/g++482
ln -s /opt/compiler/gcc-4.8.2/bin/gcj /usr/local/bin/gcj482

优势:可同时使用多个GCC版本,避免污染系统环境

3. 库路径补充配置

若需显式指定所有依赖库路径:

1
2
3
4
5
6
7
# 在 ~/.bash_profile 或 /etc/profile 中添加
LD_LIBRARY_PATH=/opt/compiler/gcc-4.8.2/gmp-4.3.2/lib:\
/opt/compiler/gcc-4.8.2/mpfr-2.4.2/lib:\
/opt/compiler/gcc-4.8.2/mpc-0.8.1/lib:\
/opt/compiler/gcc-4.8.2/lib:\
$LD_LIBRARY_PATH
export LD_LIBRARY_PATH

四、验证安装

1
2
3
4
5
6
7
# 检查版本
gcc --version # 应显示 gcc (GCC) 4.8.2

# 测试编译
echo 'int main(){return 0;}' > test.c
gcc482 test.c -o test # 使用软链接命令
./test

常见问题处理

  1. 编译错误:确保依赖库安装顺序正确(GMP → MPFR → MPC)
  2. 库路径问题:临时设置 LD_LIBRARY_PATH 再编译GCC
  3. 权限问题:安装目录 /opt/compiler/ 需有写权限
  4. 拼写错误修正
    • bintuilsbinutils
    • mprfmpfr

Shiro

  • 登陆、授权、拦截
  • 按钮权限控制

一、目标

  • Maven+Spring+shiro
  • 自定义登陆、授权
  • 自定义拦截器
  • 加载数据库资源构建拦截链

使用总结:

1、需要设计的数据库:用户、角色、权限、资源

2、可以通过,角色,权限,两个拦截器同时确定是否能访问

3、角色与权限的关系,role1=permission1,permission2,多级的权限:sys:permission1,拥有高级权限同时用于低级权限。

4、perms[“permission1”] 为权限

5、拦截器机制介绍了拦截角色还是权限

6、角色与权限 是两个概念

7、权限-资源,一对一。资源分为上下级,因此权限分为父权限,子权限。创建资源的时候,创建权限。权限里资源的别名

8、角色-权限,一对多。角色里权限的别名

9、按钮是通过权限来控制的

10、防止有父级资源可以访问,子级资源不能访问的情况,不适用 sys:add 权限写法

二、代码

1、Pom.xml

复制代码

1 <properties>
2 <spring.version>4.3.4.RELEASE</spring.version>
3 </properties>
4 <dependency>
5 <groupId>junit</groupId>
6 <artifactId>junit</artifactId>
7 <version>4.9</version>
8 </dependency>
9 <dependency>
10 <groupId>commons-logging</groupId>
11 <artifactId>commons-logging</artifactId>
12 <version>1.1.3</version>
13 </dependency>
14 <dependency>
15 <groupId>org.apache.shiro</groupId>
16 <artifactId>shiro-core</artifactId>
17 <version>1.2.2</version>
18 </dependency>
19 <dependency>
20 <groupId>org.apache.shiro</groupId>
21 <artifactId>shiro-spring</artifactId>
22 <version>1.2.2</version>
23 </dependency>
24 <dependency>
25 <groupId>javax.servlet</groupId>
26 <artifactId>javax.servlet-api</artifactId>
27 <version>3.0.1</version>
28 <scope>provided</scope>
29 </dependency>
30 <dependency>
31 <groupId>org.springframework</groupId>
32 <artifactId>spring-web</artifactId>
33 <version>${spring.version}</version>
34 </dependency>
35
36 org.apache.shiro
37 shiro-ehcache
38 1.2.2
39

40
41 org.springframework
42 spring-context
43 ${spring.version}
44

45
46 org.apache.shiro
47 shiro-web
48 1.2.2
49

50
51 net.sf.ehcache
52 ehcache
53 2.10.1
54

复制代码

2、web.xml

  Servlet拦截访问,使用注解更方便,需要删除项目中的servlet使用javax.servlet-api 3.0 包

复制代码

1 package com.cyd.shiro;
2
3 import java.io.IOException;
4
5 import javax.servlet.ServletException;
6 import javax.servlet.annotation.WebServlet;
7 import javax.servlet.http.HttpServlet;
8 import javax.servlet.http.HttpServletRequest;
9 import javax.servlet.http.HttpServletResponse;
10
11 import org.apache.shiro.SecurityUtils;
12 import org.apache.shiro.authc.AuthenticationException;
13 import org.apache.shiro.authc.IncorrectCredentialsException;
14 import org.apache.shiro.authc.UnknownAccountException;
15 import org.apache.shiro.authc.UsernamePasswordToken;
16 import org.apache.shiro.subject.Subject;
17 import org.apache.shiro.web.util.SavedRequest;
18 import org.apache.shiro.web.util.WebUtils;
19 import org.junit.Test;
20
21 @WebServlet(name = “loginServlet”, urlPatterns = “/loginController”)
22 public class LoginServlet extends HttpServlet {
23 @Override
24 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
25 req.getRequestDispatcher(“login.jsp”).forward(req, resp);
26 }
27
28 @Override
29 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
30 System.out.println(LoginServlet.class.toString());
31 String error = null;
32 String username = req.getParameter(“username”);
33 String password = req.getParameter(“password”);
34 Subject subject = SecurityUtils.getSubject();
35 UsernamePasswordToken token = new UsernamePasswordToken(username, password);
36 try {
37 subject.login(token);
38 } catch (UnknownAccountException e) {
39 error = “用户名/密码错误”;
40 } catch (IncorrectCredentialsException e) {
41 error = “用户名/密码错误”;
42 } catch (AuthenticationException e) {
43 // 其他错误,比如锁定,如果想单独处理请单独catch处理
44 error = “其他错误:” + e.getMessage();
45 }
46 if (error != null) {// 出错了,返回登录页面
47 req.setAttribute(“error”, error);
48 req.getRequestDispatcher(“login.jsp”).forward(req, resp);
49 } else {// 登录成功
50 //跳转到拦截登陆前的地址
51 SavedRequest request=WebUtils.getSavedRequest(req);
52 String url =request.getRequestURI();
53 req.getRequestDispatcher(url.substring(url.lastIndexOf(‘/‘))).forward(req, resp);
54 }
55 }
56
57 }

复制代码

3、Spring-shiro.xml

复制代码

<beans xmlns=“http://www.springframework.org/schema/beans“ xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“ xmlns:context=“http://www.springframework.org/schema/context“ xmlns:util=“http://www.springframework.org/schema/util“ xsi:schemaLocation=“http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.2.xsd"\>

<context:component-scan base-package\="com.cyd.shiro.\*"\></context:component-scan\>

<!-- Shiro的Web过滤器 \-->
<bean id\="shiroFilter" class\="com.cyd.shiro.ExtendShiroFilterFactoryBean"\>
    <property name\="securityManager" ref\="securityManager" />
    <property name\="loginUrl" value\="/login.jsp" />
    <!-- <property name="successUrl" value="/index.jsp" /> \-->
    <property name\="unauthorizedUrl" value\="/unauthorized.jsp" />
    <property name\="filters"\>
        <util:map\>
            <!-- <entry key="onperms" value-ref="URLPermissionsFilter" /> \-->
            <entry key\="onrole" value-ref\="ExtendRolesAuthorizationFilter" />
        </util:map\>
    </property\> 
    <property name\="filterChainDefinitions"\>
        <value\> /unauthorized.jsp = anon
            /logoutController=anon
            /login.jsp=authc
        </value\>
    </property\>
</bean\>

<!-- 安全管理器 \-->
<bean id\="securityManager" class\="org.apache.shiro.web.mgt.DefaultWebSecurityManager"\>
    <property name\="realm" ref\="myRealm" />
    <property name\="cacheManager" ref\="cacheManager" />
</bean\>
<!-- 自定义认证,授权 \-->
<bean id\="myRealm" class\="com.cyd.shiro.AdminRealm"\></bean\>

<!-- 注册ehcache,不然每次访问都要登陆 \-->
<bean id\="cacheManager" class\="org.apache.shiro.cache.ehcache.EhCacheManager"\>
    <property name\="cacheManagerConfigFile" value\="classpath:ehcache.xml" />
</bean\>
<!-- 自定义鉴权拦截器 \-->
<bean id\="URLPermissionsFilter" class\="com.cyd.shiro.URLPermissionsFilter" />
<bean id\="ExtendRolesAuthorizationFilter" class\="com.cyd.shiro.ExtendRolesAuthorizationFilter" />

</beans>

复制代码

4、Ehcache.xml 缓存

复制代码

<ehcache xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance“ xsi:noNamespaceSchemaLocation=“../config/ehcache.xsd”>
<diskStore path=“java.io.tmpdir”/>
<defaultCache
maxElementsInMemory=“10000” eternal=“false” timeToIdleSeconds=“600” timeToLiveSeconds=“600” overflowToDisk=“true” maxElementsOnDisk=“10000000” diskPersistent=“false” diskExpiryThreadIntervalSeconds=“120” memoryStoreEvictionPolicy=“LRU”
/>

</ehcache>

复制代码

5、登陆Servlet

复制代码

package com.cyd.shiro;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.SavedRequest;
import org.apache.shiro.web.util.WebUtils;

@WebServlet(name = “loginServlet”, urlPatterns = “/loginController”)
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher(“login.jsp”).forward(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    System.out.println(LoginServlet.class.toString());
    String error = null;
    String username = req.getParameter("username");
    String password = req.getParameter("password");
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        subject.login(token); 
    } catch (UnknownAccountException e) { 
        error = "用户名/密码错误";
    } catch (IncorrectCredentialsException e) {
        error = "用户名/密码错误";
    } catch (AuthenticationException e) {
        // 其他错误,比如锁定,如果想单独处理请单独catch处理
        error = "其他错误:" + e.getMessage();
    }
    if (error != null) {// 出错了,返回登录页面
        req.setAttribute("error", error);
        req.getRequestDispatcher("login.jsp").forward(req, resp);
    } else {// 登录成功
        //跳转到拦截登陆前的地址
        SavedRequest request=WebUtils.getSavedRequest(req);
        String url =request.getRequestURI();
        req.getRequestDispatcher(url.substring(url.lastIndexOf('/'))).forward(req, resp);
    }
}

}

复制代码

6、自定义登陆、授权。

  根据需求自定义登陆异常。从数据库查询出当前用户拥有的权限并授权

复制代码

1 package com.cyd.shiro;
2
3 import java.util.HashSet;
4 import java.util.LinkedList;
5 import java.util.List;
6 import java.util.Set;
7
8 import org.apache.shiro.authc.AuthenticationException;
9 import org.apache.shiro.authc.AuthenticationInfo;
10 import org.apache.shiro.authc.AuthenticationToken;
11 import org.apache.shiro.authc.SimpleAuthenticationInfo;
12 import org.apache.shiro.authc.UnknownAccountException;
13 import org.apache.shiro.authz.AuthorizationInfo;
14 import org.apache.shiro.authz.SimpleAuthorizationInfo;
15 import org.apache.shiro.realm.AuthorizingRealm;
16 import org.apache.shiro.subject.PrincipalCollection;
17 import org.springframework.beans.factory.annotation.Autowired;
18
19 import com.cyd.helloworld.SysRoles;
20 import com.cyd.helloworld.SysUsers;
21 import com.cyd.shiro.admin.SysUsersService;
22
23 public class AdminRealm extends AuthorizingRealm {
24
25 @Autowired
26 private SysUsersService sysusersservice;
27 // 认证登陆
28 @Override
29 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
30 System.out.println(“do doGetAuthenticationInfo”);
31 String username = (String) token.getPrincipal();
32 SysUsers user = sysusersservice.getSysUsers(username);
33 if (user == null) {
34 throw new UnknownAccountException();// 没找到帐号
35 }
36 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUserName(), // 用户名
37 user.getPassWorld(), // 密码
38 getName() // realm name
39 );
40 return authenticationInfo;
41 }
42
43 // 用户授权
44 @Override
45 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
46 System.out.println(“do doGetAuthorizationInfo”);
47 String username = (String)principals.getPrimaryPrincipal();
48 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
49 //从数据库加载当前用户的角色,例如:[admin]
50 authorizationInfo.setRoles(new HashSet(sysusersservice.getSysRoles(username)));
51 //从数据库加载当前用户可以访问的资源,例如:[index.jsp, abc.jsp]
52 authorizationInfo.setStringPermissions(new HashSet(sysusersservice.getSysResource(username)));
53
54 return authorizationInfo;
55 }
56 }

复制代码

7、自定义拦截器。

  重写拦截器是因为shiro 验证是否有权限访问是需要当前用户拥有拦截器链的所有权限。一般需求只需要拥有部分权限即可。

       角色验证拦截,hasRole和hasAllRoles 验证是否有权限。

复制代码

1 package com.cyd.shiro;
2
3 import java.io.IOException;
4 import java.util.Set;
5
6 import javax.servlet.ServletRequest;
7 import javax.servlet.ServletResponse;
8
9 import org.apache.shiro.subject.Subject;
10 import org.apache.shiro.util.CollectionUtils;
11 import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
12
13 /**
14 * 通过角色验证权限
15 * @author chenyd
16 * 2017年11月21日
17 */
18 public class ExtendRolesAuthorizationFilter extends RolesAuthorizationFilter{
19
20 @Override
21 public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
22
23 System.out.println(ExtendRolesAuthorizationFilter.class.toString());
24 Subject subject = getSubject(request, response);
25 String[] rolesArray = (String[]) mappedValue;
26
27 if (rolesArray == null || rolesArray.length == 0) {
28 //no roles specified, so nothing to check - allow access.
29 return true;
30 }
31 //AbstractFilter
32 Set roles = CollectionUtils.asSet(rolesArray);
33
34 boolean flag=false;
35 for(String role: roles){
36 if(subject.hasRole(role)){
37 flag=true;
38 break;
39 }
40 }
41 return flag;
42 }
43 }

复制代码

       url拦截校验,isPermitted和isPermittedAll验证是否有权限访问,

复制代码

1 package com.cyd.shiro;
2
3 import java.io.IOException;
4
5 import javax.servlet.ServletRequest;
6 import javax.servlet.ServletResponse;
7 import javax.servlet.http.HttpServletRequest;
8
9 import org.apache.shiro.subject.Subject;
10 import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
11 /**
12 * 通过字符串验证权限
13 * @author chenyd
14 * 2017年11月21日
15 */
16 public class URLPermissionsFilter extends PermissionsAuthorizationFilter {
17
18 /**
19 * mappedValue 访问该url时需要的权限
20 * subject.isPermitted 判断访问的用户是否拥有mappedValue权限
21 * 重写拦截器,只要符合配置的一个权限,即可通过
22 */
23 @Override
24 public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
25 throws IOException {
26 System.out.println(URLPermissionsFilter.class.toString());
27 Subject subject = getSubject(request, response);
28 // DefaultFilterChainManager
29 // PathMatchingFilterChainResolver
30 String[] perms = (String[]) mappedValue;
31 boolean isPermitted = false;
32 if (perms != null && perms.length > 0) {
33 for (String str : perms) {
34 if (subject.isPermitted(str)) {
35 isPermitted = true;
36 }
37 }
38 }
39
40 return isPermitted;
41 }
42 }

复制代码

8、加载数据库资源构建拦截器链

复制代码

1 package com.cyd.shiro;
2
3 import java.util.Map;
4
5 import org.apache.shiro.config.Ini;
6 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
7 import org.apache.shiro.util.CollectionUtils;
8 import org.apache.shiro.web.config.IniFilterChainResolverFactory;
9 import org.springframework.beans.factory.annotation.Autowired;
10
11 import com.cyd.shiro.admin.SysUsersService;
12
13 public class ExtendShiroFilterFactoryBean extends ShiroFilterFactoryBean{
14
15 @Autowired
16 private SysUsersService sysusersservice;
17 //PathMatchingFilter
18 @Override
19 public void setFilterChainDefinitions(String definitions) {
20 //数据库中获取权限,{/index.jsp=authc,onrole[“admin2”,”admin”], /abc.jsp=authc,onrole[“admin2”,”admin”]}
21 Map<String, String> otherChains = sysusersservice.getFilterChain();
22 Ini ini = new Ini();
23 ini.load(definitions);
24 Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
25 if (CollectionUtils.isEmpty(section)) {
26 section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
27 }
28 section.putAll(otherChains);
29 setFilterChainDefinitionMap(section);
30 }
31
32 }

复制代码

三、  学习笔记

1、INI文件配置

复制代码

[users] #提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2

zhang=123,admin

[roles] #提供了角色及权限之间关系的配置,角色=权限1,权限2

admin=index.jsp

[urls] #配置拦截器链,/** 为拦截器链名称(filterChain),authc,roles[admin],perms[“index.jsp”]拦截器列表名

/login.jsp=anon

/loginController=anon

/unauthorized.jsp=anon

/**=authc,roles[admin],perms[“index.jsp”]

复制代码

2、拦截器链

  Shiro的所有拦截器链名定义在源码DefaultFilter中。

anon            

例子/admins/**=anon 没有参数,表示可以匿名使用。 

authc

例如/admins/user/**=authc表示需要认证(登录)才能使用,没有参数  

roles

 例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,  

 并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[“admin,guest”],  

 每个参数通过才算通过,相当于hasAllRoles()方法。  

perms

例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,  

例如/admins/user/**=perms[“user:add:*,user:modify:*“],当有多个参数时必须每个参数都通过才通过,  

想当于isPermitedAll()方法。

rest

例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,  

 其中method为post,get,delete等。

port

例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,  其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。

authcBasic                                 

例如/admins/user/**=authcBasic没有参数表示httpBasic认证

ssl

例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https

user

例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查  

注:anon,authcBasic,auchc,user是认证过滤器,

perms,roles,ssl,rest,port是授权过滤器

3、拦截器链源码类关系图

①   NameableFilter有一个name属性,定义每一个filter的名字。

②   OncePerRequestFilter保证客户端请求后该filter的doFilter只会执行一次。

  doFilterInternal非常重要,在shiro整个filter体系中的核心方法及实质入口。另外,shiro是通过在request中设置一个该filter特定的属性值来保证该filter只会执行一次的。

③   AdviceFilter中主要是对doFilterInternal做了更细致的切分。

  springmvc中的Interceptor,doFilterInternal会先调用preHandle做一些前置判断,如果返回false则filter链不继续往下执行,

④   AccessControlFilter中的对onPreHandle方法做了进一步细化。

  isAccessAllowed方法和onAccessDenied方法达到控制效果。这两个方法都是抽象方法,由子类去实现。到这一层应该明白。isAccessAllowed和onAccessDenied方法会影响到onPreHandle方法,而onPreHandle方法会影响到preHandle方法,而preHandle方法会达到控制filter链是否执行下去的效果。所以如果正在执行的filter中isAccessAllowed和onAccessDenied都返回false,则整个filter控制链都将结束,不会到达目标方法(客户端请求的接口),而是直接跳转到某个页面(由filter定义的,将会在authc中看到)。

⑤   FormAuthenticationFiltershiro提供的登录的filter,

  saveRequestAndRedirectToLogin保存request并拦截到登陆页面,登陆成功后可从WebUtils.getSavedRequest(req);中取出。

四、未实现的功能

  • 动态URL权限控制。当修改权限时,重新加载拦截器链。
  • 密码加密
  • 记住我
  • 在线人数控制
  • 集成验证码

 

五、参考链接

同学们有福了,花了一些时间,重新整理了一个最完整的Mybatis Generator(简称MBG)的最完整配置文件,带详解,再也不用去看EN的User Guide了;

复制代码

<generatorConfiguration>

<context id=“mysql” defaultModelType=“hierarchical” targetRuntime=“MyBatis3Simple” >

<!-- 自动识别数据库关键字,默认false,如果设置为true,根据SqlReservedWords中定义的关键字列表;
    一般保留默认值,遇到数据库关键字(Java关键字),使用columnOverride覆盖 \-->
<property name\="autoDelimitKeywords" value\="false"/>
<!-- 生成的Java文件的编码 \-->
<property name\="javaFileEncoding" value\="UTF-8"/>
<!-- 格式化java代码 \-->
<property name\="javaFormatter" value\="org.mybatis.generator.api.dom.DefaultJavaFormatter"/>
<!-- 格式化XML代码 \-->
<property name\="xmlFormatter" value\="org.mybatis.generator.api.dom.DefaultXmlFormatter"/>

<!-- beginningDelimiter和endingDelimiter:指明数据库的用于标记数据库对象名的符号,比如ORACLE就是双引号,MYSQL默认是\`反引号; \-->
<property name\="beginningDelimiter" value\="\`"/>
<property name\="endingDelimiter" value\="\`"/>

<!-- 必须要有的,使用这个配置链接数据库
    @TODO:是否可以扩展 \-->
<jdbcConnection driverClass\="com.mysql.jdbc.Driver" connectionURL\="jdbc:mysql:///pss" userId\="root" password\="admin"\>
    <!-- 这里面可以设置property属性,每一个property属性都设置到配置的Driver上 \-->
</jdbcConnection\>

<!-- java类型处理器 
    用于处理DB中的类型到Java中的类型,默认使用JavaTypeResolverDefaultImpl;
    注意一点,默认会先尝试使用Integer,Long,Short等来对应DECIMAL和 NUMERIC数据类型; \-->
<javaTypeResolver type\="org.mybatis.generator.internal.types.JavaTypeResolverDefaultImpl"\>
    <!-- true:使用BigDecimal对应DECIMAL和 NUMERIC数据类型
        false:默认,
            scale>0;length>18:使用BigDecimal;
            scale=0;length\[10,18\]:使用Long;
            scale=0;length\[5,9\]:使用Integer;
            scale=0;length<5:使用Short; \-->
    <property name\="forceBigDecimals" value\="false"/>
</javaTypeResolver\>

<!-- java模型创建器,是必须要的元素
    负责:1,key类(见context的defaultModelType);2,java类;3,查询类
    targetPackage:生成的类要放的包,真实的包受enableSubPackages属性控制;
    targetProject:目标项目,指定一个存在的目录下,生成的内容会放到指定目录中,如果目录不存在,MBG不会自动建目录 \-->
<javaModelGenerator targetPackage\="com.\_520it.mybatis.domain" targetProject\="src/main/java"\>
    <!-- for MyBatis3/MyBatis3Simple
        自动为每一个生成的类创建一个构造方法,构造方法包含了所有的field;而不是使用setter; \-->
    <property name\="constructorBased" value\="false"/>

    <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false \-->
    <property name\="enableSubPackages" value\="true"/>

    <!-- for MyBatis3 / MyBatis3Simple
        是否创建一个不可变的类,如果为true,
        那么MBG会创建一个没有setter方法的类,取而代之的是类似constructorBased的类 \-->
    <property name\="immutable" value\="false"/>

    <!-- 设置一个根对象,
        如果设置了这个根对象,那么生成的keyClass或者recordClass会继承这个类;在Table的rootClass属性中可以覆盖该选项
        注意:如果在key class或者record class中有root class相同的属性,MBG就不会重新生成这些属性了,包括:
            1,属性名相同,类型相同,有相同的getter/setter方法; \-->
    <property name\="rootClass" value\="com.\_520it.mybatis.domain.BaseDomain"/>

    <!-- 设置是否在getter方法中,对String类型字段调用trim()方法 \-->
    <property name\="trimStrings" value\="true"/>
</javaModelGenerator\>

<!-- 生成SQL map的XML文件生成器,
    注意,在Mybatis3之后,我们可以使用mapper.xml文件+Mapper接口(或者不用mapper接口),
        或者只使用Mapper接口+Annotation,所以,如果 javaClientGenerator配置中配置了需要生成XML的话,这个元素就必须配置
    targetPackage/targetProject:同javaModelGenerator \-->
<sqlMapGenerator targetPackage\="com.\_520it.mybatis.mapper" targetProject\="src/main/resources"\>
    <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false \-->
    <property name\="enableSubPackages" value\="true"/>
</sqlMapGenerator\>

<!-- 对于mybatis来说,即生成Mapper接口,注意,如果没有配置该元素,那么默认不会生成Mapper接口 
    targetPackage/targetProject:同javaModelGenerator
    type:选择怎么生成mapper接口(在MyBatis3/MyBatis3Simple下):
        1,ANNOTATEDMAPPER:会生成使用Mapper接口+Annotation的方式创建(SQL生成在annotation中),不会生成对应的XML;
        2,MIXEDMAPPER:使用混合配置,会生成Mapper接口,并适当添加合适的Annotation,但是XML会生成在XML中;
        3,XMLMAPPER:会生成Mapper接口,接口完全依赖XML;
    注意,如果context是MyBatis3Simple:只支持ANNOTATEDMAPPER和XMLMAPPER \-->
<javaClientGenerator targetPackage\="com.\_520it.mybatis.mapper" type\="ANNOTATEDMAPPER" targetProject\="src/main/java"\>
    <!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false \-->
    <property name\="enableSubPackages" value\="true"/>

    <!-- 可以为所有生成的接口添加一个父接口,但是MBG只负责生成,不负责检查
    <property name="rootInterface" value=""/> \-->
</javaClientGenerator\>

<!-- 选择一个table来生成相关文件,可以有一个或多个table,必须要有table元素
    选择的table会生成一下文件:
    1,SQL map文件
    2,生成一个主键类;
    3,除了BLOB和主键的其他字段的类;
    4,包含BLOB的类;
    5,一个用户生成动态查询的条件类(selectByExample, deleteByExample),可选;
    6,Mapper接口(可选)

    tableName(必要):要生成对象的表名;
    注意:大小写敏感问题。正常情况下,MBG会自动的去识别数据库标识符的大小写敏感度,在一般情况下,MBG会
        根据设置的schema,catalog或tablename去查询数据表,按照下面的流程:
        1,如果schema,catalog或tablename中有空格,那么设置的是什么格式,就精确的使用指定的大小写格式去查询;
        2,否则,如果数据库的标识符使用大写的,那么MBG自动把表名变成大写再查找;
        3,否则,如果数据库的标识符使用小写的,那么MBG自动把表名变成小写再查找;
        4,否则,使用指定的大小写格式查询;
    另外的,如果在创建表的时候,使用的""把数据库对象规定大小写,就算数据库标识符是使用的大写,在这种情况下也会使用给定的大小写来创建表名;
    这个时候,请设置delimitIdentifiers="true"即可保留大小写格式;

    可选:
    1,schema:数据库的schema;
    2,catalog:数据库的catalog;
    3,alias:为数据表设置的别名,如果设置了alias,那么生成的所有的SELECT SQL语句中,列名会变成:alias\_actualColumnName
    4,domainObjectName:生成的domain类的名字,如果不设置,直接使用表名作为domain类的名字;可以设置为somepck.domainName,那么会自动把domainName类再放到somepck包里面;
    5,enableInsert(默认true):指定是否生成insert语句;
    6,enableSelectByPrimaryKey(默认true):指定是否生成按照主键查询对象的语句(就是getById或get);
    7,enableSelectByExample(默认true):MyBatis3Simple为false,指定是否生成动态查询语句;
    8,enableUpdateByPrimaryKey(默认true):指定是否生成按照主键修改对象的语句(即update);
    9,enableDeleteByPrimaryKey(默认true):指定是否生成按照主键删除对象的语句(即delete);
    10,enableDeleteByExample(默认true):MyBatis3Simple为false,指定是否生成动态删除语句;
    11,enableCountByExample(默认true):MyBatis3Simple为false,指定是否生成动态查询总条数语句(用于分页的总条数查询);
    12,enableUpdateByExample(默认true):MyBatis3Simple为false,指定是否生成动态修改语句(只修改对象中不为空的属性);
    13,modelType:参考context元素的defaultModelType,相当于覆盖;
    14,delimitIdentifiers:参考tableName的解释,注意,默认的delimitIdentifiers是双引号,如果类似MYSQL这样的数据库,使用的是\`(反引号,那么还需要设置context的beginningDelimiter和endingDelimiter属性)
    15,delimitAllColumns:设置是否所有生成的SQL中的列名都使用标识符引起来。默认为false,delimitIdentifiers参考context的属性

    注意,table里面很多参数都是对javaModelGenerator,context等元素的默认属性的一个复写; \-->
<table tableName\="userinfo" \>

    <!-- 参考 javaModelGenerator 的 constructorBased属性\-->
    <property name\="constructorBased" value\="false"/>

    <!-- 默认为false,如果设置为true,在生成的SQL中,table名字不会加上catalog或schema; \-->
    <property name\="ignoreQualifiersAtRuntime" value\="false"/>

    <!-- 参考 javaModelGenerator 的 immutable 属性 \-->
    <property name\="immutable" value\="false"/>

    <!-- 指定是否只生成domain类,如果设置为true,只生成domain类,如果还配置了sqlMapGenerator,那么在mapper XML文件中,只生成resultMap元素 \-->
    <property name\="modelOnly" value\="false"/>

    <!-- 参考 javaModelGenerator 的 rootClass 属性 
    <property name="rootClass" value=""/> \-->

    <!-- 参考javaClientGenerator 的  rootInterface 属性
    <property name="rootInterface" value=""/> \-->

    <!-- 如果设置了runtimeCatalog,那么在生成的SQL中,使用该指定的catalog,而不是table元素上的catalog 
    <property name="runtimeCatalog" value=""/> \-->

    <!-- 如果设置了runtimeSchema,那么在生成的SQL中,使用该指定的schema,而不是table元素上的schema 
    <property name="runtimeSchema" value=""/> \-->

    <!-- 如果设置了runtimeTableName,那么在生成的SQL中,使用该指定的tablename,而不是table元素上的tablename 
    <property name="runtimeTableName" value=""/> \-->

    <!-- 注意,该属性只针对MyBatis3Simple有用;
        如果选择的runtime是MyBatis3Simple,那么会生成一个SelectAll方法,如果指定了selectAllOrderByClause,那么会在该SQL中添加指定的这个order条件; \-->
    <property name\="selectAllOrderByClause" value\="age desc,username asc"/>

    <!-- 如果设置为true,生成的model类会直接使用column本身的名字,而不会再使用驼峰命名方法,比如BORN\_DATE,生成的属性名字就是BORN\_DATE,而不会是bornDate \-->
    <property name\="useActualColumnNames" value\="false"/>

    <!-- generatedKey用于生成生成主键的方法,
        如果设置了该元素,MBG会在生成的<insert>元素中生成一条正确的<selectKey>元素,该元素可选
        column:主键的列名;
        sqlStatement:要生成的selectKey语句,有以下可选项:
            Cloudscape:相当于selectKey的SQL为: VALUES IDENTITY\_VAL\_LOCAL()
            DB2       :相当于selectKey的SQL为: VALUES IDENTITY\_VAL\_LOCAL()
            DB2\_MF    :相当于selectKey的SQL为:SELECT IDENTITY\_VAL\_LOCAL() FROM SYSIBM.SYSDUMMY1
            Derby      :相当于selectKey的SQL为:VALUES IDENTITY\_VAL\_LOCAL()
            HSQLDB      :相当于selectKey的SQL为:CALL IDENTITY()
            Informix  :相当于selectKey的SQL为:select dbinfo('sqlca.sqlerrd1') from systables where tabid=1
            MySql      :相当于selectKey的SQL为:SELECT LAST\_INSERT\_ID()
            SqlServer :相当于selectKey的SQL为:SELECT SCOPE\_IDENTITY()
            SYBASE      :相当于selectKey的SQL为:SELECT @@IDENTITY
            JDBC      :相当于在生成的insert元素上添加useGeneratedKeys="true"和keyProperty属性
    <generatedKey column="" sqlStatement=""/> \-->

    <!-- 该元素会在根据表中列名计算对象属性名之前先重命名列名,非常适合用于表中的列都有公用的前缀字符串的时候,
        比如列名为:CUST\_ID,CUST\_NAME,CUST\_EMAIL,CUST\_ADDRESS等;
        那么就可以设置searchString为"^CUST\_",并使用空白替换,那么生成的Customer对象中的属性名称就不是
        custId,custName等,而是先被替换为ID,NAME,EMAIL,然后变成属性:id,name,email;

        注意,MBG是使用java.util.regex.Matcher.replaceAll来替换searchString和replaceString的,
        如果使用了columnOverride元素,该属性无效;

    <columnRenamingRule searchString="" replaceString=""/> \-->

     <!-- 用来修改表中某个列的属性,MBG会使用修改后的列来生成domain的属性;
         column:要重新设置的列名;
         注意,一个table元素中可以有多个columnOverride元素哈~ \-->
     <columnOverride column\="username"\>
         <!-- 使用property属性来指定列要生成的属性名称 \-->
         <property name\="property" value\="userName"/>

         <!-- javaType用于指定生成的domain的属性类型,使用类型的全限定名
         <property name="javaType" value=""/> \-->

         <!-- jdbcType用于指定该列的JDBC类型 
         <property name="jdbcType" value=""/> \-->

         <!-- typeHandler 用于指定该列使用到的TypeHandler,如果要指定,配置类型处理器的全限定名
             注意,mybatis中,不会生成到mybatis-config.xml中的typeHandler
             只会生成类似:where id = #{id,jdbcType=BIGINT,typeHandler=com.\_520it.mybatis.MyTypeHandler}的参数描述
         <property name="jdbcType" value=""/> \-->

         <!-- 参考table元素的delimitAllColumns配置,默认为false
         <property name="delimitedColumnName" value=""/> \-->
     </columnOverride\>

     <!-- ignoreColumn设置一个MGB忽略的列,如果设置了改列,那么在生成的domain中,生成的SQL中,都不会有该列出现 
         column:指定要忽略的列的名字;
         delimitedColumnName:参考table元素的delimitAllColumns配置,默认为false

         注意,一个table元素中可以有多个ignoreColumn元素
     <ignoreColumn column="deptId" delimitedColumnName=""/> \-->
</table\>

</context>

</generatorConfiguration>

复制代码

1.Quartz的作用

定时自动执行任务

2.预备

相关包官方网站

1
2
quartz2.2.1
quartz-jobs2.2.1

POM文件

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>

3.Quartz核心

3.1.Job接口

被调度的任务,只有一个方法execute(JobExecutionContext xontext),Job运行时的信息保存在JobDataMap中

3.2.JobDetail类

实现Job接口,用来描述Job的相关信息,包含Name,Group,JobDataMap等

3.3 JobExecutionContext类

定时程序执行的run-time的上下文环境,用于得到Job的名字、配置的参数等

3.3 JobDataMap类

用来描述一个作业的参数,参数可以为金和基本类型或者某个对象的引用

3.3 JobListener接口

监听作业状态

3.3 TriggaerListener接口

监听触发器状态

3.3 JobStore

3.3.Tigger抽象类

触发器,描述执行Job的触发规则,有SimpleTrigger和CronTrigger两个子类

3.3.1.SimpleTrigger类

继承自Trigger类,每隔xx毫秒/秒执行一次,主要实现固定一次或者固定时间周期类任务的触发

3.3.2.CronTrigger类

继承自Trigger类,使用Cron表达式,实现各种复杂时间规则调度方案,如每天的某个时间,或每周的某几天触发执行之类

3.4.Calendar包

一些日历特定时间点的集合,包内包含以下几个类

3.4.1 BaseCalendar类

3.4.2 AnnualCalendar类

排除每一年中指定的一天或者多天

3.4.3 CalendarComparator类

3.4.4 CronCalendar类

使用表达式排除某时间段不执行

3.4.5 DailyCalendar类

指定的时间范围内每天不执行

3.4.6 HolidayCalendar类

排除节假日

3.4.7 MonthlyCalendar类

配出月份中的数天

3.4.8 WeeklyCalendar类

排除没周中的一天或者多天

3.5.Scheduler类

任务调度器,代表一个Quartz独立容器。

Scheduler可以将JobDetail和Trigger绑定,当Trigger触发时,对应的Job就会被执行,Job和Trigger是1:n(一对多)的关系

3.6Misfire类

错误的任务,本该执行单没有执行的任务调度

4.实现

1.单任务实现

1.定义一个任务,新建任务类继承自Job类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class DemoJob implements Job {

@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
}

}

2.新建类执行这个任务(SimpleTrigger)

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com;

import java.util.Date;

import org.quartz.DateBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzDemo {

public void simpleRun() throws SchedulerException {

SchedulerFactory factory = new StdSchedulerFactory();



Date runTime = DateBuilder.evenSecondDateAfterNow();


JobDetail jobDetail = JobBuilder.newJob(DemoJob.class)
.withIdentity("demo_job", "demo_group")
.build();

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("demo_trigger", "demo_group")

.startAt(new Date())
.withSchedule(
SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(1)
.withRepeatCount(5)
).build();


Scheduler scheduler = factory.getScheduler();

scheduler.scheduleJob(jobDetail,trigger);
System.out.println(jobDetail.getKey() + " 运行在: " + runTime);
scheduler.start();
}

public static void main(String[] args) {
QuartzDemo demo = new QuartzDemo();
try {
demo.simpleRun();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}

2.多任务实现

  1. 测试任务类
    新建两个DemoJonOne和DemoJobTwo,都实现Job接口,内容如下
1
2
3
4
@Override
public void execute(JobExecutionContext arg0) throws JobExecutionException {
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date())+" Runed "+getClass().getName());
}

2.新建QuartzUtil类,内容如下

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com;

import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzUtil {
private final static String JOB_GROUP_NAME = "QUARTZ_JOBGROUP_NAME";
private final static String TRIGGER_GROUP_NAME = "QUARTZ_TRIGGERGROUP_NAME";


public static void addJob(String jobName, String triggerName, Class<? extends Job> jobClass, int seconds)
throws SchedulerException {


SchedulerFactory sf = new StdSchedulerFactory();

Scheduler sche = sf.getScheduler();

JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, JOB_GROUP_NAME).build();

Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerName, TRIGGER_GROUP_NAME)
.startNow()
.withSchedule(
SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(seconds)
.repeatForever()
).build();


sche.scheduleJob(jobDetail, trigger);

sche.start();
}

public static void main(String[] args) {
try {

QuartzUtil.addJob("job1", "trigger1", DemoJobOne.class, 2);


QuartzUtil.addJob("Job2", "trigger2", DemoJobTwo.class, 5);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}

以上方法属于手动调用,如果是web项目中就不同了
添加POM

1
2
3
4
5
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
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
package servlet;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import org.quartz.SchedulerException;

import com.DemoJobOne;
import com.DemoJobTwo;
import com.QuartzUtil;

public class InitServlet extends HttpServlet {

private static final long serialVersionUID = 8507188690597926975L;


public void init() throws ServletException {
try {

QuartzUtil.addJob("job1", "trigger1", DemoJobOne.class, 2);

QuartzUtil.addJob("Job2", "trigger2", DemoJobTwo.class, 5);
} catch (SchedulerException e) {
e.printStackTrace();
}
}

}

2.注册servlet

1
2
3
4
5
6
7
8
9
10
11
<servlet>
<servlet-name>InitServlet</servlet-name>
<servlet-class>servlet.InitServlet</servlet-class>

<load-on-startup>0</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>InitServlet</servlet-name>
<url-pattern>/InitServlet</url-pattern>
</servlet-mapping>

3.复杂规则任务调度(CronTrigger)

在每分钟的1-30秒执行示例

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
package com;

import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class CronTriggerDemo {
public static void main(String[] args) throws SchedulerException {

SchedulerFactory factory = new StdSchedulerFactory();
Scheduler scheduler = factory.getScheduler();

JobDetail job = JobBuilder
.newJob(DemoJobOne.class)
.withIdentity("job","group")
.build();

Trigger trigger = TriggerBuilder
.newTrigger()
.withIdentity("trigger", "group")
.startNow().withSchedule(
CronScheduleBuilder
.cronSchedule("1-30 * * * * ?")
).build();

scheduler.scheduleJob(job,trigger);
scheduler.start();

}
}

5.Cron表达式

规则

格式

1
s M h d m w [y]

s:seconds,取值0-59,允许- * /;

M:minutes,取值0-59,允许- * /;

h:hour,取值0-23,允许- * /;

d:day of month,取值1-31,允许- * ? / L W;

m:month,取值1-12/JAN-DEC,允许- * /;

w:day of week,取值1-7/SUN-SAT,允许- * ? / L #;

y:year,可选,取值empty、1970-2099,允许- * /;

符号解释

、 指定枚举值,如在秒字段使用10、12,则表示只有第10秒和第12秒执行
- 指定区间范围,配合使用,如在小时字段使用10-12,表示在10、11、12时都会触发

* 代表所有值,单独使用,如在秒字段使用,表示每秒触发

? 代表不确定值,单独使用,不用关心的值

/ 用于递增触发,配合使用,n/m,从n开始,每次增加m,如在秒字段设置5/15,表示从第5秒开始,每15秒触发一次

L 表示最后,单独使用,如在秒字段使用,代表第59秒触发,如果在前面加上数字,则表示该数据的最后一个,如在周字段使用6L,则表示本月最后一个周五
W 表示最近的工作日,不会跨月,比如30W,30号是周六,则不会顺延至下周一来执行,如在月字段使用15W,则表示到本月15日最近的工作日(周一到周五)
# 用来指定x的第n个工作日,如在周字段使用6#3则表示该月的第三个星期五

月取值

一月:JAN/0
二月:FEB/1
三月:MAR/2
四月:APR/3
五月:MAY/4
六月:JUN/5
七月:JUL/6
八月:AUG/7
九月:SEP/8
十月:OCT/9
十一月:NOV/10
十二月:DEC/11

周取值

周日:SUN/1
周一:MON/2
周二:TUE/3
周三:WED/4
周四:THU/5
周五:FRI/6
周六:SAT/7

示例

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
0/20 * * * * ? 每20秒执行一次
1-30 * * * * ? 在1-30秒执行
15 0/2 * * * ? 偶数分钟的第15秒执行
0 0/2 8-17 * * ? 从8时到17时 ,每个偶数分钟执行一次
0 0/3 17-23 * * ? 从17时到23时,每3分钟运行一次
0 0 10am 1,15 * ? 每个月的1号和15号的上午10点 运行
0,30 * * ? * MON-FRI 周一至周五,每30秒运行一次
0,30 * * ? * SAT,SUN 周六、周日,每30秒运行一次
0 0 12 * * ? 每天12点触发
0 15 10 ? * * 每天10点15分触发
0 15 10 * * ? 每天10点15分触发
0 15 10 * * ? * 每天10点15分触发
0 15 10 * * ? 2005 2005年每天10点15分触发
0 * 14 * * ? 每天下午的 2点到2点59分每分触发
0 0/5 14 * * ? 每天下午的 2点到2点59分(整点开始,每隔5分触发)
0 0/5 14,18 * * ? 每天下午的 2点到2点59分(整点开始,每隔5分触发) 每天下午的 18点到18点59分(整点开始,每隔5分触发)
0 0-5 14 * * ? 每天下午的 2点到2点05分每分触发
0 10,44 14 ? 3 WED 3月分每周三下午的 2点10分和2点44分触发
0 15 10 ? * MON-FRI 从周一到周五每天上午的10点15分触发
0 15 10 15 * ? 每月15号上午10点15分触发
0 15 10 L * ? 每月最后一天的10点15分触发
0 15 10 ? * 6L 每月最后一周的星期五的10点15分触发
0 15 10 ? * 6L 2002-2005 从2002年到2005年每月最后一周的星期五的10点15分触发
0 15 10 ? * 6#3 每月的第三周的星期五开始触发
0 0 12 1/5 * ? 每月的第一个中午开始每隔5天触发一次
0 11 11 11 11 ? 每年的11月11号 11点11分触发(光棍节)

6.Spring整合Quartz

需要Spring-context-support包支持,POM如下

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.5.RELEASE</version>
</dependency>

新建两种Job测试类–>DemoSimpleJob类和DemoCronJob类,并继承自QuartzJobBean,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com;

import java.text.SimpleDateFormat;
import java.util.Date;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class DemoJob extends QuartzJobBean {

@Override
protected void executeInternal(JobExecutionContext arg0) throws JobExecutionException {
System.out.println(new SimpleDateFormat("hh:mm:ss").format(new Date()) + " 输出自:" + getClass().getName());
}

}

配置spring bean如下

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
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">




<bean id="demoCronJob"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.DemoCronJob" />
</bean>
<bean id="demoSimpleJob"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="com.DemoSimpleJob" />
</bean>


<bean id="simpleTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="demoSimpleJob" />
<property name="startDelay" value="1000" />
<property name="repeatInterval" value="2000" />
</bean>
<bean id="cornTrigger"
class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="demoCronJob" />
<property name="cronExpression" value="1-30 * * * * ?" />
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cornTrigger" />
<ref bean="simpleTrigger" />
</list>
</property>
</bean>

启动

1
2
3
4
5
6
7
8
9
10
11
package com;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Demo {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

}
}

有待补充

  1. WinForm程序

1)第一种方法,使用委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

private delegate void SetTextCallback(string text);
private void SetText(string text)
{
// InvokeRequired需要比较调用线程ID和创建线程ID
// 如果它们不相同则返回true
if (this.txt_Name.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else {
this.txt_Name.Text = text;
}
}

2)第二种方法,使用匿名委托

1
2
3
4
5
6
7
8
9
10
11
12
private void SetText(Object obj)
{
if (this.InvokeRequired)
{
this.Invoke(new MethodInvoker(delegate {
this.txt_Name.Text = obj;
}));
}
else {
this.txt_Name.Text = obj;
}
}

这里说一下BeginInvoke和Invoke和区别:BeginInvoke会立即返回,Invoke会等执行完后再返回。

  1. WPF程序

1)可以使用Dispatcher线程模型来修改

如果是窗体本身可使用类似如下的代码:

this.lblState.Dispatcher.Invoke(new Action(delegate
{
this.lblState.Content = “状态:” + this._statusText;
}));

那么假如是在一个公共类中弹出一个窗口、播放声音等呢?这里我们可以使用:System.Windows.Application.Current.Dispatcher,如下所示

复制代码

System.Windows.Application.Current.Dispatcher.Invoke(new Action(() =>
{
if (path.EndsWith(“.mp3”) || path.EndsWith(“.wma”) || path.EndsWith(“.wav”))
{
_player.Open(new Uri(path));
_player.Play();
}
}));

关键问题:多个视频同时播放,以上几种方法不足以解决,多个视频播放中主界面卡死和播放显示刷新不了的问题。

目前笔者的解决方法是

 pinturebox.CreateGraphics().DrawImage(imgSrc.Bitmap, new System.Drawing.Rectangle(0, 0, pinturebox.Width, pinturebox.Height));

EmguCV中的Capture类可以完成视频文件的读取,并捕捉每一帧,可以利用Capture类完成实现WinForm中视频检测跟踪环境的搭建。本文只实现最简陋的WinForm + EmguCV上的avi文件读取和播放框架,复杂的检测和跟踪算法在之后添加进去。

        这里使用WinForm实现视频的播放,主要是PictureBox类,它是支持基于事件的异步模式的典型组件,不使用EmguCV自带的UI控件等。

效果图

图1.效果图

        直接在UI线程中完成视频的播放的话整个程序只有一个线程,由于程序只能同步执行,播放视频的时候UI将停止响应用户的输入,造成界面的假死。所以视频的播放需要实现异步模式。主要有三种方法:第一是使用异步委托;第二种是使用BackgroundWorker组件;最后一种就是使用多线程(不使用CheckForIllegalCrossThreadCalls =false的危险做法)。

        Windows窗体控件,唯一可以从创建它的线程之外的线程中调用的是Invoke()、BegionInvoke()、EndInvoke()方法和InvokeRequired属性。其中BegionInvoke()、EndInvoke()方法是Invoke()方法的异步版本。这些方法会切换到创建控件的线程上,以调用赋予一个委托参数的方法,该委托参数可以传递给这些方法。

        (一)   使用多线程
        首先定义监控的类及其对应的事件参数类和异常类:
        判断是否继续执行的布尔型成员会被调用线程改变,因此声名为volatile,不进行优化。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/// <summary>
/// 红外检测子。
/// </summary>
public class ThermalSurveillant
{
#region Private Fields

/// <summary>
/// 是否停止线程,此变量供多个线程访问。
/// </summary>
private volatile bool shouldStop = false;

#endregion
#region Public Properties

#endregion
#region Public Events

/// <summary>
/// 帧刷新事件。
/// </summary>
public EventHandler<FrameRefreshEventArgs> FrameRefresh;

/// <summary>
/// 播放完成。
/// </summary>
public EventHandler<CompletedEventArgs> Completed;

#endregion
#region Protected Methods

/// <summary>
/// 处理帧刷新事件。
/// </summary>
/// <param name="e"></param>
protected virtual void OnFrameRefresh(FrameRefreshEventArgs e)
{
if (this.FrameRefresh != null)
{
this.FrameRefresh(this, e);
}
}

/// <summary>
/// 处理视频读完事件。
/// </summary>
/// <param name="e"></param>
protected virtual void OnCompleted(CompletedEventArgs e)
{
if (this.Completed != null)
{
this.Completed(this, e);
}
}

#endregion
#region Public Methods

/// <summary>
/// 视频监控。
/// </summary>
/// <param name="capture">捕捉。</param>
public void DoSurveillance(Object oCapture)
{
Capture capture = oCapture as Capture;
int id = 1;
if (capture == null)
{
throw new InvalidCaptureObjectException("传递的Capture类型无效。");
}
while (!shouldStop)
{
Image<Bgr, byte> frame = capture.QueryFrame();
if (frame != null)
{
FrameRefreshEventArgs e = new FrameRefreshEventArgs(frame.ToBitmap(), id++);
// 触发刷新事件
this.OnFrameRefresh(e);
}
else
{
break;
}
}
// 触发完成事件
this.OnCompleted(new CompletedEventArgs(id));
}

/// <summary>
/// 请求停止线程。
/// </summary>
public void Cancel()
{
this.shouldStop = true;
}

#endregion
}

        UI线程中启动播放线程:

声明:

1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// 监控线程。
/// </summary>
private Thread threadSurveillance = null;
/// <summary>
/// 捕获视频帧。
/// </summary>
private Capture captureSurveillance;
/// <summary>
/// 监控子。
/// </summary>
private ThermalSurveillant surveillant = new ThermalSurveillant();

读入视频文件:

1
2
3
4
5
captureSurveillance = new Capture(this.videoFilePath);
captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_WIDTH, this.width);
captureSurveillance.SetCaptureProperty(CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT, this.height);
Image<Bgr, byte> frame = captureSurveillance.QueryFrame();
this.pictureBox.Image = frame.ToBitmap();

播放视频文件:

        UI线程中响应监控类的事件:

定义异步调用的委托:

添加事件委托:

1
2
this.surveillant.FrameRefresh += OnRefreshFrame;
this.surveillant.Completed += OnCompleted;

        以下方法中都是由监控线程中的事件委托方法,应该使用BeginInvoke方法,这样可以优雅的结束线程,如果使用Invoke方法,则调用方式为同步调用,此时如果使用Thread.Join()方法终止线程将引发死锁(正常播放没有问题),Thread.Join()方法的使用使调用线程阻塞等待当前线程完成,在这里即UI线程阻塞等待监控线程完成,而监控线程中又触发UI线程中pictureBox的刷新,使用Invoke方法就造成了监控线程等待UI线程刷新结果,而UI线程已经阻塞,形成了死锁。死锁时只能用Thread.Abort()方法才能结束线程。或者直接强制结束应用程序。

        使用BeginInvoke方法时为异步调用,监控线程不等待刷新结果直接继续执行,可以正常结束。结束后UI才进行刷新,不会造成死锁。

图2.线程关系

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/// <summary>
/// 刷新UI线程的pixtureBox的方法。
/// </summary>
/// <param name="frame">要刷新的帧。</param>
private void RefreshFrame(Bitmap frame)
{
this.pictureBox.Image = frame;
// 这里一定不能刷新!2012年8月2日1:50:16
//this.pictureBox.Refresh();
}


/// <summary>
/// 响应pictureBox刷新。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnRefreshFrame(object sender, FrameRefreshEventArgs e)
{
// 判断是否需要跨线程调用
if (this.pictureBox.InvokeRequired == true)
{
FrameRefreshDelegate fresh = this.RefreshFrame;
this.BeginInvoke(fresh, e.Frame);
}
else
{
this.RefreshFrame(e.Frame);
}
}


/// <summary>
/// 响应Label刷新信息。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnCompleted(object sender, CompletedEventArgs e)
{
// 判断是否需要跨线程调用
CompletedDelegate fresh = this.RefreshStatus;
string message = "视频结束,共 " + e.FrameCount + " 帧。";
this.BeginInvoke(fresh, message);
}
  

关闭时需要中止播放线程之后再退出:

/// <summary>
/// 关闭窗体时发生。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnFormClosed(object sender, FormClosedEventArgs e)
{
// 检测子算法请求终止
surveillant.Cancel();

// 阻塞调用线程直到检测子线程终止
if (threadSurveillance != null)
{
if (threadSurveillance.IsAlive == true)
{
threadSurveillance.Join();
}
}
}

        (二)   使用异步委托

        创建线程的一个更简单的方法是定义一个委托,并异步调用它。委托是方法的类型安全的引用。Delegate类还支持异步地调用方法。在后台,Delegate类会创建一个执行任务的线程。

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
38
39
40
41
// asynchronous by using a delegate
PlayVideoDelegate play = this.PlayVideoFile;
IAsyncResult status = play.BeginInvoke(null, null);

/// <summary>
/// 播放视频文件。
/// </summary>
private void PlayVideoFile()
{
while (true)
{
Image<Bgr, byte> frame = capture.QueryFrame();
if (frame != null)
{
Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>();
grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC);
RefreshPictureBoxDelegate fresh = this.RefreshPictureBox;
try
{
this.BeginInvoke(fresh, grayFrame.ToBitmap());
}
catch (ObjectDisposedException ex)
{
Thread.CurrentThread.Abort();
}
}
else
{
break;
}
}
}

/// <summary>
/// 刷新UI线程的pixtureBox的方法。
/// </summary>
/// <param name="frame">要刷新的帧。</param>
private void RefreshPictureBox(Bitmap frame)
{
this.pictureBox.Image = frame;
}

        (三)   使用BackgroundWorker组件

        BackgroundWorker类是异步事件的一种实现方案,异步组件可以选择性的支持取消操作,并提供进度信息。RunWorkerAsync()方法启动异步调用。CancelAsync()方法取消。

图3.BackgroundWorker组件

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// <summary>
/// 播放视频文件。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void detectItemPlay_Click(object sender, EventArgs e)
{
if (this.videoFilePath != null)
{
// run async
this.backgroundWorker.RunWorkerAsync(capture);
}
}

/// <summary>
/// 异步调用。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnDoWork(object sender, DoWorkEventArgs e)
{
Emgu.CV.Capture capture = e.Argument as Emgu.CV.Capture;
while (!e.Cancel)
{
Image<Bgr, byte> frame = capture.QueryFrame();
if (frame != null)
{
Image<Gray, byte> grayFrame = frame.Convert<Gray, byte>();
grayFrame.Resize(this.width, this.height, INTER.CV_INTER_CUBIC);
if (this.backgroundWorker.CancellationPending == true)
{
e.Cancel = true;
break;
}
else
{
if (this.pictureBox.InvokeRequired == true)
{
RefreshPictureBoxDelegate fresh = this.RefreshPictureBox;
this.BeginInvoke(fresh, grayFrame.ToBitmap());
}
else
{
this.RefreshPictureBox(grayFrame.ToBitmap());
}
}
}
else
{
break;
}
}
}

/// <summary>
/// 关闭窗体时发生。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
if (this.backgroundWorker.IsBusy)
{
this.backgroundWorker.CancelAsync();
}
}
0%