开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

NGINX下运行静态资源、PHP应用及支持HTTPS等配置详

发表于 2017-11-12

本文主要记录了当前博客下针对静态资源、旧站301跳转、Https配置等一系列内容,nginx以server块来确定某一部分虚拟域名及相关配置,所以我们可以在server块中配置server_name虚拟域名,access_log访问日志,return跳转,root项目根目录,location匹配url做相应操作,error_page错误页面,listen监听端口,include包含配置文件以及其他的一些ssl等操作,下面总结一下当前所使用内容

301&302跳转

原有旧站blog.congcong.us等都301跳转到www.congcong.us

$scheme为当前的协议
$request_uri为请求参数
配置代码如下:

1
2
3
4
5
6
复制代码server {
server_name congcong.us blog.congcong.us;
access_log /var/log/nginx/www.access.log;
#root /usr/share/nginx/html/blogtemp;
return 301 $scheme://www.congcong.us$request_uri;
}

静态资源配置缓存

配置图片及css等内容根据需要进行缓存 针对图片的请求 access_log不进行记录 expires为过期时间

1
2
3
4
5
6
7
8
9
复制代码location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ { 
access_log off;
expires 30d;
}

location ~* ^.+\.(css|js|txt|xml|swf|wav)$ {
access_log off;
expires 24h;
}

配置php,及配置php的url美化

过滤所有的url 如果说非以index.php结尾,那么增加这个进行rewrite

过滤所有的php结尾内容 转交由php-fpm进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码location / {

if (-f $request_filename/index.html){
rewrite (.*) $1/index.html break;
}
if (-f $request_filename/index.php){
rewrite (.*) $1/index.php;
}
if (!-f $request_filename){
rewrite (.*) /index.php;
}
}

location ~ \.php$ {
root /usr/share/nginx/html/wordpress;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 1000;
include fastcgi_params;
}

配置Https,进行SSL配置

监听443端口,ssl配置开启,关联crt与key,设置ssl协议,加密算法支持等内容(腾讯云申请的免费证书)

1
2
3
4
5
6
7
8
复制代码listen 443;
ssl on;
ssl_certificate "/xxx/xxx/1_www.congcong.us_bundle.crt";
ssl_certificate_key "/xxx/xxx/2_www.congcong.us.key";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

完整配置如下:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
复制代码#
# The default server
#
server {
server_name congcong.us blog.congcong.us;
access_log /var/log/nginx/www.access.log;
return 301 $scheme://www.congcong.us$request_uri;
}

server {
server_name projects.congcong.us kindle.congcong.us;
access_log /var/log/nginx/pk.access.log;
return 302 $scheme://congcong.us;
}


server {
listen 443;
server_name www.congcong.us;
root /xxx/xxx/xxx/xxx/wordpress;
index index.php index.html index.htm;

# Load configuration files for the default server block.
include /xxx/xxx/default.d/*.conf;

location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ {
access_log off;
expires 30d;
}

location ~* ^.+\.(css|js|txt|xml|swf|wav)$ {
access_log off;
expires 24h;
}


location / {

if (-f $request_filename/index.html){
rewrite (.*) $1/index.html break;
}
if (-f $request_filename/index.php){
rewrite (.*) $1/index.php;
}
if (!-f $request_filename){
rewrite (.*) /index.php;
}
}

location ~ \.php$ {
root /xxx/xxx/xxx/xxx/wordpress;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 1000;
include fastcgi_params;
}

error_page 404 /404.html;
location = /40x.html {
}

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

ssl on;
ssl_certificate "/xxx/xxx/1_www.congcong.us_bundle.crt";
ssl_certificate_key "/xxx/xxx/2_www.congcong.us.key";
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
ssl_prefer_server_ciphers on;

}

server {
listen 80 default_server;
server_name www.congcong.us;
root /xxx/xxx/xxx/xxx/wordpress;
index index.php index.html index.htm;

# Load configuration files for the default server block.
include /xxx/xxx/default.d/*.conf;

location ~* ^.+\.(ico|gif|jpg|jpeg|png)$ {
access_log off;
expires 30d;
}

location ~* ^.+\.(css|js|txt|xml|swf|wav)$ {
access_log off;
expires 24h;
}


location / {

if (-f $request_filename/index.html){
rewrite (.*) $1/index.html break;
}
if (-f $request_filename/index.php){
rewrite (.*) $1/index.php;
}
if (!-f $request_filename){
rewrite (.*) /index.php;
}
}
location ~ \.php$ {
root /xxx/xxx/xxx/xxx/wordpress;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 1000;
include fastcgi_params;
}

error_page 404 /404.html;
location = /40x.html {
}

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

相关文章

  • [NOTE-C]C语言指针详解(一)
  • [NOTE-PHP]PHP依赖管理工具Composer详解
  • [NOTE-ENCRYPT]Java实现非对称加密RSA、DH算法
  • [NOTE-ENCRYPT]Java实现RSA、DSA、ECDSA算法的签名与验签
  • [NOTE-ENCRYPT]非对称加密算法与RSA详解
  • [NOTE-DS]数据结构概述与大O记号
  • [NOTE-C]C语言输入与输出
  • [NOTE-C]C语言运算符

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SegmentFault 技术周刊 Vol36 - 一篇入

发表于 2017-11-12

一篇带你入门 Spring Boot。

Spring Boot 初识

SpringBoot前世今生

本文主要讲述spring boot的由来,即其它诞生的背景,初衷,现状,及对未来的展望。

Spring Boot参考指南中文版–Chapter1.Spring Boot中文文档

本节提供一个Spring Boot参考文档的简明概述。你可以把它作为文档其余部分的导航。你可以从头到尾依次阅读该参考指南,或跳过你不感兴趣的章节。

Spring Boot 学习资料收集

Spring Boot QuickStart

Spring Boot QuickStart (1)

Spring Boot 简化了基于 Spring 的应用开发,你只需要 “run” 就能创建一个独立的,产品级别的 Spring 应用。

Spring 平台及第三方库提供开箱即用的设置,这样你就可以有条不紊地开始。多数 Spring Boot 应用只需要很少的Spring 配置。你可以使用 Spring Boot 创建 Java 应用,并使用 java -jar 启动它或采用传统的 war 部署方式。

  • 系统要求
  • 安装
  • 使用
  • 配置

Spring Boot QuickStart (2) - 基础

基于 Spring Boot 创建一个命令行应用,先来个最基本的体验,体验一下:

  • 配置管理(配置文件加载,多环境配置文件)
  • 日志
  • 单元测试

Spring Boot QuickStart (3) - Web & Restful

基于 Spring Boot 可以快速创建一个Web & Restful 应用。

  • 注解
  • 路由,方法
  • 请求参数
  • Cookie
  • Session
  • 模板引擎
  • 常用配置

Spring Boot QuickStart (4) - Database

到了操作数据库的环节,以 MySQL 为基准,体验一下数据库的相关操作,JPA、MyBatis 将是学习重点。体验的基线:

  • 单表。增、删、改、查(多条件组合查询、分页,排序等)
  • 多表关联。一对一,一对多,多对多

Spring Boot QuickStart (5) - Spring Data JPA

Java Persistence API,可以理解就是 Java 一个持久化标准或规范,Spring Data JPA 是对它的实现。并且提供多个 JPA 厂商适配,如 Hibernate、Apache 的 OpenJpa、Eclipse的EclipseLink等。

spring-boot-starter-data-jpa 默认使用的是 Hibernate 实现。

在 SpringBoot + Spring Data Jpa 中,不需要额外的配置什么,只需要编写实体类(Entity)与数据访问接口(Repository)就能开箱即用,Spring Data JPA 能基于接口中的方法规范命名自动的帮你生成实现(根据方法命名生成实现,是不是很牛逼?)

Spring Boot 入门实用教程

Spring Boot - 整合Jsp/FreeMarker

本文讲述了(json,jsp,freemarker)配置及整合方法,并针对web开发常用的注解的概念及功能进行了介绍,留下了一个疑问:为什么整合jsp后必须通过spring-boot:run方式启动?欢迎大家留言讨论。

Spring Boot - Servlet、过滤器、监听器、拦截器

本文讲解了注册Servlet/Filter/Listener的两种⽅方式(Servlet/Filter/Listener的概念大家自行查阅资料了解),及拦截器基本原理,并通过注解实现http拦截器,另外本文还有一个疑问:为什么Spring中实现的Http拦截器,无法对我们自定义的servlet请求进行拦截?欢迎大家留言讨论。

Spring Boot - 静态资源处理、启动加载、日志处理

  1. 静态资源处理
1. 默认资源映射(/\*\* - > /resources/static) ,重点是默认目录的优先级
2. ⾃定义资源映射 (继承WebMvcConfigurerAdapter 并重写方法addResourceHandlers)
  1. 启动加载
1. CommandLineRunner (实现接⼝,多个类加载的优先级)
  1. 日志处理
1. logback(配置,控制台输出,文件输出)

Spring Boot - 整合JdbcTemplate、MyBatis

  • 本文讲解Spring Boot整合JdbcTemplate、整合mybatis,介绍并重点讲解了事务处理和配置。
  • 本文未提及关于分⻚查询和数据库连接池,我认为分页查询的重点是分页算法如何封装,并不是Spring Boot关注的重点,大家可以自己实现。
  • 另外现在常用的数据库连接池有c3p0/dbcp/tomcat-jdbc/HikariCP。
  • 顺便提一下,在Spring Boot中更改数据源,只需要在application.properties配置文件中增加spring.datasource.type配置即可。

Spring Boot - 部署Deploy

服务发布Tomcat:

  • 修改启动类,继承 SpringBootServletInitializer 并重写 configure 方法
  • 修改pom文件中jar 为 war
  • 修改pom,排除tomcat插件
  • 打包部署到容器

Spring Boot 属性配置

SpringBoot配置属性之MVC

SpringBoot配置属性之Server

SpringBoot配置属性之DataSource

SpringBoot配置属性之NOSQL

SpringBoot配置属性之MQ

SpringBoot配置属性之Security

SpringBoot配置属性之Migration

SpringBoot配置属性之其他

SpringBoot配置文件日期属性转换实例

Spring Boot 进阶

Spring Boot - 自定义启动banner

实现的方式非常简单,我们只需要在Spring Boot工程的/src/main/resources目录下创建一个banner.txt文件,然后将ASCII字符画复制进去,就能替换默认的banner了。

spring boot validated的使用

spring-boot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。

比如,我们判断一个输入参数是否合法,可以用如下方式

spring-boot启动初探

Spring Boot充分利用了JavaConfig的配置模式以及“约定优于配置”的理念,能够极大的简化基于Spring MVC的Web应用和REST服务开发。

使用spring boot开发web应用,决定项目是否可以直接启动的是spring-boot-starter-tomcat模块,我们可以直接引入spring-boot-starter-web。

Maven管理SpringBoot Profile

完成了上面的五步,即可使项目根据你的构建参数的不同,打包出不同环境下运行的包。

  1. 第1步去掉了SpringBoot内嵌的tomcat和tomcat-jdbc。使得我们可以决定在什么情况下使用何种容器运行我们的项目。
  2. 第2步配置了Maven构建Porfile,使得构建可根据我们的指令分发不同的包。
  3. 第3步配置了Maven资源过滤,不仅使得不同Profile下的资源文件互不可见,且替换了资源文件中以“@xx@”表示的属性值。
  4. 第4步使Spring的Profile由Maven决策,这样,我们就不用每次打包都修改Spring的Profile配置了。
  5. 第5步展示了如何执行不同Profile下的构建命令,并且使用了一个Shell脚本方便我们执行构建和跳过测试(多数时候我们在构建项目时先测试,并不需要在构建时测试,测试和构建的解耦使得我们更专注。但同时,如果你忘记了前置测试,也可能会引发未察觉的测试问题)。

SpringBoot四大神器之Actuator

Spring Boot有四大神器,分别是auto-configuration、starters、cli、actuator,本文主要讲actuator。actuator是spring boot提供的对应用系统的自省和监控的集成功能,可以对应用系统进行配置查看、相关功能统计等。

SpringBoot四大神器之Starter

SpringBoot的starter主要用来简化依赖用的。本文主要分两部分,一部分是列出一些starter的依赖,另一部分是教你自己写一个starter。

SpringBoot RESTful 应用中的异常处理小结

  • @ControllerAdvice 和 @ExceptionHandler 的区别
  • 处理 Controller 中的异常
  • 处理 404 错误

Spring Boot整合jsp后必须通过spring-boot:run方式启动?

简单总结一下,本文阐述的问题并不是日常开发中的主要问题(可能连主要问题都算不上,谁会用main去调试??),但是遇到了就花时间来研究一下,还是有所收获的。

  • 分析问题思路
  • Spring Boot 初始化的部分流程
  • 请求转发和重定向的区别

另外大家注意如果pom文件中去掉,再正常部署到tomcat容器中,会有jar冲突,建议大家试验过后,修改回去。

Spring-boot 启动时碰到的错误

通过springBoot构建一个简单的Restful webService

springboot定制404错误信息

SpringBoot-vue 基于Java的微服务全栈快速开发实践

Spring Boot 讲堂

Java 微服务实践 - Spring Boot 系列

Java 微服务实践 - Spring Boot 为系列讲座,二十节专题直播,时长高达50个小时,包括目前最流行技术,深入源码分析,授人以渔的方式,帮助初学者深入浅出地掌握,为高阶从业人员抛砖引玉。

系列讲座列表:

1. Java 微服务实践 - Spring Boot 系列(一)初体验

2. Java 微服务实践 - Spring Boot 系列(二) Web篇(上)

3. Java 微服务实践 - Spring Boot 系列(三)Web篇(中)

4. Java 微服务实践 - Spring Boot 系列(四)Web篇(下)

5. Java 微服务实践 - Spring Boot 系列(五)嵌入式Web容器

6. Java 微服务实践 - Spring Boot 系列(六)数据库 JDBC

7. Java 微服务实践 - Spring Boot 系列(七)MyBatis

8. Java 微服务实践 - Spring Boot 系列(八)JPA

9. Java 微服务实践 - Spring Boot 系列(九)NoSQL

10. Java 微服务实践 - Spring Boot 系列(十)缓存

11. Java 微服务实践 - Spring Boot 系列(十一)消息

12. Java 微服务实践 - Spring Boot 系列(十二)验证

13. Java 微服务实践 - Spring Boot 系列(十三)WebSocket

14. Java 微服务实践- Spring Boot 系列(十四)WebService

15. Java 微服务实践 - Spring Boot 系列(十五)安全

16. Java 微服务实践 - Spring Boot 系列(十六)日志

17. Java 微服务实践 - Spring Boot 系列(十七)监管

18. Java 微服务实践 - Spring Boot 系列(十八)配置

19. Java 微服务实践 - Spring Boot 系列(十九)测试

20. Java 微服务实践 - Spring Boot 系列(二十)自定义启动器

插播一则消息:11.11 - 11.13,SF 讲堂将开启优惠模式,大部分讲座均有不同限量的优惠,等你来抢! >>> 电梯直达活动页

本期完:)


欢迎关注 SegmentFault 微信服务号,获取最新讲堂及优惠信息。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习 Lumen 用户认证 (二) —— 使用 jwt-au

发表于 2017-11-12

通过上一篇《学习 Lumen 用户认证 (一)》mp.weixin.qq.com/s/KVUQE2DUe…的学习,大致懂了 Lumen 的用户认证主要使用 「api」的方式,来默认进行用户认证:

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
复制代码<?php

namespace App\Providers;

use App\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}

/**
* Boot the authentication services for the application.
*
* @return void
*/
public function boot()
{
// Here you may define how you wish users to be authenticated for your Lumen
// application. The callback which receives the incoming request instance
// should return either a User instance or null. You're free to obtain
// the User instance via an API token or any other method necessary.

$this->app['auth']->viaRequest('api', function ($request) {
if ($request->input('api_token')) {
return User::where('api_token', $request->input('api_token'))->first();
}
});
}
}

