使用Naudio将Pcm转G711

我们在音频处理的时候经常会接触到PCM数据:它是模拟音频信号经模数转换(A/D变换)直接形成的二进制序列,该文件没有附加的文件头和文件结束标志。

声音本身是模拟信号,而计算机只能识别数字信号,要在计算机中处理声音,就需要将声音数字化,这个过程叫经模数转换(A/D变换)。最常见的方式是通过脉冲编码调制PCM(Pulse Code Modulation)

下面是从github上Naudio库拷贝的 WinForm 录音代码:

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
var outputFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "NAudio");
Directory.CreateDirectory(outputFolder);
var outputFilePath = Path.Combine(outputFolder,"recorded.wav");

var waveIn = new WaveInEvent();

WaveFileWriter writer = null;
MuLawChatCodec _codec = new MuLawChatCodec();
bool closing = false;
var f = new Form();
var buttonRecord = new Button() { Text = "Record" };
var buttonStop = new Button() { Text = "Stop", Left = buttonRecord.Right, Enabled = false };
f.Controls.AddRange(new Control[] { buttonRecord, buttonStop });

buttonRecord.Click += (s, a) =>
{
writer = new WaveFileWriter(outputFilePath, waveIn.WaveFormat);
waveIn.StartRecording();
buttonRecord.Enabled = false;
buttonStop.Enabled = true;
};

buttonStop.Click += (s, a) => waveIn.StopRecording();

waveIn.DataAvailable += (s, a) =>
{
// Pcm
// writer.Write(a.Buffer, 0, a.BytesRecorded);

// Pcm to G.711
byte[] encoded = _codec.Encode(a.Buffer, 0, a.BytesRecorded);
writer.Write(encoded, 0, encoded.Length);

if (writer.Position > waveIn.WaveFormat.AverageBytesPerSecond * 30)
{
waveIn.StopRecording();
}
};

waveIn.RecordingStopped += (s, a) =>
{
writer?.Dispose();
writer = null;
buttonRecord.Enabled = true;
buttonStop.Enabled = false;
if (closing)
{
waveIn.Dispose();
}
};

f.FormClosing += (s, a) => { closing=true; waveIn.StopRecording(); };
f.ShowDialog();

转G.711 mu-law 编码代码:

引用:MuLawChatCodec.cs

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
internal class MuLawChatCodec
{
public string Name => "G.711 mu-law";

public int BitsPerSecond => RecordFormat.SampleRate * 8;

public WaveFormat RecordFormat => new WaveFormat(8000, 16, 1);

public byte[] Encode(byte[] data, int offset, int length)
{
var encoded = new byte[length / 2];
int outIndex = 0;
for (int n = 0; n < length; n += 2)
{
encoded[outIndex++] = MuLawEncoder.LinearToMuLawSample(BitConverter.ToInt16(data, offset + n));
}
return encoded;
}

public byte[] Decode(byte[] data, int offset, int length)
{
var decoded = new byte[length * 2];
int outIndex = 0;
for (int n = 0; n < length; n++)
{
short decodedSample = MuLawDecoder.MuLawToLinearSample(data[n + offset]);
decoded[outIndex++] = (byte)(decodedSample & 0xFF);
decoded[outIndex++] = (byte)(decodedSample >> 8);
}
return decoded;
}

public void Dispose()
{
// nothing to do
}

public bool IsAvailable { get { return true; } }
}

采样率比特率 就像是坐标轴上的横纵坐标。

横坐标的 采样率 表示了每秒钟的采样次数。

纵坐标的 比特率 表示了用数字量来量化模拟量的时候的精度。

可以使用 MediaInfo来查看媒体文件的信息;

基础知识

对于人类的语音信号而言,实际处理一般经过以下步骤:

人说话——>声电转换——>抽样(模数转换)——>量化(将数字信号用适当的数值表示)——>编码(数据压缩)——>

传输(网络或者其他方式)——>解码(数据还原)——>反抽样(数模转换)——>电声转换——>人耳听声。

采样率(sampleRate)

采样:采样即采集样本,是模拟信息数字化的一个环节。即对模拟信号进行离散采样,使之成为数字信号。

采样率采样速度或者采样频率,定义了每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。采样率是指将模拟信号转换成数字信号时的采样频率,也就是单位时间内采样多少点。一个采样点数据有多少个比特。

声音信号为模拟信号,想要在实际中处理必须为数字信号,即采用抽样、量化、编码的处理方案。
处理的第一步为抽样,即模数转换。简单地说就是通过波形采样的方法记录1秒钟长度的声音,需要多少个数据。

根据奈魁斯特(NYQUIST)采样定理,用两倍于一个正弦波的频繁率进行采样就能完全真实地还原该波形。所以,对于声音信号而言,要想对离散信号进行还原,必须将抽样频率定为40KHz以上。

实际中,一般定为44.1KHz。44.1KHz采样率的声音就是要花费44000个数据来描述1秒钟的声音波形。原则上采样率越高,声音的质量越好,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级。22.05 KHz只能达到FM广播的声音品质,44.1KHz则是理论上的CD音质界限,48KHz则已达到DVD音质了。

采样位深(bitsPerSample)

采样时模数转换的分辨率,也就是每个样本用几位来表示,一般是 8bits 或是 16bits。

我们常见的16Bit(16比特),可以记录大概96分贝的动态范围。那么,您可以大概知道,每一个比特大约可以记录6分贝的声音。同理,20Bit可记录的动态范围大概就是120dB;24Bit就大概是144dB。假如,我们定义0dB为峰值,那么声音振幅以向下延伸计算,那么,CD音频可的动态范围就是 -96dB~0dB。,依次类推,24Bit的HD-Audio高清音频的的动态范围就是 -144dB~0dB。 。由此可见,位深度较高时,有更大的动态范围可利用,可以记录更低电平的细节。

