0%

在阅读本文之前,假设你的手机已经root,并且已经成功安装好了 XposedInstaller。

什么是Xposed

Xposed 是一个 Android 平台上的动态劫持框架,通过替换手机上的孵化器 zygote 进程为 Xposed 自带的 zygote,使其在启动过程中加载 XposedBridge.jar,模块开发者可以通过 jar 提供的 API 来实现对所有的 Function(这里可以理解为方法) 的劫持,在原 Function 执行的前后加上自定义代码。

1

Xposed框架是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。Xposed理论上能够hook到系统任意一个Java进程,由于是从底层hook,所以需要root权限,并且每次更新都要重新启动,否则不生效 。

Xposed 和 Cydia Substrate

这两个框架都是app注入的利器,只是相对来说有各自特点罢了。Cydia Substrate是一个代码修改平台。它可以修改任何主进程的代码,不管是用Java还是C/C++(native代码)编写的。而Xposed只支持 hook system/bin/app_process中的Java函数。其实Cydia Substrate 与xposed 的hook原理是一样的,二者都可以作为Java Hook的框架,看使用习惯了(iOS的越狱用到的便是Cydia Substrate)。笔者最近研究主要以Xposed为主,暂不对Cydia Substrate进行详细的说明,后续有研究会继续以文字形式记录吧。

利用Xposed执行最简单的篡改操作

Xposed的运用太过广泛,作为这个系列的第一篇笔记,这里也介绍一种Xposed最简单的应用场景。

我们修改入门android逆向的项目,将 Hello World 修改为 Hello Xposed!

a. 随意在Android Studio里创建一个项目,在这里我的包名是:com.blues.cracktest 里面没有任何后续添加的代码,编译之后只有屏幕中间最经典的“Hello World!”;
b. 我们需要做的就是通过Xposed去修改这个“Hello World”,让它替换成任何我们想要它显示成为的东西,比如我期望是“Hello Xposed! ”
首先第一步先进行配置,在清单文件AndroidManifest里 Application作用域里加上如下配置:

1
2
3
4
5
6
7
8
9
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="hello xposed" />
<meta-data
android:name="xposedminversion"
android:value="82" />

第二步,在main目录下(java、res同级目录)创建assets文件夹,在该文件夹下新建一个xposed_init文件(该文件名称固定为xposed_init),该文件用于放置hook的入口,里面是一个路径(比如我的是com.example.androidrefirst.XposedInit,XposedInit就是我hook的入口,这个类后面会提及)。

第三步,新建一个类,这里就叫它XposedInit好了,让它实现(implements) IXposedHookLoadPackage 这个接口 ,重写 handleLoadPackage 方法(该方法用于获取需要hook到的类,里面会用到一个findAndHookMethod 用于hook对应的方法),我就直接show code吧:

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
package com.example.androidrefirst;

import android.os.Bundle;
import android.widget.TextView;

import java.lang.reflect.Field;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class XposedInit implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
if (lpparam.packageName.equals("com.example.androidrefirst")) {
XposedHelpers.findAndHookMethod("com.example.androidrefirst.MainActivity", lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
//不能通过Class.forName()来获取Class ,在跨应用时会失效
Class c = lpparam.classLoader.loadClass("com.example.androidrefirst.MainActivity");
Field field = c.getDeclaredField("textView");
field.setAccessible(true);
//param.thisObject 为执行该方法的对象,在这里指MainActivity
TextView textView = (TextView) field.get(param.thisObject);
textView.setText("Hello Xposed!!!!!!!");
}
});
}
}
}

原来的MainActivity长这样:

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
package com.example.androidrefirst;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
private EditText code;
private Button btn_verify;
private TextView textView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

private void init() {
code = findViewById(R.id.editText);
btn_verify = findViewById(R.id.button);
btn_verify.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String verifyCode=code.getText().toString().trim();
if(verifyCode.equals("2019")){
Intent intent=new Intent(MainActivity.this,SuccessActivity.class);
startActivity(intent);
}else {
Toast.makeText(MainActivity.this,"验证码错误!", Toast.LENGTH_LONG).show();
}
}
});
textView=findViewById(R.id.textView1);
textView.setText("hello world!!!!!");
}
}

运行之后在 XposedInstaller 中启用我们的xposed模块 重启之后看效果。可以看到Hello Xposed!!!!!!!

参考:

https://blog.csdn.net/micaaa/article/details/82706778

逆向工程

逆向工程(又称逆向技术),是一种产品设计技术再现过程,即对一项目标产品进行逆向分析及研究,从而演绎并得出该产品的处理流程、组织结构、功能特性及技术规格等设计要素,以制作出功能相近,但又不完全一样的产品。逆向工程源于商业及军事领域中的硬件分析。其主要目的是在不能轻易获得必要的生产信息的情况下,直接从成品分析,推导出产品的设计原理。
逆向工程可能会被误认为是对知识产权的严重侵害,但是在实际应用上,反而可能会保护知识产权所有者。例如在集成电路领域,如果怀疑某公司侵犯知识产权,可以用逆向工程技术来寻找证据。

这里我们以安卓项目开始探索逆向工程之路

逆向工具

工欲善其身,必先利其器。

反编译代码的工具下载:

反编译资源的工具:

  • APKTool: 本文重要工具,APK逆向工具,使用简单下载地址: http://ibotpeaches.github.io/Apktool/install/
    这里简单介绍下大概流程,首先把后缀为.apk的文件改为.zip的一个压缩文件,方便解压。dex2jar和jd-gui配套使用,用于逆向代码部分,APKTool用于逆向res文件夹下的图片布局等部分。

新建一个简单的项目

新建一个项目,名字是 Androidrefirst,实现的逻辑是在输入框内填写 2019之后 点击按钮跳转到成功页面。

主要代码如下

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
package com.example.androidrefirst;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
private EditText code;
private Button btn_verify;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}

private void init() {
code = findViewById(R.id.editText);
btn_verify = findViewById(R.id.button);
btn_verify.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String verifyCode=code.getText().toString().trim();
if(verifyCode.equals("2019")){
Intent intent=new Intent(MainActivity.this,SuccessActivity.class);
startActivity(intent);
}else {
Toast.makeText(MainActivity.this,"验证码错误!", Toast.LENGTH_LONG).show();
}
}
});
}
}

生成 apk安装包 app-release.apk

逆向 apk

解压apk得到 classes.dex 文件

需要用到的是dex2jar包里面的三个文件(当前是在windows环境下,Mac环境用对应的.sh文件):

d2j_invoke.bat
d2j-dex2jar.bat
lib
将这三个文件复制到一个空的文件夹内,将刚才.apk解压后的classes.dex文件也一起复制到这里

使用命令 d2j-dex2jar.bat classes.dex 得到 jar文件

使用 jd-gui.exe 打开jar文件看到代码

可以看到我们的代码

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
package com.example.androidrefirst;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
private Button btn_verify;

private EditText code;

private void init() {
this.code = (EditText)findViewById(2131230782);
this.btn_verify = (Button)findViewById(2131230755);
this.btn_verify.setOnClickListener(new View.OnClickListener() {
public void onClick(View param1View) {
if (MainActivity.this.code.getText().toString().trim().equals("2019")) {
Intent intent = new Intent((Context)MainActivity.this, SuccessActivity.class);
MainActivity.this.startActivity(intent);
} else {
Toast.makeText((Context)MainActivity.this, ", 1).show();
}
}
});
}

protected void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
setContentView(2131427356);
init();
}
}

对照着自己的手写的代码,已经差不离十了,对于想要代码思路的我们来说,到这里已经基本可以摸透他的逻辑。

反编译res资源部分

apktool下载后会有两个文件,一个.jar(例如apktool_2.3.3.jar 需要把名字改成apktool.jar) 一个apktool.bat 。

apktool d

同刚才一样在cmd命令下进入刚才文件夹(同样可以新建一个),连同我们刚才那个后缀为apk的安装包一起放入,输入如下命令

1
apktool d app-release.apk  # 此处app-release为apk名称

得到一个新的app-release(对应apk名称)文件夹

这个app-release文件夹下会得到若干文件,主要内容介绍如下:

  • AndroidManifest.xml:描述文件
  • res:资源文件
  • smail:反编译出来的所有代码,语法与java不同,类似汇编,是Android虚拟机所使用的寄存器语言
    到此我们想要的都有了。下一步就是实现我们想法的时候了。

修改原代码逻辑

我们只需要修改if后面的判断条件,设置为否即可if (!MainActivity.this.code.…),这样就成功绕过了条件约束。

接下来还有一部很重要那就是修改smali文件,找到MainActivity$1.smali这个文件用代码查看工具打开

找到这个if-eqz 修改成if-nez (nez对应为非,符号“!”),到这里要修改的部分都成功了,最后一步要做的就是重新打包了。当然对smali语法感兴趣的可以一起探讨学习。