当然在实际开发中,我们不能只是简单的获取 api_token直接关联数据库查找用户信息。

在 API 开发中,用户认证是核心,是数据是否有保障的前提,目前主要有两种常用方式进行用户认证: JWT 和 OAuth2。

本文将简要说说如何利用 JWT 来进行用户认证

JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON 的开放标准 (RFC 7519)。该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。

关于 JWT 更具体的介绍,相信网上有很多帖子和文章值得参考,这里先不阐述了。

为了学习 JWT 在 Lumen 中的使用,最好的办法就是在「程序员同志网 —— GitHub」搜索有关插件,找个 stars 最多的那个拿来研究研究。

tymondesigns/jwt-auth

JSON Web Token Authentication for Laravel & Lumen

安装 jwt-auth

通过 Composer 安装:

1
复制代码composer require tymon/jwt-auth:"^1.0@dev"

注: 0.5.* 版本未对 Lumen 专门做封装

将 $app->withFacades() 和 auth 认证相关的注释去掉:

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
102
103
104
复制代码<?php

require_once __DIR__.'/../vendor/autoload.php';

try {
(new Dotenv\Dotenv(__DIR__.'/../'))->load();
} catch (Dotenv\Exception\InvalidPathException $e) {
//
}

/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/

$app = new Laravel\Lumen\Application(
realpath(__DIR__.'/../')
);

// 取消注释,这样就可以通过 Auth::user(),获取当前授权用户
$app->withFacades();

$app->withEloquent();

/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/

$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);

$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);

/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/

// $app->middleware([
// App\Http\Middleware\ExampleMiddleware::class
// ]);

// 增加 auth 中间件
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);

/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/

$app->register(App\Providers\AppServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);

/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/

$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
});

return $app;

然后在 AppServiceProvider 中注册 LumenServiceProvider:

1
复制代码$this->app->register(\Tymon\JWTAuth\Providers\LumenServiceProvider::class);

在 Lumen 项目中,默认没有 config 文件夹,需要在项目根目录创建,并将 vendor 源代码中auth.php 复制出来,同时将 api 认证指定为「jwt」:

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
复制代码<?php

return [

/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/

'defaults' => [
'guard' => env('AUTH_GUARD', 'api'),
],

/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session", "token"
|
*/

'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users'
],
],

/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/

'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => \App\User::class,
],
],

/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| Here you may set the options for resetting passwords including the view
| that is your password reset e-mail. You may also set the name of the
| table that maintains all of the reset tokens for your application.
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that the reset token should be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
*/

'passwords' => [
//
],

];

最后,因为 JWT 协议需要用到 secret,所以需要生成一个 secret:

1
复制代码php artisan jwt:secret

使用 jwt-auth

1. 更新 User Model

继承 Tymon\JWTAuth\Contracts\JWTSubject:

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
复制代码<?php

namespace App;

use Illuminate\Auth\Authenticatable;
use Laravel\Lumen\Auth\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Model implements AuthenticatableContract, AuthorizableContract, JWTSubject
{
use Authenticatable, Authorizable;

/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email',
];

/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password',
];

/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}

/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}

2. 写一个 Login 方法,验证登陆信息,并返回 token 回客户端:

1
2
复制代码// 路由
$router->post('/auth/login', 'AuthController@postLogin');

postLogin 方法:

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
复制代码<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTAuth;

class AuthController extends Controller
{
protected $jwt;

public function __construct(JWTAuth $jwt)
{
$this->jwt = $jwt;
}

public function postLogin(Request $request)
{
if (! $token = $this->jwt->attempt($request->only('email', 'password'))) {
return response()->json(['user_not_found'], 404);
}

return response()->json(compact('token'));
}
}

可以请求试试了,用 Postman 跑跑:

有了 token 了。我们就可以用来测试,看能不能认证成功,获取用户信息。

3. 使用 token 获取用户信息

1
2
3
4
5
复制代码// 使用 auth:api 中间件
$router->group(['middleware' => 'auth:api'], function($router)
{
$router->get('/test', 'ExampleController@getUser');
});

只要验证通过,就可以利用 Auth:user()方法获取用户信息了。

1
2
3
复制代码public function getUser(Request $request) {
return response()->json(['user' => Auth::user()]);
}

对照数据库:

以后只要在请求的 headers 中加入 token 信息即可,完美实现用户认证。

想了解有关 Lumen 的认证相关内容,可以参考上一篇文章《学习 Lumen 用户认证 (一)》mp.weixin.qq.com/s/KVUQE2DUe…

也可以参考 Lumen 官网
lumen.laravel-china.org/docs/5.3/au…

总结

对获取到 token 值 (eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vZGVtby5hcHAvYXV0aC9sb2dpbiIsImlhdCI6MTUxMDQ3NTQ5MiwiZXhwIjoxNTEwNDc5MDkyLCJuYmYiOjE1MTA0NzU0OTIsImp0aSI6Imx3UFpSMTN0MlV5eXRib1oiLCJzdWIiOjEsInBydiI6Ijg3ZTBhZjFlZjlmZDE1ODEyZmRlYzk3MTUzYTE0ZTBiMDQ3NTQ2YWEifQ.YTvsiO9MT3VgPZiI03v2sVEIsGLj8AUwJiDuXvCAvHI) 仔细观察,就会发现中间是由两个「.」来合并三段信息的。

下一步我们就来研究研究 JWT 的原理和也可以自己动手写个基于 JWT 的 Lumen 认证插件出来。

「未完待续」


coding01 期待您继续关注

qrcode

qrcode


也很感谢您能看到这了

qrcode

qrcode

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

killBase系列 -- 密码学

发表于 2017-11-12

前言

最近一场面试,面试官问了我
对称加密与非对称加密的问题,虽然曾经看过一些内容,但是没有系统的整理,所以当被问的时候,脑子里一片空白,没有回答上来。因此,在这里重新梳理一下密码学的知识点,夯实一下基础。

密码学

一、基础

  1. 密码学算法分类:
    • 消息编码:Base64
    • 消息摘要:MD类, SHA类,MAC
    • 对称密码:DES,3DES,AES
    • 非对称密码:RSA,DH
    • 数字签名:RSASignature,DSASignature
  2. 五元组
    1)明文:原始信息。
    2)加密算法:以密钥为参数,对明文进行多种置换和转换的规则和步骤,变换结果为密文。
    3)解密算法:加密算法的逆变换,以密文为输入、密钥为参数,变换结果为明文。:
    4)密钥:加密与解密算法的参数,直接影响对明文进行变换的结果。
    5)密文:对明文进行变换的结果。
  3. Java编程中常用类 – java.security 包
    1. 消息编码:BASE64Encoder,BASE64Decoder – java.util
    2. 消息摘要:MessageDigest
    3. 对称密码:KeyGenerator,SeretkeyFactory – javax.crypto 包(提供给AES,DES,3DES,MD5,SHA1等 对称 和 单向加密算法。),Cipher
    4. 非对称密码:KeyPairGenerator,KeyFactory – java.security 包(提供给DSA,RSA, EC等 非对称加密算法。),KeyPair,PublicKey,PrivateKey,Cipher
    5. 数字重命名:Signature
  4. 常用开源工具
    • Commons.Codec
    • Bouncy.Castle

二、Base64 算法

  1. Base64 基于64个字符编码算法,以任意 8 位字节序列组合描述形式 , BASE加密后产生的字节位数是8的倍数,如果不够位数以=符号填充。对此 Base64 算法有一套字符映射表。
  2. 使用方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    复制代码// 获取
    Base64.Encoder encoder = Base64.getEncoder();
    Base64.Decoder decoder = Base64.getDecoder();
    // 加密
    public byte[] encode(byte[] src);
    * @param src
    * the byte array to encode
    * @param dst
    * the output byte array
    * @return The number of bytes written to the output byte array
    public int encode(byte[] src,byte[] dst);
    public String encodeToString(byte[] src);
    public ByteBuffer encode(ButeBuffer buffer);
    // 解密
    public byte[] decode(byte[] src);
    * @param src
    * the byte array to encode
    * @param dst
    * the output byte array
    * @return The number of bytes written to the output byte array
    public int decode(byte[] src,byte[] dst);
    public byte[] decode(String src);
    public ByteBuffer decode(ButeBuffer buffer);

三、消息摘要

  1. 介绍:又称为 哈希算法。唯一对应一个消息或文体固定长度值,由一个单向的Hash加密函数对消息进行作用而产生。
  2. 分类: MD(Message Digest) 消息摘要算法,SHA(Secure Hash Algorithm) 安全散列算法, MAC(Message Authentication Code):消息认证算法
  3. 主要方法:
    1
    2
    复制代码// xxx 可以为 md5,sha
    MessageDigest.getInstance("xxx")

1. MD5算法

原理:
首先需要对信息进行填充,使其位长对512求余的结果等于448。
因此,信息的位长(Bits Length)将被扩展至N512+448,N为一个非负整数,N可以是零。
填充的方法如下,在信息的后面填充一个1和无数个0,直到满足上面的条件时才停止用0对信息的填充。
然后,在这个结果后面附加一个以64位二进制表示的填充前信息长度。
经过这两步的处理,信息的位长=N
512+448+64=(N+1)*512,即长度恰好是512的整数倍
MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。

代码实现

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
复制代码public class MD5Util {
/***
* MD5加密 生成32位md5码
* @param 待加密字符串
* @return 返回32位md5码
*/
public static String md5Encode(String inStr) throws Exception {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}

byte[] byteArray = inStr.getBytes("UTF-8");
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
// 转化为 16 进制
// 原理 : byte 为 8 字节。 0xff --> 11111111
// byte&0xff 如果小于16 则小于00010000
// 所以由 toHexString() 只能转化为 1 位,所以要在前面加上 ‘0’。再加上实际的值。
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
}

2. SHA 算法

原理:接收一段明文,然后以一种不可逆的方式将它转换成一段(通常更小)密文,也可以简单的理解为取一串输入码(称为预映射或信息),并把它们转化为长度较短、位数固定的输出序列即散列值(也称为信息摘要或信息认证代码)的过程。

特点:该算法输入报文的长度不限,产生的输出是一个160位的报文摘要。输入是按 512 位的分组进行处理的。

作用:通过散列算法可实现数字签名实现,数字签名的原理是将要传送的明文通过一种函数运算(Hash)转换成报文摘要(不同的明文对应不同的报文摘要),报文摘要加密后与明文一起传送给接受方,接受方将接受的明文产生新的报文摘要与发送方的发来报文摘要解密比较,比较结果一致表示明文未被改动,如果不一致表示明文已被篡改。

代码实现

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
复制代码public class SHAUtil {
/***
* SHA加密 生成40位SHA码
* @param 待加密字符串
* @return 返回40位SHA码
*/
public static String shaEncode(String inStr) throws Exception {
MessageDigest sha = null;
try {
sha = MessageDigest.getInstance("SHA");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}

byte[] byteArray = inStr.getBytes("UTF-8");
byte[] md5Bytes = sha.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}

3. HMAC 算法

原理:用公开函数和密钥产生一个固定长度的值作为认证标识,用这个 标识鉴别消息的完整性。使用一个密钥生成一个固定大小的小数据块,即MAC,并将其加入到消息中,然后传输。接收方利用与发送方共享的密钥进行鉴别认证 等。

代码实现

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
复制代码    // 构建密钥
public static byte[] getSecretKey(){
// 初始化
KeyGenerator keyGen = null;
try {
keyGen = KeyGenerator.getInstance("HmacMD5");
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
}
// 产生密钥
SecretKey secretKey1 = keyGen.generateKey();
// 得到密钥字节数组
byte[] key = secretKey1.getEncoded();
return key;
}
// 执行消息摘要
public static void doHMAC(byte[] data,String key){
// 从字节数组还原
SecretKey secretKey2 = new SecretKeySpec(key,"HmacMD5");
try {
// 实例化 Mac
Mac mac = Mac.getInstance("HmacMD5");
// 密钥初始化 Mac
mac.init(secretKey2);
// 执行消息摘要
byte[] result = mac.doFinal(data);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}

4. SHA 与 MD5比较

1)对强行攻击的安全性:最显著和最重要的区别是SHA-1摘要比MD5摘要长32 位。使用强行技术,产生任何一个报文使其摘要等于给定报摘要的难度对MD5是2^128数量级的操作,而对SHA-1则是2^160数量级的操作。这样,SHA-1对强行攻击有更大的强度。
2)对密码分析的安全性:由于MD5的设计,易受密码分析的攻击,SHA-1显得不易受这样的攻击。
3)速度:在相同的硬件上,SHA-1的运行速度比MD5慢。

四、对称加密

  1. 定义:在对称加密算法中,数据发信方将明文(原始数据)和加密密钥(mi yue)一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。
    收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。
    在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,这就要求解密方事先必须知道加密密钥。
  2. 优缺点
