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

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


  • 首页

  • 归档

  • 搜索

Spring Cloud Gateway 整合 Spring

发表于 2021-03-12

介绍

Spring-Cloud-Gatewway

Spring Cloud Gateway是基于Spring Boot 2.x,Spring WebFlux和Project Reactor构建的。结果,当您使用Spring Cloud Gateway时,许多您熟悉的同步库(例如,Spring Data和Spring Security)和模式可能不适用。如果您不熟悉这些项目,建议您在使用Spring Cloud Gateway之前先阅读它们的文档以熟悉一些新概念。

Spring-Security

Spring Security是一个提供身份验证,授权和保护以防止常见攻击的框架。凭借对命令式和响应式应用程序的一流支持,它是用于保护基于Spring的应用程序的事实上的标准。

Spring-Webflux

Spring框架中包含的原始Web框架Spring Web MVC是专门为Servlet API和Servlet容器而构建的。响应式堆栈Web框架Spring WebFlux在稍后的5.0版中添加。它是完全无阻塞的,支持 Reactive Streams背压,并在Netty,Undertow和Servlet 3.1+容器等服务器上运行。

这两个Web框架都反映了其源模块的名称(spring-webmvc和 spring-webflux),并在Spring Framework中并存。每个模块都是可选的。应用程序可以使用一个模块,也可以使用两个模块,在某些情况下,也可以使用两个模块,例如,带有react的Spring MVC控制器WebClient。

注意

由于Web容器不同,在Gateway项目中使用的WebFlux,是不能和Spring-Web混合使用的。
Spring MVC和 WebFlux 的区别:
image.png


编码

项目环境版本

  1. Spring-Cloud:2020.0.1
  2. Spring-Boot: 2.4.3

gradle 依赖

1
2
3
4
5
6
java复制代码dependencies {
implementation(
'org.springframework.cloud:spring-cloud-starter-gateway',
'org.springframework.boot:spring-boot-starter-security'
)
}

Spring-Security配置

spring security设置要采用响应式配置,基于WebFlux中WebFilter实现,与Spring MVC的Security是通过Servlet的Filter实现类似,也是一系列filter组成的过滤链。

Reactor与传统MVC配置对应:

webflux mvc 作用
@EnableWebFluxSecurity @EnableWebSecurity 开启security配置
ServerAuthenticationSuccessHandler AuthenticationSuccessHandler 登录成功Handler
ServerAuthenticationFailureHandler AuthenticationFailureHandler 登陆失败Handler
ReactiveAuthorizationManager AuthorizationManager 认证管理
ServerSecurityContextRepository SecurityContextHolder 认证信息存储管理
ReactiveUserDetailsService UserDetailsService 用户登录
ReactiveAuthorizationManager AccessDecisionManager 鉴权管理
ServerAuthenticationEntryPoint AuthenticationEntryPoint 未认证Handler
ServerAccessDeniedHandler AccessDeniedHandler 鉴权失败Handler

1. Security核心配置

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
java复制代码package com.pluto.gateway.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.DelegatingReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.LinkedList;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 10:56
* @description webflux security核心配置类
*/
@EnableWebFluxSecurity
public class WebfluxSecurityConfig {
@Resource
private DefaultAuthorizationManager defaultAuthorizationManager;

@Resource
private UserDetailsServiceImpl userDetailsServiceImpl;

@Resource
private DefaultAuthenticationSuccessHandler defaultAuthenticationSuccessHandler;

@Resource
private DefaultAuthenticationFailureHandler defaultAuthenticationFailureHandler;

@Resource
private TokenAuthenticationManager tokenAuthenticationManager;

@Resource
private DefaultSecurityContextRepository defaultSecurityContextRepository;

@Resource
private DefaultAuthenticationEntryPoint defaultAuthenticationEntryPoint;

@Resource
private DefaultAccessDeniedHandler defaultAccessDeniedHandler;

/**
* 自定义过滤权限
*/
@Value("${security.noFilter}")
private String noFilter;

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity httpSecurity) {
httpSecurity
// 登录认证处理
.authenticationManager(reactiveAuthenticationManager())
.securityContextRepository(defaultSecurityContextRepository)
// 请求拦截处理
.authorizeExchange(exchange -> exchange
.pathMatchers(noFilter).permitAll()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().access(defaultAuthorizationManager)
)
.formLogin()
// 自定义处理
.authenticationSuccessHandler(defaultAuthenticationSuccessHandler)
.authenticationFailureHandler(defaultAuthenticationFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(defaultAuthenticationEntryPoint)
.and()
.exceptionHandling()
.accessDeniedHandler(defaultAccessDeniedHandler)
.and()
.csrf().disable()
;
return httpSecurity.build();
}

/**
* BCrypt密码编码
*/
@Bean("passwordEncoder")
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

/**
* 注册用户信息验证管理器,可按需求添加多个按顺序执行
*/
@Bean
ReactiveAuthenticationManager reactiveAuthenticationManager() {
LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
managers.add(authentication -> {
// 其他登陆方式 (比如手机号验证码登陆) 可在此设置不得抛出异常或者 Mono.error
return Mono.empty();
});
// 必须放最后不然会优先使用用户名密码校验但是用户名密码不对时此 AuthenticationManager 会调用 Mono.error 造成后面的 AuthenticationManager 不生效
managers.add(new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsServiceImpl));
managers.add(tokenAuthenticationManager);
return new DelegatingReactiveAuthenticationManager(managers);
}
}

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
java复制代码package com.pluto.gateway.security;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.io.Serializable;
import java.util.Collection;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/10 13:15
* @description 自定义用户信息
*/
public class SecurityUserDetails extends User implements Serializable {

private Long userId;

public SecurityUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities, Long userId) {
super(username, password, authorities);
this.userId = userId;
}

public SecurityUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, Long userId) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.userId = userId;
}

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}
}
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
java复制代码package com.pluto.gateway.security;

import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.ArrayList;

/**
* @author ceshi
* @date 2021/3/9 14:03
* @description 用户登录处理
* @version 1.0.0
*/@Service
public class UserDetailsServiceImpl implements ReactiveUserDetailsService {

@Resource
private PasswordEncoder passwordEncoder;

@Override
public Mono<UserDetails> findByUsername(String username) {
SecurityUserDetails securityUserDetails = new SecurityUserDetails(
"user",
passwordEncoder.encode("user"),
true, true, true, true, new ArrayList<>(),
1L
);
return Mono.just(securityUserDetails);
}
}

3.1 自定义登录成功Handler

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
java复制代码package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.utils.JwtTokenUtil;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 15:00
* @description 登录成功处理
*/
@Component
public class DefaultAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {

/**
* token 过期时间
*/
@Value("${jwt.token.expired}")
private int jwtTokenExpired;

/**
* 刷新token 时间
*/
@Value("${jwt.token.refresh.expired}")
private int jwtTokenRefreshExpired;

@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
return Mono.defer(() -> Mono.just(webFilterExchange.getExchange().getResponse()).flatMap(response -> {
DataBufferFactory dataBufferFactory = response.bufferFactory();
// 生成JWT token
Map<String, Object> map = new HashMap<>(2);
SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal();
map.put("userId", userDetails.getUserId());
map.put("username", userDetails.getUsername());
map.put("roles",userDetails.getAuthorities());
String token = JwtTokenUtil.generateToken(map, userDetails.getUsername(), jwtTokenExpired);
String refreshToken = JwtTokenUtil.generateToken(map, userDetails.getUsername(), jwtTokenRefreshExpired);
Map<String, Object> tokenMap = new HashMap<>(2);
tokenMap.put("token", token);
tokenMap.put("refreshToken", refreshToken);
DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(ResultVoUtil.success(tokenMap)).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}));
}
}

3.2 自定义登录失败Handler

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
java复制代码package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import com.pluto.common.basic.vo.ResultVO;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Map;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 15:14
* @description 登录失败处理
*/
@Component
public class DefaultAuthenticationFailureHandler implements ServerAuthenticationFailureHandler {

@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
return Mono.defer(() -> Mono.just(webFilterExchange.getExchange()
.getResponse()).flatMap(response -> {
DataBufferFactory dataBufferFactory = response.bufferFactory();
ResultVO<Map<String, Object>> resultVO = ResultVoUtil.error();
// 账号不存在
if (exception instanceof UsernameNotFoundException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_NOT_EXIST);
// 用户名或密码错误
} else if (exception instanceof BadCredentialsException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.LOGIN_PASSWORD_ERROR);
// 账号已过期
} else if (exception instanceof AccountExpiredException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_EXPIRED);
// 账号已被锁定
} else if (exception instanceof LockedException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_LOCKED);
// 用户凭证已失效
} else if (exception instanceof CredentialsExpiredException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_CREDENTIAL_EXPIRED);
// 账号已被禁用
} else if (exception instanceof DisabledException) {
resultVO = ResultVoUtil.failed(UserStatusCodeEnum.ACCOUNT_DISABLE);
}
DataBuffer dataBuffer = dataBufferFactory.wrap(JSONObject.toJSONString(resultVO).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}));
}
}

3.3 自定义未认证Handler

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
java复制代码package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 15:17
* @description 未认证处理
*/
@Component
public class DefaultAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {
return Mono.defer(() -> Mono.just(exchange.getResponse())).flatMap(response -> {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBufferFactory dataBufferFactory = response.bufferFactory();
String result = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.USER_UNAUTHORIZED));
DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
Charset.defaultCharset()));
return response.writeWith(Mono.just(buffer));
});
}
}

3.4 自定义鉴权失败Handler

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
java复制代码package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 11:12
* @description 鉴权管理
*/
@Component
public class DefaultAccessDeniedHandler implements ServerAccessDeniedHandler {

@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {
return Mono.defer(() -> Mono.just(exchange.getResponse()))
.flatMap(response -> {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBufferFactory dataBufferFactory = response.bufferFactory();
String result = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.PERMISSION_DENIED));
DataBuffer buffer = dataBufferFactory.wrap(result.getBytes(
Charset.defaultCharset()));
return response.writeWith(Mono.just(buffer));
});
}
}

4.自定义JWT Token认证管理

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
java复制代码package com.pluto.gateway.security;

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.List;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 16:27
* @description 存储认证授权的相关信息
*/
@Component
public class DefaultSecurityContextRepository implements ServerSecurityContextRepository {

public final static String TOKEN_HEADER = "Authorization";

public final static String BEARER = "Bearer ";

@Resource
private TokenAuthenticationManager tokenAuthenticationManager;

@Override
public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
return Mono.empty();
}

@Override
public Mono<SecurityContext> load(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
List<String> headers = request.getHeaders().get(TOKEN_HEADER);
if (!CollectionUtils.isEmpty(headers)) {
String authorization = headers.get(0);
if (StringUtils.isNotEmpty(authorization)) {
String token = authorization.substring(BEARER.length());
if (StringUtils.isNotEmpty(token)) {
return tokenAuthenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(token, null)
).map(SecurityContextImpl::new);
}
}
}
return Mono.empty();
}
}
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
java复制代码package com.pluto.gateway.security;

import com.pluto.common.basic.utils.JwtTokenUtil;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Collection;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 13:23
* @description token 认证处理
*/
@Component
@Primary
public class TokenAuthenticationManager implements ReactiveAuthenticationManager {

@Override
@SuppressWarnings("unchecked")
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.map(auth -> JwtTokenUtil.parseJwtRsa256(auth.getPrincipal().toString()))
.map(claims -> {
Collection<? extends GrantedAuthority> roles = (Collection<? extends GrantedAuthority>) claims.get("roles");
return new UsernamePasswordAuthenticationToken(
claims.getSubject(),
null,
roles
);
});
}
}

5.自定义鉴权管理

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
java复制代码package com.pluto.gateway.security;

import com.alibaba.fastjson.JSONObject;
import com.pluto.common.basic.enums.UserStatusCodeEnum;
import com.pluto.common.basic.utils.ResultVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collection;

/**
* @author ShiLei
* @version 1.0.0
* @date 2021/3/11 13:10
* @description 用户权限鉴权处理
*/
@Component
@Slf4j
public class DefaultAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext authorizationContext) {
return authentication.map(auth -> {
ServerWebExchange exchange = authorizationContext.getExchange();
ServerHttpRequest request = exchange.getRequest();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
for (GrantedAuthority authority : authorities) {
String authorityAuthority = authority.getAuthority();
String path = request.getURI().getPath();
// TODO
// 查询用户访问所需角色进行对比
if (antPathMatcher.match(authorityAuthority, path)) {
log.info(String.format("用户请求API校验通过,GrantedAuthority:{%s} Path:{%s} ", authorityAuthority, path));
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}).defaultIfEmpty(new AuthorizationDecision(false));
}

@Override
public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) {
return check(authentication, object)
.filter(AuthorizationDecision::isGranted)
.switchIfEmpty(Mono.defer(() -> {
String body = JSONObject.toJSONString(ResultVoUtil.failed(UserStatusCodeEnum.PERMISSION_DENIED));
return Mono.error(new AccessDeniedException(body));
})).flatMap(d -> Mono.empty());
}
}

Github仓库地址(记得切换到dev-1.0.0分支)

欢迎各位热爱编程的小伙伴一起交流进步,记得点个收藏哦!

本文转载自: 掘金

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

Micronaut微服务 实战入门

发表于 2021-03-12

da130d26e1cbe2c1.jpeg

Your story may not have a happy beginning, but that doesn’t make you who you are, it is restof your story, who you choose to be.

你或许没有一个幸福的开始,但是这并不能够代表你的所有,接下来你的生活取决于你的选择。——《功夫熊猫2》

基本概述

92bf8877f200b467.png

既然决定去做一件事情,就好比把自己铺成一张稿纸,不论是信笔涂鸦还是书写酣畅,就尽情去享受和体验就好。每一进步一点,天道酬勤,谁都会是大赢家。尽管是在这个尴尬的二十几岁,正是应该丰富自己和提升自己的时候,去经历一些非议,成长才会更快。就像是缓存清零,重新启动,一切还是原来模样……在基础入门篇时候,简单概述了Micronaut配置操作。接下来,针对于这一方面做更近一步的学习。比如,在开发工具方面的配置,在Micronaut相关命令和API的运用,创建完Demo工程的项目结构解析以及项目构建等。

开发IDE配置

Micronaut开发工具有IntelliJ IDEA ,Eclipse,Visual Studio Code三种。

IntelliJ IDEA配置

[1].增加Micronaut插件IntelliJ IDEA配置
5684cfa51596fa7d.png

[2].对于Maven 和Gradle工具的配置:

f30f526def0533f6.png

创建项目工程

  • Micronaut CLI Project Creation Commands:
Command Description Options Example
create-app Creates a basic Micronaut application -p Command Flags mn create-app my-project –features mongo-reactive,security-jwt –build maven
create-cli-app Creates a command-line Micronaut application. -p Command Flags mn create-cli-app my-project –features http-client,jdbc-hikari –build maven –lang kotlin –test kotest
create-function-app Creates a Micronaut serverless function, using AWS by default. -p Command Flags mn create-function-app my-lambda-function –lang groovy –test spock
create-messaging-app Creates a Micronaut application that only communicates via a messaging protocol. Uses Kafka by default but can be switched to RabbitMQ with –features rabbitmq. -p Command Flags mn create-function-app my-lambda-function –lang groovy –test spock
create-grpc-app Creates a Micronaut application that uses gRPC. -p Command Flags mn create-grpc-app my-grpc-app –lang groovy –test spock

[⚠️注意事项]:
Micronaut创建工程支持的模板如下:
create-app:创建Micronaut基础应用程序
create-cli-app:创建Micronaut命令行应用程序
create-function-app:创建Micronaut函数应用程序,默认使用AWS
create-messaging-app:创建Micronaut消息队列应用程序,默认使用Kafka
create-grpc-app:创建Micronaut分布式GRPC应用程序

  • Create Command Flags:
Flag Description Example
-l, –lang Language to use for the project (one of java, groovy, kotlin - default is java) –lang groovy
-t, –test Test framework to use for the project (one of junit, spock - default is junit) –test spock
-b,–build Build tool (one of gradle, gradle_kotlin, maven - default is gradle for the languages java and groovy; default is gradle_kotlin for language kotlin) –build maven
-f,–features Features to use for the project, comma-separated –features security-jwt,mongo-gorm
-i,–inplace If present, generates the project in the current directory (project name is optional if this flag is set) –inplace

[⚠️注意事项]:
Micronaut创建工程支持的参数如下:
–lang:支持java, groovy, kotlin语言
–test:支持junit, spock测试框架
–build:支持gradle, gradle_kotlin, maven构建工具
–features:支持众多第三方框架
–inplace:支持替换参数

在本地工程目录:/Users/Projects/GitlabCloud/pandora-cloud-platform中:

1
2
3
4
5
6
7
8
9
10
shell复制代码MacBook-Pro:pandora-cloud-platform root$ cd /Users/Projects/GitlabCloud/pandora-cloud-platform
MacBook-Pro:pandora-cloud-platform root$ ls
LICENSE pandora-cloud-gateway
README.en.md pandora-cloud-model
README.md pandora-cloud-platform.iml
pandora-cloud-console pandora-cloud-program
pandora-cloud-core pandora-cloud-schedule
pandora-cloud-dependencies pom.xml
pandora-cloud-framework
MacBook-Pro:pandora-cloud-platform root$

输入:mn create-app com.pandora-cloud-monitor –build maven –lang=java

1
2
3
4
shell复制代码MacBook-Pro:pandora-cloud-platform root$ mn create-app com.pandora-cloud-monitor --build maven --lang=java
| Generating Java project...
| Application created at /Users/Projects/GitlabCloud/pandora-cloud-platform/pandora-cloud-monitor
MacBook-Pro:pandora-cloud-platform root$

工程结构