重新打包

在apktool文件夹路径的cmd下输入:

1
2
apktool b [文件夹] -o test2.apk   #(test2为新apk名称,[文件夹]为对应的有修改需要打包的文件夹)
#例如:我当前就可以这样写 apktool b [E:\tools\apktool\app-release] -o test2.apk

至此,我们的目标apk文件已经生成,当然如果你想装到你自己手机上还需要重新签名一下。

重新签名

首先我们需要一个用于签名的.keystore文件,生成命令如下(这里我们假设生成的是demo.keystore)。

1
2
keytool -genkey -alias demo.keystore -keyalg RSA -validity 40000 -keystore demo.keystore
# (cmd到apktool文件夹下跟待签名的apk放同个文件夹内便于操作)

这里我们利用Java JDK提供的一个jarsigner进行签名,在刚才的cmd下继续操作,输入:

1
jarsigner -verbose -keystore demo.keystore test2.apk demo.keystore

以上。我们目的apk已经可以投入使用,如果需要更快更好的体验还需要进行一次字节对齐的操作(后续分析)。

参考:

https://blog.csdn.net/micaaa/article/details/82426710

什么是identityServer4

IdentityServer4是一个用于ASP.Net Core的OpenID Connect和OAuth 2.0框架

通过使用 IdentityServer4 你可以

认证服务

可以为你的应用(如网站、本地应用、移动端、服务)做集中式的登录逻辑和工作流控制。IdentityServer是完全实现了OpenID Connect协议标准。

单点登录登出(SSO)

在各种类型的应用上实现单点登录登出。

API访问控制

为各种各样的客户端颁发access token令牌,如服务与服务之间的通讯、网站应用、SPAS和本地应用或者移动应用。

联合网关

支持来自Azure Active Directory, Google, Facebook这些知名应用的身份认证,可以不必关心连接到这些应用的细节就可以保护你的应用

接下来我们用几个例子来快速入门IdentifyServer4

使用客户端授权模式 保护你的api资源

我们先来一个基础的应用场景,保护我们的Api资源

在这个场景中我们定义一个IdentityServer服务,一个API的客户端,一个要访问这个api的客户端。

验证是否成功是从IdentityServer获取一个访问令牌,然后用这个令牌来获得Api的访问权限。

IdentityServer

我们将要保护的资源定义在 Scopes 中,这是我们命名是 api1

1
2
3
4
5
6
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource> {
new ApiResource("api1", "我的 API")
};
}

定义能够访问api的客户端。
我们使用客户端密码(Client Secret)来认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",

// 没有交互性用户,使用 clientid/secret 实现认证。
AllowedGrantTypes = GrantTypes.ClientCredentials,

// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 客户端有权访问的范围(Scopes)
AllowedScopes = { "api1" }
}
};
}

Startup.cs中配置上IdentityServer

为了让 IdentityServer 使用你的 Scopes 和 客户端 定义,你需要向 ConfigureServices 方法中添加一些代码。你可以使用便捷的扩展方法来实现 —— 它们在幕后会添加相关的存储和数据到 DI 系统中:

1
2
3
4
5
6
7
8
9
10
public void ConfigureServices(IServiceCollection services)
{

services.AddControllers();
// 使用内存存储,密钥,客户端和资源来配置身份服务器。
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients());

}

这个时候访问 http://localhost:5000/.well-known/openid-configuration 可以看到 发现文档

API

新建一个站点并添加控制器

1
2
3
4
5
6
7
8
9
10
[Route("identity")]
[Authorize()]
public class IdentityController : ControllerBase
{
[HttpGet()]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}

将 IdentityServer4.AccessTokenValidation NuGet 程序包添加到你的 API 项目

然后添加中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});


}
1
2
3
4
5
6
7
8
9
10
11
12
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme
, jwtOptions =>
{
jwtOptions.RequireHttpsMetadata = false;
jwtOptions.Authority = "http://localhost:5000";
jwtOptions.ApiName = "api1";
});
}

将站点启动地址设置为 http://localhost:5001

这个时候,启动访问 http://localhost:5001/identity 可以看到401错误信息,说明访问还没有授权。

创建客户端

我们来编写一个客户端来请求访问令牌,然后使用这个令牌来访问 API。为此你需要为你的解决方案添加一个控制台应用程序。

IdentityServer 上的令牌端点实现了 OAuth 2.0 协议,你应该使用合法的 HTTP 来访问它。然而,我们有一个叫做 IdentityModel 的客户端库,它将协议交互封装到了一个易于使用的 API 里面。

添加 IdentityModel NuGet 程序包到你的客户端项目中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static async System.Threading.Tasks.Task Main(string[] args)
{


var client = new HttpClient();
var tokenClient = new TokenClient(client, new TokenClientOptions
{
Address = "http://localhost:5000/connect/token",
ClientId = "client",
ClientSecret = "secret"
});
var response = await tokenClient.RequestClientCredentialsTokenAsync("api1");

Console.WriteLine(response.AccessToken);
client.SetBearerToken(response.AccessToken);
string resStr = await client.GetStringAsync("http://localhost:5001/identity");
Console.WriteLine(resStr);
Console.ReadLine();
}

我们同时启动三个项目,可以看到 http://localhost:5001/identity 返回的结果内容被正常的打印在控制台。
默认情况下访问令牌将包含 scope 身份信息,生命周期(nbf 和 exp),客户端 ID(client_id) 和 发行者名称(iss)

使用密码保护API资源

我们现在使用 客户端发送用户名和密码到令牌服务并获得一个表示该用户的访问令牌 access_token

添加用户

为了方便演示 我们直接使用IdentityServer中的TestUser 类型表示一个测试用户及其身份信息。让我们向配置类(如果你有严格按照顺序进行演练,那么配置类应该在 QuickstartIdentityServer 项目的 Config.cs 文件中)中添加以下代码以创建一对用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static List<TestUser> GetUsers()
{
return new List<TestUser>()
{
new TestUser
{
SubjectId="1",
Username="爱丽丝",
Password="password"
},
new TestUser
{
SubjectId="2",
Username="博德",
Password="password"
}
};
}

然后将测试用户注册到 IdentityServer

1
2
3
4
5
6
7
8
9
10
11
12
public void ConfigureServices(IServiceCollection services)
{

services.AddControllers();
// 使用内存存储,密钥,客户端和资源来配置身份服务器。
services.AddIdentityServer().AddDeveloperSigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());

}

AddTestUsers 扩展方法在背后做了以下几件事:

  • 为资源所有者密码授权添加支持
  • 添加对用户相关服务的支持,这服务通常为登录 UI 所使用(我们将在下一个快速入门中用到登录 UI)
  • 为基于测试用户的身份信息服务添加支持(你将在下一个快速入门中学习更多与之相关的东西)

为资源所有者密码授权添加一个客户端定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new Client
{
ClientId = "ro.client",

// 没有交互性用户,使用 clientid/secret 实现认证。
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

// 用于认证的密码
ClientSecrets =
{
new Secret("secret".Sha256())
},
// 客户端有权访问的范围(Scopes)
AllowedScopes = { "api1" }
}

使用密码授权请求一个令牌

客户端看起来跟之前 客户端凭证授权 的客户端是相似的。主要差别在于现在的客户端将会以某种方式收集用户密码,然后在令牌请求期间发送到令牌服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using (var client = new HttpClient())
{
var tokenClient = new TokenClient(client, new TokenClientOptions
{
Address = "http://localhost:5000/connect/token",
ClientId = "ro.client",
ClientSecret = "secret"
});
var response = await tokenClient.RequestPasswordTokenAsync("爱丽丝", "password", "api1");

Console.WriteLine(response.AccessToken);
client.SetBearerToken(response.AccessToken);
string resStr = await client.GetStringAsync("http://localhost:5001/identity");
Console.WriteLine(resStr);
Console.ReadLine();
}

当你发送令牌到身份 API 端点的时候,你会发现与客户端凭证授权
相比,资源所有者密码授权有一个很小但很重要的区别。访问令牌现在将包含一个 sub 信息,该信息是用户的唯一标识。sub 信息可以在调用 API 后通过检查内容变量来被查看,并且也将被控制台应用程序显示到屏幕上。

sub 信息的存在(或缺失)使得 API 能够区分代表客户端的调用和代表用户的调用

基于 OpenID Connect 的用户认证

OpenID Connect 1.0是OAuth 2.0协议之上的一个简单的身份层。 它允许客户端基于授权服务器执行的身份验证来验证最终用户的身份,以及以可互操作和类似REST的方式获取关于最终用户的基本配置文件信息。 OpenID Connect允许所有类型的客户端(包括基于Web的移动和JavaScript客户端)请求和接收关于认证会话和最终用户的信息。 规范套件是可扩展的,允许参与者使用可选功能,例如身份数据的加密,OpenID提供商的发现和会话管理