* 优点:算法公开、计算量小、加密速度快、加密效率高。
* 缺点:  
(1)交易双方都使用同样钥匙,安全性得不到保证。  
(2)每对用户每次使用对称加密算法时,都需要使用其他人不知道的惟一钥匙,这会使得发收信双方所拥有的钥匙数量呈几何级数增长,  
**密钥管理**成为用户的负担。对称加密算法在分布式网络系统上使用较为困难,主要是因为密钥管理困难,使用成本较高。
  1. 常用的对称加密算法。
    DES(Data Encryption Standard):数据加密标准,速度较快,适用于加密大量数据的场合。
    3DES(Triple DES):是基于DES,对一块数据用三个不同的密钥进行三次加密,强度更高。
    AES(Advanced Encryption Standard):高级加密标准,是下一代的加密算法标准,速度快,安全级别最高
  2. 对称密码常用的数学运算
    • 移位和循环移位
        移位就是将一段数码按照规定的位数整体性地左移或右移。循环右移就是当右移时,把数码的最后的位移到数码的最前头,循环左移正相反。例如,对十进制数码12345678循环右移1位(十进制位)的结果为81234567,而循环左移1位的结果则为23456781。
    • 置换
        就是将数码中的某一位的值根据置换表的规定,用另一位代替。它不像移位操作那样整齐有序,看上去杂乱无章。这正是加密所需,被经常应用。
    • 扩展
        就是将一段数码扩展成比原来位数更长的数码。扩展方法有多种,例如,可以用置换的方法,以扩展置换表来规定扩展后的数码每一位的替代值。
    • 压缩
        就是将一段数码压缩成比原来位数更短的数码。压缩方法有多种,例如,也可以用置换的方法,以表来规定压缩后的数码每一位的替代值。
    • 异或
        这是一种二进制布尔代数运算。异或的数学符号为⊕ ,它的运算法则如下:
      1⊕ 1 = 0
      0⊕ 0 = 0
      1⊕ 0 = 1
      0⊕ 1 = 1
        也可以简单地理解为,参与异或运算的两数位如相等,则结果为0,不等则为1。
    • 迭代
        迭代就是多次重复相同的运算,这在密码算法中经常使用,以使得形成的密文更加难以破解。
  3. 分组加密
    参考 分组加密的四种模式
    ECB模式 – 电子密码本模式
    CBC模式 – 密码分组链接模式
    CFB模式 – 密文反馈模式
    OFB模式 – 输出反馈模式
    CTR模式 – 计数器模式
  4. 常用的填充方式
    在Java进行DES、3DES和AES三种对称加密算法时,常采用的是NoPadding(不填充)、Zeros填充(0填充)、PKCS5Padding填充。
  • ZerosPadding

    全部填充为0的字节,结果如下:
    F1 F2 F3 F4 F5 F6 F7 F8 //第一块
    F9 00 00 00 00 00 00 00 //第二块

  • PKCS5Padding

    每个填充的字节都记录了填充的总字节数,结果如下:
    F1 F2 F3 F4 F5 F6 F7 F8 //第一块
    F9 07 07 07 07 07 07 07 //第二块
    注: 如果

1. DES(Data Encryption Standard)

1、 介绍:

DES算法的入口参数有三个:Key、Data、Mode。
Key为8个字节共64位,其中密钥 56 位,校验位 8 位(每组的 第8位都被用作奇偶校验),是DES算法的工作密钥;
Data也为8个字节64位,是要被加密或被解密的数据;
Mode为DES的工作方式,有两种:加密或解密。

2、 加密过程:

简略版:

  • 首先要生成一套加密密钥,从用户处取得一个64位长的密码口令,然后通过等分、移位、选取和迭代形成一套16个加密密钥,分别供每一轮运算中使用。
    过程 1,2
  • DES对64位(bit)的明文分组M进行操作,M经过一个初始置换IP,置换成m0。将m0明文分成左半部分和右半部分m0 = (L0,R0),各32位长。然后进行16轮完全相同的运算(迭代),这些运算被称为函数f,在每一轮运算过程中数据与相应的密钥结合。
    过程 4
  • 在每一轮中,密钥位移位,然后再从密钥的56位中选出48位。通过一个扩展置换将数据的右半部分扩展成48位,并通过一个异或操作替代成新的48位数据,再将其压缩置换成32位。这四步运算构成了函数f。然后,通过另一个异或运算,函数f的输出与左半部分结合,其结果成为新的右半部分,原来的右半部分成为新的左半部分。将该操作重复16次。
    过程 3 ,5 ,6 ,7 , 8 , 9
  • 经过16轮迭代后,左,右半部分合在一起经过一个逆置换(数据整理),恢复原先的顺序,这样就完成了加密过程。
    过程 10.

详细版请见 附录

3、 解密过程

  加密和解密使用相同的算法!
  DES加密和解密唯一的不同是密钥的次序相反。如果各轮加密密钥分别是K1,K2,K3…K16,那么解密密钥就是K16,K15,K14…K1。这也就是DES被称为对称算法的理由吧。

4、流程如图:

5、注意:

DES算法中只用到64位密钥中的其中56位,而第8、16、24、……64位8个位并未参与DES运算

6、3DES

3DES(或称为Triple DES)

原理:
使用3条56位的密钥对 数据进行三次加密。

7、Java 实现

相关的类:

1
2
3
4
5
6
复制代码// 生成密钥
KeyGenerator,SecretKeyFactory
// 密钥
SecretKey , SecretKeySpec
// 密码
Cipher

这里重点讲一下 Cipher 类

  1. 首先要设置参数
    Cipher.getInstance(加解密算法,加解密模式,填充模式)
  2. 初始化
    Cipher.init(加解密模式 – Cypher.ENCRIPT/DECRYPT,密钥)
  3. 完成加解密
    Cipher.doFinal(bytes) – 将bytes 内容 加密/解密 然后返回。