声道数(NumChannels)

1 => 单声道 | 2 => 双声道

比特率(Bit Per Second)

俗称码率,表示经过编码(压缩)后的音频数据每秒传送的比特(bit)数。 单位为 bps(Bit Per Second)或kbps即千位每秒,比特率越高,传送的数据越大,音质越好。与体积成正比:码率越大,体积越大;码率越小,体积越小。

PCM编码的公式:
比特率 =采样率 x 采用位数 x声道数

关于比特率的计算,比如,采样率为44.1KHz,采样大小为16bit,声道数为2,那么它的音频比特率的计算为:44.1kHz162 = 1411.2 kbps,然后我们在除以8,将bit转化为Byte,所以1秒钟的数据量就是:1411.2Kbps/8 = 176.4KB/s。这表示存储一秒钟采样率为44.1KHz,采样大小为16bit,双声道的PCM编码的音频信号,需要176.4KB的空间,1分钟则约为10.34M。这对大部分用户是不可接受的,尤其是喜欢在电脑上听音乐的朋友,要降低磁盘占用,只有2种方法,降低采样指标或者压缩。降低指标是不可取的,因此专家们研发了各种压缩方案。最原始的有DPCM、ADPCM,其中最出名的为MP3。所以,采用了数据压缩以后的码率远小于原始码率。

对于音频信号而言,实际上必须进行编码。在这里,编码指信源编码,即数据压缩。如果,未经过数据压缩,直接量化进行传输则被称为PCM(脉冲编码调制)。

参考:

关于音频采样率与码率

音视频基本概念:分辨率、帧速率、码流、采样位深、采样率、比特率

什么是音频视频比特率,采样率,讲的很不错

视频音频比特率(码率)与采样率有什么联系?

MediaInfo

ABP框架学习记录(9)- Timing解析

Timing这个功能主要用于以统一的方式表示时间。因为ABP中有大量的module,还支持自定义module,所以将时间统一表示为local时间(默认)或utc时间是必要的。

Clock(时钟)

TimingSettingProvider:继承 SettingProvider 以设置统一的时间格式。

QQ截图20190509150917.png

AbpKernelModulePreInitialize 方法中引用:

QQ截图20190509151217.png

QQ截图20190509151246.png

ABP 提供 IClockProvider 获取当前时间和标准化时间的接口,三个实现 IClockProvider接口的类: UtcClockProviderUnspecifiedClockProviderLocalClockProvider

IClockProvider

QQ截图20190509151848.png

LocalClockProvider

QQ截图20190509152004.png

UtcClockProvider

QQ截图20190509152201.png

UnspecifiedClockProvider

QQ截图20190509152238.png

ClockProviders:提供三种 Providers

QQ截图20190509152730.png

Clock:封装了 IClockProvider,对外提供当前时间和标准化时间的方法。默认使用 UnspecifiedClockProvider

QQ截图20190509152858.png

也可以指定 Provider

1
Clock.Provider = ClockProviders.Utc;

DateTimeRange(时间区间)

IDateTimeRange/DateTimeRange:表示一个时间区间的实体。

QQ截图20190509155915.png

使用:

QQ截图20190509160338.png

IZonedDateTimeRange/ZonedDateTimeRange:使用时区定义 DateTime 的范围。

QQ截图20190509155958.png

TimeZoneConverter/ITimeZoneConverter:时区转换类。

QQ截图20190509164413.png

TimezoneHelper:用于时区操作的帮助程序类。

QQ截图20190509164913.png

IanaTimeZone :时区信息数据库,又称TZ database、Zoneinfo database,是一个主要应用于计算机程序以及操作系统的,可协作编辑世界时区信息的数据库。由于该数据库由David Olson创立,因而有些地方也将其称作Olson数据库。数据库由Paul Eggert进行编辑和维护

参考:

ABP源码分析十一:Timing

时区信息数据库/IANA time zone

ABP框架学习记录(8)- Unit Of Work解析

目录结构:在ABP项目/Domain/Uow目录下:

QQ截图20190509132703.png

AbpBootstrapper 类,对拦截器的注册方法中,提供对 UnitOfWork 的注册:

QQ截图20190603160354.png

AbpKernelModulePostInitialize 方法注册默认 IUnitOfWork 接口的实现 NullUnitOfWork

QQ截图20190604141032.png

AbpKernelModule 注册过滤器:

QQ截图20190604135212.png

UnitOfWorkRegistrar

UnitOfWorkRegistrar :为Unit Of Work机制注册所需类的拦截器。

Initialize 初始化方法:将 UnitOfWorkInterceptor 拦截器添加到标注了 UnitOfWorkAttribute 特性方法的类,以及实现IRepositoryIApplicationService 的类上。

QQ截图20190603160828.png

HandleTypesWithUnitOfWorkAttributeHandleConventionalUnitOfWorkTypes 方法:

QQ截图20190603161027.png

应用拦截器:

QQ截图20190611151213.png

UnitOfWorkDefaultOptions

UnitOfWorkDefaultOptions/IUnitOfWorkDefaultOptions:配置选项,内部类型或成员只能在同一程序集的文件中访问。

UnitOfWorkDefaultOptions 提供 RegisterFilter 方法来注册 过滤器:

QQ截图20190611145011.png

DataFilterConfiguration:数据过滤器配置类:

QQ截图20190611145513.png

DefaultConnectionStringResolver:实现 IConnectionStringResolver 接口,获取连接数据库字符串;

UnitOfWorkInterceptor

UnitOfWorkInterceptor 继承 Castle.DynamicProxy.IInterceptor 接口,用于管理数据库连接和事务。

通过构造注入 IUnitOfWorkManagerIUnitOfWorkDefaultOptions

QQ截图20190603164632.png

实现 IInterceptor 接口 Intercept 方法,通过注入 IUnitOfWorkDefaultOptions 接口,获取到 UnitOfWorkDefaultOptions 类的实例,并通过 UnitOfWorkDefaultOptions 的扩展方法,获取到使用 UnitOfWorkAttribute 特性标注的 类,接口和方法:

Intercept 方法:应用拦截器,

QQ截图20190611151458.png

使用 UnitOfWorkDefaultOptionsExtensions 扩展类,查询应用了 UnitOfWorkAttribute 特性的方法/类;

QQ截图20190611151724.png

UnitOfWorkDefaultOptions 的扩展类 UnitOfWorkDefaultOptionsExtensions

QQ截图20190604111433.png

UnitOfWorkAttribute

UnitOfWorkAttribute:用于指示声明方法是原子的,应该被视为一个工作单元。拦截具有此属性的方法,打开数据库连接并在调用方法之前启动事务。在方法调用结束时,如果没有异常,则提交事务并将所有更改应用于数据库,否则它会回滚。如果在调用此方法之前已有工作单元,则此属性无效,如果是,则使用相同的事务。

QQ截图20190611152126.png

UnitOfWork 事务

基于接口隔离原则的考量,ABP作者将UnitOfWork的方法分到了三个不同的接口中。

IUnitOfWorkCompleteHandle:定义了UOW同步和异步的complete方法。实现UOW完成时候的逻辑。

IActiveUnitOfWork:一个UOW除了以上两个接口中定义的方法和属性外,其他的属性和方法都在这个接口定义的。比如Completed,Disposed,Failed事件代理,Filter的enable和disable,以及同步、异步的SaveChanges方法。

IUnitOfWork:继承了上面两个接口。定义了外层的IUnitOfWork的引用和UOW的begin方法。 ABP是通过构建一个UnitOfWork的链,将不同的方法纳入到一个事务中。

Unit Of Work运行流程

  • 1,UOW拦截器被注入到需要UOW的类中。
  • 2,ABP执行标注了UnitOfWork特性的方法时。UOW拦截器以begin()->realAction()->complete()->dispose()顺序执行,其中realAction是被调用的真实业务操作。 UOW拦截器是通过using这种方式调用IUnitOfWork的某个具体实现,这就确保begin 和 dispose也总是会被执行的。 这里需要注意complete却不一定会被执行,比如在complete方法被调用前方法的执行产生了异常。
  • 3,当执行一连串的操作时(A方法->B方法->C方法,假设这三个方法都标注了UnitOfWork特性),ABP在执行A方法前会创建整个过程中唯一的IUnitOfWork对象,该对象会启动.NET事务。在执行到B,C方法只会创建InnerUnitOfWorkCompleteHandle。 InnerUnitOfWorkCompleteHandle与IUnitOfWork对象的差异在于它不会创建真实的事务。但ABP会调用其Complete,以告知ABP其对应的方法以成功完成,可以提交事务。
  • 4,事务可以回滚的关键关键在于IUnitOfWork对象在被Dispose时候会检查Complete方法有没有被执行,没有的话就认为这个UOW标注的方法没有顺利完成,从而导致事务的回滚操作。
  • 5,整个事务的提交是通过第一个UOW(也是唯一个)的Complete方法执行时提交的。

UnitOfWorkBase

UnitOfWorkBase : 继承 IUnitOfWork 的抽象类,真正实现事务控制的方法是由这个抽象类的子类实现的(比如,真正创建 TransactionScope 的操作是在 EfUnitOfWorkNhUnitOfWork 这样的之类中实现的)。UOW中除了事务控制逻辑以外的逻辑都是由 UnitOfWorkBase 抽象类实现的。

ABP中共有以下4个具体的UOW类型,他们都继承自 UnitOfWorkBaseEntity Framework, Nhibernate

构造函数:

QQ截图20190604141528.png

启用/禁用 filter:

QQ截图20190604143018.png

提供 CompletedDisposedFailed 事件:

QQ截图20190604143152.png

QQ截图20190604143424.png

BeginSaveChangesSaveChangesAsync

QQ截图20190611161819.png

UnitOfWorkManager

工作单元管理类;对外提供UOW的功能(用于创建UnitOfWork,并开启UnitOfWork流程),对内调用各种UOW功能的各种组件。

QQ截图20190611162244.png

启用工作单元:

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
public IUnitOfWorkCompleteHandle Begin(UnitOfWorkOptions options)
{
options.FillDefaultsForNonProvidedOptions(_defaultOptions);

var outerUow = _currentUnitOfWorkProvider.Current;

/// 如果启用 工作单元事务,且外部 Uow 不为空,则使用 InnerUnitOfWorkCompleteHandle
/// 对象。
if (options.Scope == TransactionScopeOption.Required && outerUow != null)
{
return new InnerUnitOfWorkCompleteHandle();
}

var uow = _iocResolver.Resolve<IUnitOfWork>();

uow.Completed += (sender, args) =>
{
_currentUnitOfWorkProvider.Current = null;
};

uow.Failed += (sender, args) =>
{
_currentUnitOfWorkProvider.Current = null;
};

uow.Disposed += (sender, args) =>
{
_iocResolver.Release(uow);
};

//Inherit filters from outer UOW
if (outerUow != null)
{
options.FillOuterUowFiltersForNonProvidedOptions(outerUow.Filters.ToList());
}

uow.Begin(options);

//Inherit tenant from outer UOW
if (outerUow != null)
{
uow.SetTenantId(outerUow.GetTenantId(), false);
}

_currentUnitOfWorkProvider.Current = uow;

return uow;
}