OpenID Connect 在OAuth2上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。我们都知道OAuth2是一个授权协议,它无法提供完善的身份认证功能,OpenID Connect 使用OAuth2的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且可以适用于各种类型的客户端(比如服务端应用,移动APP,JS应用,winform应用),且完全兼容OAuth2,也就是说你搭建了一个OpenID Connect 的服务后,也可以当作一个OAuth2的服务来用。

csharp-dotnet-core-identityServer4-2020419134350

新建一个 MVC的Ids

IdentityServer提供了 可以快速创建UI的脚本 https://github.com/IdentityServer/IdentityServer4.Quickstart.UI

然后在start.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
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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Quickstart.UI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace IdentityServer4DotNet.ClientAuth.Ids4s
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

// configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});

// configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});

var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddTestUsers(TestUsers.Users);

// in-memory, code config
builder.AddInMemoryIdentityResources(Config.Ids);
builder.AddInMemoryApiResources(Config.Apis);
builder.AddInMemoryClients(Config.Clients);

// or in-memory, json config
//builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
//builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
//builder.AddInMemoryClients(Configuration.GetSection("clients"));

// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();

}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();

app.UseRouting();

app.UseIdentityServer();
app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

注意现在的中的Clients添加一个client

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
   // mvc client, authorization code
new Client
{
ClientId = "mvc client",
ClientName = "ASP.NET Core MVC Client",

AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
ClientSecrets = { new Secret("mvc secret".Sha256()) },

RedirectUris = { "http://localhost:5002/signin-oidc" },

FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },

AlwaysIncludeUserClaimsInIdToken = true,

AllowOfflineAccess = true, // offline_access
AccessTokenLifetime = 60, // 60 seconds

AllowedScopes =
{
"api1",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Address,
IdentityServerConstants.StandardScopes.Phone,
IdentityServerConstants.StandardScopes.Profile
}
},

新建一个 MVC 客户端

接下来你将向解决方案添加一个 MVC 应用程序,可以使用 ASP.NET Core “Web 应用程序” 模板来实现。 将应用程序配置为使用 5002 端口(可以查看概览部分以了解如何配置)。

为了能向 MVC 应用程序添加 OpenID Connect 认证支持,请添加如下 NuGet 程序包:

Microsoft.AspNetCore.Authentication.Cookies

Microsoft.AspNetCore.Authentication.OpenIdConnect

mvc的client的start.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
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
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Threading.Tasks;
using IdentityModel;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace IdentityServer4DotNet.ClientAuth.MVCClient
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});

services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc client";
options.ClientSecret = "mvc secret";
options.SaveTokens = true;
options.ResponseType = "code";

options.Scope.Clear();
options.Scope.Add("api1");
options.Scope.Add(OidcConstants.StandardScopes.OpenId);
options.Scope.Add(OidcConstants.StandardScopes.Profile);
options.Scope.Add(OidcConstants.StandardScopes.Email);
options.Scope.Add(OidcConstants.StandardScopes.Phone);
options.Scope.Add(OidcConstants.StandardScopes.Address);
options.Scope.Add(OidcConstants.StandardScopes.OfflineAccess);
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}

app.UseStaticFiles();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

现在我们多启动项目,看到mvcclient自动跳转到 ids网站,输入testuser的账号密码之后看到如下界面

csharp-dotnet-core-identityServer4-2020579280

Yes,Allow

然后我们看到看到跳转到了

csharp-dotnet-core-identityServer4-20205792942

到此我们的 基本 OpenID Connect 的用户认证完成

尝试一个外部认证

接下来我们以Google身份认证为例,添加对Google的支持

首先我们需要在Google的开发人员控制台申请 OAuth 2.0 客户端 ID
回调地址填写 http://localhost:5000/signin-google

然后在ids中添加nuget引用 Microsoft.AspNetCore.Authentcation.Google 到项目中
然后我们在中间件中添加引用

1
2
3
4
5
6
7
8
9
10
11
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

// register your IdentityServer with Google at https://console.developers.google.com
// enable the Google+ API
// set the redirect URI to http://localhost:5000/signin-google
options.ClientId = "629456249245-4vf1gvlltllq592coo46hrc8ofqa8cg6.apps.googleusercontent.com";
options.ClientSecret = "XhUa6NODSIGOtSPBuGrTeeeW";
});

注意:在 ASP.NET Core Identity 中使用外部认证的时候,SignInScheme 必须设置为 Identity.External,而不是 IdentityServerConstants.ExternalCookieAuthenticationScheme。

再次运行项目可以看到登录页面是多了个Google按钮。

通过认证后,你可以看到现在的身份信息是由 Google 数据提供的了。

更多实验

你可以使用 QQ认证,微信认证等来实验 IdentityServer4的神奇之处。

当然 你也可以在 https://github.com/IdentityServer/IdentityServer4/tree/master/samples 中看到更多开源的例子。

什么是Quartz

 Quartz.Net是根据Java的Quartz用C#改写而来,源码在https://github.com/quartznet/quartznet 。主要作用是做一些周期性的工作,或者定时工作。比如每天凌晨2点对前一天的数据统计。

Quartz 核心概念

我们需要明白 Quartz 的几个核心概念,这样理解起 Quartz 的原理就会变得简单了。

  1. Job 表示一个工作,要执行的具体内容。

  2. JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。

  3. Trigger 代表一个调度参数的配置,什么时候去调。
    触发器常用的有两种:SimpleTrigger触发器和CronTrigger触发器。

    • SimpleTrigger:能是实现简单业务,如每隔几分钟,几小时触发执行,并限制执行次数。
    1
    2
    3
    4
    5
    var trigger = TriggerBuilder.Create()
    .WithSimpleSchedule(x => x.WithIntervalInSeconds(2).WithRepeatCount(5))//间隔2秒 执行6次
    .UsingJobData("key1", 321)
    .WithIdentity("trigger", "group")
    .Build();
    • CronTrigger:Cron表达式包含7个字段,秒 分 时 月内日期 月 周内日期 年(可选)。
      • csharp-dotnet-core-Quartz-202041712646

      • csharp-dotnet-core-Quartz-20204171272

      • 举例

        1
        2
        3
        4
        5
        6
        var trigger = TriggerBuilder.Create()
        .WithCronSchedule("0 0 0 1 1 ?")// 每年元旦1月1日 0 点触发
        .UsingJobData("key1", 321)
        .UsingJobData("key2", "trigger-key2")
        .WithIdentity("trigger4", "group14")
        .Build();
  4. Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了。

代码案例

以WebApi项目举例,用VS脚手架功能新建WebApi项目。

注册ISchedulerFactory的实例

1
2
3
4
5
6
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();//注册ISchedulerFactory的实例。
}

Controller中

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Quartz;

namespace QuartzNet.Web.Controllers
{
[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
private readonly ISchedulerFactory _schedulerFactory;
private IScheduler _scheduler;
public ValuesController(ISchedulerFactory schedulerFactory)
{
this._schedulerFactory = schedulerFactory;
}
[HttpGet]
public async Task<string[]> Get()
{
//1、通过调度工厂获得调度器
_scheduler = await _schedulerFactory.GetScheduler();
//2、开启调度器
await _scheduler.Start();
//3、创建一个触发器
var trigger = TriggerBuilder.Create()
.WithSimpleSchedule(x => x.WithIntervalInSeconds(2).RepeatForever())//每两秒执行一次
.Build();
//4、创建任务
var jobDetail = JobBuilder.Create<MyJob>()
.WithIdentity("job", "group")
.Build();
//5、将触发器和任务器绑定到调度器中
await _scheduler.ScheduleJob(jobDetail, trigger);
return await Task.FromResult(new string[] { "value1", "value2" });
}
}
}

MyJob

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
using Quartz;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace QuartzNet.Web
{
public class MyJob : IJob//创建IJob的实现类,并实现Excute方法。
{
public Task Execute(IJobExecutionContext context)
{
return Task.Run(() =>
{
using (StreamWriter sw = new StreamWriter(@"error.log", true, Encoding.UTF8))
{
sw.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss"));
}
});
}
}
}

可以看到在error.log中输出结果

1
2
3
4
5
6
2020-04-17 11-55-45
2020-04-17 11-55-47
2020-04-17 11-55-49
2020-04-17 11-55-51
2020-04-17 11-55-53
2020-04-17 11-55-55

你可以给Job增加参数

  1. 在Trigger中添加参数值