这里使用 SecretKeyFactory的密钥 选择CBC模式 进行加解密。

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
复制代码public class DESCryptography {  

public static void main(String[] args) {
// TODO Auto-generated method stub

String content="aaaaaaaabbbbbbbbaaaaaaaa";
String key="01234567";

System.out.println("加密前:"+byteToHexString(content.getBytes()));
byte[] encrypted=DES_CBC_Encrypt(content.getBytes(), key.getBytes());
System.out.println("加密后:"+byteToHexString(encrypted));
byte[] decrypted=DES_CBC_Decrypt(encrypted, key.getBytes());
System.out.println("解密后:"+byteToHexString(decrypted));
}

public static byte[] DES_CBC_Encrypt(byte[] content, byte[] keyBytes){
try {
DESKeySpec keySpec=new DESKeySpec(keyBytes);
SecretKeyFactory keyFactory=SecretKeyFactory.getInstance("DES");
SecretKey key=keyFactory.generateSecret(keySpec);

Cipher cipher=Cipher.getInstance("DES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(keySpec.getKey()));
byte[] result=cipher.doFinal(content);
return result;
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("exception:"+e.toString());
}
return null;
}

public static byte[] DES_CBC_Decrypt(byte[] content, byte[] keyBytes){
try {
DESKeySpec keySpec=new DESKeySpec(keyBytes);
SecretKeyFactory keyFactory=SecretKeyFactory.getInstance("DES");
SecretKey key=keyFactory.generateSecret(keySpec);

Cipher cipher=Cipher.getInstance("DES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(keyBytes));
byte[] result=cipher.doFinal(content);
return result;
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("exception:"+e.toString());
}
return null;
}

public static String byteToHexString(byte[] bytes) {
StringBuffer sb = new StringBuffer(bytes.length);
String sTemp;
for (int i = 0; i < bytes.length; i++) {
sTemp = Integer.toHexString(0xFF & bytes[i]);
if (sTemp.length() < 2)
sb.append(0);
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}

private static byte toByte(char c) {
byte b = (byte) "0123456789ABCDEF".indexOf(c);
return b;
}
}

2. AES(Advanced Encryption Standard)

有时间 再写。。。 看了一天的 加密 ,累死。。。

五、非对称加密

1. 基础

定义:需要两个密钥,一个是公开密钥,另一个是私有密钥;一个用作加密的时候,另一个则用作解密。
使用其中一个密钥把明文加密后所得的密文,只能用相对应的另一个密钥才能解密得到原本的明文;甚至连最初用来加密的密钥也不能用作解密。
由于加密和解密需要两个不同的密钥,故被称为非对称加密

数论知识:

非对称加密运用了一部分数论知识,有兴趣的自己去看下。。。 这里提供一下链接。
阮一峰大神写了一部分,可以帮助理解

一、互质关系:

如果两个正整数,除了1以外,没有其他公因子,我们就称这两个数是互质关系(coprime)。比如,15和32没有公因子,所以它们是互质关系。这说明,不是质数也可以构成互质关系。
二、欧拉函数
三、欧拉定理
四、模反元素(模逆元)
五、扩展欧几里得算法

2. RSA 算法

2.1 过程
  1. 随机选择两个不相等的质数 p 和 q
    p = 61, q = 53

  2. 计算 p 和 q 的乘积 n
    n = 61*53 = 3233

  3. 计算 n 的欧拉函数 φ(n)
    φ(n) = (p-1)(q-1) = 60 * 52 = 3120

  4. 随机选择一个整数 e , 条件是 1 *<* e *<* φ(n) , 且 e 与 φ(n) 互质
    e = 17 ( 实际应用中,常常选择 65537 )

  5. 计算 e 对于 φ(n) 的模反元素 d

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    复制代码 所谓"模反元素"就是指有一个整数d,可以使得ed被φ(n)除的余数为1。
      ed ≡ 1 (mod φ(n))
    这个式子等价于
      ed - 1 = kφ(n)
    于是,找到模反元素d,实质上就是对下面这个二元一次方程求解。
      ex + φ(n)y = 1
    已知 e=17, φ(n)=3120,
      17x + 3120y = 1
    这个方程可以用"扩展欧几里得算法"求解,此处省略具体过程。总之,爱丽丝算出一组整数解为 (x,y)=(2753,-15),即 d=2753。
    至此所有计算完成。
  6. 将 n 和 e 封装成公钥, n 和 d 封装成私钥
    公钥 (3233,17), 私钥 (3233,2753)

  7. 加密与解密

    • 加密用 (n , e)
      加密信息 – 明文为 m , m 小于 n
      $m^e$ ≡ c (mod n)
      公钥是 (3233,17), m 假设为 65
      $65^{17}$ ≡ 2790(mod 3233)
      所以 c = 2790
    • 解密用 (n , d)
      密文 为 c
      $c^d$ ≡ m(mod n)
      $2790^{2753}$ ≡ 65 (mod 3233)
      所以 m = 65
  8. 私钥解密的证明 – 有兴趣的同学自己去找资料看下,也是数论的知识。

2.2 RSA 算法的可靠性 与 破解

以上密钥的生成步骤,出现了六个数字

p, q, n, φ(n), e, d
公钥为 n, e
如果想要得到 d,需要进行以下逆推

1
2
3
4
5
> 复制代码  (1)ed≡1 (mod φ(n))。只有知道e和φ(n),才能算出d。
>   (2)φ(n)=(p-1)(q-1)。只有知道p和q,才能算出φ(n)。
>   (3)n=pq。只有将n因数分解,才能算出p和q。
>
>

所以 如果将 n 进行 因数分解,就意味着私钥被破解。 可是,大整数的因数分解,是一件非常困难的事情。目前,除了暴力破解,还没有发现别的有效方法。

注意:这里说大整数,不是 像上文 3233 这样的数字,历史上最大的已经进行因数分解的整数为

1
2
3
4
5
6
7
8
9
复制代码  12301866845301177551304949
  58384962720772853569595334
  79219732245215172640050726
  36575187452021997864693899
  56474942774063845925192557
  32630345373154826850791702
  61221429134616704292143116
  02221240479274737794080665
  351419597459856902143413

它等于这样两个质数的乘积

1
2
3
4
5
6
7
8
9
10
11
复制代码  33478071698956898786044169
  84821269081770479498371376
  85689124313889828837938780
  02287614711652531743087737
  814467999489
    ×
  36746043666799590428244633
  79962795263227915816434308
  76426760322838157396665112
  79233373417143396810270092
  798736308917

破解: 这里有一篇关于 RSA 破解的文章,有兴趣的同学可以看一下。
RSA计时攻击

2.3 Java 实现

使用到的类: java.security

1
2
3
4
5
6
7
8
9
10
复制代码// 生成 公钥,密钥
KeyPairGenerator --> KeyPair , KeyFactory --> RSA XXX Spec
// 公钥 密钥
KeyPair
RSAPublicKeySpec --> RSAPublicKey
RSAPrivateKeySpec --> RSAPrivateKey
// 密码
Cipher -- 1.Cipher.getInstance("RSA")
2.init(mode, key)
3.cipher.doFinal()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码public static void main(String[] args) throws Exception {  
// TODO Auto-generated method stub
HashMap<String, Object> map = RSAUtils.getKeys();
//生成公钥和私钥
RSAPublicKey publicKey = (RSAPublicKey) map.get("public");
RSAPrivateKey privateKey = (RSAPrivateKey) map.get("private");

//模
String modulus = publicKey.getModulus().toString();
//公钥指数
String public_exponent = publicKey.getPublicExponent().toString();
//私钥指数
String private_exponent = privateKey.getPrivateExponent().toString();
//明文
String ming = "123456789";
//使用模和指数生成公钥和私钥
RSAPublicKey pubKey = RSAUtils.getPublicKey(modulus, public_exponent);
RSAPrivateKey priKey = RSAUtils.getPrivateKey(modulus, private_exponent);
//加密后的密文
String mi = RSAUtils.encryptByPublicKey(ming, pubKey);
System.err.println(mi);
//解密后的明文
ming = RSAUtils.decryptByPrivateKey(mi, priKey);
System.err.println(ming);
}

RSAUtils.java

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
复制代码public class RSAUtils {  

/**
* 生成公钥和私钥
* @throws NoSuchAlgorithmException
*
*/
public static HashMap<String, Object> getKeys() throws NoSuchAlgorithmException{
HashMap<String, Object> map = new HashMap<String, Object>();
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
map.put("public", publicKey);
map.put("private", privateKey);
return map;
}
/**
* 使用模和指数生成RSA公钥
* 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA
* /None/NoPadding】
*
* @param modulus
* 模
* @param exponent
* 指数
* @return
*/
public static RSAPublicKey getPublicKey(String modulus, String exponent) {
try {
BigInteger b1 = new BigInteger(modulus);
BigInteger b2 = new BigInteger(exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(b1, b2);
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 使用模和指数生成RSA私钥
* 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA
* /None/NoPadding】
*
* @param modulus
* 模
* @param exponent
* 指数
* @return
*/
public static RSAPrivateKey getPrivateKey(String modulus, String exponent) {
try {
BigInteger b1 = new BigInteger(modulus);
BigInteger b2 = new BigInteger(exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(b1, b2);
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
* @throws Exception
*/
public static String encryptByPublicKey(String data, RSAPublicKey publicKey)
throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 模长
int key_len = publicKey.getModulus().bitLength() / 8;
// 加密数据长度 <= 模长-11
String[] datas = splitString(data, key_len - 11);
String mi = "";
//如果明文长度大于模长-11则要分组加密
for (String s : datas) {
mi += bcd2Str(cipher.doFinal(s.getBytes()));
}
return mi;
}

/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
* @throws Exception
*/
public static String decryptByPrivateKey(String data, RSAPrivateKey privateKey)
throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//模长
int key_len = privateKey.getModulus().bitLength() / 8;
byte[] bytes = data.getBytes();
byte[] bcd = ASCII_To_BCD(bytes, bytes.length);
System.err.println(bcd.length);
//如果密文长度大于模长则要分组解密
String ming = "";
byte[][] arrays = splitArray(bcd, key_len);
for(byte[] arr : arrays){
ming += new String(cipher.doFinal(arr));
}
return ming;
}
/**
* ASCII码转BCD码
*
*/
public static byte[] ASCII_To_BCD(byte[] ascii, int asc_len) {
byte[] bcd = new byte[asc_len / 2];
int j = 0;
for (int i = 0; i < (asc_len + 1) / 2; i++) {
bcd[i] = asc_to_bcd(ascii[j++]);
bcd[i] = (byte) (((j >= asc_len) ? 0x00 : asc_to_bcd(ascii[j++])) + (bcd[i] << 4));
}
return bcd;
}
public static byte asc_to_bcd(byte asc) {
byte bcd;

if ((asc >= '0') && (asc <= '9'))
bcd = (byte) (asc - '0');
else if ((asc >= 'A') && (asc <= 'F'))
bcd = (byte) (asc - 'A' + 10);
else if ((asc >= 'a') && (asc <= 'f'))
bcd = (byte) (asc - 'a' + 10);
else
bcd = (byte) (asc - 48);
return bcd;
}
/**
* BCD转字符串
*/
public static String bcd2Str(byte[] bytes) {
char temp[] = new char[bytes.length * 2], val;

for (int i = 0; i < bytes.length; i++) {
val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f);
temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0');

val = (char) (bytes[i] & 0x0f);
temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0');
}
return new String(temp);
}
/**
* 拆分字符串
*/
public static String[] splitString(String string, int len) {
int x = string.length() / len;
int y = string.length() % len;
int z = 0;
if (y != 0) {
z = 1;
}
String[] strings = new String[x + z];
String str = "";
for (int i=0; i<x+z; i++) {
if (i==x+z-1 && y!=0) {
str = string.substring(i*len, i*len+y);
}else{
str = string.substring(i*len, i*len+len);
}
strings[i] = str;
}
return strings;
}
/**
*拆分数组
*/
public static byte[][] splitArray(byte[] data,int len){
int x = data.length / len;
int y = data.length % len;
int z = 0;
if(y!=0){
z = 1;
}
byte[][] arrays = new byte[x+z][];
byte[] arr;
for(int i=0; i<x+z; i++){
arr = new byte[len];
if(i==x+z-1 && y!=0){
System.arraycopy(data, i*len, arr, 0, y);
}else{
System.arraycopy(data, i*len, arr, 0, len);
}
arrays[i] = arr;
}
return arrays;
}
}
2.4 问题

公钥(n,e) 只能 加密小于 n 的整数 m ,那么如果要加密大于 n 的整数,怎么办?
在 Java 中 进行 RSA 加密时,有 一个 错误为 ArrayIndexOutOfBoundsException: too much data for RSA block
该错误就是加密数据过长导致的。

这里涉及到几个知识点 – 密钥长度/密文长度/明文长度

  1. 明文长度
    明文长度(bytes) <= 密钥长度(bytes)-11.
    如果 明文长度 大于 规定,则出现上述的问题,可以按照下文中的解决方法处理
  2. 密钥长度
    下限是96bits(12bytes)
    上限未知。不过目前为止,被破解的最长的密钥长度 为 768位,所以 1024 位基本安全, 2048 位绝对安全
  3. 密文长度
    • 不分片加密 – 密文长度 == 密钥长度
    • 分片加密– 密文长度 == 密钥长度分片数
      例如 明文 8 bytes , 密钥 128 bits
      每片明文长度 = 128/8 - 11 = 5 bytes
      分片数 = 8/5 +1 = 2
      密文长度 = 128/8
      2 = 32 bytes

解决方法

  1. 分片加密 – 是把长信息分割成若干段短消息,每段分别加密;
  2. 先选择一种”对称性加密算法”(比如DES),用这种算法的密钥加密信息,再用RSA公钥加密DES密钥。

附录

1. DES 详细加密过程

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
102
103
复制代码1. **对输入的密钥进行变换**。
用户的64bit密钥,其中第8, 16, 24, 32, 40, 48, 56, 64位是校验位, 使得每个密钥都有奇数个1。所以密钥事实上是56位。对这56位密钥进行如下表的换位。

57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36,
63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4,

表的意思是第57位移到第1位,第49位移到第2位,...... 以此类推。变换后得到56bit数据,将它分成两部分,C[0][28], D[0][28]。

2. **计算16个子密钥**,计算方法C[i][28] D[i][28]为对前一个C[i-1][28], D[i-1][28]做循环左移操作。16次的左移位数如下表:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 (第i次)
1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 (左移位数)

3. **串联**计算出来的C[i][28] D[i][28] 得到56位,然后对它进行如下变换得到48位子密钥K[i][48]

14, 17, 11, 24, 1, 5, 3, 28, 15, 6, 21, 10, 23, 19, 12, 4, 26, 8, 16, 7, 27, 20, 13, 2,
41, 52, 31, 37, 47, 55, 30, 40, 51, 45, 33, 48, 44, 49, 39, 56, 34, 53, 46, 42, 50, 36, 29, 32,

表的意思是第14位移到第1位,第17位移到第2位,以此类推。在此过程中,发现第9,18,22,25, 35,38,43,54位丢弃。

4. 对64bit的明文输入进行换位变换。换位表如下:

58, 50, 12, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4,
62, 54, 46, 38, 30, 22, 14, 6, 64, 56, 48, 40, 32, 24, 16, 8,
57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7

表的意思就是第一次变换时,第58位移到第1位,第50位移到第2位,...... 依此类推。得到64位数据,将这数据前后分成两块L[0][32], R[0][32]。

5. 加密过程,对R[i][32]进行扩展变换成48位数,方法如下, 记为E(R[i][32])

32, 1, 2, 3, 4, 5,
4, 5, 6, 7, 8, 9,
8, 9, 10, 11, 12, 13,
12, 13, 14, 15, 16, 17,
16, 17, 18, 19, 20, 21,
20, 21, 22, 23, 24, 25,
24, 25, 26, 27, 28, 29,
28, 29, 30, 31, 32, 1,

6. 将E(R[i][32])与K[i][48]作异或运算,得到48位数,将48位数顺序分成8份,6位一份,B[8][6]。

7. 使用S[i]替换B[i][6]。过程如下: 取出B[i][6]的第1位和第6位连成一个2位数m, m就是S[i]中对应的行数(0-3),取出B[i][6]的第2到第5位连成一个4位数n(0-15),n就是S[i]中对应的列数,用S[i][m][n]代替B[i][6]。S是4行16列的对应表,里面是4位的数,一共有8个S,定义如下:

S[1]:
14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7,
  0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8,
  4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0,
  15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13,
S[2]:
15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10,
3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5,
0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15,
13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9,
S[3]:
10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8,
13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1,
13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7,
1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12,
S[4]:
7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15,
  13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9,
  10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4,
  3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14,
S[5]:
  2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9,
  14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6,
  4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14,
  11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3,
S[6]:
  12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11,
  10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8,
  9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6,
  4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13,
S[7]:
  4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1,
  13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6,
  1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2,
  6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12,
S[8]:
  13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7,
  1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2,
  7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8,
  2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11,

8. 将从B[i][6]经过S得到的8个4位数连起来得到32位数。对这个数进行如下变换:

16,7,20,21,29,12,28,17, 1,15,23,26, 5,18,31,10,
  2,8,24,14,32,27, 3, 9,19,13,30, 6,22,11, 4,25,

得到的结果与L[i][32]作异或运算,把结果赋给R[i][32]。

9. 把R[i-1][32]的值赋给L[i],从5开始循环。直到K[16][48]结束。

10. 将最后的L,R合并成64位,然后进行如下转化得到最后的结果。这是对第4步的一个逆变化。
40, 8, 48, 16, 56, 24, 64, 32,
39, 7, 47, 15, 55, 23, 63, 31,
38, 6, 46, 14, 54, 22, 62, 30,
37, 5, 45, 13, 53, 21, 61, 29,
36, 4, 44, 12, 52, 20, 60, 28,
35, 3, 43, 11, 51, 19, 59, 27,
34, 2, 42, 10, 50, 18, 58, 26,
33, 1, 41, 9, 49, 17, 57, 25

2. https 的加密算法

由于之前看过 https 是 由 secure socket layer 实现的。 也是通过 公钥私钥 保证其安全性,所以在学习这篇文章的时候,就想 https 是由哪种 加密算法 做为其 底层实现的呢。 因此,就有了下面这部分。

关于 https 与 http 的区别 请看我的这篇博客,不再赘述。网络基础知识

原理:

  • 浏览器把自身支持的一系列Cipher Suite(密钥算法套件,后文简称Cipher)[C1,C2,C3, …]发给服务器;
  • 服务器接收到浏览器的所有Cipher后,与自己支持的套件作对比,如果找到双方都支持的Cipher,则告知浏览器;
  • 浏览器与服务器使用匹配的Cipher进行后续通信。如果服务器没有找到匹配的算法,浏览器(以 Chrome 56为例)将给出错误信息:

下面讲一下如何分析。

  1. 准备: 通过可以抓取网络包的工具,这里通过 Wireshark 分析。关于wireshark 的介绍请点击这里.查看浏览器发送给服务器的 Ciper服务器的 Ciper
  2. 流程:
    • 浏览器首先发起握手协议, 一个’Client Hello’消息,如下图,按照Protocol协议顺序排序,然后,找到Client Hello,选中,依次查找 ‘Secure Sockets Layer’ -> TLSv1.2 Record Layer -> Handshake protocal ->Ciper Suites.
    • 可以看到, Cipher有很多。总共16,第一个是Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b)。
    • 如果按照顺序继续寻找第一个 Info 为’Sever Hello’ 的报文,可以找到相应的Cipher Suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b) 。
.
  1. Cipher介绍:
    • 密钥交换算法,用于决定客户端与服务器之间在握手的过程中如何认证,用到的算法包括RSA,Diffie-Hellman,ECDH,PSK等
    • 加密算法,用于加密消息流,该名称后通常会带有两个数字,分别表示密钥的长度和初始向量的长度,比如DES 56/56, RC2 56/128, RC4 128/128, AES 128/128, AES 256/256
    • 报文认证信息码(MAC)算法,用于创建报文摘要,确保消息的完整性(没有被篡改),算法包括MD5,SHA等。
    • PRF(伪随机数函数),用于生成“master secret”。
    • TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (0xc02b):
      • 基于TLS协议
      • 使用 ECDHE,ECDSA作为密钥交换算法
      • 加密算法 AES(密钥与初始向量的长度为128)
      • MAC 算法 SHA
  2. 总结:
    Client端密钥算法套件[C1,C2,C3],Server端密钥算法套件[C4,C2,C1,C3],
    则,IIS(Internet Infomation Services),C2将被优先返回

3. wireshark 的使用问题

问题:第一次使用 wireshark 的时候,不显示接口。原因是。。。
刚开始使用 在windows 上需要 winpacp 并且开启 npf 服务。
注: 如果 没有安装 winpacp ,想直接 通过 net start npf 开启服务,将会提示。 发生系统错误2

  1. winpacp 安装 。。。
    这里是下载网站
    直接安装即可。
  2. 开启 npf 服务
    打开 cmd ,输入 net start npf ,提示:服务已经启动。
  3. 进入界面,选择相应的网卡。

这里,可以通过 网络连接 看出来。

所以,我的是无线网络连接。
4. 最终界面

WireShark 主要分为这几个界面
5. Display Filter(显示过滤器), 用于过滤
6. Packet List Pane(封包列表), 显示捕获到的封包, 有源地址和目标地址,端口号。 颜色不同,代表
7. Packet Details Pane(封包详细信息), 显示封包中的字段
8. Dissector Pane(16进制数据)
9. Miscellanous(地址栏,杂项)

结语

都看到这里了,点个关注,点波赞再走,QAQ。
你的小手轻点,是我最大的动力哦。

一只想当程序员的1米88处女座大可爱如此说。

参考

  1. DES 加密算法解析
  2. 分组加密的四种模式
  3. 阮一峰–RSA算法原理
  4. java中RSA加解密的实现
  5. 关于RSA算法密钥长度/密文长度/明文长度
  6. https背后的加密算法

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

J2EE常见面试题(一)

发表于 2017-11-12

StringBuilder和StringBuffer的区别:

String 字符串常量 不可变 使用字符串拼接时是不同的2个空间

StringBuffer 字符串变量 可变 线程安全 字符串拼接直接在字符串后追加

StringBuilder 字符串变量 可变 非线程安全 字符串拼接直接在字符串后追加

1.StringBuilder执行效率高于StringBuffer高于String.

2.String是一个常量,是不可变的,所以对于每一次+=赋值都会创建一个新的对象, StringBuffer和 StringBuilder都是可变的,当进行字符串拼接时采用append方 法,在原来的基础上进行追加,所以性能比String 要高,又因为StringBuffer 是 线程安全的而StringBuilder是线程非安全的,所以 StringBuilder的效率高于 StringBuffer.

3.对于大数据量的字符串的拼接,采用StringBuffer,StringBuilder.

Vector,ArrayList,LinkedList之间的区别:

一、同步性

ArrayList,LinkedList是不同步的,而Vestor是同步的。所以如果不要求线程安全的话,可以使用ArrayList或LinkedList,可以节省为同步而耗费的开销。但在多线程的情况下,有时候就不得不使用Vector了。当然,也可以通过一些办法包装ArrayList,LinkedList,使他们也达到同步,但效率可能会有所降低。

二、数据增长
从内部实现机制来讲ArrayList和Vector都是使用Objec的数组形式来存储的。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

三、检索、插入、删除对象的效率

ArrayList和Vector中,从指定的位置(用index)检索一个对象,或在集合的末尾插入、删除一个对象的时间是一样的,可表示为O(1)。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行(n-i)个对象的位移操作。
LinkedList中,在插入、删除集合中任何位置的元素所花费的时间都是一样的—O(1),但它在索引一个元素的时候比较慢,为O(i),其中i是索引的位置。

HashTable HashMap TreeMap之间的区别:

HashMap不是线程安全的,HashTable是线程安全。

HashMap允许空(null)的键和值(key ),HashTable则不允许。

HashMap性能优于Hashtable。

TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)

http报文包含哪些部分:

请求首行;

请求头信息;

空行;

请求体;

post请求协议格式:

GET /Hello/index.jsp HTTP/1.1:GET请求,请求服务器路径为Hello/index.jsp,协议为1.1 ;

Host:localhost:请求的主机名为localhost;

User-Agent: Mozilla/4.0 (compatible; MSIE 8.0…:与浏览器和OS相关的信息。有些网站会显示用户的系统版本和浏览器版本信息,这都是通过获取User-Agent 头信息而来的;

Accept: */*:告诉服务器,当前客户端可以接收的文档类型, */*,就表示什么都可以接收;

Accept-Language: zh-CN:当前客户端支持的语言,可以在浏览器的工具à选项中找到语言相关信息;

Accept-Encoding: gzip, deflate:支持的压缩格式。数据在网络上传递时,服务器会把数据压缩后再发送;

Connection: keep-alive:客户端支持的链接方式,保持一段时间链接。

什么是Sql注入?如何防止sql注入?

所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。具体来说,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。怎么防止SQL注入,使用存储过程来执行所有的查询;检查用户输入的合法性;将用户的登录名、密码等数据加密保存。

Redirect和Forwod之间的区别?

1、从数据共享上

Forword是一个请求的延续,可以共享request的数据


Redirect开启一个新的请求,不可以共享request的数据

2、从地址栏

Forword转发地址栏不发生变化


Redirect转发地址栏发生变化

谈谈线程同步,乐观锁,悲观锁的实现?

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

实现过程
2.悲观锁:悲观锁的实现采用的数据库内部的锁机制,一个典型的倚赖数据库的悲观锁调用:
select * from account where name=”张三” for update
这条sql 语句锁定了account 表中所有符合检索条件(name=”Erica”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。也就是我们可以在查询数据的时候先用for update把这条数据锁住,然后更改完这条数据再提交。这样别的线程没法更新这条数据,也就保证了不会丢失更新。
2.1.悲观锁带来的性能问题。我们试想一个场景:如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时(如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数据、开始修改直至提交修改结果的全过程),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果?所以我们这个时候可以使用乐观锁。

1.乐观锁:乐观锁的实现可以通过在表里面加一个版本号的形式,下面是一个实例。

讲解:也就是每个人更新的时候都会判断当前的版本号是否跟我查询出来得到的版本号是否一致,不一致就更新失败,一致就更新这条记录并更改版本号。

Sql查询语句的优化?DB索引使用场景?

1、在表中建立索引,优先考虑where、group by使用到的字段。

2、尽量避免使用select *,返回无用的字段会降低查询效率。如下:
SELECT * FROM t
优化方式:使用具体的字段代替*,只返回使用到的字段。

3、尽量避免使用in 和not in,会导致数据库引擎放弃索引进行全表扫描。如下:
SELECT * FROM t WHERE id IN (2,3)
SELECT * FROM t1 WHERE username IN (SELECT username FROM t2)
优化方式:如果是连续数值,可以用between代替。如下:
SELECT * FROM t WHERE id BETWEEN 2 AND 3
如果是子查询,可以用exists代替。如下:
SELECT
* FROM t1 WHERE EXISTS (SELECT * FROM t2 WHERE t1.username = t2.username)

4、尽量避免使用or,会导致数据库引擎放弃索引进行全表扫描。如下:
SELECT * FROM t WHERE id = 1 OR id = 3
优化方式:可以用union代替or。如下:
SELECT * FROM t WHERE id = 1
UNION
SELECT * FROM t WHERE id = 3
(PS:如果or两边的字段是同一个,如例子中这样。貌似两种方式效率差不多,即使union扫描的是索引,or扫描的是全表)

5、尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描。如下:
SELECT * FROM t WHERE username LIKE ‘%li%’
优化方式:尽量在字段后面使用模糊查询。如下:
SELECT * FROM t WHERE username LIKE ‘li%’

6、尽量避免进行null值的判断,会导致数据库引擎放弃索引进行全表扫描。如下:
SELECT * FROM t WHERE score IS NULL
优化方式:可以给字段添加默认值0,对0值进行判断。如下:
SELECT
* FROM t WHERE score = 0

7、尽量避免在where条件中等号的左侧进行表达式、函数操作,会导致数据库引擎放弃索引进行全表扫描。如下:
SELECT * FROM t2 WHERE score/10 = 9
SELECT * FROM t2 WHERE SUBSTR(username,1,2) = ‘li’
优化方式:可以将表达式、函数操作移动到等号右侧。如下:
SELECT * FROM t2 WHERE score = 10*9
SELECT * FROM t2 WHERE username
LIKE ‘li%’

8、当数据量大时,避免使用where 1=1的条件。通常为了方便拼装查询条件,我们会默认使用该条件,数据库引擎会放弃索引进行全表扫描。如下:
SELECT * FROM t WHERE 1=1
优化方式:用代码拼装sql时进行判断,没where加where,有where加and。

Spring在项目中怎么用 ioc Aop实现原理 功能 使用场景?

ioc依赖注入的思想是通过反射机制实现的,在实例化一个类时,它通过反射调用类中set方法将事先保存在HashMap中的类属性注入到类中。 总而言之,在传统的对象创建方式中,通常由调用者来创建被调用者的实例,而在Spring中创建被调用者的工作由Spring来完成,然后注入调用者,即所谓的依赖注入or控制反转。 注入方式有两种:依赖注入和设置注入; IoC的优点:降低了组件之间的耦合,降低了业务对象之间替换的复杂性,使之能够灵活的管理对象。

AOP利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了 多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的 逻辑或责任封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

AOP使用场景:

Authentication 权限检查

Caching 缓存

Context passing 内容传递

Error handling 错误处理

Lazy loading 延迟加载

Debugging  调试

logging, tracing, profiling and monitoring 日志记录,跟踪,优化,校准

Performance optimization 性能优化,效率检查

Persistence  持久化

Resource pooling 资源池

Synchronization 同步

Transactions 事务管理

异步编程的理解 :

异步编程提供了一个非阻塞的,事件驱动的编程模型。 这种编程模型利用系统中多核执行任务来提供并行,因此提供了应用的吞吐率。此处吞吐率是指在单位时间内所做任务的数量。 在这种编程方式下, 一个工作单元将独立于主应用线程而执行, 并且会将它的状态通知调用线程:成功,处理中或者失败。

我们需要异步来消除阻塞模型。其实异步编程模型可以使用同样的线程来处理多个请求, 这些请求不会阻塞这个线程。想象一个应用正在使用的线程正在执行任务, 然后等待任务完成才进行下一步。 log框架就是一个很好的例子:典型地你想将异常和错误日志记录到一个目标中, 比如文件,数据库或者其它类似地方。你不会让你的程序等待日志写完才执行,否则程序的响应就会受到影响。 相反,如果对log框架的调用是异步地,应用就可以并发执行其它任务而无需等待。这是一个非阻塞执行的例子。

为了在Java中实现异步,你需要使用Future 和 FutureTask, 它们位于java.util.concurrent包下. Future是一个接口而FutureTask是它的一个实现类。实际上,如果在你的代码中使用Future, 你的异步任务会立即执行, 并且调用线程可以得到结果promise。

下面的代码片段定义了一个包含两个方法的接口。 一个是同步方法,另外一个是异步方法。

1
2
3
4
5
6
7
复制代码import java.util.concurrent.Future;
public interface IDataManager {
// synchronous method
public String getDataSynchronously();
// asynchronous method
public Future<String> getDataAsynchronously();
}

值得注意的是回调模型的弊端就是当回调嵌套时很麻烦。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

python设计模式-建造者模式

发表于 2017-11-12

问题:在上一篇python设计模式:抽象工厂模式中,我们尝试用抽象工厂模式规范化了 Pizza 原材料的供应以及 Pizza 的创建。但是我们忽略了一个问题,那就是每种 Pizza 的烘焙时间依赖于生面团的厚度和使用的配料,它们所需的时间是不一样的。那这时我们改如何处理呢?

Pizza 的制作流程包括:准备(擀面皮、加佐料),然后烘烤、切片、装盒。这些有特定的顺序,不能错乱。

为了保证 生产 Pizza 的步骤不会出错,我们打算指派一个创建者,创建者用于控制 Pizza 的制作流程。

创建 Pizza 创建者

首先我们定义一个 Pizza

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码class Pizza:

def __init__(self, name):
self.name = name
self.dough = None
self.sauce = None
self.toppings = []

def prepare_dough(self, dough):
self.dough = dough
print(self.dough)
print('preparing the {} dough of your {}...'.format(self.dough, self))
time.sleep(STEP_DELAY)
print('Done with the {} dough'.format(self.dough))

def __str__(self):
return self.name

然后我们抽象出一个创建者:

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
复制代码class PizzaBuilder(object):

name = None

def __init__(self):
self.progress = PIZZA_PROGRESS
self.baking_time = 5

def prepare_dough(self):
raise NotImplementedError()

def add_sauce(self):
raise NotImplementedError()

def add_topping(self):
raise NotImplementedError()

def bake(self):
raise NotImplementedError()

def cut(self):
raise NotImplementedError()

def box(self):
raise NotImplementedError()

@property
def pizza(self):
return Pizza(self.name)

创建具体建造者

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
复制代码class NYStyleCheeseBuilder(PizzaBuilder):

name = 'NY Style Sauce and Cheese Pizza'

def prepare_dough(self):
self.progress = PIZZA_PROGRESS[0]
self.pizza.prepare_dough('thin')

def add_sauce(self):
print('adding the tomato sauce to your pizza..')
self.pizza.sauce = 'tomato'
time.sleep(STEP_DELAY)
print('done with the tomato sauce')

def add_topping(self):
print('adding the topping (grated reggiano cheese) to your pizza')
self.pizza.toppings.append(["Grated", "Reggiano", "Cheese"])
time.sleep(STEP_DELAY)
print('done with the topping (grated reggiano cheese)')

def bake(self):
self.progress = PIZZA_PROGRESS[1]
print('baking your pizza for {} seconds'.format(self.baking_time))
time.sleep(self.baking_time)

def cut(self):
self.progress = PIZZA_PROGRESS[2]
print("Cutting the pizza into diagonal slices")

def box(self):
self.progress = PIZZA_PROGRESS[3]
print("Place pizza in official PizzaStore box")

创建指挥者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码class Waiter:
# 指挥者

def __init__(self):
self.builder = None

def construct_pizza(self, builder):
self.builder = builder
# 一旦我们有了一个 pizza,需要做一些准备(擀面皮、加佐料),然后烘烤、切片、装盒
[step() for step in (builder.prepare_dough, builder.add_sauce,
builder.add_topping, builder.bake,
builder.cut, builder.box)]

@property
def pizza(self):
return self.builder.pizza

完整代码参考:python-design-patter-builder

从这个例子我可以看出,建造者模式包含如下角色:

  • Builder:抽象建造者(Builder)(引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现。这样更容易扩展。一般至少会有两个抽象方法,一个用来建造产品,一个是用来返回产品。)
  • ConcreteBuilder:具体建造者(CommonBuilder、SuperBuilder)(实现抽象类的所有未实现的方法,具体来说一般是两项任务:组建产品;返回组建好的产品。)
  • Director:指挥者(Director)(负责调用适当的建造者来组建产品,指挥者类一般不与产品类发生依赖关系,与指挥者类直接交互的是建造者类。一般来说,指挥者类被用来封装程序中易变的部分。)
  • Product:产品角色(Role)

建造者模式

造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。也可以说,每个产品的建造会遵循同样的流程,不过流程内的每一个步骤都不尽相同。

建造者模式又可以称为生成器模式。

建造者模式类图

建造者模式类图

建造者模式在软件中的应用

  • django-widgy是一个 Django的第三方树编辑器扩展,可用作内容管理系统(Content Management System,CMS)。它包含一个网页构建器,用来创建具有不同布局的HTML页面。
  • django-query-builder是另一个基于建造者模式的Django第三方扩展库,该扩展库可用于动态 地构建SQL查询。使用它,我们能够控制一个查询的方方面面,并能创建不同种类的查询,从简 单的到非常复杂的都可以

建造者模式和工厂模式的区别

看上边这个例子,你可能会疑惑,为什么明明可以使用工厂方法模式可以解决的问题,要换成建造者模式呢?

通过代码可以看出,建造者模式和工厂方法模式最大的区别是,建造者模式多了一个指挥者的角色。建造者负责创建复杂对象的各个组成部分。而指挥者使用一个建造者实例控制建造的过程。

与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——指挥者类。

建造者模式通常用于补充工厂模式的不足,尤其是在如下场景中:

  • 要求一个对象有不同的表现,并且希望将对象的构造与表现解耦
  • 要求在某个时间点创建对象,但在稍后的时间点再访问

参考链接

  • 讲故事,学(Java)设计模式—建造者模式
  • 设计模式(九)——建造者模式
  • 23种设计模式(4):建造者模式

最后,感谢女朋友支持。

欢迎关注(April_Louisa) 请我喝芬达
欢迎关注 欢迎关注 请我喝芬达 请我喝芬达

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Java并发系列】 1Java并发机制的底层实现

发表于 2017-11-11

在Java并发实现的机制中,大部分的容器和框架都是依赖于volatile/synchronized/原子操作实现的,了解底层的并发机制,对于并发编程会带来很多帮助

一、 synchronized的应用

synchronized在多线程并发编程中已经是一个元老级的存在,通常被称作是重量级锁。既然是常用的一种锁,那么就需要对它的底层实现有深入的了解。

1. synchronized的实现原理

当一个线程在访问同步代码块时,就必须要先获取该代码块中对象的锁,退出或者抛出异常时,就必须要释放锁。synchronized的同步实现,是JVM基于进入和退出同步对象的Monitor对象来实现方法同步和代码块同步的。
每个Monitor对象都有与之关联的monitor,当且仅当monitor被持有后,它才会处于锁定状态。同步代码是使用monitorenter和monitorexit指令实现的。monitorenter指令时在代码编译后插入到同步代码块的开始位置,monitorexit指令则是插入到方法的结束和异常处。当线程执行到monitorenter时,会尝试去获取对象的monitor的所有权,即尝试获取对象的锁。

2. synchronized的使用形式及意义
  • 修饰普通方法:锁是当前实例对象
  • 修饰静态方法:锁是当前类的Class对象
  • 修饰代码块:锁是synchronized括号里面配置的对象
3. 锁的升级

锁共有四种状态:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态

偏向锁:当线程访问同步块并获取锁时,会在对象头和栈针中的锁记录中存储锁偏向的ID,以后该线程在进入和退出同步块时就不需要在使用CAS操作来进行加锁和解锁操作,而仅仅需要测试一下对象头中的Mark Word字段中存储的线程ID是否是当前线程即可。如果是,那么表示获取锁成功;如果不是,则需要检查对象头中偏向锁的字段是否设置为了1(表示当前锁是偏向锁),如果没有设置,则需要使用CAS来竞争获取锁,如果已经设置了,则尝试使用CAS将对象头的偏向锁ID指向当前线程。
轻量级锁;

  • 轻量级锁加锁:线程在执行同步块之前,首先会在自己的线程栈中创建一个用于存储锁记录的空间,并将对象中的Mark Word 赋值到锁记录中。然后线程尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则表示当前线程获取锁,如果失败,则表示其他线程也在竞争锁,当前线程便使用自循的方式来获取锁。
  • 轻量级锁解锁:轻量级锁解锁时,会使用原子CAS操作将锁记录替换会对象头,如果成功,则表示没有竞争发生。如果失败,则表示当前锁存在竞争,锁就会膨胀为重量级锁。
4. 锁的对比
锁类型 优点 缺点 适用场景
重量级锁 线程竞争不使用自旋,不会浪费CPU 线程阻塞,响应时间慢 追求吞吐量,同步块执行的时间长的场景
轻量级锁 竞争的线程不会阻塞,提高程序的响应速度 如果线程长时间得不到锁,那么自旋就会浪费CPU 追求响应时间,同步块执行速度快
偏向锁 加锁和解锁不需要额外的资源消耗 如果线程间存在竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步场景

二、volatile的应用

volatile是轻量级的synchronized,volatile在多线程开发中保证了共享变量的可见性,所谓可见性,指的是当一个线程修改了共享变量之后,对于其他线程来说,能够读到这个修改的值。

1. volatile的定义及实现原理

volatile定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁获得这个变量。
实现原理:当对声明了volatile的变量进行写操作时,JVM就会向处理器发送一条带有lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存中。同时,在多处理器的情况下,需要执行缓存一致性协议,即每个处理器都需要通过嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器需要对这个数据进行操作时,再从系统内存中把数据读取到缓存行中。

2. 实现volatile的两条原则
  • 带有Lock前缀的指令会引起处理器缓存写回到内存;
  • 一个处理器的缓存写回到内存,会导致其他处理器的缓存无效。

三、原子操作的原理

见文章[并发编程系列]Java中的原子操作类

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

结合 TreeMap 源码分析红黑树在 java 中的实现

发表于 2017-11-11

csdn 链接:blog.csdn.net/ziwang_/art…

注:本文的源码摘自 jdk1.8 中 TreeMap

  • 红黑树的意义
  • 红黑树的性质
  • 左旋、右旋
  • 增
  • 删
  • 总结

红黑树的意义

红黑树本质上是一种特殊的二叉查找树,红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(lgN)。那么红黑树是如何实现这个特性的呢?红黑树区别于其他二叉查找树的规则在于它的每个结点拥有红色或黑色中的一种颜色,然后按照一定的规则组成红黑树,而这个规则就是我们这篇文章所想要阐述的了。
红黑树的性质


红黑树遵循以下五点性质:

  • 性质1 结点是红色或黑色。
  • 性质2 根结点是黑色。
  • 性质3 每个叶子结点(NIL结点,空结点)是黑色的。
  • 性质4 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  • 性质5 从任一结点到其每个叶子结点的所有路径都包含相同数目的黑色结点。

以下有几个违反上述规则的结点示例:

违反性质1

违反性质1

结点必须是红色或黑色

违反性质2

违反性质2

根结点必须是黑色的

违反性质3

违反性质3

叶子结点必须是黑色的

违反性质4

违反性质4

违反性质4

违反性质4

违反性质4

违反性质4

以上三个都是错误的红黑树示例,每个红色结点的两个子结点都是黑色,而如下是合格的

遵循性质4

遵循性质4

当然,细心的读者应该发现了我只是展示了前四条性质而没有展示第五条性质,没有什么理由,笔者就是懒,第五条挺好理解的。

左旋、右旋

在学习红黑树之前想要介绍一个概念——左旋、右旋。这是一种结点操作,是红黑树里面时常出现的一个操作,请看下图 ——
左旋右旋概念图

左旋右旋概念图

这里的左旋右旋都是针对根节点而言的,所以左图到右图是 y 结点右旋,右图到左图是 x 结点左旋。

  • 左旋:根结点退居右位,左子结点上位,同时左子结点的右子结点变成根节点左结点。
  • 右旋:根节点退居左位,右子节点上位,同时右子结点的左子结点变成根节点右结点。

现在不理解这俩概念有什么用不重要,但是希望读者能理解它的变幻过程,到后面会涉及到。

说起来枯燥无意,我们可以结合 TreeMap 来看看左旋右旋的源码 ——

方法图

方法图

在这里我们就针对左旋源码看看 ——

左旋源码

左旋源码

笔者就直接一行一行解释吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right; // r 是根结点右子结点
p.right = r.left; // 为根结点的左结点指向右子结点(也就是 r)的左结点
if (r.left != null)
r.left.parent = p; // 意义同第二步,这步是右子结点(也就是 r)的左结点将父结点引用指向 p
r.parent = p.parent; // 将 r 结点的父引用指向 p 结点的父引用
if (p.parent == null)
root = r; // 将根结点替换为 r
else if (p.parent.left == p)
p.parent.left = r; // 意义同上
else
p.parent.right = r; // 意义同上
r.left = p; // r 左结点引用指向 p 结点
p.parent = r; // p 结点父引用指向 r 结点
}
}

增

假设现在我们找到了相应的结点插入位置,那么我们接下来就可以插入相应的结点了,这个时候迎来一个头疼的问题,我们知道红黑树结点是有颜色的,那么我们应该给它设置成黑色的还是红色的呢?
设置成黑色的吧,就违反了性质5,设置成了红色的吧,就容易违反了性质4。那怎么办?总要给一个颜色,那我们就给红色的吧。为什么?因为如果设置成黑色的话,该分支的黑色结点数量肯定比其他分支多一个,而这样的话相当地不好做调整。如果将插入结点颜色置为红色的话,运气比较好的情况下该父结点就是黑色的,那这样就不需要做任何调整。另一种情况是插入结点的父结点颜色是红色的,这种情况我们就需要详细讨论了,具体分为以下两种(此处我们以插入结点的父结点是爷爷结点的左子结点为例(有点拗口),镜像操作道理相同):

  • 1.父结点与叔叔结点都为红

父结点与叔叔结点都为红

父结点与叔叔结点都为红

父结点与叔叔结点都为红的话那么必定爷爷结点为黑,实际上此时我们最简单的操作就是将父结点和叔叔结点染黑,将爷爷结点染红(将爷爷结点染红的目的是为了保证爷爷结点路径的黑色结点数量不改变),如下 ——

染黑

染黑

现在目标结点、父结点、叔叔结点都符合要求了,但是爷爷结点的父结点是红色的,那么就冲突了,聪明的读者可能已经发现了,此时的爷爷结点就相当于目标结点,我们不妨将爷爷结点置换为目标结点,再进行递归操作就可以达到解决冲突的目的了。

  • 2.父结点为红,叔叔结点为黑

父结点为红,叔叔结点为黑

父结点为红,叔叔结点为黑

但凡有一个结点是红色,那么它的父结点必定是黑色(性质4),所以爷爷结点一定是黑色的。

有细心的小伙伴可能觉察到,上图违反了性质五。实际上上图是一张简化后的图,为了我们后面的内容更加便于理解,上图的原图应该是以下模样 ——

上图原图

上图原图

ps:上图中叔叔结点和兄弟结点可以理解成 java 中的 null 结点,笔者特地将它们的个头缩小了,以便区分。

那么此时该怎么操作呢?爷爷结点右旋,爷爷结点置红,父结点置黑。这条操作过后,性质4、5都没有违反。

爷爷结点右旋,爷爷结点置红,父结点置黑

爷爷结点右旋,爷爷结点置红,父结点置黑

当然,上图也只是一张简化图,实际上原图如下:

上图原图

上图原图

那么结合 TreeMap 源码我们来看看:

插入调整源码

插入调整源码

翻译如下:

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
复制代码private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED; // 目标结点颜色赋红

// 目标结点非空,非根,同时父结点为红,此时才需要调整
while (x != null && x != root && x.parent.color == RED) {
// 父结点是爷爷的左子结点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x))); // y 是叔叔结点
// 情况1 叔叔结点也为红
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK); // 父结点赋黑
setColor(y, BLACK); // 叔叔结点赋黑
setColor(parentOf(parentOf(x)), RED); // 爷爷结点赋红
x = parentOf(parentOf(x)); // 爷爷结点置为目标结点,递归
} else {
// 情况2 叔叔结点为黑
// 小插曲,如果目标结点是父结点的右子结点,左旋父结点
// 当然,此时目标结点应改为父结点
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK); // 父结点赋黑
setColor(parentOf(parentOf(x)), RED); // 爷爷结点赋红
rotateRight(parentOf(parentOf(x))); // 爷爷结点右旋
}
} else {
// 镜像操作,道理同上
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK; // 根结点必须赋黑
}