EF Core 启用事务:

QQ截图20190611164826.png

UnitOfWork 拦截器调用 UnitOfWorkManager 开启UOW流程的代码。

QQ截图20190611162510.png

InnerUnitOfWorkCompleteHandle

InnerUnitOfWorkCompleteHandle:实现 IUnitOfWorkCompleteHandle, 用户工作范围的内联单元,内部工作单元实际使用外部工作单元,并且对 IUnitOfWorkCompleteHandle.Complete 调用没有影响。但是如果没有调用它,则会在UOW结束时抛出异常以回滚UOW。

QQ截图20190611163052.png

提交事务:

QQ截图20190611165617.png

AsyncLocalCurrentUnitOfWorkProvider

AsyncLocalCurrentUnitOfWorkProvider:实现 ICurrentUnitOfWorkProvider 接口,获取或设置当前 IUnitOfWork 实例;

IActiveUnitOfWork

IActiveUnitOfWork:用于处理活动的工作单元,此接口不能被注入,可以使用 IUnitOfWorkManager 替换。

1
2
3
4
/// <summary>
/// Gets current unit of work.
/// </summary>
protected IActiveUnitOfWork CurrentUnitOfWork { get { return UnitOfWorkManager.Current; } }

参考:

ABP源码分析十:Unit Of Work

ABP框架学习记录(7)- 后台工作任务

文主要说明ABP中后台工作者模块(BackgroundWorker)的实现方式,和后台工作模块(BackgroundJob)。
ABP通过 BackgroundWorkerManager 来管理 BackgroundJobManager ,然后通过 BackgroundJobManager 来管理 BackgroundJobBackgroundJob 就代表一个真正的后台任务。

AbpKernelModulePostInitialize 方法完成对 BackgroundWorkerManagerBackgroundJobManager的初始化。

QQ截图20190508151157.png

BackgroundWorkerManager 工作者

目录结构:

QQ截图20190508151546.png

IRunnable/RunnableBase:定义了启动/终止一个任务的方法的接口和基本实现抽象类。共三个方法:start, stop, waittostop。

QQ截图20190508151806.png

BackgroundWorkerBase:实现 IBackgroundWorker 的一个抽象类,同时添加了UOW,Setting 和本地化的一些辅助方法。

QQ截图20190508152210.png

PeriodicBackgroundWorkerBase:继承 BackgroundWorkerBase, 通过封装 AbpTimer 实现定时启动执行任务的功能。这个类型定义个一个抽象方法 DoWorkAbpTimer 最终会定时执行这个方法。

QQ截图20190508161027.png

QQ截图20190508162829.png

AbpTimer是整个ABP框架实现后台工作的核心类,其实现原理就是通过一个CLR中的timer定时启动执行任务。

使用Timer需要注意的问题:

第一,用timer有一个弊端,就是当timer间隔时间内,任务如果没执行完,timer就会新建一个线程,从头开始执行这个任务,而上一个线程仍然继续执行,这样就会导致系统中产生的线程过多,一会儿系统的资源就耗尽了。ABP的解决思路是在执行真正的业务方法之前,通过将timer的duetime设为无限大,从而timer就失效了。业务方法执行完以后在恢复timer的设置。

QQ截图20190508173455.png

Timer 官方文档解释为:

QQ截图20190508173557.png

System.Threading.Timer

第二,如何知道一个Timer真正结束了呢?也就是说如何知道一个Timer要执行的任务已经完成(定义为A效果),同时timer已失效(定义为B效果)?ABP通过stop方法实现B,通过WaitToStop实现A效果。WaitToStop会一直阻塞调用他的线程直到_performingTasks变成false,也就是说Timer要执行的任务已经完成(任务完成时会将_performingTasks设为False,并且释放锁)。

QQ截图20190508174549.png

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
/// <summary>
/// _taskTimer的回调方法
/// </summary>
/// <param name="state">Not used argument</param>
private void TimerCallBack(object state)
{
lock (_taskTimer)
{
if (!_running || _performingTasks)
{
return;
}

_taskTimer.Change(Timeout.Infinite, Timeout.Infinite);
_performingTasks = true;
}

try
{
if (Elapsed != null)
{
Elapsed(this, new EventArgs());
}
}
catch
{}
finally
{
lock (_taskTimer)
{
_performingTasks = false;
if (_running)
{
// 指示Timer继续执行
_taskTimer.Change(Period, Timeout.Infinite);
}
// 释放锁
Monitor.Pulse(_taskTimer);
}
}
}

IBackgroundWorkerManager/BackgroundWorkerManager:用于管理后台工作任务:IBackgroundWorker 实例(添加 IBackgroundWorker 实例到管理器,启动,终止和注销后台任务)。

QQ截图20190508160425.png

BackgroundWorkerManager 工作者

在ABP项目的目录结构:

QQ截图20190509104729.png

IBackgroundJob/BackgroundJob:定义一个后台工作任务的接口/和基本实现。具体的后台任务类可从 BackgroundJob 继承,这是定义最终需要被执行的逻辑的地方。

QQ截图20190509113529.png