1
2
3
4
5
6
var trigger3 = TriggerBuilder.Create()
.WithSimpleSchedule(x =>x.WithIntervalInSeconds(2).RepeatForever())//间隔2秒 一直执行
.UsingJobData("key1", 321) //通过在Trigger中添加参数值
.UsingJobData("key2", "123")
.WithIdentity("trigger2", "group1")
.Build();
  1. 在Job中添加参数值
1
2
3
4
5
IJobDetail job = JobBuilder.Create<MyJob>()
.UsingJobData("key1", 123)//通过Job添加参数值
.UsingJobData("key2", "123")
.WithIdentity("job1", "group1")
.Build();

在job中获取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
var jobData = context.JobDetail.JobDataMap;//获取Job中的参数

var triggerData = context.Trigger.JobDataMap;//获取Trigger中的参数

var data = context.MergedJobDataMap;//获取Job和Trigger中合并的参数

var value1= jobData.GetInt("key1");
var value2= jobData.GetString("key2");

var dateString = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");return Task.Run(() =>
{
using (StreamWriter sw = new StreamWriter(@"error.log", true, Encoding.UTF8))
{
sw.WriteLine(dateString);
}
});
}
}

当Job中的参数和Trigger中的参数名称一样时,用 context.MergedJobDataMap获取参数时,Trigger中的值会覆盖Job中的值。

  1. 上面那种情况只能适应那种,参数值不变的情况。假如有这种情况,这次的参数值是上一次执行后计算的值,就不能使用上面方法了。如 每两秒实现累加一操作,现在初始值是0,如果按照上面那种获取值的操作,一直都是0+1,返回值一直都是1。为了满足这个情况,只需要加一个特性[PersistJobDataAfterExecution]。
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
[PersistJobDataAfterExecution]//更新JobDetail的JobDataMap的存储副本,以便下一次执行这个任务接收更新的值而不是原始存储的值
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
var jobData = context.JobDetail.JobDataMap;
var triggerData = context.Trigger.JobDataMap;
var data = context.MergedJobDataMap;

var value1 = jobData.GetInt("key1");
var value2 = jobData.GetString("key2");
var value3 = data.GetString("key2");

var dateString = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");
Random random = new Random();

jobData["key1"] = random.Next(1, 20);//这里面给key赋值,下次再进来执行的时候,获取的值为更新的值,而不是原始值
jobData["key2"] = dateString;

return Task.Run(() =>
{
using (StreamWriter sw = new StreamWriter(@"C:\Users\Administrator\Desktop\error.log", true, Encoding.UTF8))
{
sw.WriteLine($"{dateString} value1:{value1} value2:{value2}");
}
//context.Scheduler.DeleteJob(context.JobDetail.Key);
//context.Scheduler.Shutdown();
});
}
}

上面的例子是在api中使用Quartz

你也可以在 startup.cs中注册代码

1
2
3
4
5
6
7
8
9
10
11
12
13
public void ConfigureServices(IServiceCollection services)
{
services.AddQuartz(typeof(QuartzJob));

//........
}


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
QuartzService.StartJobs<QuartzJob>();
//........
}

或者在 HostedService中注册代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HostedService : IHostedService
{
private JobManager _jobManager;
public HostedService( JobManager jobManager)
{
_jobManager = jobManager;
}

public Task StartAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(async () =>
{
await _jobManager.Start();
}, cancellationToken);
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.Factory.StartNew(async () =>
{
await _jobManager.Stop();
}, cancellationToken);
}
}

参考:

https://www.cnblogs.com/MicroHeart/p/9402731.html
https://www.cnblogs.com/dangzhensheng/p/10496278.html

什么是Consul

Consul是一个服务网格(微服务间的 TCP/IP,负责服务之间的网络调用、限流、熔断和监控)解决方案,它是一个一个分布式的,高度可用的系统,而且开发使用都很简便。它提供了一个功能齐全的控制平面,主要特点是:服务发现、健康检查、键值存储、安全服务通信、多数据中心。

Consul是用Go开发的分布式服务协调管理的工具,它提供了服务发现,健康检查,Key/Value存储等功能,并且支持跨数据中心的功能。consul提供的一些关键特性:

  • service discovery:consul通过DNS或者HTTP接口使服务注册和服务发现变的很容易,一些外部服务,例如saas提供的也可以一样注册。
  • health checking:健康检测使consul可以快速的告警在集群中的操作。和服务发现的集成,可以防止服务转发到故障的服务上面。
  • key/value storage:一个用来存储动态配置的系统。提供简单的HTTP接口,可以在任何地方操作。
  • multi-datacenter:无需复杂的配置,即可支持任意数量的区域。

Consul的应用

服务注册与发现

其实服务注册与发现的原理很简单。

当我们在本机运行Consul时,他会自动监听8500端口;然后我们通过一个开源类库(这个开源类库可以在nuget上检索到,文章下面会介绍),调用其下不同的方法来向这个Consul进程发送TCP消息,来注册服务或者发现服务。

Consul进程在接收到注册消息时,就把注册的服务信息存储到本地磁盘或内存(因为我没有具体去调查Consul存储数据是否使用了数据库,但我们都知道数据库的数据也是保存在本地磁盘的,所以,它肯定是把数据存进磁盘或者内存中了)。

数据中心

Consul存储数据的地方,官方为其命名为数据中心,也就是上面说的保存我们注册的服务信息的本地磁盘或者内存。

Consul提供负载均衡的集群

Consul的集群也很好理解,在我们成功启动Consul以后,它除了监听8500端口以外,它还监听了一个8031端口。

这个8031端口就是用于Consul集群相互通信的。

我们都知道集群是要两台以上的电脑的,所以,我们就必须找到两台或以上的电脑安装Consul中间件。

然后,使用Consul的命令行,将两台电脑连接到一起,这样集群就形成了。

在集群内每台电脑上安装的Consul中间件,我们统称为服务器代理(Agent);当集群启动后,会在多个代理服务器之间选举出一个Leader。

选举Leader自然就是服务器代理之间的通信了,也就是通过上面提到的8031端口通信的。

选举了Leader,服务器代理就可以将自身的负载信息发送给Leader了,这样客户端调用Consul检索服务数据时,就可以去性能最优的那台机器上获取信息了。(注:这个就是举例说明,并非Consul的负载均衡的真实处理模式)

代码例子

服务注册与发现

我们新建一个 api Service

在 startup.cs 中的 Configure 增加如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
string ip = Configuration["ip"];//部署到不同服务器的时候不能写成127.0.0.1或者0.0.0.0,因为这是让服务消费者调用的地址
int port = int.Parse(Configuration["port"]);//获取服务端口
var client = new ConsulClient(ConfigurationOverview); //回调获取
var result = client.Agent.ServiceRegister(new AgentServiceRegistration()
{
ID = "ServerNameFirst" + Guid.NewGuid(),//服务编号保证不重复
Name = "MsgServer",//服务的名称
Address = ip,//服务ip地址
Port = port,//服务端口
Check = new AgentServiceCheck //健康检查
{
DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(5),//服务启动多久后反注册
Interval = TimeSpan.FromSeconds(10),//健康检查时间间隔,或者称为心跳间隔(定时检查服务是否健康)
HTTP = $"http://{ip}:{port}/api/Health",//健康检查地址
Timeout = TimeSpan.FromSeconds(5)//服务的注册时间
}
});
1
2
3
4
5
6
7
private static void ConfigurationOverview(ConsulClientConfiguration obj)
{
//consul的地址
obj.Address = new Uri("http://192.168.1.37:8500");
//数据中心命名
obj.Datacenter = "dc1";
}

客户端调用

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
static List<string> Urls = new List<string>();

static void Main(string[] args)
{
Console.WriteLine("开始输出当前所有服务地址");
Catalog_Nodes().GetAwaiter().GetResult();
//Console.WriteLine(HelloConsul().GetAwaiter().GetResult());
Console.WriteLine("开始随机请求一个地址服务地址");
int index = new Random().Next(Urls.Count);
string url = Urls[index];

Console.WriteLine("请求的随机地址:" + url);
HttpClient client = new HttpClient();
var result = client.GetAsync(url).Result;
//string result = HttpClientHelpClass.PostResponse(url, param, out string statusCode);
Console.WriteLine("返回状态:" + result.StatusCode);
Console.WriteLine("返回结果:" + result.Content);
Console.ReadLine();
}
public static async Task Catalog_Nodes()
{
var client = new ConsulClient(ConfigurationOverview);
var nodeList = await client.Agent.Services();
var url = nodeList.Response.Values;

foreach (var item in url)
{
string Address = item.Address;
int port = item.Port;
string name = item.Service;
Console.WriteLine($"地址:{Address}:{port},name:{name}");
Urls.Add($"http://{Address}:{port}/weatherforecast");
}
}

private static void ConfigurationOverview(ConsulClientConfiguration obj)
{
//consul的地址
obj.Address = new Uri("http://192.168.1.37:8500");
//数据中心命名
obj.Datacenter = "dc1";
}