看完代码我们发现我们好像漏了一个小插曲(当然,这是笔者故意的),那么小插曲是一个什么情况呢?言语来说,在叔叔结点为黑的前提下,当目标结点是父结点的右子结点的时候,需要对父结点进行左旋然后才能接续下一步操作,为什么会这样,我们一图胜千言 ——

小插曲

小插曲

如果忽略上述情况,那么最终会得到以下情况:

小插曲忽略情况下实现

小插曲忽略情况下实现

由于目标结点是父结点的右子节点,在爷爷结点右旋过程中,它会转为原爷爷结点的左子结点,这样的话就违反了特性4和特性5。解决方法就是上面所提到的将父结点先进行左旋然后再进行前面所提到的操作,如下图 ——

小插曲修正

小插曲修正

当然,不要忘了,现在需要调整的结点是原父结点,也就是要将上图左下角那个结点作为目标结点进行调整。

所以红黑树的添操作分为以下三步:

  • 找到相应的插入位置
  • 将目标结点设置为红色并插入
  • 通过着色和旋转等操作使之重新成为一棵二叉树

删

这一小节我想先 show 出源码再来解释 ——
删除结点源码

删除结点源码

翻译如下:

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
复制代码private void deleteEntry(Entry<K,V> p) {
// 优先选择左子结点作为被删结点的替代结点
Entry<K,V> replacement = (p.left != null ? p.left : p.right);

// 如果替代结点不为空
if (replacement != null) {
replacement.parent = p.parent;
// 如果删除结点为根节点,那么根节点重定向引用指向替代结点
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
// 如果删除结点是其父结点的左子结点,更改父结点左子结点引用指向替代结点
p.parent.left = replacement;
else
// 如果删除结点是其父结点的右子结点,更改父结点右子结点引用指向替代结点
p.parent.right = replacement;

// 将删除结点的各个引用置 null
p.left = p.right = p.parent = null;

// 如果删除结点颜色为黑色,那么需要进行删后调整
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
// 如果替代结点为空且删除结点为 root 结点
root = null;
} else {
// 如果删除结点为空且不是 root 结点
// 如果删除结点颜色为黑色,那么需要进行删后调整
if (p.color == BLACK)
fixAfterDeletion(p);

// 将删除结点的各个引用置 null
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}

删除时可能分为三种情况,具体的做法也在上述代码中做了清晰的解释,笔者在此就不扩展了,细心的读者可能发现了,上述删除操作凡是涉及到了删除结点是黑色的情况下,都需要调用 fixAfterDeletion() 方法对红黑树进行调整。这是因为如果删除结点是黑色的,当它被删除后就会违反性质5,所以我们需要对红黑树进行结构调整。

为了便于理解红色结点为什么不会影响红黑树整体结构,笔者还是举了一个例子给各位读者理解一下,下图是删除前:

删除前

删除前

下图是删除后:

删除后

删除后

实际上红黑树是使用以下2点思想来进行调整的(笔者认为,在分析 fixAfterDeletion() 代码实现之前,作为开发者应该去自行思考一下如果我们作为源码设计者,我们会如何来解决这个问题。) ——

1.给删除结点的路径增加一个黑色结点(将兄弟路径的一个黑色结点移过来)
2.给删除结点的兄弟路径减少一个黑色结点(将兄弟路径的一个红色结点染黑)

ps:后面我们会针对第一条称为思想1,第二条称为思想2。

说完思想,我们讨论一下具体删除操作是如何进行的。红黑树在保障删除结点的兄弟结点为黑色的情况下(没有什么特殊缘由,仅仅是为了后期好操作),分以下两点来进行分析:

1.兄弟结点的两个子结点都是黑色的
2.另一种情况(兄弟结点的两个子结点至多一个黑色的)

ps:后面我们会针对第一条称为情况1,第二条称为情况2。

对于情况1来说,红黑树采用思想2,将兄弟结点置为红色,但是这样带来了两个问题——对于父路径来说,它与兄弟路径黑色结点数量不同,违反性质5;且如果父结点也是红色,那么它势必与孩子结点冲突,还会违反性质4,如下图——

下图示例违反性质5:

原图

原图

违反性质5

违反性质5

下图示例违反性质5且违反性质4:

原图

原图

违反性质4、5

违反性质4、5

对于前一个问题用递归的思想来解决,将父亲结点置为目标结点,让父亲结点的兄弟结点也要减少一个黑色结点就可以了(借鉴思想2);而对于后一个问题,只需要将父结点置黑即可(借鉴思想2)。jdk 中相关实现源码如下:

1
2
3
4
5
6
7
8
9
10
复制代码while (x != root && colorOf(x) == BLACK) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
}
}