BackgroundJobInfo:用于持久化job信息的实体类,对应于数据库中的表 AbpBackgroundJobs。一个job对应一个要执行的任务。

QQ截图20190509131552.png

IBackgroundJobConfiguration/BackgroundJobConfiguration:配置是否激活后台工作任务功能。

QQ截图20190509114407.png

BackgroundJobPriority:后台job的优先级。

IBackgroundJobStore/InMemoryBackgroundJobStore: 用于持久化后台任务 BackgroundJobInfo。可以实现这个接口将后台任务 BackgroundJobInfo 存储到数据库。

IBackgroundJobManager/BackgroundJobManagerIBackgroundJobManager 继承 IBackgroundWorkerBackgroundJobManager 默认实现 IBackgroundJobManager ,并 继承 PeriodicBackgroundWorkerBase。它可以被其他的后台工作提供者替代(Hangfire)。BackgroundJobManager之所以能在后台执行任务,是因为其继承了PeriodicBackgroundWorkerBase 基类,并重写了DoWork方法。从 BackgroundJobStore(可以自定义实现从数据库中读取)取最多1000个 BackgroundJobInfo,然后反射执行 BackgroundJobInfo 中定义的任务。

QQ截图20190509131058.png

QQ截图20190509131329.png

下面是一个ABP中通过 BackgroundJobManager 安排 BackgroundJob 的例子。

QQ截图20190509132129.png

参考:

ABP源码分析九:后台工作任务

ABP框架学习记录(6)- Logging解析

ABP使用Castle日志记录工具,并且可以使用不同的日志类库,比如:Log4Net, NLog, Serilog… 等等。对于所有的日志类库,Castle提供了一个通用的接口来实现,我们可以很方便的处理各种特殊的日志库,而且当业务需要的时候,很容易替换日志组件。

LogSeverity:表示日志的严重性,枚举类型,定义了5个日志级别:Info,Debug,Warn,Error, Fatal。

IHasLogSeverity:封装了LogSeverity。AbpAuthorizationExceptionAbpValidationExceptionUserFriendlyException 实现了这个接口。Loghelper 在对 Exception 做log的时候可以方便的通过实现了IHasLogSeverityException 的实例获取到 LogSeverity,以确定log 级别。

AbpAuthorizationException:

QQ截图20190507105847.png

LogHelper 的实现:把继承 IHasLogSeverity 接口的对象,转换成 IHasLogSeverity 对象,并获取log级别。

QQ截图20190507110228.png

LoggerExtensions: 扩展了Castle的Ilogger接口的方法,封装更便捷的方法供Loghelper调用。

QQ截图20190507110501.png

web项目的application_start方法中注入logger实例:

QQ截图20190507110701.png

参考:

ABP源码分析八:Logger集成

ABP框架学习记录(5)- Setting解析

ABP中的 SettingConfiguration

Setting一般用于需要通过外部配置文件(或数据库)设置的简单类型数据(一般就是字符串)。

Configuration一般只需要通过内部代码完成的配置,一般用于设置复杂类型的数据。

Setting的实现

SettingDefinition:用于定义Setting。

SettingDefinitionGroup:用于给SettingDefinition分组。

SettingsConfiguration / ISettingsConfiguration:用于集中化设置和管理 SettingProvider 的对象。其封装了一个ITypeList<SettingProvider> Providers的集合类。可以通过Configuration.Setting来获取ISettingsConfiguration实例,然后将自定义的 SettingProvider 添加到 SettingsConfiguration 对象中,下图:

QQ截图20190506162020.png

SettingDefinitionManager:主要完成注册到ABP中的 SettingDefinition 初始化,通过 ISettingsConfiguration 实例获取 setting providers 集合,然后在 Initialize 方法中通过 setting providers 获取 SettingDefinition 的数组。并将其保存在 Dictionary 中,其key就是 SettingDefinition 的Name,下图:

QQ截图20190506162528.png

SettingDefinitionManager 继承 ISingletonDependency接口,将在BasicConventionalRegistrar类中实现约定规则的注入:

QQ截图20190506163107.png

AbpKernelModulePostInitialize 调用 SettingDefinitionManagerInitialize方法:

QQ截图20190506173110.png

SettingDefinitionProviderContext:用于对 ISettingDefinitionManager 的封装:

QQ截图20190506162348.png

SettingScopes:标注了Flags特性的枚举类型,表示setting的应用范围:

QQ截图20190506162316.png

SettingProvider:为具体的功能模块所需的设置定义 SettingDefinition,并且以数组的形式返回:

QQ截图20190506170342.png

SettingManager / ISettingManager :用户获取配置详情。

QQ截图20190506172935.png

公共方法:

QQ截图20190506173541.png

以mail的实现来使用Setting

目录结构:

QQ截图20190506173847.png

IEmailSenderConfiguration/EmailSenderConfigurationISmtpEmailSenderConfiguration/SmtpEmailSenderConfiguration:定义配置。

EmailSenderConfiguration

QQ截图20190509145449.png

SmtpEmailSenderConfiguration

QQ截图20190509145529.png

EmailSettingProvider:继承自 SettingProvider, 将SMTP的各项设置封装成SettingDefinition,并以数组形式返回。

QQ截图20190509145337.png

EmailSenderBase/IEmailSenderISmtpEmailSender/SmtpEmailSender:用户发送邮件。

参考:

ABP源码分析七:Setting 以及 Mail

ABP框架学习记录(4)- Configuration解析

目录结构:

QQ截图20190506094754.png