运行即可看到 我们已经成功调用了Consul,也成功的获取到了服务信息。

参考:
https://www.cnblogs.com/shanyou/archive/2015/08/09/4714838.html
https://www.cnblogs.com/kiba/p/11941731.html
https://www.cnblogs.com/yanbigfeg/p/9199590.html

在实际的云服务生产环境中,因为可以按量付费,经常会遇到硬盘不够了需要扩容的情况。

  1. 在云平台对对应的硬盘扩容操作

  2. 进入主机系统内,查看服务器的磁盘信息
    fidk -l 之后 找到 硬盘/dev/sda

  3. 使用分区工具parted分区
    parted /dev/sda
    然后使用: print free 查看 Free Space

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Model: Msft Virtual Disk (scsi)
    Disk /dev/sda: 322GB
    Sector size (logical/physical): 512B/4096B
    Partition Table: msdos
    Disk Flags:

    Number Start End Size Type File system Flags
    32.3kB 1049kB 1016kB Free Space
    1 1049kB 1075MB 1074MB primary xfs boot
    2 1075MB 136GB 135GB primary lvm
    136GB 322GB 186GB Free Space
  4. 使用mkpart开始分区
    mkpart test 186GB 100% 然后看到 /dev/sda3

  5. 使用fdisk调理分区为LVM
    fdisk /dev/sda 然后依次t(change a partition's system id) 3 l(type L to list all codes) 8e(8e Linux LVM) w

  6. 使用pvcreate把/dev/sda3搞成物理卷
    pvcreate /dev/sda3

  7. 划分到 卷组
    vgextend centos /dev/sda3

  8. 划分到逻辑卷
    lvextend -l +100%FREE /dev/mapper/centos-home

  9. 保存此次操作
    xfs_growfs /dev/mapper/centos-home

如果你的pv在create的时候没有使用分区是话可以使用 pvresize 调整一个卷组中的物理卷的大小

1
pvresize /dev/sdc

然后添加到 lv中

lvextend -l +100%FREE /dev/mapper/testvg-testlv

最后 resize 一下

resize2fs /dev/mapper/testvg-testlv

说明:当有在操作系统时发现磁盘空间不足时,可以通过增加磁盘空间大小来满足,但是以哪种方式来增加可能有不同方法,现在针对操作系统在安装采用LVM方式来动态调整磁盘空间大小,

知识点解释:

 LVM是逻辑盘卷管理(Logical VolumeManager)的简称,它是Linux环境下对磁盘分区进行管理的一种机制,LVM是建立在硬盘和分区之上的一个逻辑层,来提高磁盘分区管理的灵活性。通过LVM系统管理员可以轻松管理磁盘分区,如:将若干个磁盘分区连接为一个整块的卷组(volumegroup),形成一个存储池。管理员可以在卷组上随意创建逻辑卷组(logicalvolumes),并进一步在逻辑卷组上创建文件系统。管理员通过LVM可以方便的调整存储卷组的大小,并且可以对磁盘存储按照组的方式进行命名、管理和分配

Linux的LVM非常强大,可以在生产运行系统上面直接在线扩展硬盘分区,可以把分区umount以后收缩分区大小,还可以在系统运行过程中把一个分区从一块硬盘搬到另一块硬盘上面去等等,简直就像变魔术,而且这一切都可以在一个繁忙运行的系统上面直接操作,不会对你的系统运行产生任何影响,很安全。

LVM使用有局限性。虽然能很方便的扩容和缩容磁盘的空间(扩容磁盘大小,文件不丢失),但是一旦出现问题,数据丢失,想要恢复数据就有点困难!

三步:

  • 创建一个物理分区-搞成物理卷
  • 制作成一个卷组
  • 划分成逻辑卷
    linux-centos7-LVM-2020415144543
  1. 如果在安装系统选择是以LVM方式安装的话,可以通过已经安装好后系统磁盘查询
1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# df -h
Filesystem Size Used Avail Use% Mounted on
devtmpfs 477M 0 477M 0% /dev
tmpfs 488M 0 488M 0% /dev/shm
tmpfs 488M 6.6M 481M 2% /run
tmpfs 488M 0 488M 0% /sys/fs/cgroup
/dev/mapper/centos-root 50G 1.9G 49G 4% /
/dev/sda1 1014M 162M 853M 16% /boot
/dev/mapper/centos-home 74G 33M 74G 1% /home
/dev/sdb1 2.0T 81M 1.9T 1% /data
tmpfs 98M 0 98M 0% /run/user/0

以上说明 centos-root 和centos-home 是通过LVM方式来配置的
2. 使用df -T -h命令查看操作系统的文件类型

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# df -T -h
Filesystem Type Size Used Avail Use% Mounted on
devtmpfs devtmpfs 477M 0 477M 0% /dev
tmpfs tmpfs 488M 0 488M 0% /dev/shm
tmpfs tmpfs 488M 6.6M 481M 2% /run
tmpfs tmpfs 488M 0 488M 0% /sys/fs/cgroup
/dev/mapper/centos-root xfs 50G 1.9G 49G 4% /
/dev/sda1 xfs 1014M 162M 853M 16% /boot
/dev/mapper/centos-home xfs 74G 33M 74G 1% /home
/dev/sdb1 ext4 2.0T 81M 1.9T 1% /data
tmpfs tmpfs 98M 0 98M 0% /run/user/0

说明:通过查询发现操作系统的文件格式是:xfs

  1. vgdisplay:查看卷组名称及卷组使用情况
    VG Size –总共的空间大小

  2. lvdisplay:查看当前逻辑卷的空间状态

  3. 现在感觉 /dev/centos/home的73.99G不够用,想扩容增加10G新插入一块10G的硬盘;

    • fdisk -l 查看 得知新硬盘是 /dev/sdc
    1
    2
    3
    4
    Disk /dev/sdc: 10.7 GB, 10737418240 bytes, 20971520 sectors
    Units = sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 4096 bytes
    I/O size (minimum/optimal): 4096 bytes / 4096 bytes
    • 格式化/dev/sdc

    fdisk /dev/sdc 然后依次 n p 回车 回车 w

    • 将 /dev/sdc配置为LVM格式

    fdisk /dev/sdc 然后依次 t(change a partition’s system id) l(type L to list all codes) 8e(8e Linux LVM) w

    • 查询配置为LVM格式后结果如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Disk /dev/sdc: 10.7 GB, 10737418240 bytes, 20971520 sectors
    Units = sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 4096 bytes
    I/O size (minimum/optimal): 4096 bytes / 4096 bytes
    Disk label type: dos
    Disk identifier: 0xd87afe50

    Device Boot Start End Blocks Id System
    /dev/sdc1 2048 20971519 10484736 8e Linux LVM
    • 将新加的分区 /dev/sdc1 创建为物理卷

    root@localhost ~]# pvcreate /dev/sdc1
    Physical volume “/dev/sdc1” successfully created.

    • .给卷组‘centos ’扩容,将物理卷 /dev/sdc1 扩展至‘centos ’卷组

    vgextend centos /dev/sdc1

    • 此时卷组‘centos ’有10G空余空间,及 /dev/sdc1,将其全部扩展至 /home

    lvextend -l +100%FREE /dev/mapper/centos-home

    • 用xfs_growfs 命令把文件写入系统中,使扩容生效

    xfs_growfs /dev/mapper/centos-home

    • 关于把文件系统缩小!

    如果文件系统是xfs格式的,fs文件系统只支持增大分区空间的情况,不支持减小的情况(谨记谨记谨记)

    硬要减小的话,只能在减小后将逻辑分区重新通过mkfs.xfs命令重新格式化才能挂载上,这样的话这个逻辑分区上原来的数据就丢失了。记得备份原来数据。

    也就是说当执行过 xfs_growfs 之后 是无法在不丢失数据是情况下 执行 lvreduce -L -10G /dev/mapper/centos-home 缩小分区的。

本次扩容指令汇总:

  1. 创建分区                                                #fdisk /dev/sdb

  2. 创建物理卷                                             #pvcreat /dev/sdb1

  3. 查看卷组名称及使用情况                         #vgdisplay

  4. 将物理卷扩展到卷组                               #vgextend cl /dev/sdb1   (此处‘cl’是卷组名称)

  5. 将卷组中空闲空间扩展到 /home           #lvextend -l +100%FREE /dev/mapper/cl-home

  6. 刷新文件系统是扩容生效 xfs_growfs /dev/mapper/centos-root

  7. 若是ext4文件格式使用扩容生效                     #resize2fs /dev/mapper/centos-home

解决群晖高温自动关机的问题

群晖总是在超过60度的时候shutdown 可以参考使用 这篇帖子 来解决