setColor(x, BLACK);

前面阐述的是针对情况1而言,针对于情况2而言,红黑树采用的是思想1,具体做法分为又得分为以下两种小情况:

  • 兄弟结点的右子结点不为黑
  • 兄弟结点的右子结点为黑

对于第一种小情况,红黑树采用以下操作:

1.兄弟结点置父结点颜色(准备谋权篡位)
2.父结点置黑、兄弟结点右结点置黑
3.父结点左旋

该思想不仅保证了更新结点后不会冲突(父结点与兄弟结点不冲突,兄弟结点与右子结点不冲突,兄弟结点左子结点与父结点不冲突),并且保证了黑色结点数量不会改变,一图胜千言——

第一种小情况原图

第一种小情况原图

第一种小情况删除后修正

第一种小情况删除后修正

jdk 中相关源码如下:

1
2
3
4
5
6
7
8
9
复制代码while (x != root && colorOf(x) == BLACK) {
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}

setColor(x, BLACK);

而对于第二种小情况,红黑树采用以下操作:

1.将兄弟结点的左子结点染黑
2.兄弟结点染红
3.兄弟结点右旋

第二种小情况原图

第二种小情况原图

第二种小情况删除后修正

第二种小情况删除后修正

实际上细心的读者发现了,转换后的结构是等同于第一种小情况的初始结构,所以接下来就按照第一种小情况的步骤去变换结构,相关源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码while (x != root && colorOf(x) == BLACK) {
if (colorOf(rightOf(sib)) == BLACK) { // 情况2
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}

// 情况1
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}