通过 AbpStartupConfiguration,Castle的依赖注入,Dictionary 对象和扩展方法实现了配置中心化。配置中心化是一个支持模块开发的框架必备功能。

核心模块配置

模块配置

ABP中核心功能模块中的一些功能的运行时的行为依赖于一些外部配置。比如 Localization 这个功能模块,最基本Abp需要知道要做哪些语言的本地化。而这些具体的配置对于Abp底层框架来说是不可预知的,那么ABP底层框架就很有必要提供一种手段供外部模块自定义 Congfiguration。 这就是下文要分析的 IAbpStartupConfiguration 和各种I***Configuration

通过 AbpBootstrapperInitialize 方法,注册 AbpCoreInstaller:

具体请参考:ABP框架学习记录(2) - ABP初始化

QQ截图20190506101758.png

AbpCoreInstaller:

QQ截图20190506101856.png

IAbpStartupConfiguration/AbpStartupConfiguration : Initialize 方法,通过调用容器,获取基础配置实例。

QQ截图20190506102036.png

调用配置

抽象类 AbpModule 提供 Configuration 字段,继承 AbpModule 的子类,可以直接调用或修改某个组件的Configuration

AbpModule

QQ截图20190506102516.png

调用/修改:

QQ截图20190506102855.png

自定义模块配置

注册

IAbpWebCommonModuleConfiguration 为例:

定义 IAbpWebCommonModuleConfiguration 接口:

QQ截图20190506113226.png

AbpWebCommonModule 中的 PreInitialize 方法注册实例:

QQ截图20190506113321.png

获取

DictionaryBasedConfig 定义的 CustomSettings 就是最终保存自定义的module的Configuration的地方。

QQ截图20190506111029.png

AbpStartupConfiguration 继承 DictionaryBasedConfig

QQ截图20190506111449.png

IModuleConfigurations 中定义 IAbpStartupConfiguration

QQ截图20190506111717.png

ModuleConfigurations 的实现:

QQ截图20190506113929.png

AbpWebConfigurationExtensions 提供对 IModuleConfigurations 的扩展:

QQ截图20190506112702.png

AbpStartupConfiguration 获取配置方法:

QQ截图20190506131005.png

DictionaryBasedConfig中定义的方法:

QQ截图20190506131238.png

使用 AbpWebConfigurationExtensions

QQ截图20190506113848.png

参考:

ABP源码分析四:Configuration

ABP框架学习记录(3)- 依赖注入解析

ABP的依赖注入的实现本质上是依赖于Castle依赖注入的框架。
实现途径有两种:一种实现途径是通过实现IConventionalDependencyRegistrar 的实例定义注入的约定(规则),然后通过 IocManager 来读取这个规则完成依赖注入。另一种实现途径是直接 IocManagerRegister 方法直接完成注入。

目录结构:

代码在Abp项目文件的Dependency文件夹:

QQ截图20190430174143.png

注入方式

直接注入

Abp定义 IocManager 类,以提供依赖注入的管理,内部提供 IocManager 静态字段和 IocContainer 实例字段。

IocManager 类定义了静态构造和实例构造,以确保在创建第一个实例或引用任何静态成员之前自动调用静态构造函数。

QQ截图20190505153836.png

提供Register方法:

QQ截图20190505164520.png

AbpModule 有个受保护的 IocManager 的成员,所以 AbpModule 的派生类都可以使用这个 IocManager 完成注册。

QQ截图20190505154320.png

约定(规则)

ConventionalRegistrationConfig:封装了一个bool属性 InstallInstallers,用以告诉Abp底层框架是否要register相应assembly中的通过IWindsorInstaller接口指定的register规则,默认true。

IConventionalRegistrationContext/ConventionalRegistrationContext: 和其他上下文类起的作用类似。主要就是作为方法参数方便方法间的传递数据。这里主要封装了 AssemblyIocManagerConventionalRegistrationConfig

IConventionalDependencyRegistrar:用于按约定注册依赖项。

IocManager 提供 IConventionalDependencyRegistrar 的集合,IocManagerRegisterAssemblyByConvention 方法中遍历这个list,并根据IConventionalDependencyRegistrar 的实例中定义的规则来完成register。

1
private readonly List<IConventionalDependencyRegistrar> _conventionalRegistrars;

QQ截图20190505163142.png

IConventionalDependencyRegistrar 接口实现:

QQ截图20190506163107.png

QQ截图20190505163519.png

扩展方法

ABP提供 IocRegistrarExtensions,IocResolverExtensions 来扩展 IIocRegistrarIIocResolver 接口的实现类。

IocRegistrarExtensions:

QQ截图20190505165639.png

IocResolverExtensions:

QQ截图20190505170652.png

IDisposableDependencyObjectWrapper<out T>/IDisposableDependencyObjectWrapper 接口:提供释放已解析的对象的方法。

QQ截图20190505171227.png

应用:

QQ截图20190505170929.png

参考:

ABP源码分析六:依赖注入的实现

ABP框架学习记录(2)- ABP初始化

ASP.NET Web应用程序的第一个执行的方法是 Global.asax 下定义的Start方法。执行这个方法前 HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成了。

AbpWebApplication

Global.asax 中 MvcApplication 继承自泛型 AbpWebApplication<> ,并提供 AbpZeroTemplateWebModule 作为 StartupModule

QQ截图20190430093710.png

泛型 AbpWebApplication<> (下图),继承自 HttpApplication,实例化 AbpBootstrapper 对象:

QQ截图20190430134242.png

AbpBootstrapper

AbpBootstrapper 的构造函数中,实例化 AbpBootstrapperOptions 对象,以提供 IocManager 实例:

QQ截图20190430135239.png

AbpBootstrapperOptions 类:

QQ截图20190430135457.png

AbpBootstrapper 类提供私有构造函数,并且提供泛型 Create 方法以创建实例:

QQ截图20190510165701.png

Create :方法

QQ截图20190510165854.png

拦截器注册:

QQ截图20190510170025.png

QQ截图20190510170146.png

QQ截图20190510170458.png

1,Initialize 方法:

QQ截图20190430140353.png

2,Initialize 作用:

(1),安装 AbpCoreInstallerAbpCoreInstaller 的作用是用来注册系统框架级的所有配置类。

AbpCoreInstaller 类:

QQ截图20190430140706.png

(2),增加插件

1
IocManager.Resolve<AbpPlugInManager>().PlugInSources.AddRange(PlugInSources);

(3),初始化配置

QQ截图20190430143142.png

(4),通过 AbpModuleManager 管理 AdpModule;

1
2
3
_moduleManager = IocManager.Resolve<AbpModuleManager>();
_moduleManager.Initialize(StartupModule);
_moduleManager.StartModules();

参考:

ABP源码分析二:ABP中配置的注册和初始化

Abp是一种基于模块化设计的思想构建的。开发人员可以将自定义的功能以模块(module)的形式集成到ABP中。具体的功能都可以设计成一个单独的Module。Abp底层框架提供便捷的方法集成每个Module。

AbpModule

Abp 项目下的 Abp.Modules 文件夹下定义了抽象类 AbpModule ,它提供了两个受保护的属性和四个虚方法:

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
/// <summary>
/// IOC 管理
/// </summary>
protected internal IIocManager IocManager { get; internal set; }

/// <summary>
/// 配置
/// </summary>
protected internal IAbpStartupConfiguration Configuration { get; internal set; }

/// <summary>
/// 应用程序启动时调用的第一个事件,在依赖注入之前运行
/// </summary>
public virtual void PreInitialize(){}

/// <summary>
/// 注册依赖项
/// </summary>
public virtual void Initialize(){}

/// <summary>
/// 应用程序启动时调用
/// </summary>
public virtual void PostInitialize(){}

/// <summary>
/// 关闭应用程序时调用
/// </summary>
public virtual void Shutdown(){}

DependsOnAttribute

AbpModule 还提供 FindDependedModuleTypes 方法,获取使用 DependsOnAttribute 属性的
Module 集合:

QQ截图20190429152244.png

AbpModuleManager

Abp.Modules 文件夹下定义了 AbpModuleManager 类,来管理 ABPModule

QQ截图20190429161029.png

AbpModuleCollection

AbpModuleCollection 类继承 List<AbpModuleInfo>

QQ截图20190429162910.png

AbpModuleManager 得到所有的 AbpModuleAbpModuleInfo 以后,逐个调用这些 ModulePreInitializeInitializePostInitialize 以完成初始化:

QQ截图20190430132848.png

1
2
3
4
5
6
7
public virtual void StartModules()
{
var sortedModules = _modules.GetSortedModuleListByDependency();
sortedModules.ForEach(module => module.Instance.PreInitialize());
sortedModules.ForEach(module => module.Instance.Initialize());
sortedModules.ForEach(module => module.Instance.PostInitialize());
}

AbpKernelModule

Abp底层框架的一些功能模块的类型通过 AbpKernelModule 实现:

QQ截图20190429163427.png

QQ截图20190429163852.png

参考:

ABP源码分析三:ABP Module

CSharp中DebuggerStepThrough特性节省Debug时间

DebuggerStepThroughAttribute: 指示调试器逐句通过代码,而不是单步执行代码。

只支持类,结构,构造方法,方法。

此属性避免必须进入编译器提供的代码,只进入开发人员提供的代码。例如,如果使用F11(Step Into)键逐步执行代码,则该属性将使步骤的行为类似于编译器提供的代码的F10(Step Over)键。该方法不会被引入,但它将被执行。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[DebuggerStepThrough]
public static class Check
{
[ContractAnnotation("value:null => halt")]
public static T NotNull<T>(T value, [InvokerParameterName] [NotNull] string parameterName)
{
if (value == null)
{
throw new ArgumentNullException(parameterName);
}

return value;
}
}

// 执行代码
partdto part = new partdto();
Check.NotNull(part.PartDto, "partDTO"); // 按F11执行单步调试,则编译器会执行类似F10的行为

参考:

DebuggerStepThroughAttribute Class

仅我的代码

ABP框架学习记录 - 文章目录

什么是ASP.NET Boilerplate?
ASP.NET Boilerplate(ABP)是一个开源且文档齐全的应用程序框架。 它不仅仅是一个框架,它还提供了一个基于域驱动设计的强大架构模型,并考虑了所有最佳实践。

ABP与最新的ASP.NET Core和EF Core一起使用,但也支持ASP.NET MVC 5.x和EF 6.x。

参考:

https://aspnetboilerplate.com/Pages/Documents

ABP源码分析一:整体项目结构及目录

WCF接口时间类型序列化问题

使用 Json.Net 序列化或者反序列化 DateTime 类型时,通过设置 JsonSerializerSettings 对象,即可支持 WCF 时间类型,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class WCFModel
{
public DateTime CreatedDate { get; set; }
}

private static void WCFDateTimeConvert()
{
WCFModel p = new WCFModel { CreatedDate = TimeZone.CurrentTimeZone.ToLocalTime(DateTime.UtcNow) };

var settings = new JsonSerializerSettings
{
DateFormatHandling = DateFormatHandling.MicrosoftDateFormat
};

string json = JsonConvert.SerializeObject(p, settings);

var dObj = JsonConvert.DeserializeObject<WCFModel>(json, settings);
}