具体步骤

  1. 使用ssh到群晖上 打开 /usr/syno/etc.defaults/scemd.xml 文件

  2. 查找文本

    1
    2
    找到如下内容的文本 关键字`temperature` 和 `SHUTDOWN` 
    <disk_temperature fan_speed="99%00hz" action="SHUTDOWN">61</disk_temperature>
  3. 把其中的 61 或者 58 的数字 修改成90

  4. 保存文件 然后重启就好了

FastDFS简介

fastDFS 是以C语言开发的一项开源轻量级分布式文件系统,他对文件进行管理,主要功能有:文件存储,文件同步,文件访问(文件上传/下载),特别适合以文件为载体的在线服务,如图片网站,视频网站等

分布式文件系统:

基于客户端/服务器的文件存储系统对等特性允许一些系统扮演客户端和服务器的双重角色,可供多个用户访问的服务器,比如,用户可以“发表”一个允许其他客户机访问的目录,一旦被访问,这个目录对客户机来说就像使用本地驱动器一样

FastDFS由跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)构成。

Tracker server 追踪服务器

追踪服务器负责接收客户端的请求,选择合适的组合storage server ,tracker server 与 storage server之间也会用心跳机制来检测对方是否活着。
Tracker需要管理的信息也都放在内存中,并且里面所有的Tracker都是对等的(每个节点地位相等),很容易扩展
客户端访问集群的时候会随机分配一个Tracker来和客户端交互。

Storage server 储存服务器

实际存储数据,分成若干个组(group),实际traker就是管理的storage中的组,而组内机器中则存储数据,group可以隔离不同应用的数据,不同的应用的数据放在不同group里面,

优点:
海量的存储:主从型分布式存储,存储空间方便拓展,
fastDFS对文件内容做hash处理,避免出现重复文件
然后fastDFS结合Nginx集成, 提供网站效率

linux-centos7-install-fastdfs-2020415143340

读写操作

写入数据

写操作的时候,storage会将他所挂载的所有数据存储目录的底下都创建2级子目录,每一级256个总共65536个,新写的文件会以hash的方式被路由到其中某个子目录下,然后将文件数据作为本地文件存储到该目录中。

linux-centos7-install-fastdfs-202041514373

下载文件

当客户端向Tracker发起下载请求时,并不会直接下载,而是先查询storage server(检测同步状态),返回storage server的ip和端口,
然后客户端会带着文件信息(组名,路径,文件名),去访问相关的storage,然后下载文件。

linux-centos7-install-fastdfs-2020415143642

源代码方式安装

首先下载源代码:

libfastcommon

fastdfs

fastdfs-nginx-module

nginx

1.把需要的安装包上传到服务器

mkdir 目录名

2.安装所需依赖

yum -y install zlib zlib-devel pcre pcre-devel gcc gcc-c++ openssl openssl-devel libevent libevent-devel perl unzip net-tools wget && yum install lrzsz -y

3.进入程序所在的目录

a.安装libfastcommon

1
2
3
4
5
6
7
8
9
unzip libfastcommon.zip
cd libfastcommon
./make.sh
./make.sh install
#如果没有报错,执行建立软件链接的命令:
ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so
ln -s /usr/lib64/libfastcommon.so /usr/lib/libfastcommon.so
ln -s /usr/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so
ln -s /usr/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so

b.安装FastDfs:

1
2
3
4
unzip fastdfs-5.11.zip
cd fastdfs-5.11
./make.sh
./make.sh install

查看目录:

ll /etc/fdfs

有3个模板文件,各复制一份重命名为对应的conf文件

1
2
3
cp client.conf.sample client.conf
cp storage.conf.sample storage.conf
cp tracker.conf.sample tracker.conf

c.安装tracker:

创建tarcker工作目录,根据服务器硬盘分布创建工作目录:

mkdir -p /lvdata/fastdfs/fastdfs_tracker

配置tracker

vim /etc/fdfs/tracker.conf

disabled=false #默认开启 
port=22122 #默认端口号 
base_path=/lvdata/fastdfs/fastdfs_tracker #刚刚创建的目录 
http.server_port=6666 #默认端口是8080

:wq!

保存修改后,启动tracker:
service fdfs_trackerd start
设置开机启动:
systemctl enable fdfs_trackerd

ll /lvdata/fastdfs/fastdfs_tracker,多了data,logs目录

查看tracker运行情况:
netstat -unltp|grep fdfs
可以看到在监听22122端口

d.安装storage

创建数据目录:

mkdir -p /lvdata/fastdfs/fastdfs_storage
mkdir -p /lvdata/fastdfs/fastdfs_storage_data

修改配置文件:

vim /etc/fdfs/storage.conf

1.disabled=false 
2.group_name=group1 #组名,根据实际情况修改 
3.port=23000  #设置storage的端口号,默认是23000,同一个组的storage端口号必须一致 
4.base_path=/lvdata/fastdfs/fastdfs_storage  #设置storage数据文件和日志目录 
5.store_path_count=1 #存储路径个数,需要和store_path个数匹配 
6.base_path0=/lvdata/fastdfs/fastdfs_storage_data #实际文件存储路径 注意检查
7.tracker_server=192.168.150.132:22122 #我CentOS7的ip地址 
8.http.server_port=8888 #设置 http 端口号
:wq!

修改后,保存退出。创建软链接:

ln -s /usr/bin/fdfs_storaged /usr/local/bin

启动storage:

service fdfs_storaged start

设置开机启动:

systemctl enable fdfs_storaged

查看服务运行情况:

netstat -unltp |grep fdfs

可以看到在监听23000端口。

确认下storage是否注册到了tracker:

/usr/bin/fdfs_monitor /etc/fdfs/storage.conf

成功后可以看到:

ip_addr = 192.168.150.132 (localhost.localdomain) ACTIVE 的字样

修改客户端配置文件:

vim /etc/fdfs/client.conf

base_path=/lvdata/fastdfs/fastdfs_tracker #tracker服务器文件路径
tracker_server=192.168.150.132:22122 #tracker服务器IP地址和端口号
http.tracker_server_port=6666 # tracker 服务器的 http端口号,必须和tracker的设置对应起来

:wq!

修改后,保存。

测试上传一张照片:

/usr/bin/fdfs_upload_file  /etc/fdfs/client.conf  /root/test.png

成功后,会返回图片的路径如:

group1/M00/00/00/wKiWhFrdeCeAC_vCAABqgowGIFg399.png

查看是否上传成功:

cd /usr/testdfsdev/fastdfs/fastdfs_storage_data/data
cd 00/00;ls
wKiWhFrdeCeAC_vCAABqgowGIFg399.png

表明上传成功。

data下有256个1级目录,每级目录下又有256个2级子目录,总共65536个文件,新写的文件会以hash的方式被路由到其中某个子目录下,然后将文件数据直接作为一个本地文件存储到该目录中。

在4.05版本的时候已经移除http支持,需要http支持,安装nginx及相关模块:
安装依赖包:

yum -y install pcre pcre-devel && yum -y install zlib zlib-devel && yum -y install openssl openssl-devel

安装nginx并添加fastdfs-nginx-module:

解压nginx和fastdfs-nginx-module:

tar xzvf nginx-1.12.0.tar.gz unzip fastdfs-nginx-module.zip vim /root/fastdfs-nginx-module/src/config,修改后的内容如下:

ngx_module_incs="/usr/include/fastdfs /usr/include/fastcommon/"
CORE_INCS="$CORE_INCS /usr/include/fastdfs /usr/include/fastcommon/"

:wq!


cd nginx-1.12.0
./configure --prefix=/usr/local/nginx --add-module=/data/fdfs.install/fastdfs-nginx-module-1.22/src/

ng
回车,如果没有错误信息,直接安装:

make && make install

修改nginx配置:

vim /usr/local/nginx/conf/nginx.conf
server {
        listen       9999;
        server_name  localhost;

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

        location /group1/M00 {
            root /lvdata/fastdfs/fastdfs_storage_data/data;
            ngx_fastdfs_module;
        }
:wq!

保存修改。

进入fastdfs安装时解压的目录下的conf,将http.conf,mime.types复制到/etc/fdfs目录下:

cp http.conf /etc/fdfs/
cp mime.types /etc/fdfs/

将fastdfs-nginx-module安装目录中src目录下的mod_fastdfs.conf也拷贝到/etc/fdfs目录下:

cp /lvdata/nginx/fastdfs-nginx-module/src/mod_fastdfs.conf /etc/fdfs/

vim /etc/fdfs/mod_fastdfs.conf
base_path=/lvdata/fastdfs/fastdfs_storage #保存日志目录
tracker_server=192.168.150.132:22122 #tracker服务器的IP地址以及端口号
storage_server_port=23000 #storage服务器的端口号
url_have_group_name = true #文件 url 中是否有 group 名
store_path0=/lvdata/fastdfs/fastdfs_storage_data #存储路径
group_count = 3 #设置组的个数,事实上这次只使用了group1

在文件的最后设置group:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[group1]
group_name=group1
storage_server_port=23000
store_path_count=1
store_path0=/lvdata/fastdfs/fastdfs_storage_data
store_path1=/lvdata/fastdfs/fastdfs_storage_data

# group settings for group #2
# since v1.14
# when support multi-group, uncomment following section as neccessary
[group2]
group_name=group2
storage_server_port=23000
store_path_count=1
store_path0=/lvdata/fastdfs/fastdfs_storage_data

[group3]
group_name=group3
storage_server_port=23000
store_path_count=1
store_path0=/lvdata/fastdfs/fastdfs_storage_data

创建M00至storage存储目录的符号链接:

ln  -s  /lvdata/fastdfs/fastdfs_storage_data/data/ /lvdata/fastdfs/fastdfs_storage_data/data/M00

启动nginx:

/usr/local/nginx/sbin/nginx

成功启动:

ngx_http_fastdfs_set pid=1231

使用刚才返回的图片路径,使用浏览器测试:

X.X.X.X:9999/group1/M00/00/00/wKiWhFrdeCeAC_vCAABqgowGIFg399.png

如果可以看到刚才的图片,说明nginx配置正常。

docker 方式安装

待补充

什么是RabbitMQ

 RabbitMQ是一个开源的,基于AMQP(Advanced Message Queuing Protocol)协议的完整,可复用的企业级消息队列(Message Queue 一种应用程序与应用程序之间的一种通信方法)系统,RabbitMQ可以实现点对点,发布订阅等消息处理模式

使用 RabbitMQ

RabbitMQ从信息接收者角度可以看做三种模式:

  1. 一对一(简单队列模式)
  2. 一对多(是Worker模式 此一对多并不是发布订阅,而是每条信息只有一个接收者)
  3. 发布订阅(包括发布订阅模式,路由模式和通配符模式,可以总结为都是使用只是交换机(Exchange)类型不一致)

代码示例

简单模式

  1. 首先,我们需要创建两个控制台项目.Send(发送者)和Receive(接收者),然后为两个项目安装RabbitMQ.Client驱动

install-package rabbitmq.client

  1. 然后在Send和Receive项目中编写我们的消息队列代码
  • 发送者代码
    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
    using RabbitMQ.Client;
    using System;
    using System.Text;

    namespace HelloWorldRabbitMQ.Send
    {
    class Program
    {
    static void Main(string[] args)
    {
    Console.WriteLine("Start");
    IConnectionFactory conFactory = new ConnectionFactory//创建连接工厂对象
    {
    HostName = "192.168.1.37",//IP地址
    Port = 5672,//端口号
    UserName = "sa",//用户账号
    Password = "Sa123456"//用户密码
    };
    using (IConnection con = conFactory.CreateConnection())// 创建连接对象
    {
    using (IModel channel = con.CreateModel())// 创建连接会话对象
    {
    String queryName = "queue1";
    channel.QueueDeclare(
    queue: queryName,
    durable: false,
    exclusive: false,
    autoDelete: false,
    arguments: null
    );
    while (true)
    {
    Console.WriteLine("消息内容");
    string message = Console.ReadLine();
    // 消息内容
    byte[] body = Encoding.UTF8.GetBytes(message);
    // 发送消息
    channel.BasicPublish(exchange: "", routingKey: queryName, basicProperties: null, body: body);
    Console.WriteLine("成功发送消息:" + message);
    }
    }
    }
    }
    }
    }

可以看到RabbitMQ使用了IConnectionFactory,IConnection和IModel来创建链接和通信管道,IConnection实例对象只负责与Rabbit的连接,而发送接收这些实际操作全部由会话通道进行,而后使用QueneDeclare方法进行创建消息队列,创建完成后可以在RabbitMQ的管理工具中看到此队列,QueneDelare方法需要一个消息队列名称的必须参数.后面那些参数则代表缓存,参数等信息.最后使用BasicPublish来发送消息,在一对一中routingKey必须和 queueName一致

  • 接收者代码
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
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.Receive
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Start");
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
String queueName = String.Empty;
if (args.Length > 0)
queueName = args[0];
else
queueName = "queue1";
//声明一个队列
channel.QueueDeclare(
queue: queueName,//消息队列名称
durable: false,//是否缓存
exclusive: false,
autoDelete: false,
arguments: null
);
//创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
byte[] message = ea.Body.ToArray();//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
};
//消费者开启监听
channel.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
Console.ReadKey();
}
}
}

}
}

在接收者中是定义一个EventingBasicConsumer对象的消费者(接收者),这个消费者与会话对象关联,

  然后定义接收事件,输出从消息队列中接收的数据,

  最后使用会话对象的BasicConsume方法来启动消费者监听.消费者的定义也是如此简单.

  不过注意一点,可以看到在接收者代码中也有声明队列的方法,其实这句代码可以去掉,但是如果去掉的话接收者在程序启动时监听队列,而此时这个队列还未存在,所以会出异常,所以往往会在消费者中也添加一个声明队列方法

  此时,简单消息队列传输就算写好了,我们可以运行代码就行测试

csharp-dotnet-core-rabbitmq-2020416191154

Worker模式

Worker模式其实是一对多的模式,但是这个一对多并不是像发布订阅那种,而是信息以顺序的传输给每个接收者,我们可以使用上个例子来运行worker模式甚至,只需要运行多个接收者即可
csharp-dotnet-core-rabbitmq-202041619152

可以看到运行两个接收者,然后发送者发送了1-5这五个消息,第一个接收者接收的是奇数,而第二个接收者接收的是偶数,但是现在的worker存在这很大的问题,

  1. 丢失数据:一旦其中一个宕机,那么另外接收者的无法接收原本这个接收者所要接收的数据

  2. 无法实现能者多劳:如果其中的接收者接收的较慢,那么便会极大的浪费性能,所以需要实现接收快的多接收

下面针对上面的两个问题进行处理

首先我们先来看一下所说的宕机丢失数据一说,我们在上个例子Receive接收事件中添加线程等待

1
2
3
4
5
6
consumer.Received += (model, ea) =>
{
Thread.Sleep(1000);//等待1秒,
byte[] message = ea.Body;//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
};

然后再次启动三个接收者进行测试

csharp-dotnet-core-rabbitmq-202041795658

可以看到发送者发送了1-9的数字,第二个接收者在接收数据途中宕机,第一个接收者也并没有去接收第二个接收者宕机后的数据,有的时候我们会有当接收者宕机后,其余数据交给其它接收者进行消费,那么该怎么进行处理呢,解决这个问题得方法就是改变其消息确认模式

在Rabbit中存在两种消息确认模式

  • 自动确认:只要消息从队列获取,无论消费者获取到消息后是否成功消费,都认为是消息成功消费,也就是说上面第二个接收者其实已经消费了它所接收的数据

  • 手动确认:消费从队列中获取消息后,服务器会将该消息处于不可用状态,等待消费者反馈

  也就是说我们只要将消息确认模式改为手动即可,改为手动确认方式只需改两处,1.开启监听时将autoAck参数改为false,2.消息消费成功后返回确认

1
2
3
4
5
6
7
8
9
10
11
consumer.Received += (model, ea) =>
{
Thread.Sleep(1000);//等待1秒,
byte[] message = ea.Body;//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
//返回消息确认
channel.BasicAck(ea.DeliveryTag, true);
};
//消费者开启监听
//将autoAck设置false 关闭自动确认
channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);

然后再次测试便会出现完全接收的结果

能者多劳是建立在手动确认基础上,下面修改一下代码中等待的时间

1
2
3
4
5
6
7
8
consumer.Received += (model, ea) =>
{
Thread.Sleep((new Random().Next(1,6))*1000);//随机等待,实现能者多劳,
byte[] message = ea.Body;//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
//返回消息确认
channel.BasicAck(ea.DeliveryTag, true);
};

然后只需要再添加BasicQos方法即可

1
2
3
4
5
6
7
8
9
10
//声明一个队列
channel.QueueDeclare(
queue: queueName,//消息队列名称
durable: false,//是否缓存
exclusive: false,
autoDelete: false,
arguments: null
);
//告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息
channel.BasicQos(0, 1, false);

运行看效果 可以看到此时已实现能者多劳

worker模式接收者完整代码

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
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;