项目工程机构如下:
ca83664c6a831016.png

  • .gitignore:分布式版本控制系统git的配置文件,意思为忽略提交
    在 .gitingore 文件中,遵循相应的语法,即在每一行指定一个忽略规则。 如:.log、/target/、.idea
  • mvnw:全名是maven wrapper的文件
    它的作用是在maven-wrapper.properties文件中记录你要使用的maven版本,当用户执行mvnw clean 命令时,发现当前用户的maven版本和期望的版本不一致,那么就下载期望的版本,然后用期望的版本来执行mvn命令,比如mvn clean命令。
  • mvn文件夹:存放mvnw相关文件
    存放着maven-wrapper.properties和相关jar包以及名为MavenWrapperDownloader的java文件
  • mvn.cmd:执行mvnw命令的cmd入口
    注:mvnw文件适用于Linux(bash),mvnw.cmd适用于Windows 环境。
  • pom.xml:项目对象模型
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pandora.cloud.generator</groupId>
<artifactId>pandora-cloud-generator</artifactId>
<version>0.1</version>
<properties>
<micronaut.version>1.2.6</micronaut.version>
<jdk.version>1.8</jdk.version>
<maven.compiler.target>${jdk.version}</maven.compiler.target>
<maven.compiler.source>${jdk.version}</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<exec.mainClass>pandora.cloud.generator.Application</exec.mainClass>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<maven-failsafe-plugin.version>2.22.2</maven-failsafe-plugin.version>
</properties>
<repositories>
<repository>
<id>jcenter.bintray.com</id>
<url>https://jcenter.bintray.com</url>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-bom</artifactId>
<version>${micronaut.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-runtime</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>${exec.mainClass}</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>-noverify</argument>
<argument>-XX:TieredStopAtLevel=1</argument>
<argument>-Dcom.sun.management.jmxremote</argument>
<argument>${exec.mainClass}</argument>
</arguments>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<detail>true</detail>
<includes>
<include>%regex[.*]</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-failsafe-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>1.2.6</version>
</path>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
<version>1.2.6</version>
</path>
</annotationProcessorPaths>
</configuration>
<executions>
<execution>
<id>test-compile</id>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>1.2.6</version>
</path>
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
<version>1.2.6</version>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

pom.xml主要描述了项目的maven坐标,依赖关系,开发者需要遵循的规则,缺陷管理系统,组织和licenses,以及其他所有的项目相关因素,是项目级别的配置文件。

  • src:存放开发代码和资源目录
    6cd941b828d50687.png
  • Dockerfile :构建Docker镜像的配置文件
1
2
3
4
yml复制代码FROM adoptopenjdk/openjdk11-openj9:jdk-11.0.1.13-alpine-slim
COPY target/pandora-cloud-generator-*.jar pandora-cloud-generator.jar
EXPOSE 8080
CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar pandora-cloud-generator.jar
  • micronaut-cli.yml:micronaut 应用的配置文件
1
2
3
4
5
yml复制代码profile: service
defaultPackage: pandora.cloud.generator
---
testFramework: junit
sourceLanguage: java

项目启动

Micronaut应用程序的启动方式和Springboot应用启动一样,通过调用Micronaut.run来实现:
e400cd0f0ef1d7f6.png

3d59552697bf8857.png

版权声明:本文为博主原创文章,遵循相关版权协议,如若转载或者分享请附上原文出处链接和链接来源。

本文转载自: 掘金

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

开源表单系统:Tduck 填鸭 —— 表单收集器

发表于 2021-03-12

logo

Tduck 填鸭 —— 表单收集器

平台简介

Tduck, Form collection system

Tduck 填鸭:是一款基于B/S架构的表单收集系统,为企业构建自己的信息反馈系统的综合解决方案,助力企业提高反馈收集客户服务效率。

应用场景

主要应用与泛零售、电商、金融、调研、资料收集等行业用户,提供多种工具、多渠道、多方式收集有效信息,更好的提升客户服务,增加客户满意度。

功能特性

界面美观,全新element-ui支持,使用流畅
  • 以一种全新的设计体验,告别繁琐的设计流程
  • 通过简单有趣的方式,轻轻松松完成问卷设计
  • 支持问卷样式模板选择,只为显现更精美的表单问卷
三大模块助力企业能力升级:
  • 新建表单:自定义可拖拽式表单设计
  • 表单设置:支持多种收集方式设置
  • 反馈统计:多维度统计收集的反馈数据

在线体验

首页

演示地址:
www.tduckcloud.com

演示账号/密码:可注册登录或使用测试账号 test@tduck.com/12345678

技术体系

服务端

1
2
3
4
5
diff复制代码- SpringBoot  2.3.5
- Mybatis-Plus
- Lombok
- Hutool
- Guava

客户端

1
2
3
4
5
diff复制代码- Vue2
- ElementUI
- Echarts
- Axios
- nprogress

后端项目结构

  • tduck-common 通用模块
  • tduck-account 账号模块
  • tduck-storage 存储模块
  • tduck-project 项目模块
  • tduck-wx-mp 微信公众号模块
  • tduck-api 客户端API

快速启动

  1. 配置最小开发环境:
* [MySQL](https://dev.mysql.com/downloads/mysql/)
* [JDK1.8或以上](http://www.oracle.com/technetwork/java/javase/overview/index.html)
* [Maven](https://maven.apache.org/download.cgi)
* [Nodejs](https://nodejs.org/en/download/)
  1. 创建一个tduck的数据库,并执行项目目录下doc/tduck.sql文件
  2. 启动后端服务

打开命令行,输入以下命令

1
2
3
bash复制代码cd tduck-platform/tduck-api
mvn clean package -DskipTests
java -Dfile.encoding=UTF-8 -jar tduck-api.jar
  1. 启动管理后台前端

打开命令行,输入以下命令

1
2
3
4
bash复制代码npm install -g cnpm --registry=https://registry.npm.taobao.org
cd tduck-front
cnpm install
cnpm run serve

此时,浏览器打开,输入网址http://localhost:8888, 进入页面。

项目源码

后端源码 前端源码
gitee.com/TDuckApp/td… gitee.com/TDuckApp/td…

项目规划

tduck登录页
首页
我的表单
自定义编辑
个人中心
目前完成框架,细节正在不断完善中,欢迎提出更多意见~

本文转载自: 掘金

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

你以为在做的是微服务?不!你只是做了个比单体还糟糕的分布式单

发表于 2021-03-12

昨晚睡觉前,顺手撸了几个群聊的聊天记录。发现一个很有意思的名词“分布式单体”,顺藤摸瓜看了一下之前的聊天记录,由于内容骂骂咧咧,我就不贴出来了。。。大致内容就是某公司在做微服务改造,但改的不伦不类,形式上像微服务,而本质上依然是单体,甚至连单体都不如。

这样的改造现象,其实在国内还是蛮多见的。下面就来聊聊这个有趣的话题:分布式单体。各位看官,看看你们公司是不是也犯了这样的错误?

分布式单体为什么不好

先思考一个问题:从单体改造到微服务的时候,你们是不是按这样的步骤来的?

  1. 确定业务领域,拆分存储,定义各微服务的边界
  2. 改造代码逻辑,将原来的内部service调用改成dubbo或feign这样的远程调用

通过这样的改造,我们得到了很多好处,比如:

  1. 代码库分开了,减少了麻烦的解决代码冲突的困扰
  2. CI/CD分开了,每个拆分后的服务都可以独立开发、部署、运行
  3. 数据库分开了,独立运行,不同业务模块不会互相影响

这样一顿操作,我们把一个臃肿的单体应用变成了多个精炼的分布式应用,似乎完美的实现了改造?但这样就实现了微服务的核心目标了吗?继续思考下面的问题:

  1. 代码库是分开了,但每个服务都在独立迭代吗?是不是每个需求都要协调一大堆同步接口?
  2. CI/CD是分开了,但每次发布都是自由的吗?是不是每次功能的发布都拖上了一大推的服务要一起发布?
  3. 数据库是分开了,但似乎有个服务挂了,依然导致很多功能就都不正常了?

看似我们得到了很多好处,但我们的开发效率真的得到了提升吗?虽然我们以前一个单体应用启动要3分钟,现在拆分后,一个项目启动30秒,但每次开发调试要同时开好几个项目同时启动?这样的开发体验真的爽到了吗?

看似完成了微服务改造,实则依然是个单体应用,只是从原本的集中式实现,变成是分布式实现。原来我们只是做了一次无用功,真正的收益微乎其微。

而实际上,这样的改造,除了收益不高之外,实际上还带出了更多的坏处。如果你们公司是这样做的,有没有发现,这样做之后,好像系统故障的频率更高了?稳定性似乎比单体应用还差?(如果没有,那一定要感谢你们的运维团队真的很给力,同时建议把这篇转给运维团队,采访下这样的改造是不是他们变得更累了?!)

为什么这样的改造会导致系统更加不稳定呢?其实道理很简单,原本我们在单体应用中,未拆分的远程调用都是内部调用,这个内部调用所能引发的故障率是微乎其微的,而将这部分内容拆成了远程调用后,每一个调用都增加了网络IO的因素,每一次调用的故障率都增加了。那么系统的整体故障率是随着系统拥有多少同步远程调用的数量增加而增加的。当运维团队与开发水平没有没有支持好这部分增加的复杂度的时候,那么改造的系统,必然的稳定性会比原来的单体应用更差。

所以,这样改造的结果,不但没有得到很多的收益,反而会带来很多稳定性上的损失。本文首发不伦不类的微服务改造:分布式单体 ,禁止未经授权转载。

改造走样的元凶

那么为什么会造成上面所说的问题呢?我觉得主要有两方面:

  1. 领域拆分的不合理,引出了过多的同步远程调用

这个是最根本的问题,也是在改造过程中最常见的。这部分说实话是整个改造过程中最难的,因为需要对业务有非常深入的认识,对系统设计的领域模型、用户行为有足够的理解。在做拆分的时候,尽可能的减少同步远程调用,取而代之的是走消息的异步交互,同时根据业务需要也可以做适当的数据冗余。这样就能保证,每个被拆分后的微服务之间可以获得更低耦合度。

因为更低的耦合度,我们才能在不做任何优化的情况下,获得更少的分布式所带来的稳定性损失。对于后面要将的第2点的工作量也就越少。同时,对于真正的独立开发、部署、运行也成为可能。

  1. 简单粗暴的实现,缺少分布式的保护机制

在很多团队里,因为业务需求多与人员配置少的矛盾之下下,开发人员很容易出现对远程调用不做足够的保护机制,比如:接口提供方的限流策略(保护自己不被别人搞死),接口调用方的降级策略(保护业务更高的可用性),接口调用方的熔断策略(保护自己不被别人拖死)。只有认真对待每一个分布式环境下的依赖点,那么才能解决因为分布式改造所牵连出的诸多问题。

但要做好这一点的核心,还是对第一点的把握,只有在领域模型上做更合理的拆分规划,才能支持开发人员做好这个点,不然随意的拆分,一大堆接口调用压给本就压力很大的开发人员,那这部分的开发质量是很难保障了,自然而然的系统稳定性就开始随着接口复杂度的增加而不断下降了。最后,开发人员就会开始来我们群里吐槽了…甚至大家也开始怀疑微服务根本带不来效率的提升!

最后,思考一下:你们的微服务改在有出现这里我说的情况吗?还是有其他不一样的问题呢?加入我们的Spring技术交流群,聊聊你的观点!

推荐阅读

  • 微服务(Microservices)中文版
  • 《微服务》九大特性重读笔记
  • 云原生应用的12要素

欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源、日常干货及福利赠送。

本文转载自: 掘金

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

MySQL与redis缓存的同步方案

发表于 2021-03-11

公众号:Java小咖秀,网站:javaxks.com

作者 :江南、董少 ,链接: dongshao.blog.csdn.net/article/det…

本文介绍MySQL与Redis缓存的同步的两种方案

  • 方案1:通过MySQL自动同步刷新Redis,MySQL触发器+UDF函数实现
  • 方案2:解析MySQL的binlog实现,将数据库中的数据同步到Redis

一、方案1(UDF)

场景分析:

当我们对MySQL数据库进行数据操作时,同时将相应的数据同步到Redis中,同步到Redis之后,查询的操作就从Redis中查找

过程大致如下:

  • 在MySQL中对要操作的数据设置触发器Trigger,监听操作
  • 客户端(NodeServer)向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数
  • UDF函数可以把数据写入到Redis中,从而达到同步的效果

image.png

方案分析:

  • 这种方案适合于读多写少,并且不存并发写的场景
  • 因为MySQL触发器本身就会造成效率的降低,如果一个表经常被操作,这种方案显示是不合适的

演示案例

  • 下面是MySQL的表

image.png

  • 下面是UDF的解析代码

image.png

  • 定义对应的触发器

image.png

image.png

二、方案2(解析binlog)

  • 在介绍方案2之前我们先来介绍一下MySQL复制的原理,如下图所示:
+ 主服务器操作数据,并将数据写入Bin log
+ 从服务器调用I/O线程读取主服务器的Bin log,并且写入到自己的Relay log中,再调用SQL线程从Relay log中解析数据,从而同步到自己的数据库中

image.png

  • 方案2就是:
+ 上面MySQL的整个复制流程可以总结为一句话,那就是:从服务器读取主服务器Bin log中的数据,从而同步到自己的数据库中
+ 我们方案2也是如此,就是在概念上把主服务器改为MySQL,把从服务器改为Redis而已(如下图所示),当MySQL中有数据写入时,我们就解析MySQL的Bin log,然后将解析出来的数据写入到Redis中,从而达到同步的效果

image.png

  • 例如下面是一个云数据库实例分析:
+ 云数据库与本地数据库是主从关系。云数据库作为主数据库主要提供写,本地数据库作为从数据库从主数据库中读取数据
+ 本地数据库读取到数据之后,解析Bin log,然后将数据写入写入同步到Redis中,然后客户端从Redis读数据

image.png

  • 这个技术方案的难点就在于:如何解析MySQL的Bin Log。但是这需要对binlog文件以及MySQL有非常深入的理解,同时由于binlog存在Statement/Row/Mixedlevel多种形式,分析binlog实现同步的工作量是非常大的

Canal开源技术

  • canal是阿里巴巴旗下的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)
  • 开源参考地址有:github.com/liukelin/ca…
  • 工作原理(模仿MySQL复制):
+ canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
+ mysql master收到dump请求,开始推送binary log给slave(也就是canal)
+ canal解析binary log对象(原始为byte流)

image.png

  • 架构:
+ server代表一个canal运行实例,对应于一个jvm
+ instance对应于一个数据队列 (1个server对应1..n个instance)
+ instance模块:



1
2
3
4
5
6
7
scss复制代码eventParser (数据源接入,模拟slave协议和master进行交互,协议解析)

eventSink (Parser和Store链接器,进行数据过滤,加工,分发的工作)

eventStore (数据存储)

metaManager (增量订阅&消费信息管理器)

image.png

  • 大致的解析过程如下:
    • parse解析MySQL的Bin log,然后将数据放入到sink中
    • sink对数据进行过滤,加工,分发
    • store从sink中读取解析好的数据存储起来
    • 然后自己用设计代码将store中的数据同步写入Redis中就可以了
    • 其中parse/sink是框架封装好的,我们做的是store的数据读取那一步

image.png

  • 更多关于Cancl可以百度搜索
  • 下面是运行拓扑图

image.png

  • MySQL表的同步,采用责任链模式,每张表对应一个Filter。例如zvsync中要用到的类设计如下:

image.png

  • 下面是具体化的zvsync中要用到的类,每当新增或者删除表时,直接进行增删就可以了

image.png

三、附加

  • 本文上面所介绍的都是从MySQL中同步到缓存中。但是在实际开发中可能有人会用下面的方案:
    • 客户端有数据来了之后,先将其保存到Redis中,然后再同步到MySQL中
    • 这种方案本身也是不安全/不可靠的,因此如果Redis存在短暂的宕机或失效,那么会丢失数据

image.png

本文转载自: 掘金

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

数据结构与算法

发表于 2021-03-11

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。

本文是数据结构与算法系列的第 4 篇文章,完整文章目录请移步到文章末尾~

历史上的今天

2000 年 3 月 12 日,万维网诞生的标志性文件出现。
在 CERN(欧洲核子研究中心)工作的伯纳斯·李给上司提交了一份建议书,名为《关于信息化管理的建议》(”Information Management: A Proposal”),它拉开了万维网等待序幕。—— 《了不起的程序员》


前言

  • 高楼丢鸡蛋问题可以说是比较有名的面试题了,最初出自谷歌的面试题。在这篇文章里,我将分享我的思考 & 学习过程,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


  1. 问题描述

这道题在 LeetCode 有对应的题目:887. Super Egg Drop 鸡蛋掉落 【题解】,简单来说:给定 N 层楼和 K 个鸡蛋,要求找到扔下鸡蛋不碎的最高楼层(临界楼层 F),那么最少尝试几次一定能找到这个临界楼层?其中:N≥1N \geq 1N≥1 ,K≥1K \geq 1K≥1 ,0≤F≤N0 \leq F \leq N0≤F≤N,比较常见的情况是 N = 100,K = 2,也就是所谓的双蛋问题。

这道题是在问:最坏情况下最少的尝试次数。

举个例子,有 100 层楼和 1 个鸡蛋,这个时候,无非是在 [1,100] 中任意找第 k 层丢一下,如果没碎说明 F 在 [k,100] 的楼层中,如果碎了,说明 F 在 [1,k - 1] 的楼层中间,但是因为没有更多的鸡蛋了,此时就无法继续找到临界楼层 F 了。

所以,只有一个鸡蛋的时候就要悠着点用了,只能从一楼依次往高楼层尝试,运气最好的情况下,鸡蛋在一楼就碎了,F 就是 1,尝试 1 次;运气最坏情况下,鸡蛋在 100 楼才碎,F 就是 100,尝试 100 次。

由于在尝试之前,临界楼层 F 是未知的。因此,为了兼容 F 的每种情况,只有做最坏的打算(使用最坏情况的尝试次数),才能确保一定能找到临界楼层。否则,假如只给 50 次尝试机会,在 F = 1 时可以找到,但是当 F 属于 [51,100] 时,尝试次数就不够了。


  1. 边界测试用例

我们先看分析一些简单的边界条件:

  • N = 1

只有一层楼的情况,只需要尝试 1 次就可以知道 F = 0 还是 F = 1;

  • K = 1,N = *

只有一个鸡蛋,从最低层往最高层线性搜索,最少尝试次数等于楼层数 N,这个结论我们在 第 1 节,已经讨论过,相信你已经明白了;

  • K > N

鸡蛋数大于等于楼层数的情况,其他文章里会说成鸡蛋数无穷大,其实不需要那么多鸡蛋,只要足够每一层都丢一次就行。这种情况可以使用很常见的二分搜索算法,即:从中间楼层丢一下,如果碎了则下楼,如果没碎则上楼,每次尝试后楼层数都少了一半,知道最后只剩下一层楼,如果没碎它就是 F ,否则它的下一层是 F。

将整个过程画成一棵递归树如下图所示:

那么,总共尝试了多少步呢?其实,尝试步数就刚好等于这棵树的高度。由于叶子节点有 101 个(0≤F≤N0 \leq F \leq N0≤F≤N),所以树的高度 h 要满足:2h>=1012^h >= 1012h>=101 才能保证覆盖 F 的每种情况,即:h>=log⁡2101≈6.66h >= \log_2 101 \approx 6.66h>=log2101≈6.66,向上取整,结论是最少需要尝试 7 次。

提示: 看了李永乐老师那期视频的同学,也许会认为这里讲错了。需要注意的是,在李永乐老师描述的那道题里,1≤F≤N1 \leq F \leq N1≤F≤N,自然树的高度 h>=log⁡2Nh >= \log_2 Nh>=log2N;而 LeetCode 这道题目描述 F 可以等于 0, 多了一个 0 节点,所以应 h>=log⁡2(N+1)h >= \log_2 (N + 1)h>=log2(N+1)

  • K = 0
    没有更多鸡蛋,很明显问题无解,虽然题目已经明确告知 K > 0,但是我们要留意我们的算法中会不会出现 K = 0 的情况

  1. 双蛋问题

最常见的问法是 N = 100,K = 2,也是就所谓的双蛋问题。跟只有 1 蛋相比,2 个鸡蛋可谓是富足,这个时候就没必要如履薄冰地线性搜索了,步子可以大一点。比如说:

  • 等间隔丢

第一个鸡蛋可以每隔 10 层丢一次:10、20、30…100,如果碎了第二个鸡蛋再从前面的 9 层线性搜索。比如说第一个蛋在 10 层碎了,那么第二个蛋就在 [1,9] 之间试,也就是:

第一个鸡蛋尝试:10 20 30 40 50 60 70 80 90 100(最多尝试 10 次)
第二个鸡蛋最多尝试 9 次

因此,总的来说,最好的情况是第一个蛋在第 10 层就碎了,总次数是 10 次,最坏的情况是第一个蛋在第 100 层碎,总的次数就是 19 次。最好和最坏的情况相差比较大,这是因为第一个蛋每次都是等间隔丢,所以第二个蛋丢的时候,无论如何最坏都要尝试 9 次。

  • 不等间隔丢

使用等间隔丢的方法,如果间距取得比较大,当第一个蛋碎的时候,第二个蛋要试的次数就比较大;当间距取比较小的时候,当 F 的位置越靠后,第一个蛋要试的次数就越大。

有没有办法让两个蛋丢的次数均衡一下呢,试试刚开始的时候间距取大一些,越往后间距逐渐缩小。即:第一次的间隔 为 n,如果没碎第二次的间隔为 n…,一直到最后一层间隔为 1。使用高斯公式可以知道n∗(n+1)/2=100n*(n+1)/2 = 100n∗(n+1)/2=100,则n≈13.65n \approx 13.65n≈13.65,向上取整 n 等于 14,也就是:

第一个鸡蛋尝试:14 27 39 50 60 89 77 84 90 95 99 100(最多尝试12次)
第二个鸡蛋最多尝试的区间是 [1,13],一共是13次

因此,总的来说,最好的情况是 12 次,最坏的情况是 14次。相对于等间距的 10 - 19 次要平均一些了,最坏情况的次数也更少。


  1. 问题建模

经过前面的讨论,我们已经找到了尝试 14 次一定能解决双蛋问题的算法,但是这种算法就一定是最好的吗?为了验证 & 找到最好的方法,我们应使用动态规划去思考这个问题,对于动态规划我们已经有了一套解题模板了,一步步来呗:

第一步:定义问题:

回到最初的问题,给定 NNN 层楼和 KKK 个鸡蛋,要求找到扔下鸡蛋不碎的最高楼层(临界楼层 FFF),那么最少尝试几次一定能找到这个临界楼层?我们可以定义问题如下:给定输入 NNN 、KKK,输出为最少尝试次数 YYY,即:
Y=dp(N,K)Y = dp(N,K)Y=dp(N,K)

假设在第 iii 楼尝试,会存在两种情况(碎和不碎):

  • 如果碎了,需要在低楼层 [1,i−1][1,i - 1][1,i−1] 搜索,问题规模缩小为:y1=dp(i−1,K−1)y_1 = dp(i - 1,K - 1)y1=dp(i−1,K−1)
  • 如果没碎,需要在高楼层 [i−1,N][i - 1,N][i−1,N] 搜索,问题规模缩小为:y2=dp(N−i,K)y_2 = dp(N - i,K)y2=dp(N−i,K)

提示: [1,10][1,10][1,10] 层 2 个鸡蛋 和 [11,20][11,20][11,20] 层 2个鸡蛋,两个问题是等价的,都是Y=dp(10,2)Y = dp(10,2)Y=dp(10,2),问题的关键是楼层数量和鸡蛋个数,而不是楼层编号,很好理解,对吧。

因此,对于在第 i 楼的尝试,最坏情况下的尝试次数 Yi=max(y1,y2)Y_i = max(y_1,y_2)Yi=max(y1,y2)。而 iii 可以在 [1,N][1,N][1,N] 中选择一个,根据题意,我们要找出这 NNN 种选择里最少的尝试次数,即:

Y=min⁡1≤i≤NYi+1=min⁡1≤i≤N(max{dp(i−1,K−1),dp(N−i,K)})+1Y = \min\_{1\leq i \leq N} Y\_i + 1 = \min\_{1\leq i \leq N} (max{\{dp(i - 1,K - 1),dp(N - i,K)\})} + 1Y=min1≤i≤NYi+1=min1≤i≤N(max{dp(i−1,K−1),dp(N−i,K)})+1

提示:加 1 是因为划分子问题的时候也丢了一次。

第二步:检查重叠子问题:

这个问题是存在重叠子问题的,例如前面说的 [1,10] 层 2 个鸡蛋 和 [11,20] 层 2个鸡蛋,两个问题是等价的,问题的关键是楼层数量和鸡蛋个数,而不是楼层编号。假如我们曾经计算过函数值 YN=10,K=2Y_{N=10,K=2}YN=10,K=2,那么下一次遇到 N = 10,K = 2 的问题,就可以直接返回前者的答案。

为此,我们需要使用 “备忘录” 把前者的答案记忆起来。用程序实现无非是基于数组或者基于散列表,这里使用二维数组即可:

1
2
3
4
scss复制代码K 行 N 列(鸡蛋的数目较少,作为行)
val dp = Array(K + 1) {
IntArray(N + 1) { -1 }
}

提示: dp 数组的初始值可以是 0、-1 或其它,根据具体问题选择最方便的数值即可。通常来说,-1 带有:“这里有个值,但是不会真正去读取它” 这样的含义。

第三步:尝试编码:

到这里,基本可以写代码了。可以看到,除开边界代码,这么复杂的问题的核心的部分不过 10 行 【题解】:

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
kotlin复制代码class Solution {
fun superEggDrop(K: Int, N: Int): Int {

// K 行 N 列(鸡蛋的数目较少,作为行)
val dp = Array(K + 1) {
IntArray(N + 1) { -1 }
}

fun dp(K: Int, N: Int): Int {
// 1、边界测试用例
if (N <= 2) {
// 边界1. 空楼/一层楼/两层楼的情况
return N
}
if (K == 1) {
// 边界2. 只有一个鸡蛋,只能线性搜索
return N
}
if (K >= N) {
// 边界3,鸡蛋“充足”,使用二分搜索
return log2(N + 1) // 树的高度要能覆盖[0,N] 共 N + 1 个叶子节点
}
if (K == 0) {
// 边界4,没有鸡蛋
return 0
}

// 2、重叠子问题
if (-1 != dp[K][N]) {
return dp[K][N]
}
// 3、动态规划
var y_k_n = Integer.MAX_VALUE
for (i in 1..N) {
val y_i = Math.max(
dp(K - 1, i - 1),// 碎 -> [1,i - 1]
dp(K, N - i) // 没碎 -> [i - 1, N]
)
y_k_n = Math.min(y_k_n, y_i)
}
// 加一是因为划分子问题时丢了一次
return (y_k_n + 1).apply {
dp[K][N] = this
}
}
return dp(K, N).apply{
for(n in 0 .. K)dp(i
System.out.println("n = $n = " + dp[n].joinToString())
}
}
}

// 以 2 为底的对数
private fun log2(n: Int) = Math.ceil(Math.log(n.toDouble()) / Math.log(2.0)).toInt() // 向上取整
}

我们分析下算法复杂度:

  • 时间复杂度
    这是一个递归问题,递归问题的时间复杂度 = 子问题个数 x 递归函数本身的时间复杂度。其中,子问题个数就是不同 N & K的状态组合,总共有 NKNKNK 种;而递归函数里有一个 for 循环,每一轮循环里比较了碎与不碎两种情况的最大值,因此递归函数本身的时间复杂度是O(n)O(n)O(n),综上,总的时间复杂度就是O(KN2)O(KN^2)O(KN2)。
  • 空间复杂度
    使用了二维数组,空间复杂度是 O(KN)O(KN)O(KN)。

可以看到,这个解法的时间复杂度是O(n2)O(n^2)O(n2)级的,比较高,无法通过全部测试用例,需要想优化方法。

image.png

第四步:优化:

基本优化的方法就是对递归树进行剪枝,在这篇文章里,我们专门总结过:数据结构与算法 #6 回溯算法解题框架总结,请关注!一步步来呗:

  • 重复状态

两个完全相同的状态最终得到的结果一定是一样的。这道题确实是存在重复状态,正如前面分析的,楼层数量和鸡蛋个数相同的问题,答案是一样。重复状态在 第二步:检查重叠子问题 已经优化过了。

  • 最终解确定

当我们在一个选择的分支中确定了最终解后,就没必要去尝试其他的选择了。这道题没有找到这样的判断方法。

  • 无效选择

当我们可以根据已知条件判断某个选择无法找到最终解时,就没必要去尝试这个选择了。那么,有这样的已知条件吗?有,比较隐晦的。观察上一节我们定义的问题:

Y=dp(N,K)=min⁡1≤i≤NYi+1Y = dp(N,K)= \min_{1\leq i \leq N} Y_i + 1Y=dp(N,K)=min1≤i≤NYi+1

在 dpdpdp 函数里,参数是 NNN 和 KKK,因为我试图解决任意楼层任意鸡蛋数的问题,对吗。但是对于具体的某一个问题(比如双蛋问题),我们就应该把 NNN 和 KKK 看作常量,为了方便你理解,下面我会用NcN_cNc 和 KcK_cKc 表示,提醒你这是个常量,即:

Y=f(i)=min⁡1≤i≤NcYi+1Y = f(i) = \min_{1\leq i \leq N_c} Y_i + 1Y=f(i)=min1≤i≤NcYi+1

可以看出,这个问题里的变量其实只有一个 iii,表示第一步的选择楼层(最开始输入的最大规模问题),YiY_iYi是一个递归的问题,表示选择第 iii 后续选择的的尝试次数。而我们的问题就要找出 iii ,使得它对应的 YiY_iYi 最小,用坐标轴表示可能比较清晰:

在上一份的代码中,我们的做法是求出从 [1,N] 所有的 YiY_iYi的值,从中找出它们的最小值,即:

1
2
3
4
ini复制代码for (i in 1..N) {
val y_i = ...
y_k_n = Math.min(y_k_n, y_i)
}

这并没有错,只是效率并不是太高的。思考一个问题,给定两组数据,一组数据是从大到小有序排列的,一组数据大小是杂乱无章的,分别求出两个数组中的最小值。聪明的你一定想到有序的数据可以使用二分搜索对吧。

那么,我们这个问题是属于有序的数据还是杂乱无章的数据呢?是的,还真的是有序的数据。观察 YiY_iYi 的函数:

Yi=max(y1,y2)=max{dp(i−1,Kc−1),dp(Nc−i,Kc)}Y_i = max(y_1,y_2) = max{dp(i - 1,K_c - 1),dp(N_c - i,K_c)}Yi=max(y1,y2)=max{dp(i−1,Kc−1),dp(Nc−i,Kc)}

可以看到,YiY_iYi取决于dp(i−1,Kc−1)dp(i - 1,K_c - 1)dp(i−1,Kc−1) 和 dp(Nc−i,K)dp(N_c - i,K)dp(Nc−i,K) 的值,取较大的那个。而NcN_cNc 和 KcK_cKc都是常量,所以两者的最大值依旧是关于 iii的函数:

  • y1=dp(i−1,Kc−1)y_1 = dp(i - 1,K_c - 1)y1=dp(i−1,Kc−1):随着 iii 增大,是一个楼层增大的问题,函数值是单调递增的
  • y2=dp(Nc−i,K)y_2 = dp(N_c - i,K)y2=dp(Nc−i,K):随着 iii 增大,是一个楼层减少的问题,函数值是单调递减的

提示: 100层楼 2 个鸡蛋的尝试次数,不可能小于 99 层 2个鸡蛋的尝试次数,对吗。这里就不证明了,比较直观的结论。

用坐标轴表示可能比较清晰:

这不就是找出数据中的峰值问题吗?162. Find Peak Element 寻找峰值 【题解】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码fun findPeakElement(nums: IntArray): Int {

var left = 0
var right = nums.size - 1

while (left < right) {
val mid = (left + right) ushr 1 // 左中位数
if (nums[mid] < nums[mid + 1]) {
// 如果 mid 严格小于右边一位的值,那么 mid 一定不是峰值,并且峰值在右侧
left = mid + 1
} else {
right = mid
}
}
return left
}

我们要做的其实就是找到 YiY_iYi 函数的谷底:即取区间的中间点 midmidmid,比较 brokenbrokenbroken 和 not_brokennot\_brokennot_broken 两者的大小,如果 brokenbrokenbroken 严格小于 not_brokennot\_brokennot_broken,说明 midmidmid 一定不是谷底,并且谷底在右侧,反之在左侧,参考代码如下 【题解】:

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
kotlin复制代码class Solution {
fun superEggDrop(K: Int, N: Int): Int {

// K 行 N 列(鸡蛋的数目较少,作为行)
val dp = Array(K + 1) {
IntArray(N + 1) { -1 }
}

fun dp(K: Int, N: Int): Int {
// 1、边界测试用例
if (N <= 2) {
// 边界1. 空楼/一层楼/两层楼的情况
return N
}
if (K == 1) {
// 边界2. 只有一个鸡蛋,只能线性搜索
return N
}
if (K >= N) {
// 边界3,鸡蛋“充足”,使用二分搜索
return log2(N + 1) // 树的高度要能覆盖[0,N] 共 N + 1 个叶子节点
}
if (K == 0) {
// 边界4,没有鸡蛋
return 0
}

// 2、重叠子问题
if (-1 != dp[K][N]) {
return dp[K][N]
}

// 3、动态规划 + 二分搜索找谷底
var left = 1
var right = N
var y_k_n = Integer.MAX_VALUE

while (left < right) {
val mid = (left + right) ushr 1 // 左中位数

val broken = dp(K - 1, mid - 1) // 碎 -> [1,mid - 1]
val no_broken = dp(K, N - mid) // 没碎 -> [mid - 1, N]

if (broken < no_broken) { // 如果 broken 严格小于 no_broken,那么 mid 一定不是谷底,并且谷底在右侧
left = mid + 1
y_k_n = Math.min(y_k_n, no_broken)
} else {
right = mid
y_k_n = Math.min(y_k_n, broken)
}
}
// 加 1 是因为划分子问题时丢了 1 次
return (y_k_n + 1).apply {
dp[K][N] = this
}
}
return dp(K, N)
}

// 以 2 为底的对数
private fun log2(n: Int) = Math.ceil(Math.log(n.toDouble()) / Math.log(2.0)).toInt() // 向上取整
}

到这里,我们的动态规划解法就完成了,输出的 dp 数组里明显多了很多 -1 ,这说明改进后的程序跳过了很多无效的选择。

我们分析下算法复杂度:

  • 时间复杂度

由于很多无效的选择被剪枝了,所以子问题的个数一定是严格小于 NKNKNK 个;至于递归函数本身,使用二分查找的时间复杂度是O(lgn)O(lgn)O(lgn),综上,总的时间复杂度是O(KN∗logN)O(KN*logN)O(KN∗logN),这只是一个不太紧的上界。

  • 空间复杂度
    使用了二维数组,空间复杂度是 O(KN)O(KN)O(KN)。

在 《LeetCode 官方的题解》中,还提出了时间复杂度为O(K∗N)O(K∗N)O(K∗N)的解法,不在我的能力范围内,有兴趣可以一起讨论。


本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情


参考资料

  • 《复工复产找工作?先来看看这道面试题:双蛋问题》 —— 李永乐老师 讲
  • 《liwewei 的题解》 —— liwewei 著
  • 《LeetCode 官方的题解》 —— LeetCode
  • 《经典动态规划问题:高楼扔鸡蛋》 —— labuladong 著
  • 《高楼扔鸡蛋问题进阶解法》 —— labuladong 著

推荐阅读

数据结构与算法系列完整目录如下(2023/07/11 更新):

  • #1 链表问题总结
  • #2 链表相交 & 成环问题总结
  • #3 计算器与逆波兰表达式总结
  • #4 高楼丢鸡蛋问题总结
  • #5 为什么你学不会递归?谈谈我的经验
  • #6 回溯算法解题框架总结
  • #7 下次面试遇到二分查找,别再写错了
  • #8 什么是二叉树?
  • #9 什么是二叉堆 & Top K 问题
  • #10 使用前缀和数组解决 “区间和查询” 问题
  • #11 面试遇到线段树,已经这么卷了吗?
  • #12 使用单调队列解决 “滑动窗口最大值” 问题
  • #13 使用单调栈解决 “下一个更大元素” 问题
  • #14 使用并查集解决 “朋友圈” 问题
  • #15 如何实现一个优秀的 HashTable 散列表
  • #16 简答一波 HashMap 常见面试题
  • #17 二叉树高频题型汇总
  • #18 下跳棋,极富想象力的同向双指针模拟

Java & Android 集合框架系列文章: 跳转阅读

LeetCode 上分之旅系列文章:跳转阅读

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

掰碎了的正则表达式 Java 篇

发表于 2021-03-11

总文档 :文章目录

Github : github.com/black-ant

一 . Get Start

正则测试网站 : regex101.com/

Git 案例 : Git 不易 , 给个Star 吧

二 . 知识点

2.1 标识符

2.1.1 开始结束

1
2
3
4
java复制代码^ 表示开始
$ 表示结束

/^a.*b$/ -- : 以a开始 , 以b结束

2.1.2 元字符符号

符号 含义 补充
\d 配置一个数字字符 [0~9] digit
\D 匹配一个非数字字符
\w 匹配一个单词字符 word
\W 匹配一个非单词字符
\s 匹配一个空格 space
\S 匹配一个非空格
. 匹配换行符以外的任意字符
[^x] 匹配除了x 以外的任意字符
[first-last] 与从 first 到 last 的范围中的任意单个字符匹配 [A-Z]
[abc] 匹配 其中的任何单个字符
\p{name} 与 name 指定的 Unicode 通用类别或命名块中的任何单个字符匹配
\P{name} 与不在 name 指定的 Unicode 通用类别或命名块中的任何单个字符匹配

2.1.3 定位字符

符号 含义 补充
^ 匹配字符串的开始
$ 匹配字符串的结束
\A 匹配必须出现在字符串的开头
\Z 匹配必须出现在字符串的末尾或出现在字符串末尾的 \n 之前。
\b 配置单词的开始或者结束
\B 配置不是单词开头或者结束的位置
\G 上一个匹配结束的地方
\z 匹配必须出现在字符串的末尾 -\d{3}\z -901-333”中的“-333
\A 匹配必须出现在字符串的开头 \A\d{3} 901-333-“中的“901

2.1.4 量词符号

符号 作用 补充
+ 表示前面字符的一次或多处出现 1 ~ n / 前面字符
* 表示前面字符的零次或者多次出现 0 ~ n
? 表示前面字符可能出现 , 也可能不出现 0 ~ 1
{m,n} 表示出现次数从 m 到 n m <= x <= n
{m} 表示一定且必须出现三次 x = m
{m , } 表示至少出现 m 次 m <= x
{0 , } 至少出现 0 次 无意义
{0 , 1} 出现 0 次 或者 1 次
() 分组 ,将多种规则进行分组表示 见后文详细介绍

2.1.5 其他符号

符号 作用 补充
[] 多选 , 表示匹配其中的一个字符 类似于or 概念
() 分组 详情可看后文

2.2 使用方式

2.2.1 分组

分组案例 详见 Git

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
java复制代码// 使用方式
通过小括号来指定子表达式,然后再指定子表达式的相关设置

// 组的切分
group 通过 () 来包含 , 每一个 () 代表一个组 , 组由外向内 , 由前向后 编号 , 分组的边好主要为了引用
- 0 是特殊分组 , 是整个表达式

// 分组的作用:
- 组可以在正则中进行使用 , 使用 matcher2.group(i) 可以获取分组对应的匹配结果
- 分组可以通过反向引用实现不同的效果

// 分组的常规概念 :
1 正常分组 : a(bc)d , 其中 bc 就是一个分组
2 分组配置数量 : ^a(bc){2}s$ --> abcbcs
3 嵌套分组 : ^a(b(c)?){2}s$ --> abcbcs / abcbs / abbs
?- 嵌套分组时 多个括号嵌套
?- 其中有2个组 (c) , 用 ? 表示可能出现 , (b(c)?) 用 {2} 表示该组会出现2此
4 捕获分组 :
?- 分组0是一个特殊分组,内容是整个匹配的字符串
?- 每次分组后分组匹配的子字符串可以在后续访问,好像被捕获了一样
5 分组多选 (http|ftp|file)
? () + | 表示可以匹配其中一个分组即可
? ^a(bc|ef|g){2}s$ -> abcbcs / abcgs

// 分组的理解
- 分组的括号如果没有量词匹配或者没有特殊含义匹配 , 可以忽略括号 : (ab) = ab

2.2.2 分组的演示

1
2
3
4
5
6
7
8
java复制代码// 案例一 : group 在没有量词匹配时 , 不具有含义 
String source0 = "aaaa bbbb ccco";
Pattern pattern0 = Pattern.compile("\\w*?\\s");
Pattern pattern01 = Pattern.compile("(\\w)*?\\s");

// 案例二 : Group 匹配量词
String pattern = "^a(bc){2}s$";
logger.info("------> this abf is {} <-------", Pattern.matches(pattern, "abcbcs"));

group001.png

2.2.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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码// 引用是需要配合分组共同使用的 , 再次说明分组的规则
- 分组 0 对应整个表达式
- 顺序 由左到右
- 命名的组号大于未命名的组号(第一遍给未命名的分组,第二遍给命名的分组)

// 什么才是捕获 ?
有一点需要提前理解 , 分组等同于在大表达式中生成了一个小表达式 , 但是捕获不是说按照小表达式再去匹配一次
而是说!!! -> 把整个文本中 , 符合小表达式的那一段单独提取出来 , 例如下面的代码


Pattern pattern03 = Pattern.compile("(a+)\\s(b+)\\s(c+)");
String source03 = "aaaa bbbb cccc";
Matcher matcher03 = pattern03.matcher(source03);
if (matcher03.matches()) {
System.out.println("group 0" + ":" + matcher03.group(0));
System.out.println("group 1" + ":" + matcher03.group(1));
System.out.println("group 2" + ":" + matcher03.group(2));
System.out.println("group 3" + ":" + matcher03.group(2));
}

// ---- 打印结果 , 1 ,2 ,3 分别对应匹配的字符中和group 对应的数据
group 0:aaaa bbbb cccc
group 1:aaaa
group 2:bbbb
group 3:bbbb

// 捕获能做什么 -> 例如 , 你有一系列邮箱 , 你想知道邮箱的类型分别有哪些 ?
Pattern pattern01 = Pattern.compile("([^@]+)?@([^@]+)?,");
Matcher matcher01 = pattern01.matcher(source);
List<String> userList = new ArrayList<>();
List<String> emailList = new ArrayList<>();
while (matcher01.find()) {
userList.add(matcher01.group(1));
emailList.add(matcher01.group(2));
}

logger.info("------> 邮箱用户有 : {} <-------", userList.toString());
logger.info("------> 邮箱类型有 : {} <-------", emailList.toString());
邮箱用户有 : [123, 124, 125, 126, 128, 129] <-------
邮箱类型有 : [qq.com, qq.com, qq.com, qq.com, 163.com, 163.com] <-------

// 捕获类型 : 捕获包括下方表格的前三行 , 主要的作用是捕获
- (?<name>exp) -> 正常分组使用 0 , 1 , 2 命名 ,该方式可以通过 name 命名
类型 特殊语法 说明 exp : 需要处理的文本
正常分组 (exp) 捕获括号中的值,并且自动命名
非捕获分组 ( ?: exp ) 匹配exp , 但是不捕获文本,也不分配组号
捕获并且命名 (?<name>subexpression) (?’name‘subexpression) 匹配exp,并捕获文本到名称为name的组里,也可以写成(?’name’exp)
平衡组 (?<name1-name2>subexpression)(?’name1-name2‘subexpression)
原子分组 ( ?> exp ) 原子分组是贪婪的匹配,当文本和这个分组匹配的成功后正则表达式引擎在匹配后面的表达式时不会发生回溯行为及尽可能多的匹配
正前向查找分组零宽度正预测先行断言 ( ?= exp ) 在正前向分组里面的表达式匹配成功后正则表达式引擎回溯到正前向分组开始匹配的字符处再进行后面正则表达式的匹配如果后面的正则表达式也匹配成功,整个匹配过程才算成功
负前向查找分组零宽度负预测先行断言 ( ?! exp ) 这种分组功能和正前向查找分组一样唯一的不同就是当前向查找分组里面的正则表达式匹配失败的时候才继续后面的匹配过程
正后向查找分组零宽度正回顾后发断言 ( ? <= exp ) 可以理解成在正后向查找分组前面的正则表达式匹配成功后正则表达式引擎从最后的位置往字符串左边进行回溯然后和(?<=regex)进行匹配如果匹配失败则整个匹配过程失败;如果匹配成功,则将指针移动到正后向查找分组开始进行匹配的位置继续进行后面正则表达式的匹配过程
负后向查找分组零宽度负回顾后发断言 ( ? < ! exp ) 这种分组功能和正负向查找分组一样唯一的不同就是当负后向查找分组里面的正则表达式匹配失败的时候才继续后面的匹配过程
(#comment) 文本注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码// 查找断言 : 基于断言在断言前面或者后面匹配

// 解释 :
这个实现要理解断言的目的 , 断言不是为了匹配 ,断言是坐标 , 用于前后方的匹配

// 案例一 : 原子分组
String source3 = "543543 bcc";
Pattern pattern3 = Pattern.compile("(\\d+)\\s+(?>bc|b)(\\w)");
group 1:543543
group 2:c

// 案例二 : 正前向查找分组 ,会先匹配分组前面的正则 , 再匹配分组后的正则
\w+(?=ing) + I'm singing while you're dancing = sing / danc
Pattern pattern5 = Pattern.compile("(\\d+)\\s+(?=s)(\\w+)");
String source5 = "543543 streets";
group 1:543543
group 2:streets

// !! 这个过程很难用语言说清楚 , 所有的步骤都在该显目中 , 建议自行摸索

2.2.5 反向引用

表达式 作用 案例
\ number 匹配Group 编号 (\w)\1
\k 命名后向引用。 匹配命名表达式的值 (?\w)\k
1
复制代码

2.2.6 贪婪 懒惰

1
2
3
java复制代码// 贪婪的主要表现形式是 ? 
// 贪婪表达式的意思在于一个正则是以何种匹配数量来匹配,包括最大匹配和最小匹配
- 例如 /d* -> 0000 / 00000000
标识 作用
*? 重复任意次,但是尽可能少
+? 重复一次或者更多次,尽可能少
?? 重复 0 -1 次 ,尽可能少
{n,m}? 重复 n 到 m 次 ,尽可能少
{n,}? 重复 n 次以上,尽可能少
1
2
3
4
5
6
7
8
9
10
11
java复制代码// 注意事项 : 
当量符和 ? 交替使用时 , 一定要注意
正则默认时贪婪的 ,通过 ? 表示懒惰匹配

// 演示 : 懒惰是最小匹配
贪婪 -> Pattern.compile(".?o"); -> zoboco
懒惰 -> Pattern.compile(".*?o"); -> zo / bo / co
Matcher matcher123 = pattern123.matcher("zoboco");
while (matcher123.find()) {
System.out.println(matcher123.group(0));
}

2.2.7 转义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码\\ : 用于字符转义

\\\\ : 用于匹配 \\ 本省

! 转义的操作可能超过你的想象 ,它还可以配合多进制 , Unicode 码 ,ASCII 码使用

// \\040 八进制匹配空格 \ nnn
String source = "a bc d";
Pattern pattern01 = Pattern.compile("\\w\\040\\w");
Matcher matcher01 = pattern01.matcher(source);
while (matcher01.find()) {
System.out.println(matcher01.group(0));
}

// 十六进制 Unicode \\u nnnn
String source2 = "a bc d";
Pattern pattern2 = Pattern.compile("\\w\\u0020\\w");
Matcher matcher2 = pattern2.matcher(source2);
while (matcher2.find()) {
logger.info("------> 十六进制 Unicode source [{}] group [{}]<-------", source2, matcher2.group(0));
}

转义 Git 案例

2.2.8 替代

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码// 使用 : $num -> Group Num

// ------> this is response :John Doe <-------
public void replaceSample() {
String source = "Doe, John";
Pattern pattern1 = Pattern.compile("(\\w+),\\s*(\\w+)");
Matcher matcher1 = pattern1.matcher(source);
String response = matcher1.replaceAll("$2 $1");
logger.info("------> this is response :{} <-------", response);
}

// 从案例可以看出来 , 替换中 $2 $1 分别指向的 Group2 Group1
符号 含义 补充
$num 通过组编号替换
$name 通过 group Name 匹配替换

2.2.9 注释

1
2
3
4
5
6
java复制代码// 注释的方式
(?# comment )
--- 内联注释。 该注释在第一个右括号处终止

# [至行尾]
--- X 模式注释。 该注释以非转义的 # 开头,并继续到行的结尾

2.2.10 正则表达式选项值

1
java复制代码TODO : 感觉没有使用场景 , 暂时不录入

三 . 使用

3.2 常规写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
// 准备 Pattern , 准备 Source ,通过 Match 匹配
Pattern pattern123 = Pattern.compile(".*?o");
Matcher matcher123 = pattern123.matcher("zoboco");
while (matcher123.find()) {
System.out.println(matcher123.group(0));
}

// 以下2种方式均可以搜索出数据 , 然后通过 matcher123.group(0) 返回数据
-> matcher123.find()
-> matcher.matches()

// 注意 , 使用前一定需要匹配一次 , 否则会抛出 No match found
while (matcher123.find()) {
System.out.println(matcher123.group(0));
}
// -----------> 直接 group 会返回 No match found
System.out.println(matcher123.group(0));

3.2 Java 中的用法

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码> Pattern 对象 : 一个正则表达式可以定义为一个 Pattern 对象
String regex = "<(\\w+)>(.*)</\\1>";
Pattern pattern = Pattern.compile(regex);


C- Pattern
M- compile(String regex) :
M- compile(String regex, int flags) : 指定匹配模式 :
- Pattern.DOTALL : 单行模式(点号模式)
- Pattern.MULTILINE : 多行模式
- Pattern.CASE_INSENSITIVE :大小写无关模式
M- quote

3.3 Group 语法

1
2
3
4
5
java复制代码public int groupCount( )返回matcher对象中的group的数目。不包括group0。
public String group( ) 返回上次匹配操作(比方说find( ))的group 0(整个匹配)
public String group(int i)返回上次匹配操作的某个group。如果匹配成功,但是没能找到group,则返回null。
public int start(int group)返回上次匹配所找到的,group的开始位置。
public int end(int group)返回上次匹配所找到的,group的结束位置,最后一个字符的下标加一。

3.4 其他

1
2
java复制代码start( ) : 返回此次匹配的开始位置
end( ) : 返回此次匹配的结束位置

3.5 切分

1
2
3
java复制代码// 将以正则表达式为界,将字符串分割成String数组。
String[] split(CharSequence charseq)
String[] split(CharSequence charseq, int limit)

3.6 替换

1
2
3
java复制代码replaceFirst(String replacement)将字符串里,第一个与模式相匹配的子串替换成replacement。
replaceAll(String replacement),将输入字符串里所有与模式相匹配的子串全部替换成replacement。
appendReplacement(StringBuffer sbuf, String replacement)对sbuf进行逐次替换,而不是像replaceFirst( )或replaceAll( )那样,只替换第一个或全部子串。

替换案例 Git

3.7 查询和匹配

1
2
3
4
java复制代码find() : 尝试在目标字符串里查找下一个匹配子串。
find(int start) : 重设Matcher对象,并且尝试在目标字符串里从指定的位置开始查找下一个匹配的子串。

matches() : 尝试对整个目标字符展开匹配检测,也就是只有整个目标字符串完全匹配时才返回真值

四 . 源码梳理

1
java复制代码TODO

附录


1
2
3
java复制代码文档参考 : 
https://www.cnblogs.com/xyou/p/7427779.html
https://docs.microsoft.com/zh-cn/dotnet/standard/base-types/regular-expression-language-quick-reference

基础案例

常见案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码// . 用于匹配任何字符 -- a.f 
-> abf
-> acf

// [] 用于匹配其中任意字符
[abcd] --> 匹配a, b, c, d中的任意一个字符
[0123456789] -> 匹配任意一个数字字符
[0-9a-zA-Z_]

// 排除指定字符 (^ 如果不在开头 , 应该看为一个正常字符)
[^abcd] --> 匹配除了a, b, c, d以外的任意一个字符
[^0-9] --> 匹配一个非数字字符

// 字符组运算
[[abc][def]] === [abcdef]
[a-z&&[^de]] --> 匹配的字符是a到z,但不能是d或e

// 匹配以 xxx 开头

数字匹配

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
java复制代码1 数字:
--> ^[0-9]*$

2 n位的数字:
--> ^\d{n}$

3 至少n位的数字:
--> ^\d{n,}$

4 m-n位的数字:
--> ^\d{m,n}$

5 零和非零开头的数字:
--> ^(0|[1-9][0-9]*)$

6 非零开头的最多带两位小数的数字:
--> ^([1-9][0-9]*)+(.[0-9]{1,2})?$

7 带1-2位小数的正数或负数:
--> ^(-)?\d+(.\d{1,2})?$

8 正数、负数、和小数:
--> ^(-|+)?\d+(.\d+)?$

9 有两位小数的正实数:
--> ^[0-9]+(.[0-9]{2})?$

10 有1~3位小数的正实数:
--> ^[0-9]+(.[0-9]{1,3})?$

11 非零的正整数:
--> ^[1-9]\d$ 或 ^([1-9][0-9]){1,3}$ 或 ^+?[1-9][0-9]*$

12 非零的负整数:
--> ^-[1-9][]0-9″$ 或 ^-[1-9]\d$

13 非负整数:
--> ^\d+$ 或 ^[1-9]\d*|0$

14 非正整数:
--> ^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$

15 非负浮点数:
--> ^\d+(.\d+)?$ 或 ^[1-9]\d.\d|0.\d[1-9]\d|0?.0+|0$

16 非正浮点数:
--> ^((-\d+(.\d+)?)|(0+(.0+)?))$ 或 ^(-([1-9]\d.\d|0.\d[1-9]\d))|0?.0+|0$

17 浮点数:
--> ^(-?\d+)(.\d+)?$ 或 ^-?([1-9]\d.\d|0.\d[1-9]\d|0?.0+|0)$

国内邮政编码

1
java复制代码/^[0-9]{6}$/.test(100000)

匹配指定开头结尾得字符串

1
2
3
4
5
6
7
8
9
10
java复制代码-> 匹配网址
https://.+.com/

-> 前后匹配
error code.*?: LdapErr

-> 指定字符串之间的数据
(?<=error code ).*?(?=: LdapErr)

(?<="access_token": ").*?(?=","token_type": "bearer",)

字符匹配

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码1. 汉字:^[\u4e00-\u9fa5]{0,}$
2. 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
3. 长度为3-20的所有字符:^.{3,20}$
4. 由26个英文字母组成的字符串:^[A-Za-z]+$
5. 由26个大写英文字母组成的字符串:^[A-Z]+$
6. 由26个小写英文字母组成的字符串:^[a-z]+$
7. 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8. 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
9. 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10. 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11. 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
12. 禁止输入含有~的字符:[^~\x22]+

特殊需求表达式

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
java复制代码1. Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
2. 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3. InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
4. 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$

6. 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
7. 身份证号(15位、18位数字):^\d{15}|\d{18}$
8. 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
9. 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10. 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
11. 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
12. 日期格式:^\d{4}-\d{1,2}-\d{1,2}
13. 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14. 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
15. 钱的输入格式:
16. 1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$
17. 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
18. 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
19. 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
20. 5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$
21. 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
22. 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
23. 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25. xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
26. 中文字符的正则表达式:[\u4e00-\u9fa5]
27. 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
28. 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
29. HTML标记的正则表达式:<(\S*?)[^>]*>.*?</\1>|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
30. 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31. 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32. 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
33. IP地址:\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)
34. IP地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))
5. 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$

附录 : 思维导图

正则案例.png

正则表达式匹配.png

正则表达式位置匹配.png

本文转载自: 掘金

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

数据结构与算法

发表于 2021-03-11

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。

本文是数据结构与算法系列的第 3 篇文章,完整文章目录请移步到文章末尾~


历史上的今天

2016 年 3 月 9 日,AlphaGo 大战李世石 拉开序幕。9 日至15 日,在韩国首尔举行的人机围棋比赛中,谷歌旗下 DeepMind 开发的人工智能围棋软件 AlphaGo 最终以 4:1 战胜世界围棋冠军、职业九段选手李世石。继 “深蓝” 战胜卡斯帕罗夫之后,这场比赛被视为人类与人工智能的又一场较量。—— 《了不起的程序员》


目录


  1. 题目描述

  • 224. 基本计算器 I
  • 227. 基本计算器 II
  • 772. 基本计算器 III 【题解】
  • 770. 基本计算器 IV
  • 150. 逆波兰表达式求值 【题解】
  • 1006. 笨阶乘 【题解】

实现一个基本的计算器来计算一个简单的字符串表达式的值。

字符串表达式仅包含非负整数,+、 - 、*、/ 四种运算符,以及空格、左括号和右括号 。 整数除法仅保留整数部分。

这几道题其实是计算器系列题目:

  • 第一道:加减法和括号
  • 第二道:四则运算
  • 第三道:四则运算 + 括号

理解了第三道的解法,相信前两道都不会有大问题。


  1. 思路分析

2.1 逆波兰表达式

首先,你需要理解计算机是如何进行 “表达式求值” 的。我们平时使用的算术表达式,例如: 1 + 2 * 3,这种叫做 「中缀表达式」,也就是运算符都是放在两个操作数中间。中缀表达式符合人类的思考习惯,但是对计算机来说却并不友好,所以还有另外两种表达式形式:

  • 中缀表达式: 二元运算符置于两个操作数之间,例如:(1 + 2) * 3
  • 前缀表达式(波兰表达式): 二元运算符置于两个操作数之间,例如:* + 1 2 3
  • 后缀表达式(逆波兰表达式): 二元运算符置于两个操作数之间,例如:1 2 + 3 *

相对于中缀表达式,前缀表达式和后缀表达式对计算机来说就友好很多了。它们在计算机看来却是比较简单易懂的结构,只需要依靠简单的入栈和出栈两种操作就可以完成计算。

我们以后缀表达式求值为例:

2.2 逆波兰表达式求值

后缀表达式也叫逆波兰表达式(Reverse Polish notation,RPN),逆波兰表达式求值步骤如下:

  • 1、从左往右扫描逆波兰表达式:
  • 2、如果是数字,直接入栈;
  • 3、如果是运算符,那么弹出栈顶两个数字(四则运算需要两个运算数),执行运算;
  • 4、将运算结果重新入栈;
  • 5、重复步骤 3、4,直到遍历完成,最后栈中唯一的元素就是运算结果。

提示: 波兰表达式和逆波兰表达式是没有括号的,中缀表达式中的括号在转换过程中已经被 “消化”。

动画引用自 —— github.com/Mrxxd/LeetC…

参考代码:

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
kotlin复制代码class Solution {
fun evalRPN(tokens: Array<String>): Int {
if (tokens.isEmpty()) {
return 0
}

val stack = ArrayDeque<Int>()

for (token in tokens) {
when (token) {
"+" -> {
stack.push(stack.eval { o1, o2 -> o1 + o2 })
}
"-" -> {
stack.push(stack.eval { o1, o2 -> o1 - o2 })
}
"*" -> {
stack.push(stack.eval { o1, o2 -> o1 * o2 })
}
"/" -> {
stack.push(stack.eval { o1, o2 -> o1 / o2 })
}
else -> {
stack.push(token.toInt())
}
}
}

return stack.pop()
}

private fun Deque<Int>.eval(operator: (Int, Int) -> Int): Int {
val num1 = pop()
val num2 = pop()

return operator(num2, num1)
}
}

2.3 中缀 -> 逆波兰

逆波兰表达式求值很简单,但是我们的输入是普通的中缀表达式,我们要先转换为逆波兰表达式的形式,主要思想是 使用栈的特性来处理运算符和括号的优先级,步骤如下:

  • 1、从左往右扫描中缀表达式;
  • 2、如果是数字,直接输出;
  • 3、如果是左括号(,直接入栈;
  • 4、如果是右边括号),弹出栈顶元素,直到遇到左括号(;
  • 5、如果是运算符,则需要判断跟栈顶运算符的优先级:
    • 5.1 空栈或者优先级大于栈顶运算符,继续入栈;
    • 5.2 优先级小于等于栈顶运算符,弹出栈顶运算符,并输出;
    • 5.3 重复步骤 5.2,直到运算符大于栈顶运算符。
  • 6、重复步骤 2、3、4、5,直到遍历完成,最后将栈中剩余元素出栈。

  1. AC 代码

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
kotlin复制代码class Solution {
fun calculate(s: String): Int {
// 1、中缀表达式转换为逆波兰表达式
// 2、逆波兰表达式求值
return s.toRPN().evalRPN()
}

// ------------------------------------------------------------------------
// 1、中缀表达式转换为逆波兰表达式
// ------------------------------------------------------------------------

private fun String.toRPN(): List<String> {

val list = ArrayList<String>()
val stack = ArrayDeque<Char>()

var num = 0
var sign = 1

outerFor@
for ((index, token) in this.withIndex()) {
when (token) {
' ' -> continue@outerFor

in '0'..'9' -> {
num = num * 10 + (token.toInt() - 48)
if (index + 1 >= length || !(this[index + 1] in '0'..'9')) {
list.add("${sign * num}")
num = 0
}
}

'(' -> stack.push(token)

')' -> {
while ('(' != stack.peek()) {
list.add("${stack.pop()}")
}
stack.pop() // 最后弹出 (
}

else -> {
// 先输出优先级相同或者更高的运算符
while (token.priority() <= stack.peek().priority()) {
list.add("${stack.pop()}")
}
stack.push(token)
}
}
}
while (stack.isNotEmpty()) {
list.add("${stack.pop()}")
}
return list
}

// 运算符优先级
private fun Char?.priority(): Int {
return when (this) {
'+', '-' -> 1
'*', '/' -> 2
null -> -1
else -> -1
}
}

// ------------------------------------------------------------------------
// 2、逆波兰表达式求值
// ------------------------------------------------------------------------

private fun Deque<Int>.eval(operator: (Int, Int) -> Int): Int {
if(size > 1){
val num1 = pop()
val num2 = pop()
return operator(num2, num1)
}else{
// 类似 -1 + 1 的情况
return operator(0, pop())
}
}

private fun List<String>.evalRPN(): Int {

val stack = ArrayDeque<Int>()

for (token in this) {
when (token) {
"+" -> {
stack.push(stack.eval { o1, o2 -> o1 + o2 })
}
"-" -> {
stack.push(stack.eval { o1, o2 -> o1 - o2 })
}
"*" -> {
stack.push(stack.eval { o1, o2 -> o1 * o2 })
}
"/" -> {
stack.push(stack.eval { o1, o2 -> o1 / o2 })
}
else -> {
stack.push(token.toInt())
}
}
}
return stack.pop()
}
}

推荐阅读

数据结构与算法系列完整目录如下(2023/07/11 更新):

  • #1 链表问题总结
  • #2 链表相交 & 成环问题总结
  • #3 计算器与逆波兰表达式总结
  • #4 高楼丢鸡蛋问题总结
  • #5 为什么你学不会递归?谈谈我的经验
  • #6 回溯算法解题框架总结
  • #7 下次面试遇到二分查找,别再写错了
  • #8 什么是二叉树?
  • #9 什么是二叉堆 & Top K 问题
  • #10 使用前缀和数组解决 “区间和查询” 问题
  • #11 面试遇到线段树,已经这么卷了吗?
  • #12 使用单调队列解决 “滑动窗口最大值” 问题
  • #13 使用单调栈解决 “下一个更大元素” 问题
  • #14 使用并查集解决 “朋友圈” 问题
  • #15 如何实现一个优秀的 HashTable 散列表
  • #16 简答一波 HashMap 常见面试题
  • #17 二叉树高频题型汇总
  • #18 下跳棋,极富想象力的同向双指针模拟

Java & Android 集合框架系列文章: 跳转阅读

LeetCode 上分之旅系列文章:跳转阅读

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

2万字系统总结,带你实现 Linux 命令自由? 前言 Li

发表于 2021-03-11

前言

Linux 的学习对于一个程序员的重要性是不言而喻的。前端开发相比后端开发,接触 Linux 机会相对较少,因此往往容易忽视它。但是学好它却是程序员必备修养之一。

如果本文对你有所帮助,请点个👍 吧。

作者使用的是阿里云服务器 ECS (最便宜的那种) CentOS 7.7 64位。当然你也可以在自己的电脑安装虚拟机,虚拟机中再去安装 CentOS 系统(这就完全免费了)。至于它的安装教程可以去谷歌搜索下,相关教程非常多。

Linux 基础

操作系统

操作系统 Operating System 简称 OS ,是软件的一部分,它是硬件基础上的第一层软件,是硬件和其它软件沟通的桥梁。

操作系统会控制其他程序运行,管理系统资源,提供最基本的计算功能,如管理及配置内存、决定系统资源供需的优先次序等,同时还提供一些基本的服务程序。

image.png

什么是 Linux

Linux 系统内核与 Linux 发行套件的区别

  • Linux 系统内核指的是由 Linus Torvalds 负责维护,提供硬件抽象层、硬盘及文件系统控制及多任务功能的系统核心程序。
  • Linux 发行套件系统是我们常说的 Linux 操作系统,也即是由 Linux 内核与各种常用软件的集合产品。

总结:真正的 Linux 指的是系统内核,而我们常说的 Linux 指的是“发行版完整的包含一些基础软件的操作系统”。

Linux 对比 Windows

  1. 稳定且有效率;
  2. 免费(或少许费用);
  3. 漏洞少且快速修补;
  4. 多任务多用户;
  5. 更加安全的用户与文件权限策略;
  6. 适合小内核程序的嵌入系统;
  7. 相对不耗资源。

Linux 系统种类

  • 红帽企业版 Linux : RHEL 是全世界内使用最广泛的 Linux 系统。它具有极强的性能与稳定性,是众多生成环境中使用的(收费的)系统。
  • Fedora :由红帽公司发布的桌面版系统套件,用户可以免费体验到最新的技术或工具,这些技术或工具在成熟后会被加入到 RHEL 系统中,因此 Fedora 也成为 RHEL 系统的试验版本。
  • CentOS :通过把 RHEL 系统重新编译并发布给用户免费使用的 Linux 系统,具有广泛的使用人群。
  • Deepin :中国发行,对优秀的开源成品进行集成和配置。
  • Debian :稳定性、安全性强,提供了免费的基础支持,在国外拥有很高的认可度和使用率。
  • Ubuntu :是一款派生自 Debian 的操作系统,对新款硬件具有极强的兼容能力。 Ubuntu 与 Fedora 都是极其出色的 Linux 桌面系统,而且 Ubuntu 也可用于服务器领域。

终端连接阿里云服务器

image.png
通过执行 ssh root@121.42.11.34 命令,然后输入服务器连接密码就可以顺利登陆远程服务器。从现在开始我们就可以在本地电脑操作远程服务器。

  1. 这个黑色的面板就是终端也就是 Shell (命令行环境)。
  2. ssh root@xxx 这是一条命令,必须要在 Shell 中才能执行。

Shell

Shell 这个单词的原意是“外壳”,跟 kernel(内核)相对应,比喻内核外面的一层,即用户跟内核交互的对话界面。

  • Shell 是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境( command line interface ,简写为 CLI )。 Shell 接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。
  • Shell 是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 命令写出各种小程序,又称为 Shell 脚本。这些脚本都通过 Shell 的解释执行,而不通过编译。
  • Shell 是一个工具箱,提供了各种小工具,供用户方便地使用操作系统的功能。

Shell 的种类

Shell 有很多种,只要能给用户提供命令行环境的程序,都可以看作是 Shell 。

历史上,主要的 Shell 有下面这些:

  • Bourne Shell(sh)
  • Bourne Again shell(bash)
  • C Shell(csh)
  • TENEX C Shell(tcsh)
  • Korn shell(ksh)
  • Z Shell(zsh)
  • Friendly Interactive Shell(fish)

其中 Bash 是目前最常用的 Shell 。 MacOS 中的默认 Shell 就是 Bash 。

通过执行 echo $SHELL 命令可以查看到当前正在使用的 Shell 。还可以通过 cat /etc/shells 查看当前系统安装的所有 Shell 种类。

命令

命令行提示符

进入命令行环境以后,用户会看到 Shell 的提示符。提示符往往是一串前缀,最后以一个美元符号 $ 结尾,用户可以在这个符号后面输入各种命令。

执行一个简单的命令 pwd :

1
2
bash复制代码[root@iZm5e8dsxce9ufaic7hi3uZ ~]# pwd
/root

命令解析:

  • root:表示用户名;
  • iZm5e8dsxce9ufaic7hi3uZ:表示主机名;
  • ~:表示目前所在目录为家目录,其中 root 用户的家目录是 /root 普通用户的家目录在 /home 下;
  • #:指示你所具有的权限( root 用户为 # ,普通用户为 $ )。
  • 执行 whoami 命令可以查看当前用户名;
  • 执行 hostname 命令可以查看当前主机名;

关于如何创建、切换、删除用户,在后面的用户与权限会具体讲解,这里先使用 root 用户进行演示。

[备注] root 是超级用户,具备操作系统的一切权限。

命令格式

1
bash复制代码command parameters(命令 参数)

长短参数

1
2
3
4
5
css复制代码单个参数:ls -a(a 是英文 all 的缩写,表示“全部”)
多个参数:ls -al(全部文件 + 列表形式展示)
单个长参数:ls --all
多个长参数:ls --reverse --all
长短混合参数:ls --all -l

参数值

1
2
bash复制代码短参数:command -p 10(例如:ssh root@121.42.11.34 -p 22)
长参数:command --paramters=10(例如:ssh root@121.42.11.34 --port=22)

快捷方式

在开始学习 Linux 命令之前,有这么一些快捷方式,是必须要提前掌握的,它将贯穿整个 Linux 使用生涯。

  • 通过上下方向键 ↑ ↓ 来调取过往执行过的 Linux 命令;
  • 命令或参数仅需输入前几位就可以用 Tab 键补全;
  • Ctrl + R :用于查找使用过的命令(history 命令用于列出之前使用过的所有命令,然后输入 ! 命令加上编号( !2 )就可以直接执行该历史命令);
  • Ctrl + L:清除屏幕并将当前行移到页面顶部;
  • Ctrl + C:中止当前正在执行的命令;
  • Ctrl + U:从光标位置剪切到行首;
  • Ctrl + K:从光标位置剪切到行尾;
  • Ctrl + W:剪切光标左侧的一个单词;
  • Ctrl + Y:粘贴 Ctrl + U | K | Y 剪切的命令;
  • Ctrl + A:光标跳到命令行的开头;
  • Ctrl + E:光标跳到命令行的结尾;
  • Ctrl + D:关闭 Shell 会话;

文件和目录

文件的组织

image.png

查看路径

pwd

显示当前目录的路径

image.png

which

查看命令的可执行文件所在路径, Linux 下,每一条命令其实都对应一个可执行程序,在终端中输入命令,按回车的时候,就是执行了对应的那个程序, which 命令本身对应的程序也存在于 Linux 中。

总的来说一个命令就是一个可执行程序。

image.png

浏览和切换目录

ls

列出文件和目录,它是 Linux 最常用的命令之一。

【常用参数】

  • -a 显示所有文件和目录包括隐藏的
  • -l 显示详细列表
  • -h 适合人类阅读的
  • -t 按文件最近一次修改时间排序
  • -i 显示文件的 inode ( inode 是文件内容的标识)

image.png

cd

cd 是英语 change directory 的缩写,表示切换目录。

1
2
3
4
5
6
kotlin复制代码cd /	--> 跳转到根目录
cd ~ --> 跳转到家目录
cd .. --> 跳转到上级目录
cd ./home --> 跳转到当前目录的home目录下
cd /home/lion --> 跳转到根目录下的home目录下的lion目录
cd --> 不添加任何参数,也是回到家目录

[注意] 输入cd /ho + 单次 tab 键会自动补全路径 + 两次 tab 键会列出所有可能的目录列表。

du

列举目录大小信息。

【常用参数】

  • -h 适合人类阅读的;
  • -a 同时列举出目录下文件的大小信息;
  • -s 只显示总计大小,不显示具体信息。

浏览和创建文件

cat

一次性显示文件所有内容,更适合查看小的文件。

1
kotlin复制代码cat cloud-init.log

【常用参数】

  • -n 显示行号。

less

分页显示文件内容,更适合查看大的文件。

1
kotlin复制代码less cloud-init.log

【快捷操作】

  • 空格键:前进一页(一个屏幕);
  • b 键:后退一页;
  • 回车键:前进一行;
  • y 键:后退一行;
  • 上下键:回退或前进一行;
  • d 键:前进半页;
  • u 键:后退半页;
  • q 键:停止读取文件,中止 less 命令;
  • = 键:显示当前页面的内容是文件中的第几行到第几行以及一些其它关于本页内容的详细信息;
  • h 键:显示帮助文档;
  • / 键:进入搜索模式后,按 n 键跳到一个符合项目,按 N 键跳到上一个符合项目,同时也可以输入正则表达式匹配。

head

显示文件的开头几行(默认是10行)

1
bash复制代码head cloud-init.log

【参数】

  • -n 指定行数 head cloud-init.log -n 2

tail

显示文件的结尾几行(默认是10行)

1
bash复制代码tail cloud-init.log

【参数】

  • -n 指定行数 tail cloud-init.log -n 2
  • -f 会每过1秒检查下文件是否有更新内容,也可以用 -s 参数指定间隔时间 tail -f -s 4 xxx.log

touch

创建一个文件

1
bash复制代码touch new_file

mkdir

创建一个目录

1
bash复制代码mkdir new_folder

【常用参数】

  • -p 递归的创建目录结构 mkdir -p one/two/three

文件的复制和移动

cp

拷贝文件和目录

1
2
3
4
bash复制代码cp file file_copy	--> file 是目标文件,file_copy 是拷贝出来的文件
cp file one --> 把 file 文件拷贝到 one 目录下,并且文件名依然为 file
cp file one/file_copy --> 把 file 文件拷贝到 one 目录下,文件名为file_copy
cp *.txt folder --> 把当前目录下所有 txt 文件拷贝到 folder 目录下

【常用参数】

  • -r 递归的拷贝,常用来拷贝一整个目录

mv

移动(重命名)文件或目录,与cp命令用法相似。

1
2
3
4
bash复制代码mv file one	--> 将 file 文件移动到 one 目录下
mv new_folder one --> 将 new_folder 文件夹移动到one目录下
mv *.txt folder --> 把当前目录下所有 txt 文件移动到 folder 目录下
mv file new_file --> file 文件重命名为 new_file

文件的删除和链接

rm

删除文件和目录,由于 Linux 下没有回收站,一旦删除非常难恢复,因此需要谨慎操作

1
2
bash复制代码rm new_file 	--> 删除 new_file 文件
rm f1 f2 f3 --> 同时删除 f1 f2 f3 3个文件

【常用参数】

  • -i 向用户确认是否删除;
  • -f 文件强制删除;
  • -r 递归删除文件夹,著名的删除操作 rm -rf 。

ln

英文 Link 的缩写,表示创建链接。

学习创建链接之前,首先要理解链接是什么,我们先来看看 Linux 的文件是如何存储的:

Linux 文件的存储方式分为3个部分,文件名、文件内容以及权限,其中文件名的列表是存储在硬盘的其它地方和文件内容是分开存放的,每个文件名通过 inode 标识绑定到文件内容。

Linux 下有两种链接类型:硬链接和软链接。

硬链接

使链接的两个文件共享同样文件内容,就是同样的 inode ,一旦文件1和文件2之间有了硬链接,那么修改任何一个文件,修改的都是同一块内容,它的缺点是,只能创建指向文件的硬链接,不能创建指向目录的(其实也可以,但比较复杂)而软链接都可以,因此软链接使用更加广泛。

1
bash复制代码ln file1 file2 	--> 创建 file2 为 file1 的硬链接

image.png

如果我们用 rm file1 来删除 file1 ,对 file2 没有什么影响,对于硬链接来说,删除任意一方的文件,共同指向的文件内容并不会从硬盘上删除。只有同时删除了 file1 与 file2 后,它们共同指向的文件内容才会消失。

软链接

软链接就类似 windows 下快捷方式。

1
bash复制代码ln -s file1 file2

image.png

执行 ls -l 命名查看当前目录下文件的具体信息

1
2
3
bash复制代码total 0
-rw-r--r-- 1 root root 0 Jan 14 06:29 file1
lrwxrwxrwx 1 root root 5 Jan 14 06:42 file2 -> file1 # 表示file2 指向 file1

其实 file2 只是 file1 的一个快捷方式,它指向的是 file1 ,所以显示的是 file1 的内容,但其实 file2 的 inode 与 file1 并不相同。如果我们删除了 file2 的话, file1 是不会受影响的,但如果删除 file1 的话, file2 就会变成死链接,因为指向的文件不见了。

用户与权限

用户

Linux 是一个多用户的操作系统。在 Linux 中,理论上来说,我们可以创建无数个用户,但是这些用户是被划分到不同的群组里面的,有一个用户,名叫 root ,是一个很特殊的用户,它是超级用户,拥有最高权限。

image.png

自己创建的用户是有限权限的用户,这样大大提高了 Linux 系统的安全性,有效防止误操作或是病毒攻击,但是我们执行的某些命令需要更高权限时可以使用 sudo 命令。

sudo

以 root 身份运行命令

1
bash复制代码sudo date  --> 当然查看日期是不需要sudo的这里只是演示,sudo 完之后一般还需要输入用户密码的

useradd + passwd

  • useradd 添加新用户
  • passwd 修改用户密码

这两个命令需要 root 用户权限

1
2
bash复制代码useradd lion	--> 添加一个lion用户,添加完之后在 /home 路径下可以查看
passwd lion --> 修改lion用户的密码

userdel

删除用户,需要 root 用户权限

1
2
bash复制代码userdel lion	--> 只会删除用户名,不会从/home中删除对应文件夹
userdel lion -r --> 会同时删除/home下的对应文件夹

su

切换用户,需要 root 用户权限

1
2
3
bash复制代码sudo su	--> 切换为root用户(exit 命令或 CTRL + D 快捷键都可以使普通用户切换为 root 用户)
su lion --> 切换为普通用户
su - --> 切换为root用户

群组的管理

Linux 中每个用户都属于一个特定的群组,如果你不设置用户的群组,默认会创建一个和它的用户名一样的群组,并且把用户划归到这个群组。

groupadd

创建群组,用法和 useradd 类似。

1
bash复制代码groupadd friends

groupdel

删除一个已存在的群组

1
bash复制代码groupdel foo 	--> 删除foo群组

groups

查看用户所在群组

1
bash复制代码groups lion 	--> 查看 lion 用户所在的群组

usermod

用于修改用户的账户。

【常用参数】

  • -l 对用户重命名。需要注意的是 /home 中的用户家目录的名字不会改变,需要手动修改。
  • -g 修改用户所在的群组,例如 usermod -g friends lion 修改 lion 用户的群组为 friends 。
  • -G 一次性让用户添加多个群组,例如 usermod -G friends,foo,bar lion 。
  • -a -G 会让你离开原先的群组,如果你不想这样做的话,就得再添加 -a 参数,意味着 append 追加的意思。

chgrp

用于修改文件的群组。

1
bash复制代码chgrp bar file.txt	--> file.txt文件的群组修改为bar

chown

改变文件的所有者,需要 root 身份才能运行。

1
2
bash复制代码chown lion file.txt	--> 把其它用户创建的file.txt转让给lion用户
chown lion:bar file.txt --> 把file.txt的用户改为lion,群组改为bar

【常用参数】

  • -R 递归设置子目录和子文件, chown -R lion:lion /home/frank 把 frank 文件夹的用户和群组都改为 lion 。

文件权限管理

chmod

修改访问权限。

1
bash复制代码chmod 740 file.txt

【常用参数】

  • -R 可以递归地修改文件访问权限,例如 chmod -R 777 /home/lion

修改权限的确简单,但是理解其深层次的意义才是更加重要的。下面我们来系统的学习 Linux 的文件权限。

1
2
3
4
bash复制代码[root@lion ~]# ls -l
drwxr-xr-x 5 root root 4096 Apr 13 2020 climb
lrwxrwxrwx 1 root root 7 Jan 14 06:41 hello2.c -> hello.c
-rw-r--r-- 1 root root 149 Jan 13 06:14 hello.c

其中 drwxr-xr-x 表示文件或目录的权限。让我们一起来解读它具体代表什么?

  • d :表示目录,就是说这是一个目录,普通文件是 - ,链接是 l 。
  • r : read 表示文件可读。
  • w : write 表示文件可写,一般有写的权限,就有删除的权限。
  • x : execute 表示文件可执行。
  • - :表示没有相应权限。

权限的整体是按用户来划分的,如下图所示:

image.png

现在再来理解这句权限 drwxr-xr-x 的意思:

  • 它是一个文件夹;
  • 它的所有者具有:读、写、执行权限;
  • 它的群组用户具有:读、执行的权限,没有写的权限;
  • 它的其它用户具有:读、执行的权限,没有写的权限。

现在理解了权限,我们使用 chmod 来尝试修改权限。 chmod 它不需要是 root 用户才能运行的,只要你是此文件所有者,就可以用 chmod 来修改文件的访问权限。

数字分配权限
权限 数字
r 4
w 2
x 1

因此要改变权限,只要做一些简单的加法就行:

1
2
3
4
5
6
7
8
bash复制代码chmod 640 hello.c 

# 分析
6 = 4 + 2 + 0 表示所有者具有 rw 权限
4 = 4 + 0 + 0 表示群组用户具有 r 权限
0 = 0 + 0 + 0 表示其它用户没有权限

对应文字权限为:-rw-r-----
用字母来分配权限
  • u : user 的缩写,用户的意思,表示所有者。
  • g : group 的缩写,群组的意思,表示群组用户。
  • o : other 的缩写,其它的意思,表示其它用户。
  • a : all 的缩写,所有的意思,表示所有用户。
  • + :加号,表示添加权限。
  • - :减号,表示去除权限。
  • = :等于号,表示分配权限。
1
2
3
4
5
6
7
bash复制代码chmod u+rx file	--> 文件file的所有者增加读和运行的权限
chmod g+r file --> 文件file的群组用户增加读的权限
chmod o-r file --> 文件file的其它用户移除读的权限
chmod g+r o-r file --> 文件file的群组用户增加读的权限,其它用户移除读的权限
chmod go-r file --> 文件file的群组和其他用户移除读的权限
chmod +x file --> 文件file的所有用户增加运行的权限
chmod u=rwx,g=r,o=- file --> 文件file的所有者分配读写和执行的权限,群组其它用户分配读的权限,其他用户没有任何权限

查找文件

locate

搜索包含关键字的所有文件和目录。后接需要查找的文件名,也可以用正则表达式。

安装 locate

1
2
bash复制代码yum -y install mlocate	--> 安装包
updatedb --> 更新数据库
1
2
bash复制代码locate file.txt
locate fil*.txt

[注意] locate 命令会去文件数据库中查找命令,而不是全磁盘查找,因此刚创建的文件并不会更新到数据库中,所以无法被查找到,可以执行 updatedb 命令去更新数据库。

find

用于查找文件,它会去遍历你的实际硬盘进行查找,而且它允许我们对每个找到的文件进行后续操作,功能非常强大。

1
bash复制代码find <何处> <何物> <做什么>
  • 何处:指定在哪个目录查找,此目录的所有子目录也会被查找。
  • 何物:查找什么,可以根据文件的名字来查找,也可以根据其大小来查找,还可以根据其最近访问时间来查找。
  • 做什么:找到文件后,可以进行后续处理,如果不指定这个参数, find 命令只会显示找到的文件。

根据文件名查找

1
2
3
4
5
6
bash复制代码find -name "file.txt"	--> 当前目录以及子目录下通过名称查找文件
find . -name "syslog" --> 当前目录以及子目录下通过名称查找文件
find / -name "syslog" --> 整个硬盘下查找syslog
find /var/log -name "syslog" --> 在指定的目录/var/log下查找syslog文件
find /var/log -name "syslog*" --> 查找syslog1、syslog2 ... 等文件,通配符表示所有
find /var/log -name "*syslog*" --> 查找包含syslog的文件

[注意] find 命令只会查找完全符合 “何物” 字符串的文件,而 locate 会查找所有包含关键字的文件。

根据文件大小查找

1
2
3
4
bash复制代码find /var -size +10M	--> /var 目录下查找文件大小超过 10M 的文件
find /var -size -50k --> /var 目录下查找文件大小小于 50k 的文件
find /var -size +1G --> /var 目录下查找文件大小查过 1G 的文件
find /var -size 1M --> /var 目录下查找文件大小等于 1M 的文件

根据文件最近访问时间查找

1
bash复制代码find -name "*.txt" -atime -7 	--> 近 7天内访问过的.txt结尾的文件

仅查找目录或文件

1
2
bash复制代码find . -name "file" -type f 	--> 只查找当前目录下的file文件
find . -name "file" -type d --> 只查找当前目录下的file目录

操作查找结果

1
2
3
4
bash复制代码find -name "*.txt" -printf "%p - %u\n"	--> 找出所有后缀为txt的文件,并按照 %p - %u\n 格式打印,其中%p=文件名,%u=文件所有者
find -name "*.jpg" -delete --> 删除当前目录以及子目录下所有.jpg为后缀的文件,不会有删除提示,因此要慎用
find -name "*.c" -exec chmod 600 {} \; --> 对每个.c结尾的文件,都进行 -exec 参数指定的操作,{} 会被查找到的文件替代,\; 是必须的结尾
find -name "*.c" -ok chmod 600 {} \; --> 和上面的功能一直,会多一个确认提示

软件仓库

Linux 下软件是以包的形式存在,一个软件包其实就是软件的所有文件的压缩包,是二进制的形式,包含了安装软件的所有指令。 Red Hat 家族的软件包后缀名一般为 .rpm , Debian 家族的软件包后缀是 .deb 。

Linux 的包都存在一个仓库,叫做软件仓库,它可以使用 yum 来管理软件包, yum 是 CentOS 中默认的包管理工具,适用于 Red Hat 一族。可以理解成 Node.js 的 npm 。

yum 常用命令

  • yum update | yum upgrade 更新软件包
  • yum search xxx 搜索相应的软件包
  • yum install xxx 安装软件包
  • yum remove xxx 删除软件包

切换 CentOS 软件源

有时候 CentOS 默认的 yum 源不一定是国内镜像,导致 yum 在线安装及更新速度不是很理想。这时候需要将 yum 源设置为国内镜像站点。国内主要开源的镜像站点是网易和阿里云。

1、首先备份系统自带 yum 源配置文件 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

2、下载阿里云的 yum 源配置文件到 /etc/yum.repos.d/CentOS7

1
bash复制代码wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo

3、生成缓存

1
bash复制代码yum makecache

阅读手册

Linux 命令种类繁杂,我们凭借记忆不可能全部记住,因此学会查用手册是非常重要的。

man

安装更新 man

1
2
bash复制代码sudo yum install -y man-pages	--> 安装
sudo mandb --> 更新

man 手册种类

  1. 可执行程序或 Shell 命令;
  2. 系统调用( Linux 内核提供的函数);
  3. 库调用(程序库中的函数);
  4. 文件(例如 /etc/passwd );
  5. 特殊文件(通常在 /dev 下);
  6. 游戏;
  7. 杂项( man(7) ,groff(7) );
  8. 系统管理命令(通常只能被 root 用户使用);
  9. 内核子程序。

man + 数字 + 命令

输入 man + 数字 + 命令/函数,可以查到相关的命令和函数,若不加数字, man 默认从数字较小的手册中寻找相关命令和函数

1
2
bash复制代码man 3 rand 	--> 表示在手册的第三部分查找 rand 函数
man ls --> 查找 ls 用法手册

man 手册核心区域解析:(以 man pwd 为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码NAME # 命令名称和简单描述
pwd -- return working directory name

SYNOPSIS # 使用此命令的所有方法
pwd [-L | -P]

DESCRIPTION # 包括所有参数以及用法
The pwd utility writes the absolute pathname of the current working directory to the standard output.

Some shells may provide a builtin pwd command which is similar or identical to this utility. Consult the builtin(1) manual page.

The options are as follows:

-L Display the logical current working directory.

-P Display the physical current working directory (all symbolic links resolved).

If no options are specified, the -L option is assumed.

SEE ALSO # 扩展阅读相关命令
builtin(1), cd(1), csh(1), sh(1), getcwd(3)

help

man 命令像新华词典一样可以查询到命令或函数的详细信息,但其实我们还有更加快捷的方式去查询, command --help 或 command -h ,它没有 man 命令显示的那么详细,但是它更加易于阅读。

Linux 进阶

文本操作

grep

全局搜索一个正则表达式,并且打印到屏幕。简单来说就是,在文件中查找关键字,并显示关键字所在行。

基础语法

1
2
3
4
5
6
7
8
9
10
bash复制代码grep text file # text代表要搜索的文本,file代表供搜索的文件

# 实例
[root@lion ~]# grep path /etc/profile
pathmunge () {
pathmunge /usr/sbin
pathmunge /usr/local/sbin
pathmunge /usr/local/sbin after
pathmunge /usr/sbin after
unset -f pathmunge

常用参数

  • -i 忽略大小写, grep -i path /etc/profile
  • -n 显示行号,grep -n path /etc/profile
  • -v 只显示搜索文本不在的那些行,grep -v path /etc/profile
  • -r 递归查找, grep -r hello /etc ,Linux 中还有一个 rgrep 命令,作用相当于 grep -r

高级用法

grep 可以配合正则表达式使用。

1
2
3
bash复制代码grep -E path /etc/profile --> 完全匹配path
grep -E ^path /etc/profile --> 匹配path开头的字符串
grep -E [Pp]ath /etc/profile --> 匹配path或Path

sort

对文件的行进行排序。

基础语法

1
bash复制代码sort name.txt # 对name.txt文件进行排序

实例用法

为了演示方便,我们首先创建一个文件 name.txt ,放入以下内容:

1
2
3
4
5
6
7
bash复制代码Christopher
Shawn
Ted
Rock
Noah
Zachary
Bella

执行 sort name.txt 命令,会对文本内容进行排序。

常用参数

  • -o 将排序后的文件写入新文件, sort -o name_sorted.txt name.txt ;
  • -r 倒序排序, sort -r name.txt ;
  • -R 随机排序, sort -R name.txt ;
  • -n 对数字进行排序,默认是把数字识别成字符串的,因此 138 会排在 25 前面,如果添加了 -n 数字排序的话,则 25 会在 138 前面。

wc

word count 的缩写,用于文件的统计。它可以统计单词数目、行数、字符数,字节数等。

基础语法

1
bash复制代码wc name.txt # 统计name.txt

实例用法

1
2
bash复制代码[root@lion ~]# wc name.txt 
13 13 91 name.txt
  • 第一个13,表示行数;
  • 第二个13,表示单词数;
  • 第三个91,表示字节数。

常用参数

  • -l 只统计行数, wc -l name.txt ;
  • -w 只统计单词数, wc -w name.txt ;
  • -c 只统计字节数, wc -c name.txt ;
  • -m 只统计字符数, wc -m name.txt 。

uniq

删除文件中的重复内容。

基础语法

1
2
bash复制代码uniq name.txt # 去除name.txt重复的行数,并打印到屏幕上
uniq name.txt uniq_name.txt # 把去除重复后的文件保存为 uniq_name.txt

【注意】它只能去除连续重复的行数。

常用参数

  • -c 统计重复行数, uniq -c name.txt ;
  • -d 只显示重复的行数, uniq -d name.txt 。

cut

剪切文件的一部分内容。

基础语法

1
bash复制代码cut -c 2-4 name.txt # 剪切每一行第二到第四个字符

常用参数

  • -d 用于指定用什么分隔符(比如逗号、分号、双引号等等) cut -d , name.txt ;
  • -f 表示剪切下用分隔符分割的哪一块或哪几块区域, cut -d , -f 1 name.txt 。

重定向 管道 流

在 Linux 中一个命令的去向可以有3个地方:终端、文件、作为另外一个命令的入参。
未命名文件 (4).png
命令一般都是通过键盘输入,然后输出到终端、文件等地方,它的标准用语是 stdin 、 stdout 以及 stderr 。

  • 标准输入 stdin ,终端接收键盘输入的命令,会产生两种输出;
  • 标准输出 stdout ,终端输出的信息(不包含错误信息);
  • 标准错误输出 stderr ,终端输出的错误信息。

image.png

重定向

把本来要显示在终端的命令结果,输送到别的地方(到文件中或者作为其他命令的输入)。

输出重定向 >

> 表示重定向到新的文件, cut -d , -f 1 notes.csv > name.csv ,它表示通过逗号剪切 notes.csv 文件(剪切完有3个部分)获取第一个部分,重定向到 name.csv 文件。

我们来看一个具体示例,学习它的使用,假设我们有一个文件 notes.csv ,文件内容如下:

1
2
3
4
5
6
javascript复制代码Mark1,951/100,很不错1
Mark2,952/100,很不错2
Mark3,953/100,很不错3
Mark4,954/100,很不错4
Mark5,955/100,很不错5
Mark6,956/100,很不错6

执行命令: cut -d , -f 1 notes.csv > name.csv 最后输出如下内容:

1
2
3
4
5
6
javascript复制代码Mark1
Mark2
Mark3
Mark4
Mark5
Mark6

【注意】使用 > 要注意,如果输出的文件不存在它会新建一个,如果输出的文件已经存在,则会覆盖。因此执行这个操作要非常小心,以免覆盖其它重要文件。

输出重定向 >>

表示重定向到文件末尾,因此它不会像 > 命令这么危险,它是追加到文件的末尾(当然如果文件不存在,也会被创建)。

再次执行 cut -d , -f 1 notes.csv >> name.csv ,则会把名字追加到 name.csv 里面。

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码Mark1
Mark2
Mark3
Mark4
Mark5
Mark6
Mark1
Mark2
Mark3
Mark4
Mark5
Mark6

我们平时读的 log 日志文件其实都是用这个命令输出的。

输出重定向 2>

标准错误输出

1
javascript复制代码cat not_exist_file.csv > res.txt 2> errors.log
  • 当我们 cat 一个文件时,会把文件内容打印到屏幕上,这个是标准输出;
  • 当使用了 > res.txt 时,则不会打印到屏幕,会把标准输出写入文件 res.txt 文件中;
  • 2> errors.log 当发生错误时会写入 errors.log 文件中。

输出重定向 2>>

标准错误输出(追加到文件末尾)同 >> 相似。

输出重定向 2>&1

标准输出和标准错误输出都重定向都一个地方

1
2
javascript复制代码cat not_exist_file.csv > res.txt 2>&1  # 覆盖输出
cat not_exist_file.csv >> res.txt 2>&1 # 追加输出

目前为止,我们接触的命令的输入都来自命令的参数,其实命令的输入还可以来自文件或者键盘的输入。

未命名文件 (2).png

输入重定向 <

< 符号用于指定命令的输入。

1
javascript复制代码cat < name.csv # 指定命令的输入为 name.csv

虽然它的运行结果与 cat name.csv 一样,但是它们的原理却完全不同。

  • cat name.csv 表示 cat 命令接收的输入是 notes.csv 文件名,那么要先打开这个文件,然后打印出文件内容。
  • cat < name.csv 表示 cat 命令接收的输入直接是 notes.csv 这个文件的内容, cat 命令只负责将其内容打印,打开文件并将文件内容传递给 cat 命令的工作则交给终端完成。

输入重定向 <<

将键盘的输入重定向为某个命令的输入。

1
2
3
javascript复制代码sort -n << END # 输入这个命令之后,按下回车,终端就进入键盘输入模式,其中END为结束命令(这个可以自定义)

wc -m << END # 统计输入的单词

管道 |

把两个命令连起来使用,一个命令的输出作为另外一个命令的输入,英文是 pipeline ,可以想象一个个水管连接起来,管道算是重定向流的一种。

未命名文件 (1).png

举几个实际用法案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码cut -d , -f 1 name.csv | sort > sorted_name.txt 
# 第一步获取到的 name 列表,通过管道符再进行排序,最后输出到sorted_name.txt

du | sort -nr | head
# du 表示列举目录大小信息
# sort 进行排序,-n 表示按数字排序,-r 表示倒序
# head 前10行文件

grep log -Ir /var/log | cut -d : -f 1 | sort | uniq
# grep log -Ir /var/log 表示在log文件夹下搜索 /var/log 文本,-r 表示递归,-I 用于排除二进制文件
# cut -d : -f 1 表示通过冒号进行剪切,获取剪切的第一部分
# sort 进行排序
# uniq 进行去重

流

流并非一个命令,在计算机科学中,流 stream 的含义是比较难理解的,记住一点即可:流就是读一点数据, 处理一点点数据。其中数据一般就是二进制格式。 上面提及的重定向或管道,就是把数据当做流去运转的。

到此我们就接触了,流、重定向、管道等 Linux 高级概念及指令。其实你会发现关于流和管道在其它语言中也有广泛的应用。 Angular 中的模板语法中可以使用管道。 Node.js 中也有 stream 流的概念。

查看进程

在 Windows 中通过 Ctrl + Alt + Delete 快捷键查看软件进程。

w

帮助我们快速了解系统中目前有哪些用户登录着,以及他们在干什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码[root@lion ~]# w
06:31:53 up 25 days, 9:53, 1 user, load average: 0.00, 0.01, 0.05
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/0 118.31.243.53 05:56 1.00s 0.02s 0.00s w

06:31:53:表示当前时间
up 25 days, 9:53:表示系统已经正常运行了“25天9小时53分钟”
1 user:表示一个用户
load average: 0.00, 0.01, 0.05:表示系统的负载,3个值分别表示“1分钟的平均负载”,“5分钟的平均负载”,“15分钟的平均负载”

USER:表示登录的用于
TTY:登录的终端名称为pts/0
FROM:连接到服务器的ip地址
LOGIN@:登录时间
IDLE:用户有多久没有活跃了
JCPU:该终端所有相关的进程使用的 CPU 时间,每当进程结束就停止计时,开始新的进程则会重新计时
PCPU:表示 CPU 执行当前程序所消耗的时间,当前进程就是在 WHAT 列里显示的程序
WHAT:表示当下用户正运行的程序是什么,这里我运行的是 w

ps

用于显示当前系统中的进程, ps 命令显示的进程列表不会随时间而更新,是静态的,是运行 ps 命令那个时刻的状态或者说是一个进程快照。

基础语法

1
2
3
4
5
6
7
8
9
javascript复制代码[root@lion ~]# ps
PID TTY TIME CMD
1793 pts/0 00:00:00 bash
4756 pts/0 00:00:00 ps

PID:进程号,每个进程都有唯一的进程号
TTY:进程运行所在的终端
TIME:进程运行时间
CMD:产生这个进程的程序名,如果在进程列表中看到有好几行都是同样的程序名,那么就是同样的程序产生了不止一个进程

常用参数

  • -ef 列出所有进程;
  • -efH 以乔木状列举出所有进程;
  • -u 列出此用户运行的进程;
  • -aux 通过 CPU 和内存使用来过滤进程 ps -aux | less ;
  • -aux --sort -pcpu 按 CPU 使用降序排列, -aux --sort -pmem 表示按内存使用降序排列;
  • -axjf 以树形结构显示进程, ps -axjf 它和 pstree 效果类似。

top

获取进程的动态列表。

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码top - 07:20:07 up 25 days, 10:41,  1 user,  load average: 0.30, 0.10, 0.07
Tasks: 67 total, 1 running, 66 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.7 us, 0.3 sy, 0.0 ni, 99.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1882072 total, 552148 free, 101048 used, 1228876 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 1594080 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
956 root 10 -10 133964 15848 10240 S 0.7 0.8 263:13.01 AliYunDun
1 root 20 0 51644 3664 2400 S 0.0 0.2 3:23.63 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.05 kthreadd
4 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H
  • top - 07:20:07 up 25 days, 10:41, 1 user, load average: 0.30, 0.10, 0.07 相当 w 命令的第一行的信息。
  • 展示的这些进程是按照使用处理器 %CPU 的使用率来排序的。

kill

结束一个进程, kill + PID 。

1
2
3
javascript复制代码kill 956 # 结束进程号为956的进程
kill 956 957 # 结束多个进程
kill -9 7291 # 强制结束进程

管理进程

进程状态

主要是切换进程的状态。我们先了解下 Linux 下进程的五种状态:

  1. 状态码 R :表示正在运行的状态;
  2. 状态码 S :表示中断(休眠中,受阻,当某个条件形成后或接受到信号时,则脱离该状态);
  3. 状态码 D :表示不可中断(进程不响应系统异步信号,即使用kill命令也不能使其中断);
  4. 状态码 Z :表示僵死(进程已终止,但进程描述符依然存在,直到父进程调用 wait4() 系统函数后将进程释放);
  5. 状态码 T :表示停止(进程收到 SIGSTOP 、 SIGSTP 、 SIGTIN 、 SIGTOU 等停止信号后停止运行)。

前台进程 & 后台进程

默认情况下,用户创建的进程都是前台进程,前台进程从键盘读取数据,并把处理结果输出到显示器。例如运行 top 命令,这就是一个一直运行的前台进程。

后台进程的优点是不必等待程序运行结束,就可以输入其它命令。在需要执行的命令后面添加 & 符号,就表示启动一个后台进程。

&

启动后台进程,它的缺点是后台进程与终端相关联,一旦关闭终端,进程就自动结束了。

1
javascript复制代码cp name.csv name-copy.csv &

nohup

使进程不受挂断(关闭终端等动作)的影响。

1
javascript复制代码nohup cp name.csv name-copy.csv

nohup 命令也可以和 & 结合使用。

1
javascript复制代码nohup cp name.csv name-copy.csv &

bg

使一个“后台暂停运行”的进程,状态改为“后台运行”。

1
javascript复制代码bg %1 # 不加任何参数的情况下,bg命令会默认作用于最近的一个后台进程,如果添加参数则会作用于指定标号的进程

实际案例1:

1
2
3
javascript复制代码1. 执行 grep -r "log" / > grep_log 2>&1 命令启动一个前台进程,并且忘记添加 & 符号
2. ctrl + z 使进程状态转为后台暂停
3. 执行 bg 将命令转为后台运行

实际案例2:

1
2
3
4
javascript复制代码前端开发时我们经常会执行 yarn start 启动项目
此时我们执行 ctrl + z 先使其暂停
然后执行 bg 使其转为后台运行
这样当前终端就空闲出来可以干其它事情了,如果想要唤醒它就使用 fg 命令即可(后面会讲)

jobs

显示当前终端后台进程状态。

1
2
3
javascript复制代码[root@lion ~]# jobs
[1]+ Stopped top
[2]- Running grep --color=auto -r "log" / > grep_log 2>&1 &

fg

fg 使进程转为前台运行,用法和 bg 命令类似。

我们用一张图来表示前后台进程切换:

image.png

我们可以使程序在后台运行,成为后台进程,这样在当前终端中我们就可以做其他事情了,而不必等待此进程运行结束。

守护进程

一个运行起来的程序被称为进程。在 Linux 中有些进程是特殊的,它不与任何进程关联,不论用户的身份如何,都在后台运行,这些进程的父进程是 PID 为1的进程, PID 为1的进程只在系统关闭时才会被销毁。它们会在后台一直运行等待分配工作。我们将这类进程称之为守护进程 daemon 。

守护进程的名字通常会在最后有一个 d ,表示 daemon 守护的意思,例如 systemd 、httpd 。

systemd

systemd 是一个 Linux 系统基础组件的集合,提供了一个系统和服务管理器,运行为 PID 1 并负责启动其它程序。

1
2
3
javascript复制代码[root@lion ~]# ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 51648 3852 ? Ss Feb01 1:50 /usr/lib/systemd/systemd --switched-root --system --deserialize 22

通过命令也可以看到 PID 为1的进程就是 systemd 的系统进程。

systemd 常用命令(它是一组命令的集合):

1
2
3
4
5
6
7
8
9
javascript复制代码systemctl start nginx # 启动服务
systemctl stop nginx # 停止服务
systemctl restart nginx # 重启服务
systemctl status nginx # 查看服务状态
systemctl reload nginx # 重载配置文件(不停止服务的情况)
systemctl enable nginx # 开机自动启动服务
systemctl disable nginx # 开机不自动启动服务
systemctl is-enabled nginx # 查看服务是否开机自动启动
systemctl list-unit-files --type=service # 查看各个级别下服务的启动和禁用情况

文件压缩解压

  • 打包:是将多个文件变成一个总的文件,它的学名叫存档、归档。
  • 压缩:是将一个大文件(通常指归档)压缩变成一个小文件。

我们常常使用 tar 将多个文件归档为一个总的文件,称为 archive 。 然后用 gzip 或 bzip2 命令将 archive 压缩为更小的文件。

未命名文件.png

tar

创建一个 tar 归档。

基础用法

1
2
javascript复制代码tar -cvf sort.tar sort/ # 将sort文件夹归档为sort.tar
tar -cvf archive.tar file1 file2 file3 # 将 file1 file2 file3 归档为archive.tar

常用参数

  • -cvf 表示 create(创建)+ verbose(细节)+ file(文件),创建归档文件并显示操作细节;
  • -tf 显示归档里的内容,并不解开归档;
  • -rvf 追加文件到归档, tar -rvf archive.tar file.txt ;
  • -xvf 解开归档, tar -xvf archive.tar 。

gzip / gunzip

“压缩/解压”归档,默认用 gzip 命令,压缩后的文件后缀名为 .tar.gz 。

1
2
javascript复制代码gzip archive.tar # 压缩
gunzip archive.tar.gz # 解压

tar 归档+压缩

可以用 tar 命令同时完成归档和压缩的操作,就是给 tar 命令多加一个选项参数,使之完成归档操作后,还是调用 gzip 或 bzip2 命令来完成压缩操作。

1
2
javascript复制代码tar -zcvf archive.tar.gz archive/ # 将archive文件夹归档并压缩
tar -zxvf archive.tar.gz # 将archive.tar.gz归档压缩文件解压

zcat、zless、zmore

之前讲过使用 cat less more 可以查看文件内容,但是压缩文件的内容是不能使用这些命令进行查看的,而要使用 zcat、zless、zmore 进行查看。

1
javascript复制代码zcat archive.tar.gz

zip/unzip

“压缩/解压” zip 文件( zip 压缩文件一般来自 windows 操作系统)。

命令安装

1
2
3
javascript复制代码# Red Hat 一族中的安装方式
yum install zip
yum install unzip

基础用法

1
2
3
4
javascript复制代码unzip archive.zip # 解压 .zip 文件
unzip -l archive.zip # 不解开 .zip 文件,只看其中内容

zip -r sort.zip sort/ # 将sort文件夹压缩为 sort.zip,其中-r表示递归

编译安装软件

之前我们学会了使用 yum 命令进行软件安装,如果碰到 yum 仓库中没有的软件,我们就需要会更高级的软件安装“源码编译安装”。

编译安装

简单来说,编译就是将程序的源代码转换成可执行文件的过程。大多数 Linux 的程序都是开放源码的,可以编译成适合我们的电脑和操纵系统属性的可执行文件。

基本步骤如下:

  1. 下载源代码
  2. 解压压缩包
  3. 配置
  4. 编译
  5. 安装

实际案例

1、下载

我们来编译安装 htop 软件,首先在它的官网下载源码:bintray.com/htop/source…

下载好的源码在本机电脑上使用如下命令同步到服务器上:

1
2
3
javascript复制代码scp 文件名 用户名@服务器ip:目标路径

scp ~/Desktop/htop-3.0.0.tar.gz root@121.42.11.34:.

也可以使用 wegt 进行下载:

1
2
3
javascript复制代码wegt+下载地址

wegt https://bintray.com/htop/source/download_file?file_path=htop-3.0.0.tar.gz
2、解压文件
1
2
3
javascript复制代码tar -zxvf htop-3.0.0.tar.gz # 解压

cd htop-3.0.0 # 进入目录
3、配置

执行 ./configure ,它会分析你的电脑去确认编译所需的工具是否都已经安装了。

4、编译

执行 make 命令

5、安装

执行 make install 命令,安装完成后执行 ls /usr/local/bin/ 查看是否有 htop 命令。如果有就可以执行 htop 命令查看系统进程了。

网络

ifconfig

查看 ip 网络相关信息,如果命令不存在的话, 执行命令 yum install net-tools 安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码[root@lion ~]# ifconfig

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.31.24.78 netmask 255.255.240.0 broadcast 172.31.31.255
ether 00:16:3e:04:9c:cd txqueuelen 1000 (Ethernet)
RX packets 1592318 bytes 183722250 (175.2 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1539361 bytes 154044090 (146.9 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1000 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

参数解析:

  • eth0 对应有线连接(对应你的有线网卡),就是用网线来连接的上网。 eth 是 Ethernet 的缩写,表示“以太网”。有些电脑可能同时有好几条网线连着,例如服务器,那么除了 eht0 ,你还会看到 eth1 、 eth2 等。
  • lo 表示本地回环( Local Loopback 的缩写,对应一个虚拟网卡)可以看到它的 ip 地址是 127.0.0.1 。每台电脑都应该有这个接口,因为它对应着“连向自己的链接”。这也是被称之为“本地回环”的原因。所有经由这个接口发送的东西都会回到你自己的电脑。看起来好像并没有什么用,但有时为了某些缘故,我们需要连接自己。例如用来测试一个网络程序,但又不想让局域网或外网的用户查看,只能在此台主机上运行和查看所有的网络接口。例如在我们启动一个前端工程时,在浏览器输入 127.0.0.1:3000 启动项目就能查看到自己的 web 网站,并且它只有你能看到。
  • wlan0 表示无线局域网(上面案例并未展示)。

host

ip 地址和主机名的互相转换。

软件安装

1
javascript复制代码yum install bind-utils

基础用法

1
2
3
4
5
javascript复制代码[root@lion ~]# host github.com
baidu.com has address 13.229.188.59

[root@lion ~]# host 13.229.188.59
59.188.229.13.in-addr.arpa domain name pointer ec2-13-229-188-59.ap-southeast-1.compute.amazonaws.com.

ssh 连接远程服务器

通过非对称加密以及对称加密的方式(同 HTTPS 安全连接原理相似)连接到远端服务器。

1
2
3
4
javascript复制代码ssh 用户@ip:port

1、ssh root@172.20.10.1:22 # 端口号可以省略不写,默认是22端口
2、输入连接密码后就可以操作远端服务器了

配置 ssh

config 文件可以配置 ssh ,方便批量管理多个 ssh 连接。

配置文件分为以下几种:

  • 全局 ssh 服务端的配置: /etc/ssh/sshd_config ;
  • 全局 ssh 客户端的配置: /etc/ssh/ssh_config(很少修改);
  • 当前用户 ssh 客户端的配置: ~/.ssh/config 。

【服务端 config 文件的常用配置参数】

服务端 config 参数 作用
Port sshd 服务端口号(默认是22)
PermitRootLogin 是否允许以 root 用户身份登录(默认是可以)
PasswordAuthentication 是否允许密码验证登录(默认是可以)
PubkeyAuthentication 是否允许公钥验证登录(默认是可以)
PermitEmptyPasswords 是否允许空密码登录(不安全,默认不可以)

[注意] 修改完服务端配置文件需要重启服务 systemctl restart sshd

【客户端 config 文件的常用配置参数】

客户端 config 参数 作用
Host 别名
HostName 远程主机名(或 IP 地址)
Port 连接到远程主机的端口
User 用户名

配置当前用户的 config :

1
2
3
4
5
6
7
8
javascript复制代码# 创建config
vim ~/.ssh/config

# 填写一下内容
Host lion # 别名
HostName 172.x.x.x # ip 地址
Port 22 # 端口
User root # 用户

这样配置完成后,下次登录时,可以这样登录 ssh lion 会自动识别为 root 用户。

[注意] 这段配置不是在服务器上,而是你自己的机器上,它仅仅是设置了一个别名。

免密登录

ssh 登录分两种,一种是基于口令(账号密码),另外一种是基于密钥的方式。

基于口令,就是每次登录输入账号和密码,显然这样做是比较麻烦的,今天主要学习如何基于密钥实现免密登录。

基于密钥验证原理

客户机生成密钥对(公钥和私钥),把公钥上传到服务器,每次登录会与服务器的公钥进行比较,这种验证登录的方法更加安全,也被称为“公钥验证登录”。

具体实现步骤

1、在客户机中生成密钥对(公钥和私钥) ssh-keygen(默认使用 RSA 非对称加密算法)

运行完 ssh-keygen 会在 ~/.ssh/ 目录下,生成两个文件:

  • id_rsa.pub :公钥
  • id_rsa :私钥

2、把客户机的公钥传送到服务

执行 ssh-copy-id root@172.x.x.x(ssh-copy-id 它会把客户机的公钥追加到服务器 ~/.ssh/authorized_keys 的文件中)。

执行完成后,运行 ssh root@172.x.x.x 就可以实现免密登录服务器了。

配合上面设置好的别名,直接执行 ssh lion 就可以登录,是不是非常方便。

wget

可以使我们直接从终端控制台下载文件,只需要给出文件的HTTP或FTP地址。

1
2
3
javascript复制代码wget [参数][URL地址]

wget http://www.minjieren.com/wordpress-3.1-zh_CN.zip

wget 非常稳定,如果是由于网络原因下载失败, wget 会不断尝试,直到整个文件下载完毕。

常用参数

  • -c 继续中断的下载。

备份

scp

它是 Secure Copy 的缩写,表示安全拷贝。 scp 可以使我们通过网络,把文件从一台电脑拷贝到另一台电脑。

scp 是基于 ssh 的原理来运作的, ssh 会在两台通过网络连接的电脑之间创建一条安全通信的管道, scp 就利用这条管道安全地拷贝文件。

1
javascript复制代码scp source_file destination_file # source_file 表示源文件,destination_file 表示目标文件

其中 source_file 和 destination_file 都可以这样表示: user@ip:file_name , user 是登录名, ip 是域名或 ip 地址。 file_name 是文件路径。

1
2
javascript复制代码scp file.txt root@192.168.1.5:/root # 表示把我的电脑中当前文件夹下的 file.txt 文件拷贝到远程电脑
scp root@192.168.1.5:/root/file.txt file.txt # 表示把远程电脑上的 file.txt 文件拷贝到本机

rsync

rsync 命令主要用于远程同步文件。它可以同步两个目录,不管它们是否处于同一台电脑。它应该是最常用于“增量备份”的命令了。它就是智能版的 scp 命令。

软件安装

1
javascript复制代码yum install rsync

基础用法

1
2
javascript复制代码rsync -arv Images/ backups/ # 将Images 目录下的所有文件备份到 backups 目录下
rsync -arv Images/ root@192.x.x.x:backups/ # 同步到服务器的backups目录下

常用参数

  • -a 保留文件的所有信息,包括权限,修改日期等;
  • -r 递归调用,表示子目录的所有文件也都包括;
  • -v 冗余模式,输出详细操作信息。

默认地, rsync 在同步时并不会删除目标目录的文件,例如你在源目录中删除一个文件,但是用 rsync 同步时,它并不会删除同步目录中的相同文件。如果向删除也可以这么做: rsync -arv --delete Images/ backups/ 。

系统

halt

关闭系统,需要 root 身份。

1
javascript复制代码halt

reboot

重启系统,需要 root 身份。

1
javascript复制代码reboot

poweroff

直接运行即可关机,不需要 root 身份。

Vim 编辑器

Vim 是什么?

Vim 是从 vi 发展出来的一个文本编辑器。其代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。和 Emacs 并列成为类 Unix 系统用户最喜欢的编辑器。

Vim 常用模式

  • 交互模式
  • 插入模式
  • 命令模式
  • 可视模式

交互模式

也成为正常模式,这是 Vim 的默认模式,每次运行 Vim 程序的时候,就会进入这个模式。

例如执行 vim name.txt 则会进入交互模式。

交互模式特征:

  • 在这个模式下,你不能输入文本;
  • 它可以让我们在文本间移动,删除一行文本,复制黏贴文本,跳转到指定行,撤销操作,等等。

插入模式

这个模式是我们熟悉的文本编辑器的模式,就是可以输入任何你想输入的内容。进入这个模式有几种方法,最常用的方法是按字母键 i ( i、I、a、A、o、O 都可以进入插入模式,只是所处的位置不同),退出这种模式,只需要按下 Esc 键。

  • i, I 进入输入模式 Insert mode : i 为“从目前光标所在处输入”, I 为“在目前所在行的第一个非空格符处开始输入”;
  • a, A 进入输入模式 Insert mode : a 为“从目前光标所在的下一个字符处开始输入”, A 为“从光标所在行的最后一个字符处开始输入”;
  • o, O 进入输入模式 Insert mode : o 为“在目前光标所在的下一行处输入新的一行”; O 为在目前光标所在处的上一行输入新的一行。

命令模式

命令模式也称为底线命令模式,这个模式下可以运行一些命令例如“退出”,“保存”,等动作。

也可以用这个模式来激活一些 Vim 配置,例如语法高亮,显示行号,等。甚至还可以发送一些命令给终端命令行,例如 ls、cp 。

为了进入命令模式,首先要进入交互模式,再按下冒号键。

用一张图表示三种模式如何切换:
image.png

基本操作

打开 Vim

在终端命令行中输入 vim 回车后 Vim 就会被运行起来,也可以用 Vim 来打开一个文件,只需要在 vim 后面再加文件名。如 vim file.name ,如果文件不存在,那么会被创建。

插入

进入文件之后,此时处于交互模式,可以通过输入 i 进入插入模式。

移动

在 Vim 的交互模式下,我们可以在文本中移动光标。

  • h 向左移动一个字符
  • j 向下移动一个字符
  • k 向上移动一个字符
  • i 向右移动一个字符

当然也可以使用四个方向键进行移动,效果是一样的。

跳至行首和行末

  • 行首:在交互模式下,为了将光标定位到一行的开始位置,只需要按下数字键 0 即可,键盘上的 Home 键也有相同效果。
  • 行末:在交互模式下,为了将光标定位到一行的末尾,只需要按下美元符号键 $ 即可,键盘上的 End 键也有相同效果。

按单词移动

在交互模式下,按字母键 w 可以一个单词一个单词的移动。

退出文件

在交互模式下,按下冒号键 : 进入命令模式,再按下 q 键,就可以退出了。

如果在退出之前又修改了文件,就直接想用 :q 退出 Vim ,那么 Vim 会显示一个红字标明错误信息。此时我们有两个选择:

  1. 保存并退出 :wq 或 :x ;
  2. 不保存且退出 :q! 。

标准操作

删除字符

在交互模式下,将光标定位到一个你想要删除的字符上,按下字母键 x 你会发现这个字符被删除了。

也可以一次性删除多个字符,只需要在按 x 键之前输入数字即可。

删除(剪切)单词,行

  • 删除一行:连按两次 d 来删除光标所在的那一行。
  • 删除多行:例如先输入数字 2 ,再按下 dd ,就会删除从光标所在行开始的两行。
  • 删除一个单词:将光标置于一个单词的首字母处,然后按下 dw 。
  • 删除多个单词:例如先按数字键 2 再按 dw 就可以删除两个单词了。
  • 从光标所在位置删除至行首: d0 。
  • 从光标所在位置删除至行末: d$ 。

复制单词,行

  • 复制行:按两次 y 会把光标所在行复制到内存中,和 dd 类似, dd 用于“剪切”光标所在行。
  • 复制单词: yw 会复制一个单词。
  • 复制到行末: y$ 是复制从光标所在处到行末的所有字符。
  • 复制到行首: y0 是复制光标所在处到行首的所有字符。

粘贴

如果之前用 dd 或者 yy 剪切复制过来的,可以使用 p 来粘贴。同样也可以使用 数字+p 来表示复制多次。

替换一个字符

在交互模式下,将光标置于想要替换的字符上。按下 r 键,接着输入你要替换的字符即可。

撤销操作

如果要撤销最近的修改,只需要按下 u 键,如果想要撤销最近四次修改,可以按下4,再按下 u 。

重做

取消撤销,也就是重做之前的修改使用 ctrl + r 。

跳转到指定行

Vim 编辑的文件中,每一行都有一个行号,行号从1开始,逐一递增。

行号默认是不显示,如果需要它显示的话,可以进入命令模式,然后输入 set nu ,如果要隐藏行号的话,使用 set nonu 。

  • 跳转到指定行: 数字+gg ,例如 7gg ,表示跳转到第7行。
  • 要跳转到最后一行,按下 G 。
  • 要跳转到第一行,按下 gg 。

高级操作

查找

处于交互模式下,按下 / 键,那么就进入查找模式,输入你要查找的字符串,然后按下回车。光标就会跳转到文件中下一个查找到的匹配处。如果字符串不存在,那么会显示 "pattern not found" 。

  • n 跳转到下一个匹配项;
  • N 跳转到上一个匹配项。

[注意] 用斜杠来进行的查找是从当前光标处开始向文件尾搜索,如果你要从当前光标处开始,向文件头搜索则使用 ? ,当然也可以先按下 gg 跳转到第一行在进行全文搜索。

查找并替换

替换光标所在行第一个匹配的字符串:

1
2
3
4
5
javascript复制代码# 语法
:s/旧字符串/新字符串

# 实例
:s/one/two

替换光标所在行所有旧字符串为新字符串:

1
2
javascript复制代码# 语法
:s/旧字符串/新字符串/g

替换第几行到第几行中所有字符串:

1
2
3
4
5
javascript复制代码# 语法
:n,m s/旧字符串/新字符串/g

# 实例
:2,4 s/one/two/g

最常用的就是全文替换了:

1
2
javascript复制代码# 语法
:%s/旧字符串/新字符串/g

合并文件

可以用冒号 +r ( :r ) 实现在光标处插入一个文件的内容。

1
javascript复制代码:r filename # 可以用Tab键来自动补全另外一个文件的路径

分屏

Vim 有一个特别便捷的功能那就是分屏,可以同时打开好几个文件,分屏之后,屏幕每一块被称为一个 viewport ,表示“视口”。

  • 横向分屏 :sp 文件名
  • 垂直分屏 :vsp 文件名

分屏模式下的快捷键

  • Ctrl + w 再加 Ctrl + w ,表示从一个 viewport 移动光标到另外一个 viewport ;
  • Ctrl + w 再加 “方向键”,就可以移动到这个方向所处的下一个视口了;
  • Ctrl + w 再加 + 号,表示扩大当前视口;
  • Ctrl + w 再加 - 号,表示缩小当前视口;
  • Ctrl + w 再加 = 号,表示平均当前视口;
  • Ctrl + w 再加 r 键,会反向调换视口位置;
  • Ctrl + w 再加 q 键,会关闭当前视口;
  • Ctrl + w 再加 o 键,会关闭除当前视口以外的所有视口;

运行外部命令 :!

在 Vim 中可以运行一些终端命令,只要先输入 :! ,然后接命令名称。

例如:

1
javascript复制代码:!ls # 在Vim中打开的文件所在的目录运行ls命令

可视模式

前面只讲了 Vim 的三种模式,其实还有一种模式叫做可视模式。

进入它的三种方式(都是从交互模式开始):

  • v 字符可视模式,进入后配合方向键选中字符后,然后再按 d 键可以删除选中。
  • V 行可视模式,进入后光标所在行默认被选中,然后再按 d 键可以删除所在行。
  • Ctrl + v 块可视模式,它是可视模式最有用的功能了,配合 d 和 I 键可以实现删除选中的内容和插入内容。

同时选中多行,并在选中行头部插入内容的具体操作步骤:

1
2
3
4
javascript复制代码1. ctrl + v 进入块可视模式
2. 使用方向键进行选中(上下左右)假设选中5行
3. 输入 I 键进行多行同时插入操作
4. 插入完成后连续按两下 esc 键,实现多行同时插入相同字符

进入可视模式之后的操作键:

  • d 键,表示删除选中;
  • I 键,表示在选中之前插入;
  • u 键,表示选中变为小写;
  • U 键,表示选中变为大写;

Vim 配置

选项参数

在 Vim 被启动后,可以运行一些指令来激活一些选项参数,但是这些选项参数的配置在退出 Vim 时会被忘记,例如前面讲解的激活行号。如果希望所在的配置是永久性的,那么需要在家目录( cd ~ )创建一个 Vim 的配置文件 .vimrc 。

.vimrc

1
2
3
4
5
javascript复制代码set number " 显示行号
syntax on " 激活语法高亮
set showcmd " 实时看到输入的命令
set ignorecase " 搜索时不区分大小写
set mouse=a " 激活鼠标,用鼠标选中时相当于进入可视模式

Vim 配置非常丰富,我们可以通过个性化配置把 Vim 打造成属于自己的 IDE 等等。在 github 上也可以搜索到一些强大的 Vim 配置文件。

总结

相信通过本文的学习,你应该会对 Linux 有一个更加全面的认识。

都看到这里了,就点个👍 👍 👍 吧。

本文转载自: 掘金

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

如何使用 WebClient vs HttpClient v

发表于 2021-03-11

当我们在用 .NET 调用 RestAPI 时通常有三种选择,分别为:WebClient, HttpWebRequest,HttpClient,这篇文章我们将会讨论如何使用这三种方式去调用 RestAPI,我还会提供相应的代码案例来帮助你更好的理解这三者的概念和使用方式,简单来说:

  • HttpWebRequest 是一种相对底层的处理 Http request/response 的方式。
  • WebClient 提供了对 HttpWebRequest 的高层封装,来简化使用者的调用。
  • HttpClient 是一种新的处理 Http request/response 工具包,具有更高的性能。

接下来我们讨论一下抽象类 WebRequest。

WebRequest

WebRequest 是一种基于特定的 http 实现, 它是一个抽象类, 所以在处理 Reqeust 请求时底层会根据传进来的 url 生成相应的子类,如:HttpWebRequest 或 FileWebRequest ,下面的代码展示了如何使用 WebRequest。

1
2
3
4
5
C#复制代码
WebRequest webRequest = WebRequest.Create(uri);
webRequest.Credentials = CredentialCache.DefaultCredentials;
webRequest.Method ="GET";
HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse();

WebRequest 是 .NET Framework 中第一个用来处理 Http 请求的类,在处理 Http请求和响应 方面给调用者提供了诸多的灵活性,你还可以使用这个类来存取 headers, cookies, protocols 和 timeouts 等等,下面的代码展示了其实现子类 HttpWebRequest 是如何使用的。

1
2
3
4
5
6
C#复制代码
HttpWebRequest http = HttpWebRequest)WebRequest.Create(“http://localhost:8900/api/default”);
WebResponse response = http.GetResponse();
MemoryStream memoryStream = response.GetResponseStream();
StreamReader streamReader = new StreamReader(memoryStream);
string data = streamReader.ReadToEnd();

WebClient

WebClient 是 HttpWebRequest 的高层封装,它给调用者提供了更便捷的使用方式,理所当然做出的牺牲就是 WebClient 的性能略逊于 HttpWebRequest,如果你的业务场景只是简单访问第三方的 Http Service,那么我建议你使用 WebClient ,同理如果你有更多的精细化配置则使用 HttpWebRequest,下面的代码展示了如何使用 WebClient 。

1
2
3
4
5
6
7
C#复制代码
string data = null;

using (var webClient = new WebClient())
{
data = webClient.DownloadString(url);
}

HttpClient

HttpClient 是在 .NET Framework 4.5 中被引入的,如果你的项目是基于 .NET 4.5 以上版本,除一些特定的原因之外,建议你优先使用 HttpClient,本质上来说,HttpClient 作为后来之物,它吸取了 HttpWebRequest 的灵活性及 WebClient 的便捷性,所以说 🐟 和 🐻 可兼得。

HttpWebRequest 在 request/response 对象上提供了非常精细化的配置,同时你也要注意 HttpClient 的出现并不是为了取代 WebClient,言外之意就是 HttpClient 也有缺点,比如说:不能提供 进度处理 和 URI 定制,不支持 FTP 等等,HttpClient 的优点也有很多,它所有关于 IO 操作的方法都是异步的,当然有特殊原因的话也可以使用同步方式,下面的代码展示了如何使用 HttpClient。

1
2
3
4
5
6
7
8
9
10
11
C#复制代码
public async Task<Author> GetAuthorsAsync(string uri)
{
Author author = null;
HttpResponseMessage response = await client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
author = await response.Content.ReadAsAsync<Author>();
}
return author;
}

值得注意的是当 response 出现错误时,默认情况下 HttpClient 并不会抛出异常,如果你一定要求 HttpClient 在这种情况下抛出异常,可更改 IsSuccessStatusCode = false 来改变这种默认行为,做法就是调用 response.EnsureSuccessStatusCode();。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C#复制代码
public async Task<Author> GetAuthorsAsync(string uri)
{
Author author = null;

HttpResponseMessage response = await client.GetAsync(uri);

response.EnsureSuccessStatusCode();

if (response.IsSuccessStatusCode)
{
author = await response.Content.ReadAsAsync<Author>();
}

return author;
}

在项目开发中,推荐的做法是保持 HttpClient 的单例化,如果不这么做的话,每次 Request 请求实例化一次 HttpClient ,那么大量的请求必将你的 socket 耗尽并抛出 SocketException 异常。

译文链接:www.infoworld.com/article/319…

本文转载自: 掘金

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

1…707708709…956

开发者博客

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