具体实现源码路径 Newtonsoft.Json\Utilities 第635行:

Json.Net 源码地址:https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/DateTimeUtils.cs

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
internal static int WriteDateTimeString(char[] chars, int start, DateTime value, TimeSpan? offset, DateTimeKind kind, DateFormatHandling format)
{
int pos = start;

if (format == DateFormatHandling.MicrosoftDateFormat)
{
TimeSpan o = offset ?? value.GetUtcOffset();

long javaScriptTicks = ConvertDateTimeToJavaScriptTicks(value, o);

@"\/Date(".CopyTo(0, chars, pos, 7);
pos += 7;

string ticksText = javaScriptTicks.ToString(CultureInfo.InvariantCulture);
ticksText.CopyTo(0, chars, pos, ticksText.Length);
pos += ticksText.Length;

switch (kind)
{
case DateTimeKind.Unspecified:
if (value != DateTime.MaxValue && value != DateTime.MinValue)
{
pos = WriteDateTimeOffset(chars, pos, o, format);
}
break;
case DateTimeKind.Local:
pos = WriteDateTimeOffset(chars, pos, o, format);
break;
}

@")\/".CopyTo(0, chars, pos, 3);
pos += 3;
}
else
{
pos = WriteDefaultIsoDate(chars, pos, value);

switch (kind)
{
case DateTimeKind.Local:
pos = WriteDateTimeOffset(chars, pos, offset ?? value.GetUtcOffset(), format);
break;
case DateTimeKind.Utc:
chars[pos++] = 'Z';
break;
}
}

return pos;
}

时间戳简介

JavaScript时间戳总毫秒数(长度13),Unix时间戳是总秒数(长度10)。

格林威治时间 UTC: 1970年01月01日00时00分00秒
北京时间 Local:1970年01月01日08时00分00秒

JavaScript时间戳转换

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
var date = new Date();
console.log(date);

// 第一种
time1 = date.getTime();
console.log(time1)

// 第二种
time2 = date.valueOf();
console.log(time2)

// 第三种
time3 = Date.parse(date);
console.log(time3)


var date1 = new Date(time1);
Y = date.getFullYear() + '-';
M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';
D = date.getDate() + ' ';
h = date.getHours() + ':';
m = date.getMinutes() + ':';
s = date.getSeconds();
console.log(Y+M+D+h+m+s);

// 输出:
// 第一,第二种更精确,第三种只精确到秒
/*
Tue Apr 23 2019 17:27:21 GMT+0800 (中国标准时间)
1556011641157
1556011641157
1556011641000
2019-04-23 17:27:21
*/

Date()参数形式有7种

1
2
3
4
5
6
7
new Date("month dd,yyyy hh:mm:ss");
new Date("month dd,yyyy");
new Date("yyyy/MM/dd hh:mm:ss");
new Date("yyyy/MM/dd");
new Date(yyyy,mth,dd,hh,mm,ss);
new Date(yyyy,mth,dd);
new Date(ms);

CSharp时间戳转换

Unix时间戳转换

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
/// <summary>
/// DateTime转换为Unix时间戳
/// </summary>
/// <returns></returns>
public static long GetUnixTimeStamp()
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
long timeStamp = (long)(TimeZone.CurrentTimeZone.ToLocalTime(DateTime.UtcNow) - startTime).TotalSeconds; // 相差秒数
return timeStamp;
}

/// <summary>
/// 获取本地时间的Unix时间戳
/// </summary>
/// <param name="time">本地时间</param>
/// var dt = new DateTime(2019, 4, 30, 15, 30, 00, DateTimeKind.Local);
/// <returns></returns>
public static long GetUnixTimeStamp(DateTime time)
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
long timeStamp = (long)(time - startTime).TotalSeconds; // 相差秒数
return timeStamp;
}

/// <summary>
/// Unix时间戳转换为DateTime
/// </summary>
/// <returns></returns>
public static DateTime GetDateTimeByUnix(long timeStamp)
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
DateTime dt = startTime.AddSeconds(timeStamp);
return dt;
}

// 调用
var timeStamp1= GetUnixTimeStamp();
Console.WriteLine(timeStamp1);
var dt1 = GetDateTimeByUnix(timeStamp1);
System.Console.WriteLine(dt1.ToString("yyyy/MM/dd HH:mm:ss"));

JavaScript时间戳转换

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
/// <summary>
/// DateTime转换为JavaScript时间戳
/// </summary>
/// <returns></returns>
public static long GetJavaScriptTimeStamp()
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
long timeStamp = (long)(TimeZone.CurrentTimeZone.ToLocalTime(DateTime.UtcNow) - startTime).TotalMilliseconds; // 相差毫秒数
return timeStamp;
}

/// <summary>
/// JavaScript时间戳转换为DateTime
/// </summary>
/// <returns></returns>
public static DateTime GetDateTimeByJavaScript(long timeStamp)
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
DateTime dt = startTime.AddMilliseconds(timeStamp);
return dt;
}

// 调用
var timeStamp = GetJavaScriptTimeStamp();
Console.WriteLine(timeStamp);
var dt = GetDateTimeByJavaScript(timeStamp);
System.Console.WriteLine(dt.ToString("yyyy/MM/dd HH:mm:ss.fff"));

参考:

How to deserialize a unix timestamp (μs) to a DateTime from JSON?

JavaScript Date 对象

C# DateTime与时间戳转换

JavaScript在线测试