setColor(x, BLACK);

这一块可能有一些复杂,但记住以下三点核心思想问题就不是很大了:

  • 父结点替换删除结点(保障了删除结点路径上的黑色结点数量不变)
  • 兄弟结点替换父结点(保障了父结点路径上的黑色结点数量不变)
  • 右子结点(结构变化前一定是红色的,变换后置黑)替换兄弟结点(保障了兄弟路径上的黑色结点数量不变)

那么接下来就是看看 fixAfterDeletion() 的代码实现了 ——

结点删除调整源码

结点删除调整源码

解释如下:

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
复制代码private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
// 目标结点是左子结点
if (x == leftOf(parentOf(x))) {
// 目标结点的兄弟结点
Entry<K,V> sib = rightOf(parentOf(x));

// 小插曲1,如果兄弟结点为红
// 这步是保障兄弟结点一定为黑
if (colorOf(sib) == RED) {
setColor(sib, BLACK); // 兄弟结点置黑
setColor(parentOf(x), RED); // 父结点置红
rotateLeft(parentOf(x)); // 父结点左旋
sib = rightOf(parentOf(x)); // 重定向兄弟结点
}

// 兄弟结点的两个子结点是黑色
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED); // 兄弟结点置红
x = parentOf(x); // 重定向目标结点为父结点
} else {
// 兄弟结点的子结点至多一个是黑色的

// 小插曲2,兄弟结点左子结点为红,右子结点为黑的情况
// 这步的意义是让兄弟结点的右子结点的数量多一个
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 将兄弟结点颜色置为父结点颜色(言外之意肯定是兄弟结点要替换父结点的位置)
setColor(sib, colorOf(parentOf(x)));
// 将父结点置黑
setColor(parentOf(x), BLACK);
// 将兄弟结点右子结点置黑
setColor(rightOf(sib), BLACK);
// 左旋父结点
rotateLeft(parentOf(x));
x = root;
}
} else { // 镜像操作
Entry<K,V> sib = leftOf(parentOf(x));

if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}

if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}

setColor(x, BLACK);
}

总结

红黑树的插入操作是基于插入结点颜色为红色,原因是如果插入结点是黑色的话,会导致涉及到该结点的路径上的黑色结点数量会比兄弟路径的黑色结点数量多一个,那么整体调节起来势必很不方便。而删除操作是基于删除结点如果是黑色的情况下,才需要进行调整,因为黑色结点的删除会导致涉及到该结点的路径上的黑色结点数量会比兄弟路径的黑色结点数量少一个,那么就需要进行整体调节。
红黑树在 java 中的运用实际上还是挺多的,例如 TreeSet 的默认底层实现实际上也是 TreeMap;jdk 8中的 HashMap 实现也由原来的数组+链表更改为了数组+链表/红黑树。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

几种分布式调用链监控组件的实践与比较(一)实践

发表于 2017-11-10

引言:最近在调研与选型分布式调用链监控组件。选了主要的三种APM组件进行了实践与比较。本来打算一篇文章写完的,篇幅太长,打算分两篇。本文主要讲下链路traceing的基本概念和几种APM组件的实践,实践部分也没给出特别详细的步骤,因为本文重点不在具体的步骤。第二篇将会讲下几种APM选型的比较与性能测试。

  1. 问题背景

微服务架构下,服务按照不同的维度进行拆分,一次请求请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具,以便发生故障的时候,能够快速定位和解决问题。

分布式调用链监控组件在这样的环境下产生了。最出名的是谷歌公开的论文提到的Dapper。开发Dapper是为了收集更多的复杂分布式系统的行为信息,然后呈现给Google的开发者们。这样的分布式系统有一个特殊的好处,因为那些大规模的低端服务器,作为互联网服务的载体,是一个特殊的经济划算的平台。想要在这个上下文中理解分布式系统的行为,就需要监控那些横跨了不同的应用、不同的服务器之间的关联动作。

市面上的APM(Application Performance Management)理论模型大多都是借鉴(borrow)Google Dapper论文,本文重点关注以下几种APM组件:

  • Zipkin
    由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
  • Pinpoint
    Pinpoint是一款对Java编写的大规模分布式系统的APM工具,由韩国人开源的分布式跟踪组件。
  • Skywalking
    国产的优秀APM组件,是一个对JAVA分布式应用程序集群的业务运行情况进行追踪、告警和分析的系统。

其他类似的组件还有美团点评的CAT,淘宝的鹰眼EgleEye。

如上所述,那么我们选择链路监控组件有哪些要求呢?Dapper中也提到了,笔者总结如下:

  • 探针的性能消耗。
    APM组件服务的影响应该做到足够小。在一些高度优化过的服务,即使一点点损耗也会很容易察觉到,而且有可能迫使在线服务的部署团队不得不将跟踪系统关停。
  • 代码的侵入性
    对于应用的程序员来说,是不需要知道有跟踪系统这回事的。如果一个跟踪系统想生效,就必须需要依赖应用的开发者主动配合,那么这个跟踪系统也太脆弱了,往往由于跟踪系统在应用中植入代码的bug或疏忽导致应用出问题,这样才是无法满足对跟踪系统“无所不在的部署”这个需求。
  • 可扩展性
    能够支持的组件越多当然越好。或者提供便捷的插件开发API,对于一些没有监控到的组件,应用开发者也可以自行扩展。
  • 数据的分析
    数据的分析要快 ,分析的维度尽可能多。跟踪系统能提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应。分析的全面,能够避免二次开发。
  1. 基础概念

上面列出的几种组件,其中Zipkin是严格按照Google Dapper论文实现的,下面介绍下其中涉及的基本概念。

  • Span
    基本工作单元,一次链路调用(可以是RPC,DB等没有特定的限制)创建一个span,通过一个64位ID标识它,uuid较为方便,span中还有其他的数据,例如描述信息,时间戳,key-value对的(Annotation)tag信息,parent-id等,其中parent-id可以表示span调用链路来源。
  • Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识。比如你运行的分布式大数据存储一次Trace就由你的一次请求组成。
  • Annotation: 注解,用来记录请求特定事件相关信息(例如时间),通常包含四个注解信息:
    (1) cs:Client Start,表示客户端发起请求

(2) sr:Server Receive,表示服务端收到请求

(3) ss:Server Send,表示服务端完成处理,并将结果发送给客户端

(4) cr:Client Received,表示客户端获取到服务端返回信息

2.1 Trace

下面看一下,在系统中Trace是什么样子。

trace

trace

每种颜色的note标注了一个span,一条链路通过TraceId唯一标识,Span标识发起的请求信息。树节点是整个架构的基本单元,而每一个节点又是对span的引用。节点之间的连线表示的span和它的父span直接的关系。虽然span在日志文件中只是简单的代表span的开始和结束时间,他们在整个树形结构中却是相对独立的。

2.2 Span

sp

sp

上图说明了span在一次大的跟踪过程中是什么样的。Dapper记录了span名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系。如果一个span没有父ID被称为root span。所有span都挂在一个特定的跟踪上,也共用一个跟踪id。

2.3 Annotation

自动的探针,不需要修改应用程序源代码,对应用开发者近乎零浸入的成本对分布式控制路径进行跟踪,几乎完全依赖于基于少量通用组件库的改造。Dapper还允许应用程序开发人员在Dapper跟踪的过程中添加额外的信息,以监控更高级别的系统行为,或帮助调试问题。

下面章节将会介绍下上述三种APM组件的使用与实践。

  1. zipkin

zipkin主要涉及几个组件:collector收集agent的数据,storage存储,web UI图形化界面,search查询Storage中存储的数据,提供简单的JSON API获取数据。

我们的项目基于微服务框架spring cloud构建微服务。spring cloud也提供了spring-cloud-sleuth来方便集成zipkin实现。所以笔者就在项目中试了下spring-cloud-sleuth-zipkin。

起了三个服务:
zipkin-server、zipkin-client-backend、zipkin-client。
其中server服务负责收集以及信息展示。client-backend调用client,产生调用链路信息。

3.1 zipkin-server实现

zipkin-server实现主要有两点需要注意,其一是收集到数据的存储,方式包括内存、数据库、ES等;其二是通信方式,包括http通信和mq异步方式通信,http通信会对正常的访问造成影响,所以还是推荐基于mq异步方式通信。

本文使用mysql作为存储,使用MQ通信,MQ通信基于Spring-cloud-Stream。本文重点不在zipkin-server的具体几种实现方式,其他方式,读者可以自己去官网查看。

(1)pom需要添加的引用如下:

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
复制代码    <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<!--zipkin依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.java</groupId>
<artifactId>zipkin-autoconfigure-ui</artifactId>
<scope>runtime</scope>
</dependency>

<!--保存到数据库需要如下依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

(2)启动类:

1
2
3
4
5
6
7
8
复制代码// 使用Stream方式启动ZipkinServer
@EnableZipkinStreamServer
@SpringBootApplication
public class ZipkinStreamServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZipkinStreamServerApplication.class,args);
}
}

@EnableZipkinStreamServer注解引入了@EnableZipkinServer注解,同时还创建了一个rabbit-mq的SleuthSink消息队列监听器。

(3)配置文件

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
复制代码server:
port: 9411

spring:
datasource:
username: root
password: root123
schema[0]: classpath:/zipkin.sql

zipkin:
storage:
type: mysql

---
spring:
application:
name: microservice-zipkin-stream-server
rabbitmq:
host: ${RABBIT_ADDR:localhost}
port: ${RABBIT_PORT:5672}
username: guest
password: guest
sleuth:
enabled: false
profiles: default
datasource:
url: jdbc:mysql://localhost:3307/zipkin?autoReconnect=true&useSSL=false

zipkin.sql可以去官网获取,设置了zipkin-server的端口号为9411。

3.2 zipkin-client

两个zipkin-client的配置一样,所以放在一起。

(1)pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

(2) 配置文件

1
2
3
4
5
6
复制代码spring:
rabbitmq:
host: 127.0.0.1
port : 5672
username: guest
password: guest

3.3 结果

服务之间的调用关系如下:

可以看到客户端的请求经过gateway,调用内网中的各个服务,部分还涉及到调用notice服务。从图中可以清楚的看出客户端请求所经过的服务。
下面看下demo2-default服务实例中的http path:

上图中demo2-default服务的几个http path按照时长排序,显示了trace调用时长和span数量。点进去可以看到:

图中列出了从父span开始,每一个span的耗时。本次trace中,涉及到两个服务demo1和demo2。demo2调用demo1,从597ms开始调用demo1,完成最终的请求总共耗时1265ms。

  1. pinpoint

对代码零侵入,运用JavaAgent字节码增强技术,只需要加启动参数即可。
pinpoint的几个组件部分和zipkin差不多,架构图如下:

Pinpoint-Collector收集各种性能数据、Pinpoint-Agent和自己运行的应用关联起来的探针、Pinpoint-Web将收集到的数据显示成WEB网页形式、HBase Storage收集到的数据存到HBase中。

4.1 pinpoint安装

主要涉及以下软件的安装:

  • jdk 1.8
    Java环境必须的,没啥好解释。
  • Hbase
    pinpoint收集来的测试数据,主要是存在Hbase数据库的。所以它可以收集大量的数据,可以进行更加详细的分析。Hbase安装完成后,需要初始化Hbase的pinpoint库,由pinpoint提供。Hbase内置了zookeeper。
  • pinpoint-collector
    collector收集agent的数据,将数据存到hbase集群,对外暴露collector的tcp和udp的监听端口9994,9995,9996。
  • pinpoint-web
    页面展示,配置文件中设置环境变量HBASE_HOST、HBASE_PORT等。
  • pinpoint-agent

到官网release页面下载pinpoint-agent-x-SNAPSHOT.tar.gz,配置pinpoint.config中相关collector的信息。

安装确实还比较麻烦,本文篇幅太长了,具体步骤后面再单独写文章讲解。

4.2 运行pinpoint-agent

笔者使用的是spring-boot项目,所以只需要在启动jar包的命令中加入-javaagent参数,并指定pinpoint-bootstrap包的绝对路径。实例代码如下:

1
复制代码java -javaagent:/aoho/auth_compose/pinpoint-bootstrap-1.6.0.jar -Dpinpoint.agentId=aoho-consumer -Dpinpoint.applicationName=aoho-consumer -jar id_generator/snowflake-id-generate-1.0-SNAPSHOT.jar

起的id生成器服务比较简单,没有用到数据库等存储介质。服务注册到consul上,本地客户端请求了id-server获取id。其调用链如下:

call tree

call tree

pinpoint提供的功能比较丰富,下图是调用/api/id接口的详细信息。

api

api

可以看到,pinpoint记录了客户端的相应时间、IP地址等,调用树在下面也有详细列出,每个方法的耗时等。

serverMap

serverMap

serverMap中还展示了服务器的堆、永久代、CPU等信息,非常强大。

  1. Skywalking

Skywalking是国内开源的APM监控组件,官网OpenSkywalking,根据官网介绍,其着力于性能和实时性两方面。
网上找到的Skywalking的架构图。

可以看到Skywalking也是四部分组成,collector、agent、web、storage。支持集群部署,集群之间还引入了grpc通信。存储支持内置的h2和elasticsearch存储。

5.1 安装

具体安装可见官网。

  • collector安装
    此处笔者使用单机版的collector,在release页面下载好压缩包,解压后,单机版的collector默认使用h2数据库,所以配置文件可以不需要修改,即可以运行bin/startup.sh。

目录结构如上,logs文件夹中,有启动的日志,可以查看启动情况。

  • web
    解压好skywalking-ui,设置server的config/collector_config.properties、log4j2以及监听端口等相关信息,
  • agent
    拷贝skywalking-agent目录到所需位置,探针包含整个目录,设置/config/agent.config中的collector信息。

5.2 运行agent

Spring boot的项目,启动和上面pinpoint agent启动方式相同。增加JVM启动参数,-javaagent:/path/to/skywalking-agent/skywalking-agent.jar。

这次起了user服务,涉及到mysql、redis、consul等组件。可以看到其调用链路图如下:

当访问/api/external/register-code和/api/external/validate-code接口时,形成了上图中的调用链。

上图TraceId为 2.59.15103021777910001的请求/api/external/register-code。这次trace中,每个涉及的span的耗时都有在图中统计。

上面这张图,是对userService中的Entry Service List接口进行了统计,包括调用数、成功率等信息。(因为内置的h2,这边在UI响应很久)

还有个对instance的统计,统计jvm的相关信息,API响应也很慢,可能与我用的存储有关吧,就不截图了。

  1. 总结

本文主要写了链路监控组件的实践。首先介绍了链路监控组件产生与应用的背景,以及选择的要求;其次介绍了opentracing中涉及的基本概念;最后大篇幅介绍了三种APM组件的安装与使用,并展示了每种APM的UI截图。本文比较简单,下一篇文章主要介绍几种APM选型的比较与性能测试。

zipkin-server-stream的源码
github: github.com/keets2012/S…
oschina: gitee.com/keets/sprin…

订阅最新文章,欢迎关注我的公众号

微信公众号

微信公众号


参考(疯狂找资料)

  1. OpenTracing官方标准-中文版
  2. Skywalking
  3. Zipkin
  4. PinPoint
  5. Spring Cloud Sleuth
  6. Dapper
  7. pinpoint 安装部署
  8. java开源APM概要
  9. 分布式系统监控系统zipkin入门
  10. 跟着小程学微服务-自己动手扩展分布式调用链

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ViewModel 和 LiveData:为设计模式打 Ca

发表于 2017-11-10
  • 原文地址:ViewModels and LiveData: Patterns + AntiPatterns
  • 原文作者:Jose Alcérreca
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:boileryao
  • 校对者:Zhiw miguoer

ViewModel 和 LiveData:为设计模式打 Call 还是唱反调?

View 层和 ViewModel 层

分离职责

用 Architecture Components 构建的 APP 中实体的典型交互

理想情况下,ViewModel 不应该知道任何关于 Android 的事情(如Activity、Fragment)。 这样会大大改善可测试性,有利于模块化,并且能够减少内存泄漏的风险。一个通用的法则是,你的 ViewModel 中没有导入像 android.*这样的包(像 android.arch.* 这样的除外)。这个经验也同样适用于 MVP 模式中的 Presenter 。

❌ 不要让 ViewModel(或Presenter)直接使用 Android 框架内的类

条件语句、循环和一般的判定等语句应该在 ViewModel 或者应用程序的其他层中完成,而不是在 Activity 或 Fragment 里。视图层通常是没有经过单元测试的(除非你用上了 Robolectric),所以在里面写的代码越少越好。View 应该仅仅负责展示数据以及发送各种事件给 ViewModel 或 Presenter。这被称为 Passive View 模式。(忧郁的 View,哈哈哈)

✅ 保持 Activity 和 Fragment 中的逻辑代码最小化

ViewModel 中的 View 引用

ViewModel 的生命周期跟 Activity 和 Fragment 不一样。当 ViewModel 正在工作的时候,一个 Activity 可能处于自己 生命周期 的任何状态。 Activity 和 Fragment 可以被销毁并且重新创建, ViewModel 将对此一无所知。

ViewModel 对配置的重新加载(比如屏幕旋转)具有“抗性” ↑

把视图层(Activity 或 Fragment)的引用传递给 ViewModel 是有 相当大的风险 的。假设 ViewModel 从网络请求数据,然后由于某些问题,数据返回的时候已经沧海桑田了。这时候,ViewModel 引用的视图层可能已经被销毁或者不可见了。这将产生内存泄漏甚至引起崩溃。

❌ 避免在 ViewModel 里持有视图层的引用

推荐使用观察者模式作为 ViewModel 层和 View 层的通信方式,可以使用 LiveData 或者其他库中的 Observable 对象作为被观察者。

观察者模式

一个很方便的设计 Android 应用中的展示层的方法是让视图层(Activity 或 Fragment)去观察 ViewModel 的变化。由于 ViewModel 对 Android 一无所知,它也就不知道 Android 是多么频繁的干掉视图层的小伙伴。这样有几个好处:

  1. ViewModel 在配置重新加载(比如屏幕旋转)的时候是不会变化的,所以没有必要从外部(比如网络和数据库)重新获取数据。
  2. 当耗时操作结束后,ViewModel 中的“被观察者”被更新,无论这些数据当前有没有观察者。这样不会有尝试直接更新不存在的视图的情况,也就不会有 NullPointerException。
  3. ViewModel 不持有视图层的引用,这大大减少了内存泄漏的风险。
1
2
3
4
5
6
7
8
9
复制代码private void subscribeToModel() {
// Observe product data
viewModel.getObservableProduct().observe(this, new Observer<Product>() {
@Override
public void onChanged(@Nullable Product product) {
mTitle.setText(product.title);
}
});
}

Activity / Fragment 中的一个典型“订阅”案例。

✅ 让 UI 观察数据的变化,而不是直接向 UI 推送数据

臃肿的 ViewModel

能减轻你的担心的主意一定是个好主意。如果你的 ViewModel 里代码太多、承担了太多职责,试着去:

  • 将一些代码移到一个和 ViewModel 具有相同生命周期的 Presenter。让 Presenter 来跟应用的其他部分进行沟通并更新 ViewModel 中持有的 LiveData。
  • 添加一个 Domain 层,使用 Clean Architecture 架构。 这个架构很方便测试和维护,同时它也有助于快速的脱离主线程。 Architecture Blueprints 里面有关于 Clean Architecture 的示例。

✅ 把代码职责分散出去。如果需要的话,加上一个 Domain 层。

使用数据仓库(Data Repository)

就像 Guide to App Architecture(应用架构指南) 里说的那样,大多数 APP 有多个数据源,比如:

  1. 远程:网络、云端
  2. 本地:数据库、文件
  3. 内存中的缓存

在应用中放一个数据层是一个好主意,数据层完全不关心展示层(MVP 中的 P)。由于保持缓存和数据库与网络同步的算法通常很琐碎复杂,所以建议为每个仓库创建一个类作为处理同步的单一入口。

如果是许多种并且差别很大的数据模型,考虑使用多个数据仓库。

✅ 添加数据仓库作为数据访问的单一入口。

关于数据状态

考虑一下这种情况:你正在观察一个 ViewModel 暴露出来的 LiveData,它包含了一个待显示数据的列表。视图层该如何区分被加载的数据,网络错误和空列表呢?

  • 你可以从 ViewModel 中暴露出一个 LiveData<MyDataState> 。 MyDataState 可能包含数据是正在加载还是已经加载成功、失败的信息。

可以将类中有状态和其他元数据(比如错误信息)的数据封装到一个类。参见示例代码中的 Resource 类。

✅ 使用一个包装类或者 LiveData 来暴露状态信息。

保存 Activity 的状态

Activity 的状态是指在 Activity 消失时重新创建屏幕内容所需的信息,Activity 消失意味着被销毁或进程被终止。旋转屏幕是最明显的情况,我们已经在 ViewModel 部分提到了。保存在 ViewModel 的状态是安全的。

但是,你可能需要在其他 ViewModel 也消失的场景中恢复状态。例如,当操作系统因资源不足杀死进程时。

为了高效地保存和恢复 UI 状态,组合使用 onSaveInstanceState() 和 ViewModel。

这里有个示例:ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders

事件

我们管只发生一次的操作叫做事件。 ViewModels 暴露数据,但对于事件怎么样呢?例如,导航事件或显示 Snackbar 消息等应该仅被执行一次的操作。

事件的概念并不能和 LiveData 存取数据的方式完美匹配。来看下面这个从 ViewModel 中取出来的字段:

1
复制代码LiveData<String> snackbarMessage = new MutableLiveData<>();

一个 Activity 开始观察这个字段,ViewModel 完成了一个操作,所以需要更新消息:

1
复制代码snackbarMessage.setValue("Item saved!");

显然,Activity 接收到这个值后会显示出来一个 SnackBar。

但是,如果用户旋转手机,则新的 Activity 被创建并开始观察这个字段。当对 LiveData 的观察开始时,Activity 会立即收到已经使用过的值,这将导致消息再次显示!

在示例中,我们继承 LiveData 创建一个叫做 SingleLiveEvent 的类来解决这个问题。它仅仅发送发生在订阅后的更新,要注意的是这个类只支持一个观察者。

✅ 使用像 SingleLiveEvent 这样的 observable 来处理导航栏或者 SnackBar 显示消息这样的情况

ViewModels 的泄漏问题

响应式范例在 Android 中运行良好,它允许在 UI 和应用程序的其他层之间建立方便的联系。 LiveData 是这个架构的关键组件,因此通常你的 Activity 和 Fragment 会观察 LiveData 实例。

ViewModel 如何与其他组件进行通信取决于你,但要注意泄漏问题和边界情况。看下面这个图,其中 Presenter 层使用观察者模式,数据层使用回调:

UI 中的观察者模式和数据层中的回凋

如果用户退出 APP,视图就消失了所以 ViewModel 也没有观察者了。如果数据仓库是个单例或者是和 Application 的生命周期绑定的,这个数据仓库在进程被杀掉之前都不会被销毁。这只会发生在系统需要资源或用户手动杀死应用程序时,如果数据仓库在 ViewModel 中持有对回调的引用,ViewModel 将发生暂时的内存泄漏。

Activity 已经被销毁了但是 ViewModel 还在苟且

如果是一个轻量级 ViewModel 或可以保证操作快速完成,这个泄漏并不是什么大问题。但是,情况并不总是这样。理想情况下,ViewModels 在没有任何观察者的情况下不应该持有 ViewModel 的引用:

实现这种机制有很多方法:

  • 通过 ViewModel.onCleared() 可以通知数据仓库丢掉对 ViewModel 的回凋。
  • 在数据仓库中可以使用 WeakReference 或者直接使用 Event Bus(二者都很容易被误用甚至可能会带来坏处)。
  • 使用 LiveData 在数据仓库和 ViewModel 中通信。就像 View 和 ViewModel 之间那样。

✅ 考虑边界情况,泄漏以及长时间的操作会对架构中的实例带来哪些影响。

❌ 不要将保存原始状态和数据相关的逻辑放在 ViewModel 中。任何从 ViewModel 所做的调用都可能是数据相关的。

数据仓库中的 LiveData

为了避免泄露 ViewModel 和回调地狱(嵌套的回凋形成的“箭头”代码),可以像这样观察数据仓库:

当 ViewModel 被移除或者视图的生命周期结束,订阅被清除:

如果尝试这种方法,有个问题:如果无法访问 LifecycleOwner ,如何从 ViewModel 中订阅数据仓库呢? 使用 Transformations 是个很简单的解决方法。 Transformations.switchMap 允许你创建响应其他 LiveData 实例的改变的 LiveData ,它还允许在调用链上传递观察者的生命周期信息:

1
2
3
4
5
6
7
复制代码LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);

在这个例子中,当触发器得到一个更新时,该函数被调用并且结果被分发到下游。 当一个 Activity 观察到repo 时,相同的 LifecycleOwner 将用于 repository.loadRepo(id) 调用。

✅ 当需要在 ViewModel 中需要 Lifecycle 对象时,使用 Transformation 可能是个好办法。

继承 LiveData

LiveData 最常见的用例是在 ViewModel 中使用 MutableLiveData 并且将它们暴露为 LiveData 来保证观察者不会改变他们。

如果你需要更多功能,扩展 LiveData 会让你知道什么时候有活跃的观察者。例如,当想要开始监听位置或传感器服务时,这将很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class MyLiveData extends LiveData<MyData> {

public MyLiveData(Context context) {
// Initialize service
}

@Override
protected void onActive() {
// Start listening
}

@Override
protected void onInactive() {
// Stop listening
}
}

什么时候不该继承 LiveData

使用 onActive() 来启动加载数据的服务是可以的,但是如果你没有一个很好的理由这样做的话就不要这样做,没有必要非得等到 LiveData 开始被观察才加载数据。一些通用的模式是这样的:

  • 为 ViewModel 添加 start() 方法,并尽早调用这个方法。 (参见Blueprints example )
  • 设置一个控制启动加载的属性 (参见 GithubBrowserExample )

❌ 通常不用拓展 LiveData。可以让 Activity 或 Fragment 告诉 ViewModel 什么时候开始加载数据。

[^是否需要关于 Architecture Component 的其他任何主题的指导(或意见)?留下评论!]:

感谢 Lyla Fujiwara、Daniel Galpin、Wojtek Kaliciński 和 Florina Muntenescu。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…394395396…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%