namespace HelloWorldRabbitMQ.Receive
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Start");
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
String queueName = String.Empty;
if (args.Length > 0)
queueName = args[0];
else
queueName = "queue1";
//声明一个队列
channel.QueueDeclare(
queue: queueName,//消息队列名称
durable: false,//是否缓存
exclusive: false,
autoDelete: false,
arguments: null
);
//告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息
channel.BasicQos(0, 1, false);
//创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
Thread.Sleep((new Random().Next(1, 6)) * 1000);//随机等待,实现能者多劳,
byte[] message = ea.Body.ToArray();//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
//返回消息确认
channel.BasicAck(ea.DeliveryTag, true);
};
//消费者开启监听
//将autoAck设置false 关闭自动确认
channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
Console.ReadKey();
}
}
}

}
}

Exchange模式(发布订阅模式,路由模式,通配符模式)

前面说过发布,路由,通配符这三种模式其实可以算为一种模式,区别仅仅是交互机类型不同.在这里出现了一个交换机的东西,发送者将消息发送发送到交换机,接收者创建各自的消息队列绑定到交换机

csharp-dotnet-core-rabbitmq-202041795659

通过上面三幅图可以看出这三种模式本质就是一种订阅模式,路由,通配符模式只是订阅模式的变种模式。使其可以选择发送订阅者中的接收者。

注意:交换机本身并不存储数据,数据存储在消息队列中,所以如果向没有绑定消息队列的交换机中发送信息,那么信息将会丢失

下面依次来看一下这三种模式

发布订阅模式(fanout)

  发送者代码

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
using RabbitMQ.Client;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.fanoutSend
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Start");
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//交换机名称
String exchangeName = String.Empty;
if (args.Length > 0)
exchangeName = args[0];
else
exchangeName = "exchange1";
//声明交换机
channel.ExchangeDeclare(exchange: exchangeName, type: "fanout");
while (true)
{
Console.WriteLine("消息内容:");
String message = Console.ReadLine();
//消息内容
byte[] body = Encoding.UTF8.GetBytes(message);
//发送消息
channel.BasicPublish(exchange: exchangeName, routingKey: "", basicProperties: null, body: body);
Console.WriteLine("成功发送消息:" + message);
}
}
}
}
}

}

发送者代码与上面没有什么差异,只是由上面的消息队列声明变成了交换机声明(交换机类型为fanout),也就说发送者发送消息从原来的直接发送消息队列变成了发送到交换机

接收者代码

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
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.fanoutReceive
{
class Program
{
static void Main(string[] args)
{
//创建一个随机数,以创建不同的消息队列
int random = new Random().Next(1, 1000);
Console.WriteLine("Start" + random.ToString());
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//交换机名称
String exchangeName = String.Empty;
if (args.Length > 0)
exchangeName = args[0];
else
exchangeName = "exchange1";
//声明交换机
channel.ExchangeDeclare(exchange: exchangeName, type: "fanout");
//消息队列名称
String queueName = exchangeName + "_" + random.ToString();
//声明队列
channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
//将队列与交换机进行绑定
channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: "");
//声明为手动确认
channel.BasicQos(0, 1, false);
//定义消费者
var consumer = new EventingBasicConsumer(channel);
//接收事件
consumer.Received += (model, ea) =>
{
byte[] message = ea.Body.ToArray();//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
//返回消息确认
channel.BasicAck(ea.DeliveryTag, true);
};
//开启监听
channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
Console.ReadKey();
}
}
}
}
}

可以看到接收者代码与上面有些差异

  • 首先是声明交换机(同上面一样,为了防止异常)

  • 然后声明消息队列并对交换机进行绑定,在这里使用了随机数,目的是声明不重复的消息队列,如果是同一个消息队列,则就变成worker模式,也就是说对于发布订阅模式有多少接收者就有多少个消息队列,而这些消息队列共同从一个交换机中获取数据

  • 然后同时开两个接收者,结果就如下
    csharp-dotnet-core-rabbitmq-2020417102839

路由模式(direct)

上面说过路由模式是订阅模式的一个变种模式,以路由进行匹配发送,例如将消息1发送给A,B两个消息队列,或者将消息2发送给B,C两个消息队列,路由模式的交换机是direct

发送者代码

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
using RabbitMQ.Client;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.directSend
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Start");
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//交换机名称
String exchangeName = "exchange2";
//路由名称
String routeKey = args[0];
//声明交换机 路由交换机类型direct
channel.ExchangeDeclare(exchange: exchangeName, type: "direct");
while (true)
{
Console.WriteLine("消息内容:");
String message = Console.ReadLine();
//消息内容
byte[] body = Encoding.UTF8.GetBytes(message);
//发送消息 发送到路由匹配的消息队列中
channel.BasicPublish(exchange: exchangeName, routingKey: routeKey, basicProperties: null, body: body);
Console.WriteLine("成功发送消息:" + message);
}
}
}
}
}
}

发送者代码相比上面只改了两处

  1. 将交换机类型改为了direct类型

  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
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.directReceive
{
class Program
{
static void Main(string[] args)
{
if (args.Length == 0) throw new ArgumentException("args");
//创建一个随机数,以创建不同的消息队列
int random = new Random().Next(1, 1000);
Console.WriteLine("Start" + random.ToString());
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "47.104.206.56",//IP地址
Port = 5672,//端口号
UserName = "yan",//用户账号
Password = "yan"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//交换机名称
String exchangeName = "exchange2";
//声明交换机
channel.ExchangeDeclare(exchange: exchangeName, type: "direct");
//消息队列名称
String queueName = exchangeName + "_" + random.ToString();
//声明队列
channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
//将队列与交换机进行绑定
foreach (var routeKey in args)
{//匹配多个路由
channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: routeKey);
}
//声明为手动确认
channel.BasicQos(0, 1, false);
//定义消费者
var consumer = new EventingBasicConsumer(channel);
//接收事件
consumer.Received += (model, ea) =>
{
byte[] message = ea.Body.ToArray();//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
//返回消息确认
channel.BasicAck(ea.DeliveryTag, true);
};
//开启监听
channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
Console.ReadKey();
}
}
}
}
}

在接收者代码中的改动点也是与发送者一致,但是一个接收者消息队列可以声明多个路由与交换机进行绑定
运行情况如下



通配符模式(topic)

通配符模式与路由模式一致,只不过通配符模式中的路由可以声明为模糊查询,RabbitMQ拥有两个通配符

  • #:匹配0-n个字符语句

  • *:匹配一个字符语句

注意:RabbitMQ中通配符并不像正则中的单个字符,而是一个以“.”分割的字符串,如 ”topic1.*“匹配的规则以topic1开始并且”.”后只有一段语句的路由 例:“topic1.aaa”,“topic1.bb”

发送者代码

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
using RabbitMQ.Client;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.topicSend
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Start");
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//交换机名称
String exchangeName = "exchange3";
//路由名称
String routeKey = args[0];
//声明交换机 通配符类型为topic
channel.ExchangeDeclare(exchange: exchangeName, type: "topic");
while (true)
{
Console.WriteLine("消息内容:");
String message = Console.ReadLine();
//消息内容
byte[] body = Encoding.UTF8.GetBytes(message);
//发送消息 发送到路由匹配的消息队列中
channel.BasicPublish(exchange: exchangeName, routingKey: routeKey, basicProperties: null, body: body);
Console.WriteLine("成功发送消息:" + message);
}
}
}
}
}
}

修改了两点:交换机名称(每个交换机只能声明一种类型,如果还用exchang2的话就会出异常),交换机类型改为topic

接收者代码

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
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

namespace HelloWorldRabbitMQ.topicRecoive
{
class Program
{
static void Main(string[] args)
{
if (args.Length == 0) throw new ArgumentException("args");
//创建一个随机数,以创建不同的消息队列
int random = new Random().Next(1, 1000);
Console.WriteLine("Start" + random.ToString());
IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象
{
HostName = "192.168.1.37",//IP地址
Port = 5672,//端口号
UserName = "sa",//用户账号
Password = "Sa123456"//用户密码
};
using (IConnection conn = connFactory.CreateConnection())
{
using (IModel channel = conn.CreateModel())
{
//交换机名称
String exchangeName = "exchange3";
//声明交换机 通配符类型为topic
channel.ExchangeDeclare(exchange: exchangeName, type: "topic");
//消息队列名称
String queueName = exchangeName + "_" + random.ToString();
//声明队列
channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
//将队列与交换机进行绑定
foreach (var routeKey in args)
{//匹配多个路由
channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: routeKey);
}
//声明为手动确认
channel.BasicQos(0, 1, false);
//定义消费者
var consumer = new EventingBasicConsumer(channel);
//接收事件
consumer.Received += (model, ea) =>
{
byte[] message = ea.Body.ToArray();//接收到的消息
Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message));
//返回消息确认
channel.BasicAck(ea.DeliveryTag, true);
};
//开启监听
channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
Console.ReadKey();
}
}
}
}
}

接收者修改与发送者一致

  运行结果如下


参考:

https://www.cnblogs.com/yan7/p/9498685.html