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

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


  • 首页

  • 归档

  • 搜索

通过 Netty、ZooKeeper 手撸一个 RPC 服务

发表于 2021-05-08
  • 说明
    • 项目链接
    • 微服务框架都包括什么?
    • 如何实现 RPC 远程调用?
    • 开源 RPC 框架
      • 限定语言
      • 跨语言 RPC 框架
  • 本地 Docker 搭建 ZooKeeper
    • 下载镜像
    • 启动容器
    • 查看容器日志
  • RPC 接口
  • Netty RPC server
    • 接口实现
    • 服务启动
    • 注册服务
    • ZooKeeper 实现
  • Netty RPC Client
    • 创建代理
    • 远程调用
  • 编解码
    • RpcDecoder
    • RpcEncoder
  • RpcServerInboundHandler
  • Server 在 ZooKeeper 的路径
  • 说明
  • 参考链接

说明

使用 Netty、ZooKeeper 和 Spring Boot 手撸一个微服务框架。

项目链接

GitHub 源码地址

微服务框架都包括什么?

详细信息可参考:RPC 实战与原理

项目可以分为调用方(client)和提供方(server),client 端只需要调用接口即可,最终调用信息会通过网络传输到 server,server 通过解码后反射调用对应的方法,并将结果通过网络返回给 client。对于 client 端可以完全忽略网络的存在,就像调用本地方法一样调用 rpc 服务。

整个项目的 model 结构如下:

如何实现 RPC 远程调用?

  • 客户端、服务端如何建立网络连接:HTTP、Socket
  • 服务端如何处理请求:NIO(使用 Netty)
  • 数据传输采用什么协议
  • 数据如何序列化、反序列化:JSON,PB,Thrift

开源 RPC 框架

限定语言

  • Dubbo:Java,阿里
  • Motan:Java,微博
  • Tars:C++,腾讯(已支持多语言)
  • Spring Cloud:Java
    • 网关 Zuul
    • 注册中心 Eureka
    • 服务超时熔断 Hystrix
    • 调用链监控 Sleuth
    • 日志分析 ELK

跨语言 RPC 框架

  • gRPC:HTTP/2
  • Thrift:TCP

本地 Docker 搭建 ZooKeeper

下载镜像

启动 Docker,并下载 ZooKeeper 镜像。详见 hub.docker.com/_/zookeeper

启动容器

启动命令如下,容器的名称是zookeeper-rpc-demo,同时向本机暴露 8080、2181、2888 和 3888 端口:

1
yaml复制代码docker run --name zookeeper-rpc-demo --restart always -p 8080:8080 -p 2181:2181 -p 2888:2888 -p 3888:3888  -d zookeeper
1
vbnet复制代码This image includes EXPOSE 2181 2888 3888 8080 (the zookeeper client port, follower port, election port, AdminServer port respectively), so standard container linking will make it automatically available to the linked containers. Since the Zookeeper "fails fast" it's better to always restart it.

查看容器日志

可以通过下面的命令进入容器,其中fb6f95cde6ba是我本机的 Docker ZooKeeper 容器 id。

1
bash复制代码docker exec -it fb6f95cde6ba /bin/bash

在容器中进入目录:/apache-zookeeper-3.7.0-bin/bin,执行命令 zkCli.sh -server 0.0.0.0:2181 链接 zk 服务。

RPC 接口

本示例提供了两个接口:HelloService 和 HiService,里面分别有一个接口方法,客户端仅需引用 rpc-sample-api,只知道接口定义,并不知道里面的具体实现。

1
2
3
java复制代码public interface HelloService {
String hello(String msg);
}
1
2
3
java复制代码public interface HiService {
String hi(String msg);
}

Netty RPC server

启动一个 Server 服务,实现上面的两个 RPC 接口,并向 ZooKeeper 进行服务注册。

接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/**
* @author yano
* GitHub 项目: https://github.com/LjyYano/Thinking_in_Java_MindMapping
* @date 2021-05-07
*/
@RpcServer(cls = HelloService.class)
public class HelloServiceImpl implements HelloService {

@Override
public String hello(String msg) {
return "hello echo: " + msg;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @author yano
* GitHub 项目: https://github.com/LjyYano/Thinking_in_Java_MindMapping
* @date 2021-05-07
*/
@RpcServer(cls = HiService.class)
public class HiServiceImpl implements HiService {

public String hi(String msg) {
return "hi echo: " + msg;
}
}

这里涉及到了两个问题:

  1. Server 应该决定将哪些接口实现注册到 ZooKeeper 上?
  2. HelloServiceImpl 和 HiService 在 ZooKeeper 的路径应该是什么样的?

服务启动

本示例 Server 使用 Spring Boot,但是我们并不需要启动一个 Web 服务,只需要保持后台运行就可以,所以将 web 设置成 WebApplicationType.NONE

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
public class RpcServerApplication {

public static void main(String[] args) {
new SpringApplicationBuilder(RpcServerApplication.class)
.web(WebApplicationType.NONE)
.run(args);
}
}

注册服务

NettyApplicationContextAware 是一个 ApplicationContextAware 的实现类,程序在启动时,将带有 RpcServer(下面会讲解)注解的实现类注册到 ZooKeeper 上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Component
public class NettyApplicationContextAware implements ApplicationContextAware {

private static final Logger logger = LoggerFactory.getLogger(NettyApplicationContextAware.class);

@Value("${zk.address}")
private String zookeeperAddress;

@Value("${zk.port}")
private int zookeeperPort;

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, Object> rpcBeanMap = new HashMap<>();
for (Object object : applicationContext.getBeansWithAnnotation(RpcServer.class).values()) {
rpcBeanMap.put("/" + object.getClass().getAnnotation(RpcServer.class).cls().getName(), object);
}
try {
NettyServer.start(zookeeperAddress, zookeeperPort, rpcBeanMap);
} catch (Exception e) {
logger.error("register error !", e);
}
}
}

RpcServer 注解的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Component
public @interface RpcServer {

/**
* 接口类,用以接口注册
*/
Class<?> cls();

}

applicationContext.getBeansWithAnnotation(RpcServer.class).values() 就是获取项目中带有 RpcServer 注解的类,并将其放入一个 rpcBeanMap 中,其中 key 就是待注册到 ZooKeeper 中的路径。注意路径使用接口的名字,而不是类的名字。

使用注解的好处是,Server A 可以仅提供 HelloService,Server B 仅提供 HiService,不会相互影响且更加灵活。

服务注册主要在 com.yano.server.NettyServer#start 中。

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

private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);

public static void start(String ip, int port, Map<String, Object> params) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();

try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline()
.addLast(new RpcDecoder(Request.class))
.addLast(new RpcEncoder(Response.class))
.addLast(new RpcServerInboundHandler(params));
}
});

ChannelFuture future = serverBootstrap.bind(ip, port).sync();
if (future.isSuccess()) {
params.keySet().forEach(key -> ZooKeeperOp.register(key, ip + ":" + port));
}
future.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}

}

这个类的作用是:

  1. 通过 Netty 启动一个 Socket 服务,端口号通过参数传入
  2. 将上面的接口实现注册到 ZooKeeper 中
1
java复制代码params.keySet().forEach(key -> ZooKeeperOp.register(key, ip + ":" + port));

ZooKeeper 实现

主要就是维护 zk 连接,并将 Server 的 ip 和 port 注册到对应的 ZooKeeper 中。这里使用 Ephemeral node,这样 Server 在下线丢失连接之后,ZooKeeper 能够自动删除节点,这样 Client 就不会获取到下线的 Server 地址了。

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

private static final String zkAddress = "localhost:2181";
private static final ZkClient zkClient = new ZkClient(zkAddress);

public static void register(String serviceName, String serviceAddress) {
if (!zkClient.exists(serviceName)) {
zkClient.createPersistent(serviceName);
}
zkClient.createEphemeral(serviceName + "/" + serviceAddress);
System.out.printf("create node %s \n", serviceName + "/" + serviceAddress);
}

public static String discover(String serviceName) {
List<String> children = zkClient.getChildren(serviceName);
if (CollectionUtils.isEmpty(children)) {
return "";
}
return children.get(ThreadLocalRandom.current().nextInt(children.size()));
}
}

Netty RPC Client

Netty RPC Client 主要是像调用本地方法一样调用上述的两个接口,验证能够正常返回即可。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class RpcClientApplication {

public static void main(String[] args) {
HiService hiService = RpcProxy.create(HiService.class);
String msg = hiService.hi("msg");
System.out.println(msg);

HelloService helloService = RpcProxy.create(HelloService.class);
msg = helloService.hello("msg");
System.out.println(msg);
}
}

运行上述代码,最终控制台会输出:

1
2
bash复制代码hi echo: msg
hello echo: msg

创建代理

1
2
java复制代码HiService hiService = RpcProxy.create(HiService.class);
String msg = hiService.hi("msg");

Client 需要通过 com.yano.RpcProxy#create 创建代理,之后就可以调用 hiService 的 hi 方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class RpcProxy {

public static <T> T create(final Class<?> cls) {
return (T) Proxy.newProxyInstance(cls.getClassLoader(), new Class<?>[] {cls}, (o, method, objects) -> {

Request request = new Request();
request.setInterfaceName("/" + cls.getName());
request.setRequestId(UUID.randomUUID().toString());
request.setParameter(objects);
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());

Response response = new NettyClient().client(request);
return response.getResult();
});
}
}

Server 端要想能够通过反射调用 Client 端请求的方法,至少需要:

  1. 类名 interfaceName
  2. 方法名 methodName
  3. 参数类型 Class<?>[] parameterTypes
  4. 传入参数 Object parameter[]
1
2
3
4
5
6
7
8
9
10
java复制代码@Data
public class Request {

private String requestId;
private String interfaceName;
private String methodName;
private Class<?>[] parameterTypes;
private Object parameter[];

}

远程调用

最终是通过下面这段代码远程调用的,其中 request 包含了调用方法的所有信息。

1
java复制代码Response response = new NettyClient().client(request);
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复制代码/**
* @author yano
* GitHub 项目: https://github.com/LjyYano/Thinking_in_Java_MindMapping
* @date 2021-05-07
*/
public class NettyClient extends SimpleChannelInboundHandler<Response> {

private Response response;

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Response response) {
this.response = response;
}

public Response client(Request request) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();

try {
// 创建并初始化 Netty 客户端 Bootstrap 对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) {
channel.pipeline()
.addLast(new RpcDecoder(Response.class))
.addLast(new RpcEncoder(Request.class))
.addLast(NettyClient.this);
}
});

// 连接 RPC 服务器
String[] discover = ZooKeeperOp.discover(request.getInterfaceName()).split(":");
ChannelFuture future = bootstrap.connect(discover[0], Integer.parseInt(discover[1])).sync();

// 写入 RPC 请求数据并关闭连接
Channel channel = future.channel();
channel.writeAndFlush(request).sync();
channel.closeFuture().sync();

return response;
} finally {
group.shutdownGracefully();
}
}

}

这段代码是核心,主要做了两件事:

  • 请求 ZooKeeper,找到对应节点下的 Server 地址。如果有多个服务提供方,ZooKeeperOp.discover 会随机返回 Server 地址
  • 与获取到的 Server 地址建立 Socket 连接,请求并等待返回

编解码

1
2
3
4
java复制代码channel.pipeline()
.addLast(new RpcDecoder(Response.class))
.addLast(new RpcEncoder(Request.class))
.addLast(NettyClient.this);

Client 和 Server 都需要对 Request、Response 编解码。本示例采用了最简单的 Json 格式。Netty 的消息编解码具体不详细讲解,具体代码如下。

RpcDecoder

RpcDecoder 是一个 ChannelInboundHandler,在 Client 端是对 Response 解码。

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
java复制代码public class RpcDecoder extends MessageToMessageDecoder<ByteBuf> {

private final Class<?> genericClass;

public RpcDecoder(Class<?> genericClass) {
this.genericClass = genericClass;
}

@Override
public void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
if (msg.readableBytes() < 4) {
return;
}
msg.markReaderIndex();
int dataLength = msg.readInt();
if (msg.readableBytes() < dataLength) {
msg.resetReaderIndex();
return;
}
byte[] data = new byte[dataLength];
msg.readBytes(data);

out.add(JSON.parseObject(data, genericClass));
}
}

RpcEncoder

RpcEncoder 是一个 ChannelOutboundHandler,在 Client 端是对 Request 编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class RpcEncoder extends MessageToByteEncoder {

private final Class<?> genericClass;

public RpcEncoder(Class<?> genericClass) {
this.genericClass = genericClass;
}

@Override
public void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) {
if (genericClass.isInstance(msg)) {
byte[] data = JSON.toJSONBytes(msg);
out.writeInt(data.length);
out.writeBytes(data);
}
}
}

RpcServerInboundHandler

这个是 Server 反射调用的核心,这里单独拿出来讲解。Netty Server 在启动时,已经在 pipeline 中加入了 RpcServerInboundHandler。

1
2
3
4
java复制代码socketChannel.pipeline()
.addLast(new RpcDecoder(Request.class))
.addLast(new RpcEncoder(Response.class))
.addLast(new RpcServerInboundHandler(params));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Request request = (Request) msg;
logger.info("request data {}", JSON.toJSONString(request));

// jdk 反射调用
Object bean = handle.get(request.getInterfaceName());
Method method = bean.getClass().getMethod(request.getMethodName(), request.getParameterTypes());
method.setAccessible(true);
Object result = method.invoke(bean, request.getParameter());

Response response = new Response();
response.setRequestId(request.getRequestId());
response.setResult(result);

// client 接收到信息后主动关闭掉连接
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}

Server 在 ZooKeeper 的路径

Server 启动后的输出如下:

其中有 2 行 log:

1
2
bash复制代码create node /com.yano.service.HelloService/127.0.0.1:3000 
create node /com.yano.service.HiService/127.0.0.1:3000

在 ZooKeeper 中查看节点,发现服务已经注册上去了。

1
2
3
4
bash复制代码[zk: 0.0.0.0:2181(CONNECTED) 0] ls /com.yano.service.HelloService
[127.0.0.1:3000]
[zk: 0.0.0.0:2181(CONNECTED) 1] ls /com.yano.service.HiService
[127.0.0.1:3000]

说明

使用 Netty、ZooKeeper 和 Spring Boot 手撸一个微服务 RPC 框架。这个 demo 只能作为一个实例,手动实现能加深理解,切勿在生产环境使用。

本文代码均可在 GitHub 源码地址 中找到,欢迎大家 star 和 fork。

参考链接

github.com/yanzhenyida…

本文转载自: 掘金

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

Spring cloud stream 31 rocket

发表于 2021-05-08

spring cloud stream 新绑定方式

新版spring cloud stream文档

新版提倡用函数式进行发送和消费信息

定义返回类型为Supplier, Function or Consumer的bean提供消息发送和消费的bean

看看绑定名称命名规则

  • input - + -in- +
  • output - + -out- +

在配置文件中指定spring.cloud.function.definition的名称后会把这个bean绑定到对应的消费者和提供者上.

如下定义 会把bean绑定在消费者consumerEvent-in-0或者提供者consumerEvent-out-0上

多个bean可以用 ; 进行分割

1
2
3
4
yaml复制代码spring:
cloud:
function:
definition: consumerEvent

指定这个消费者的topic和group

1
2
3
4
5
6
7
yaml复制代码spring:
cloud:
stream:
bindings:
consumerEvent-in-0:
destination: DEMO
group: demo-group

注册消费者的bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// 第一种方式(官方推荐)
@Bean
public Function<Flux<Message<String>>, Mono<Void>> consumerEvent() {
return flux -> flux.map(message -> {
System.out.println(message.getPayload());
return message;
}).then();
}

// 第二种方式
// 注意使用Flux 要调用 subscribe 不然这个方法不会被消费
@Bean
public Consumer<Flux<Message<String>>> consumerEvent() {
return flux -> flux.map(message -> {
System.out.println(message.getPayload());
return message;
}).subscribe();
}
// 或
@Bean
public Consumer<Message<String>> consumerEvent() {
return message -> System.out.println(message.getPayload());
}

示例

提供者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Configuration
public class EventSender {
@Bean
public Demo demo() {
return new Demo();
}

static class Demo implements CommandLineRunner {
@Autowired
StreamBridge streamBridge;

@Override
public void run(String... args) throws Exception {
final Message<T> message = MessageBuilder.withPayload("Body")
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON)
.build();

// 第一个配置的是目的地
// 如果在yaml中有配置会发送到yaml中目的地
streamBridge.send("DEMO", message);
}
}
}

配置rocketmq和stream的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码spring:
application:
name: demo
cloud:
stream:
rocketmq:
binder:
name-server: 127.0.0.1:9876
group: demo
bindings:
consumerEvent-in-0:
destination: DEMO
content-type: application/json
group: demo-group
function:
definition: consumerEvent

注册一个消费者

1
2
3
4
5
6
7
8
9
10
java复制代码@Configuration
public class EventReceptor {
@Bean
public Function<Flux<Message<String>>, Mono<Void>> consumerEvent() {
return flux -> flux.map(message -> {
System.out.println(message.getPayload());
return message;
}).then();
}
}

依赖

spring cloud 2020 默认不使用bootstrap启动 要加这个依赖spring-cloud-starter-bootstrap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pom复制代码<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5-RocketMQ-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.0.2</version>
</dependency>

Tag过滤

在新版的时候过滤tag一直失效, 后面看源码发现新版的sql和tag结合到subscription的属性中

1
2
3
4
5
java复制代码this.pushConsumer.subscribe(this.topic, RocketMQUtils.getMessageSelector(((RocketMQConsumerProperties)this.extendedConsumerProperties.getExtension()).getSubscription()));

public static MessageSelector getMessageSelector(String expression) {
return StringUtils.hasText(expression) && expression.startsWith("sql:") ? MessageSelector.bySql(expression.replaceFirst("sql:", "")) : MessageSelector.byTag(expression);
}

如果消费者要过滤某个tag需要这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码// 新版 (现在的写法)
rocketmq:
bindings:
createUserAccountEvent-in-0:
consumer:
subscription: DEMO-TAG

// 旧版 (以前的写法)
rocketmq:
bindings:
createUserAccountEvent-in-0:
consumer:
tag: DEMO-TAG

本文转载自: 掘金

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

字节跳动无恒实验室:揭秘手游外挂之基于内存蜜罐的内存修改挂分

发表于 2021-05-08

经过近几年游戏市场的变迁,手游市场也在飞速发展。同时手游本身的安全风险也逐渐暴露出来。无恒实验室也在承担着手游安全评审的相关工作,上期我们分享了游戏安全评审的技术进阶历程。2020年市场上重度手游的不断推出,游戏外挂的风险更是与日俱增,无恒实验室也加入到反外挂的战场。外挂分析作为反外挂的第一步,分析的深度、质量和时效,又往往对外挂打击起着决定性的作用。

本文从外挂分类讲起,给大家一个初步感性认知,之后对占比高达90%以上的内存修改挂的快速分析技巧进行详细介绍。

一、外挂分类

2020年伊始,外挂情报同学收集了不同游戏大量的外挂样本,从技术实现上大概分为以下几类

  • 定制挂:针对特定游戏逻辑或数据特征,通过直接修改客户端逻辑、数据或读取游戏核心数据并展示,以实现游戏作弊功能,常见的有下几类
    • root、越狱类注入型外挂
    • 基于应用多开形式的外挂
    • 基于vmos、光速虚拟机等虚拟机挂
    • 基于windows+模拟器类型的外挂
  • 通用修改器:具备内存查找修改功能的通用或者自定义作弊工具,比如gameguardian、igg、ce、葫芦侠等
  • 脚本辅助类:通过录制玩家操作反复重放,或通过取色点识图等方式进行自动操作的辅助程序。比如按键精灵、叉叉助手等
  • 破解版:修改游戏客户端逻辑、数据、资源,重打包形成具备一定作弊功能的非法客户端,常见于单机休闲类游戏。

尽管技术表现形式多种多样,但从原理上无外乎内存修改、函数调用、模拟点击、协议模拟,其中尤以内存修改类外挂占比居多,不完全统计内存修改类可占到90%以上的比例。

二、内存修改挂分析思路

内存修改主要包括代码、数据、资源、显存修改外挂,分析主要有三步骤

  1. 确定被修改内存的类型、修改前后的数据,可能存在多处修改。如果直接命中修改代码段则大概率即是外挂功能与此代码实现有关,可省略以下步骤。
  2. 过滤筛选有效内存修改:通过还原内存修改位置,逐步排除无效的内存修改点。
  3. 确认外挂原理:根据不同的游戏引擎不同的实现方式,实现方法不同,不过思想是一致的,即通过监控游戏内存对象的分配释放,搜索第二步得到的内存地址来精确匹配修改的内存对象即可。

高质量的外挂分析,既需要知道外挂做了什么,同时也应该分析清楚外挂为什么这么做,搞清楚外挂功能的内在原理,对游戏引擎、opengl、脚本等的理解提出了比较高的要求。限于篇幅,本文仅针对内存修改挂第一步提出了不同情景下的快速分析方法。

2.1 场景1 跨进程修改手游内存

此类场景相信大家并不陌生,主要是通用修改器和定制挂,定位方法也较为简单

2.1.1通用修改器

先来看一段GG外挂脚本,如下所示,清楚写明了外挂搜索替换流程,想象下如果分析外挂时能够获取到GG脚本,那么外挂分析定位将极大的简化。

1.png

然而,现实是残酷的,实际上外挂制作者为了防止外挂脚本外泄,一般都会自定义lua解释器并对lua脚本进行加密处理,如下图所示,反编译难度和时间成本大大增加。

2.png

其实没必要硬碰硬,试想如果我们能够对GG api挂钩子,然后将API调用序列和参数都打印出来,不就变相的实现了脚本反编译,在此仅提示思路,具体实现有兴趣的同学可以动手尝试。

2.1.2通用的跨进程监控分析

顺着刚才的思路继续思考,既然是跨进程的内存读写,必然要调用系统api,如果我们在系统api上做文章,不就可以得到通用的内存修改挂的分析定位方法吗?经过实践,大致总结以下四种跨进程读写方式,感兴趣的同学可以动手实战锻炼下,细节不再赘述。

  • process_vm_readv、process_vm_writev
  • /proc/pid/mem
  • /dev/mem(涉及整个物理内存的读写,外挂用的比较少)
  • Ptrace PTRACE_PEEKDATA/PTRACE_PEEKTEXT、PTRACE_POKETEXT/POKEDATA

2.2 场景2 类似注入修改类(虚拟机、多开、Window+模拟器类)

如果说场景1是定点API突破,那场景2就比较复杂了,常规思路只能通过定位外挂模块,脱壳反编译分析+动态调试定位,对于未加固的外挂程序还相对可接受,但如果外挂模块保护比较强,在短短的一天左右时间内分析清楚外挂原理,堪称地狱难度,对人的精力、技巧考验极大,这也是本文重点要讲述的问题。

不止一次的问自己,有没有更好更有效的方法,好在懒人有懒福,经过一段时间摸索思考,终于总结出一套较为实际可行的方案。内存蜜罐分析方案作为通用的分析方案,可有效解决注入类外挂的内存修改定位难题,对跨进程修改内存也有效,可以说统一内存修改类外挂的分析方法。

三、内存蜜罐原理简介

讲原理之前,我们先回顾下内存修改挂的第一步搜索定位指定数据,可能涉及偏移和多级指针,第二步才是修改。而我们的目标是定位修改的位置和长度,如果我们直接dump外挂修改前后的进程内存进行对比,则修改的位置必然在其中。但是面对茫茫多的修改位置,如何确定外挂究竟修改的哪一处呢?因此问题转换为修改后的内存精确定位问题,这也是内存蜜罐名称的由来。

内存蜜罐方案的核心就是监控对比外挂功能修改后和修改前的内存变化,精心构造具有指定关系的内存布局,模拟修改前的内存状态,诱导外挂功能关闭开启后再次修改蜜罐内存,通过蜜罐前后的内存对比,即可定位外挂被修改的所有内存位置和修改前后数据,解决分析思路第一步的问题。

针对第二步的问题,通过逐步还原外挂修改的内存并进行测试,即可定位有效内存位置及修改前后数据。

3.1概念介绍

3.1.1结构体范围

针对每一处内存修改,外挂一般通过特征搜索定位内存地址+偏移,中间可能涉及多级指针问题。因此每一步内存修改需要确定结构体范围。假设地址0x1000中的数据被修改,则构造0x900中-0x1100中的数据,其中,0x100为结构范围配置项,可考虑4字节对齐。

3.1.2指针级别

默认1-3级指针,最多支持5级指针,指针级别越高,所需内存越大。

针对结构体中的地址地址范围,进行全局搜索。

3.2蜜罐实现步骤

3.2.1 DUMP

枚举游戏进程所有内存模块,将关注的内存dump到磁盘中,作为原始内存。由于进程运行中,各种内存时刻变化,为了缩小蜜罐监控范围,可以考虑冻结部分线程,并根据游戏类型情况可有选择的去除部分内存

  • 非游戏逻辑相关的内存,比如安卓中/dev、apk、dex、jar、dalvik、zygote进程空间内存的其他内存
  • 可以考虑去除系统模块内存
  • 只监控游戏引擎核心模块内存及其分配的内存

3.2.2蜜罐构造

做完第一步,即可开启开挂功能,待外挂修改内存完毕,即可构造蜜罐。蜜罐构造期间、可尝试冻结游戏进程,减少无效修改项的干扰。根据构造方式的不同,又分为内存安全型蜜罐和内存破坏型蜜罐。

内存安全蜜罐

原理

以指针级别2,结构体范围为举例

3.png

实现流程

以指针级别2,结构体范围为举例

  1. 外挂功能开启前,dump maps文件中所有内存镜像imag0
  2. 根据级别筛选需要监控的内存范围列表
  3. 外挂功能开启后,对比监控的内存哪些位置发生改变,形成modify1(地址、原始值、修改后的值)列表,若修改代码段则仅报告修改内容,不存放到modify1中
  4. 指针级别1,申请内存,直接存放modify1列表相关的结构体内存范围
  5. 指针级别2,在imag0镜像中,搜索modify1结构体范围的指针,形成modify2(地址、原始值)列表,申请内存,直接存放modify2列表相关的结构体内存范围,并修正指针
  6. 指针级别3,在imag2镜像中,搜索modify1结构体范围的指针,形成modify3(地址、原始值)列表,申请内存,直接存放modify3列表相关的结构体内存范围,并修正指针
  7. 将以上自己构造的多个内存蜜罐保存为image1,释放modify1、modify2、modify3
  8. 关闭外挂功能并重新开启,对比监控的内存蜜罐中哪些位置发生改变,此处即为外挂实际修改的内存。

内存破坏性蜜罐

原理

该方式不存在多级指针问题,直接将所有指向一级指针的数据,改为构造的内存蜜罐中的地址劣势:可能会造成游戏crash或者功能异常。

4.png

实现流程

以指针级别2,结构体范围为举例,相比内存安全蜜罐,流程大大简化

  1. 外挂功能开启前,dump maps文件中所有内存镜像imag0
  2. 根据级别筛选需要监控的内存范围列表
  3. 外挂功能开启后,对比监控的内存哪些位置发生改变,形成modify1(地址、原始值、修改后的值)列表
  4. 指针级别1,申请内存,直接存放modify1列表相关的结构体内存范围
  5. 在进程内存空间中搜索modify1结构体地址范围,只要命中,则替换为内存蜜罐中的地址。
  6. 将以上自己构造的多个内存蜜罐保存为image1
  7. 关闭外挂功能并重新开启,对比监控的内存蜜罐中哪些位置发生改变,此处即为外挂实际修改的内存。

3.2.3计算差异

待内存蜜罐构造完成,重新关闭、打开外挂功能。由于上一步内存蜜罐已经按照外挂功能开启前后的内存变化构造了所有被新修改内存的多级内存镜像,因此重新打开外挂功能时内存蜜罐也会一并被搜索到进而修改。

通过dump的镜像内存和内存蜜罐现有内存的比对,即可定位出所有被外挂修改的蜜罐内存位置,进而映射出原始游戏进程中被蜜罐修改的内存起始位置,修改前后的数据。

3.2.4筛选有效内存

将第三步中定位出的所有原始内存修改位置,逐项还原测试外挂功能是否生效,即可精准定位有效内存的修改位置。

四、结束语

整个蜜罐原理和实现并不复杂,难点在于控制蜜罐内存占用量,实际使用中需要控制好结构体范围、多级指针深度和性能优化,由于时间仓促和保密问题,难以将整个方案详尽的展示给大家,未尽之处望大家体谅,欢迎大家拍砖讨论。

如果你也想参与到我们的游戏安全工作建设中来,体会在各类游戏中发现漏洞的乐趣、享受和黑产外挂斗的其乐无穷,欢迎加入无恒实验室,简历投递:job.toutiao.com/s/e1RPP48;或…

5.png

本文转载自: 掘金

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

服务端接口测试指南 1、了解接口 2、如何测试接口 3、如何

发表于 2021-05-08

本文为内部分享文档,意在从基础到深入分享单接口测试及系统接口测试内容。

此文借鉴和引用了多位同行的文章内容,均已标注内容来源。大家可通过链接查看相关内容原文。

测试的第一目标是质量保障,所以我们从质量保障的纬度,来了解接口测试。

1、了解接口

1.1 接口做了哪些事?

首先,从功能角度理解。
比如,从一个用户购买一个商品的业务流程来理解:
image

  • 新用户注册:通过注册接口 新增一条用户数据(Create);
  • 注册成功后登录:通过登录接口,首先根据用户名查询(Select)密码,然后校验密码,密码校验成功,通过规则和加密 新建(Create) token签名,将token发送给客户端;
  • 搜索商品:通过商品搜索接口搜索目标商品,本质是从商品数据库中进行条件查询(Select);
  • 查看商品详情:商品ID + 商品详情接口,查询商品详情(Select);
  • 选择商品加入购物车:添加购物车接口,更新购物车数据,库存数据减一(Update);
  • 创建地址并选择地址:新建地址接口,新增一个新的地址(Create);
  • 下单&结算:下单接口,新增一条新的订单数据(Create);
  • 支付:支付接口,如果支付成功,新增一条支付信息,订单状态更新为已支付,新增一条电子发票,新增一条物流信息。

接口的功能主要是客户端和服务端的数据交互,即通过接口对后端数据的增删改查,来实现用户和产品的交互。

1.2 如何保障接口质量

从京东网站的注册接口来看,我们需要从哪些纬度保障质量。

分析注册接口

注册页面:
image
image

Http 注册接口:
image

Request Header:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
http复制代码Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: keep-alive
Content-Length: 6028
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: shshshfpb=sJZnAsUTcZJjuNedVMhztBA%3D%3D;
Host: reg.jd.com
Origin: https://reg.jd.com
Referer: https://reg.jd.com/reg/person?ReturnUrl=https%3A//www.jd.com/
sec-ch-ua: "Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
X-Requested-With: XMLHttpRequest

Request Body:(忽略部分无法解读参数)

1
2
3
4
5
6
7
8
http复制代码uuid: 68455e864c894bda845de413849204d0
authCodeStrategy: 1 (验证码策略)
phone: +00861553605xxxx(手机号)
mobileCode: 116547(手机验证码)
regName: demo83520(注册的用户名)
email: xxx@163.com(注册邮箱)
mailCode: 661591(邮箱验证码)
pwd: MvaEqtzkZ4/R4P3wMoRIuZpA4egWYBmz7bikspIWRYwozJgOHJQlQW8POp8elFhi7OXchoz1OPRoFwxqjWpwcWQCUABx5oovhFxLZ0p8CqB3s0lNDz9QlF8ZYMBanwk+Cne4mXMOTop9OGD8XF8YPqb4qkox8A=(密码加密字符串)

接下来,从接口开发设计角度,分析注册接口。因为是纯黑盒角度,所以从UI交互和接口参数两方面分析注册接口的设计逻辑。

UI交互角度分析

  • 同样手机号、不同邮箱最多可以注册3个账号;
  • 用户名:支持中文、英文、数字、”-“、”_”的组合,4-20个字符,不能是纯数字;用户名不能重复;
  • 密码:长度只能在8-20个字符之间,建议使用字母、数字和符号两种及以上组合;不能频繁注册;

接口参数分析(取几种主要参数)

  • uuid:用户唯一ID?uid生成规则?
  • phone:注册手机号格式校验?
  • mobileCode:手机验证码位数、字符类型校验?
  • regName:用户名规则校验?
  • email:注册邮箱格式校验?
  • mailCode:邮箱验证码 位数、字符类型校验?
  • pwd:密码加密后字符串

2、如何测试接口

接口测试,主要分三步:

  1. 准备测试数据(可能不需要);
  2. API测试工具,发起请求;
  3. 验证返回结果的Response;

(1)测试数据

测试数据生成方式:

  • 基于API生成数据;
  • 数据库直接构造数据;
  • UI操作生成数据;

生成时机:

  • 实时创建:测试用例执行过程中生成(会导致测试用例执行时间变长);
  • 事先创建:测试执行前,批量生成所有测试数据(可能事先创建好的数据已经被修改而无法正常使用);
  • 测试环境不稳定,导致测试数据的顺利创建;

脏数据:

  • 概念:脏数据是指,数据在被实际使用前,已经被进行了非预期的修改。

测试数据分类:

  • “死水数据”:指相对稳定,不会在使用过程中改变状态,并且可以被多次使用的数据。这类数据适合事先创建。
    • 注意:“死水数据”是相对稳定的,是否稳定由测试目的决定。比如用户数据,在测试非用户相关测试时基本稳定,但是对于专门测试用户账号的测试用例来讲,往往涉及到用户注销之类功能,所以此时就是不稳定的。
  • “活水数据”:指只能被一次性使用,或者经常会被修改的数据。例如 优惠券、订单之类的数据。

(2)测试用例设计

用例设计从两个维度来设计,功能性需求和非功能性需求。

功能性需求

用例设计法参考:软件测试基础—流程和用例设计方法-piecesof

image

非功能性需求-安全纬度

  • 敏感信息是否加密:密码前后端传输是否加密
  • sql注入?(结合用户的输入数据动态构造 Sql 语句,如果用户输入的数据被构造成恶意 Sql 代码,Web 应用又未对动态构造的 Sql 语句使用的参数进行审查,则会带来意想不到的危险。)
  • 逻辑漏洞:
    • 批量注册重复消费问题?(比如,同一时间,同样的参数,高并发请求,是否只有一个Http请求注册成功)
    • 同一手机号,不同邮箱,是否可以注册超过3个用户?

非功能性需求-性能纬度

  • 基准性能是否达标?比如单请求要求小于500ms?
  • 高并发性能评估:通过性能测试手段,评估注册接口性能。具体查看:服务端性能测试-入门指南 和 服务端性能测试-工具篇

功能性需求纬度:利用正交法,最少也有29条用例。
非功能性需求纬度-安全纬度:4条用例。
非功能性需求纬度-性能纬度:2条用例。

(3)如何做接口断言?

  • Http Response断言:
    • Http 状态码
    • Response Body 字段、结构 校验
    • Response Header
  • 数据断言:
    • 对数据库中的数据断言
  • 响应时间满足要求吗?

3、如何用接口测试一个系统?

(1)复杂系统测试用例结构

参考:HttpRunner之step/case/suite

测试步骤(testStep) -> 测试用例(testCase) -> 测试场景/测试用例集(testSuite)

image

测试步骤(testStep)

对于接口测试来说,每一个测试步骤应该就对应一个 API 的请求描述。

测试用例(testCase)

测试用例(testcase)应该是为了测试某个特定的功能逻辑而精心设计的,并且至少包含如下几点:

  • 明确的测试目的
  • 明确的输入
  • 明确的运行环境
  • 明确的测试步骤描述
  • 明确的预期结果

测试用例设计原则:

  • 测试用例应该是完整且独立的,每条测试用例都应该可以独立运行;
  • 测试用例由测试脚本和测试数据两部分构成。
  • 测试脚本:测试脚本只关注被测的业务功能逻辑,包括前置条件、测试步骤、预期结果等。
  • 测试数据:是对应测试的业务数据。
  • 测试数据和测试脚本分离:方便实现数据驱动测试。通过对测试脚本传入一组数据,实现同一业务功能在不同数据逻辑下的测试验证。比如:购买商品接口,会员和非会员的商品价格是不一样的,优惠券逻辑也不一样。所以通过不同的用户数据,可以测试会员和非会员购物逻辑。

测试用例集(testSuite)

测试用例集是测试用例的无序集合,集合中的测试用例应该互相独立,不存在先后依赖关系。

如果确实存在先后依赖关系,例如登录功能和下单功能。正确的做法应该是,在下单测试用例的前置步骤中执行登录操作。

(2)测试数据管理

来源:孙高飞-测试框架中的数据管理策略

数据的两个属性:

  • 作用域:共享数据(适用于testSuite级别)、隔离数据(适用于testCase级别)
  • 创建方式:调用开发接口、使用Sql、独立开发数据模版

测试数据的作用域

共享数据:所有case或一部分case共同使用的测试数据

  • 优点:速度快,数据只需要创建一次就可以给很多 case 使用。
  • 缺点:
    • 数据为很多 case 准备的,你很难分清哪些数据是给这个 case 准备的,哪些数据是给另一个 case 准备的。case 的可读性低
    • case 之间互相有影响。因为待测的功能本身就会对数据库造成影响。很可能一个 case 的失败或者成功就会造成一批 case 的 fail
    • 数据本身不能扩展,稍微一改动,影响就很广泛。数个甚至数十个 case 的失败是很常见的。维护脚本的成本比较高

隔离数据:每个case都有独享测试数据,case之间互不影响,即每个case都做setup和teardown的操作。case执行前创造数据,执行后销毁数据。

  • 优点:case 之间互不影响,数据之间互不影响。case 的稳定性,可维护性,可读性等都大大提高
  • 缺点:速度慢。。。。灰常慢。。。因为每个 case 都有很多的磁盘 IO 操作。。。维护数据的时间比调用功能的时间都要长的情况并不奇怪。 OK,这种方式其实是我们在测试中运用的最多的方式。虽然它很慢,而且对很多人来说实现起来也比较难。但是它带来的可维护性实在太诱人。我再也不用整天维护那些不稳定的脚本了。慢点就慢点吧。反正我们做接口测试和 UI 自动化测试的持续集成策略也是定时运行的。 跑个 10 几分钟,几十分钟的我也不在乎。 只要不是做监控代码变动的策略,一切好说。

敏感数据:账号、密码、key等敏感信息,设置为有权限限制的环境变量。

  • 敏感信息不能公开的主要原因:
+ 加强权限管控:参与项目的开发人员可能会有很多,大家都有读取代码仓库的权限,但是像 key 这类极度敏感的信息不应该所有人都有权限获取;
+ 减少代码泄漏的危害性:假如代码出现泄漏,敏感数据信息不应该也同时泄漏。
  • 推荐的解决方案:
+ 对服务器进行权限管控,只有运维人员(或者核心开发人员)才有登录服务器的权限;
+ 运维人员(或者核心开发人员):在运行的机器上将敏感数据设置到系统的环境变量中;
+ 普通开发人员:只需要知道敏感信息的变量名称,在代码中通过读取环境变量的方式获取敏感数据。

如何构造数据

调用开发接口

  • 优点:在脚本中实现起来相对简单,不用深入理解后台数据库。
  • 缺点:
+ 耦合性太高,依赖产品的其他接口创造数据的方式注定了 case 是非隔离性的。注意隔离性是 case 质量的一个重要指标。一旦创造数据的接口 bug 了,你说得有多少个 case 失败。而且在真实环境中,需要调用 N 个接口去创造你需要的数据。无法判断到底哪个接口的 bug。这已然变成了端到端测试了。能够快速定位 bug 位置,也同样是 case 质量的重要指标。
+ 如果做隔离数据,产品中的接口往往很难满足你销毁数据的需要,举个最常见的例子。这个世界存在一种删除机制叫做逻辑删除,也是就是产品的接口并不是真正的删除了数据库中的数据,而是用一个逻辑标示位,标示这条数据被删除了。不要再反馈给用户了。 这样其实就做不到 “隔离数据了”
  • 使用建议:不建议使用。虽然该方式脚本维护成本低,但是造成用例耦合度高、隔离性差,问题定位成本高。一个被调用开发接口的BUG,可能会导致大量用例失败。

直接使用 sql:就是直接写 sql 创造和销毁数据。

优点:隔离性和bug 追踪都很好。
缺点:如果交给测试人员在脚本中写 sql 的话,难度,可读性都不太乐观,而且太依赖测试人员本身的能力,出错率较高。不过好在我们可以在测试框架上做一些手脚,解决这个问题。
使用建议:除查询sql外,增删改sql 慎重使用,因为实现成本高、操作风险高。需要非常了解数据库表结构和业务逻辑,删改数据很可能影响实际业务或其他同学测试。

数据模版:为核心业务测试数据,创建独立的数据模版,专人独立维护。

参考:测试能效平台的诞生-国际化商城智能物料平台 · TesterHome

  • 实现思路:
+ 对于数据构造较复杂,且数据关联业务多、异常数据风险高的数据(比如电商的物料数据),建议基于开发接口封装通用函数,且针对性的做好异常处理和定位。
  • 优点:
+ 专人开发维护,极大降低构建复杂数据成本和风险
+ 降低功能测试构造数据精力,突破相关测试人力占用瓶颈。
  • 缺点:开发成本高,使用重量级业务系统

4、接口测试进化

回顾一下前面接口测试的内容,发现几个问题:

  • 复杂系统动则几千个接口,回归测试工作量大;
  • 用例编写成本高,参数多的接口

可以参考如下几个案例,对接口测试做一些生产力的提升。

(1)回归测试工作量大?录制线上流量回归

参考:录制线上流量做回归测试的正确打开方式 · TesterHome

image

image

(2)用例编写成本高?通用接口自动测试方案

参考:通用性接口健壮性扫描方案-有赞

image

(3)快速校验接口数据结构变化?接口自动化全量字段校验

来源:接口自动化全量字段校验 · TesterHome

实现:自定义接口返回数据格式 (【契约定义】)-实际响应数据格式校验 (【契约校验】) 的功能

校验原则:

  • 实际返回字段名要严格等于或者含契约定义字段名(根据不同匹配模式来确定)
  • 字段值可以值相等或类型相等

本文转载自: 掘金

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

关于进程间通信的一点点知识

发表于 2021-05-08

前言

不知道你们有没有被问到过进程间是如何通信的,反正我是在刚毕业的时候被多次问到过,而当时我就知道个共享内存和管道,所以接受进程通信的恐惧吧哈哈哈哈。

概述

什么是进程通信?

由于每个进程都是相对独立运行的,且每个用户请求都可能导致多个进程在操作系统中运行,如果多个进程之间需要协作完成任务,那么进程间可能就需要进行相互通信获取数据,这种通信方式就称作为进程间通信(Inter-process communication IPC)。

维基百科定义

我们再看下维基百科是如何定义 IPC 的

Inter process communication (IPC) is used for exchanging data between multiple threads in one or more processes or programs. The Processes may be running on single or multiple computers connected by a network. The full form of IPC is Inter-process communication.

从定义上可以主要分为两类即本地进程和远程进程,如果是本地进程那么我们直接交换数据就可以了,如果是远程进程的话,那建立网络连接再交换不就行了吗,是不是很简单,其实事实并非如此。

为什么进程间需要通信

为啥要通信?因为只要进程涉及到协作,那么两个进程必然需要进行通信,因为不通信的话,那么基本不能完整的完成一个任务。

不知道大家刷 B 站,如果经常刷的话,可能就会看到这两个梗
在秀恩爱(撒狗粮)的视频下面大概率都有这样类似的评论:”男主什么时候搬出去,我好搬进去“ 的梗
还有大部分的视频都会有”下次一定“的弹幕

这两个例子不够典型,但是可以帮助理解进程协作的两个典型场景,一个是”互斥“,一个是”同步“。

第一个”互斥“,就是一个进程/线程要独占资源,哈哈哈,我不知道第一个例子你们能理解不,如果理解不了,那就把男主的资源想象为房子(或者女朋友哈哈哈哈),只有男主搬出去,另一个进程才能搬进来(或者成为该女朋友的对象哈哈哈哈),这个就称为”互斥“。

第二个例子是”下次一定“,即我安排了我下次的点赞、投币行为的时间,这种叫做”同步“,就是安排好接下来的进程/线程执行的顺序。

进程通信(IPC)

在类 Unix 系统中可以使用的 IPC 方法有很多种,从处理机制的角度看,它们可以分为三大类:基于通信的 IPC 方法、基于信号的 IPC 方法以及基于同步的 IPC 方法。其中,基于通信的 IPC 方法又分为以数据传输为手段的 IPC 方法和以共享内存为手段的 IPC 方法,前者包括了管道、命名管道、消息队列、 SOCKET以及 RPC。以共享内存为手段的 IPC 方法主要以共享内存为代表(它是最快的 IPC 方法)。基于信号的 IPC 方法就是我们常说的操作系统的信号机制,它是唯一的一种异步 IPC 方法。在基于同步的 IPC 方法中,最重要的就是信号量。

管道(PIPE)

管道是一种半双工(单向)的通信方式,只能用于父进程与子进程以及同祖先的子进程之间的通信。举一个常用的例子,我们常常会通过 grep 命令过滤我们想要的看的数据,一般会这么用:

image.png

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序将进程数据写入缓冲区,另一端进程则顺序读取数据,该缓冲区可以看作一个循环队列,读和写的位置都是自动增加的,一个数据只能被读取一次,读出以后数据将不存在于缓冲区中。管道也可以看成一个文件,但不是普通的文件,也不属于任何的文件系统,管道自己构成一个文件系统,存在于内存中。

image.png

虽然管道很简单,一端写入数据另一端读出数据,但是也有一定的局限性:

  • 半双工(单向),数据只能向一个方向流动
  • 管道只能在具有共同的父进程之间的两个进程使用
  • 需要双方通信时,需要建立两个管道

因为管道是半双工的,也就是说进程 A 可以将数据传输到进程 B,而进程 B 要将数据传输到进程 A,那就需要建立另外一个管道来实现。在默认情况下,所有管道都是未命名的,这就意味着当没有进程使用管道时,管道则被移除(从内存中删除)。

命名管道(FIFO)

在默认管道中只能是两个具有血缘关系的进程才能进行使用,而命名管道则突破这个限制。FIFO 不同于管道之处在于它提供一个路径与之关联,以 FIFO 的文件形式存在于系统中,只要可以访问该路径的权限,那么就能够彼此通过 FIFO 进行通信,所以,通过 FIFO 不相关的进程也能交换数据。

若没有进程使用该命名管道时,该命名管道则会被从内存/缓冲区中移除,但并不会永久消失,因为命名管道存在的形式是被持久化存在磁盘系统中的,这一点与管道是有一定的区别。

命名管道同管道相同也是半双工的通信方式,也就是两个进行若需要相互传输数据需要建立两个命名管道进行传输。

消息队列

消息队列是在两个进程之间传递数据的一种简单有效的方式,每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,有点类似于邮箱,当我接收到数据时就保存起来,当某个进程需要一种类型的数据时就去消息队列中获取即可。

消息队列的优点

  • 可以通过发送消息来避免命名管道的同步和阻塞问题
  • 可以用一些方法来提前查看紧急消息

消息队列的缺点

  • 与管道一样,每个数据块都有一个最大长度的限制
  • 系统中所有队列包含的数据块的总长度也有一个上限

image.png

SOCKET

socket 一般我们称为套接字(计算机专业的小伙伴应该对这个词很熟悉吧),socket 也是一种 IPC 方法,但是与其他 IPC 机制不同的是,socket 通信机制不需要两个进程必须在同一个计算机系统中,它则是以网络连接的形式让多个进程建立通信并相互传递数据。

socket 会设置到大量的 TCP/IP 协议栈的知识,而本篇文章只介绍进程通信,如果有小伙伴对 socket 感兴趣的话,后面我可以单独写一篇关于 socket 的文章(或者可以自行查找相关的资料哈哈哈)

共享内存

共享内存是最高效的 IPC 机制,因为它不涉及到进程之间的任何数据传输,因为它共享一个物理内存的位置,多个进程可以在该位置进行读写操作,这些进程通过将本地进程映射到共享物理内存位置(通过指针或者其他方法)来实现。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则就会产生竞态条件,因此,共享内存通常和其他进程通信方式一起使用。

竞态条件:两个进程竞争同一资源时,如果对资源的访问顺序敏感,就被称存在竞态条件

虽然共享内存是最高效的 IPC 机制,但还是有一定的局限性:使用共享内存的通信方式的进程必须处于同一计算机系统,有物理内存可以共享才行。

image.png

共享内存和消息队列,管道通信的区别:

消息队列,管道数据传递方式一般为:

  • 获取输入
  • 通过消息队列,管道写入数据,通常需要从进程拷贝到内核
  • 从内核拷贝到进程
  • 进程在拷贝到输出文件

共享内存数据传递方式一般为:

  • 将数据从文件中输入到共享内存中
  • 从共享内存中输出到文件中

所以共享内存不涉及到内核操作,而且只需要两步即可完成数据的传输,所以相对来说共享内存是最高效的 IPC 机制。

信号

操作系统信号(signal)是 IPC 中唯一一种异步的通信方法,它的本质是用软件来模拟硬件的终端机制,信号用来通知某个进程有某个事件发生了。例如,在终端中按下 cmd+c 则会停止正在运行的程序,以及我们常用的 kill 命令同样有信号的参与,如:kill -9 pid,( 不知道大家在用的时候知不知道这个 -9 是什么意思,等下会有介绍到)。

与其他 IPC 方式不同的是,信号属于不精确通信,信号只能告诉进程大概发生了什么事情,但是不能准确的告诉进程详细的细节信息,还有就是,进程间的通信并不完全是为了数据交换才进行 IPC,信号就是只会告诉对方需要作出相应的事件即可。

在 Linux 系统中每一个信号都有一个 SIG 为前缀的名字,例如 SIGINT、SIGKILL 等等,但是在操作系统内部,这些信号都是由正整数来表示,这些正整数被称为信号编号,例如一个进程不能正常退出,那我们就通过进程ID(PID)进行杀掉,则用到了 kill -9 pid 这个命令,而 9 就是信号编号,对应的信号就是 SIGKILL(在 MacOS 中对应的则是 KILL)。

image.png

信号量

当多个进程同时访问系统上的某个资源时,比如同时写一个数据库的某条记录,或者同时修改一个文件时,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问/操作。通常,程序对共享资源的访问代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。这段代码则被称为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。

信号量与信号完全是两种概念,信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待和信号。在类 Unix 系统中,等待和信号都已经具有特殊含义了,所以对信号量的这两种操作更常用P、V操作来称呼。这两个字母来自荷兰语单词 passeren(传递,进入临界区)和 vrijgeven(释放,退出临界区)。假设有信号量 SV,则对它的 P、V操作如下:

  • P(SV):如果SV的值大于 0,就将它减去1;如果 SV 的值为 0,则挂起进程的执行
  • V(SV):如果有其他进程因等待 SV 而挂起,那么就唤醒;如果没有,则将 SV 加 1信号量的取值可以是任何自然数,但常用的、最简单的信号量是二进制信号量,它只能取 0 和 1 这两个值,使用二进制信号量同步两个进程,以确保关键代码段的独占式访问的一个典型例子:

img

在图中,当关键代码可用时,二进制信号量 SV 的值为 1,进程 A 和 B 都有机会进入关键代码段。如果此时进程 A 执行了 P(SV)操作将 SV 减 1,则进程 B 若在执行 P(SV)操作就会被挂起,直到进程 A 离开关键代码段,并执行 V(SV)操作将 SV 加 1,关键代码段才重新变得可用。如果此时进程 B 因为等待 SV 而处于挂起状态,则它将被唤醒,并进入关键代码段。

信号量,有点类似于锁的概念

总结

这篇文章主要介绍了为什么需要 IPC,以及 IPC 的几种方式,理论知识较多,已经尽可能的通过一些图来辅助理解这些知识了,其实也不是很难理解吧哈哈哈。

如果我们把进程的底层原理理解明白(知道进程的大概概念也行),那么我们在编程中就有可能写出高可用的的并发编程代码(啊,理想状态),而且还是现在这个张口闭口都是高并发的时代,不谈并发都不是”合格的程序员“的时代(这是一个”合格程序员“对我说的。。挺讽刺的),进程则是支撑并发编程的基础啦。

所以总的来说,学习进程的底层原理会大大的帮助我们对并发编程的理解。

参考

  • 《深入理解计算机系统》
  • 《Linux 高性能服务器编程》
  • 《Go 并发编程实战》
  • songlee24.github.io/2015/04/21/…
  • blog.csdn.net/qq_33951180…
  • austingwalters.com/introductio…
  • docs.microsoft.com/en-us/windo…
  • www.guru99.com/inter-proce…
  • pymotw.com/2/multiproc…
  • www.geeksforgeeks.org/inter-proce…
  • juejin.cn/post/686993…
  • blog.csdn.net/weixin_4651…

本文转载自: 掘金

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

使用Node开发后端项目

发表于 2021-05-08

一、Node简单介绍

1、Node用途

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。

  1. 运行在服务器,作为web server
  2. 运行在本地,作为打包、构建工具

2、下载和安装

  1. 普通下载
    按照node官网下载即可。
  2. NVM下载

NVM是node的版本管理工具,可切换多个node版本。

window使用

github搜索 nvm-windows,按照步骤下载即可。

mac使用
  1. 打开你的Shell配置文件。如果你正在使用zsh,请打开 ~/.zshrc 文件。如果你正在使用bash,请打开 ~/.bashrc 文件。
  2. 在文件中添加以下代码:
1
2
bash复制代码export NVM_DIR="$HOME/.nvm"
[ -s "$(brew --prefix nvm)/nvm.sh" ] && . "$(brew --prefix nvm)/nvm.sh" # This loads nvm
  1. 保存并关闭文件。然后,在终端中输入以下命令,使更改生效:
1
2
bash复制代码source ~/.zshrc   # 如果你使用的是zsh
source ~/.bashrc # 如果你使用的是bash
命令介绍

使用NVM之前,需将之前已经独立安装的node版本卸载。后续都通过NMM管理node版本。

1
2
3
4
rust复制代码nvm list //查看已存在的node版本
nvm install 8.0.0 //安装node版本
nvm use 8.0.0 //切换node版本环境
nvm uninstall 8.0.0 //卸载

3、Node.js和JavaScript的区别

  1. JavaScirpt
    包括ECMAScript语法规范和Web API。
    两种结合,才能实现页面的各种操作。
    Javascript组成.png
  2. Node.js
    包括ECMAScript语法规范和Node.js API。
    两种结合,才能完成server端的任何操作。
    Node组成.png

4、Common.js 和debugger

5、 server端和前端的区别

server端的考虑点:

  • 服务稳定性
  • 考虑内存和CPU(优化,扩展)
  • 日志记录
  • 安全
  • 集群和服务拆分
  1. 服务稳定性
    server端可能会遭受各种恶意攻击或者误操作;
    PM2进场守候
  2. 考虑内存和CPU(优化,扩展)
    server端要承载很多请求,CPU和内存都是稀缺资源
    stream写日志,使用redis存session
  3. 日志记录
    server端要记录日志、存储日志、分析日志
  4. 安全
    越权操作,数据库攻击
    预防xss攻击和sql注入
  5. 集群和服务拆分
    通过扩展机器和服务拆分承载大流量

二、Node实现后端开发思路

使用node实现一个后台博客系统,简单的技术方案如下:

  • 数据如何存储
  • 如何与前端对接,即接口设计

三、Node.js的API介绍

1、http概述

一个http请求的整个过程:

  1. 前端:DNS解析,建立TCP连接(三次握手),发送http请求
  2. 后端:server接收到http请求,处理,并返回
  3. 前端:客户端接收到返回数据,处理数据(如渲染页面,执行js)

四、Node开发环境搭建

1、借助nodemon、corss-env

  • nodemon是一个工具,它可以在检测到目录中的文件更改时自动重新启动节点应用程序,从而帮助开发基于node.js的应用程序。
1
scss复制代码npm install --save-dev nodemon //安装

nodemon包装应用程序,因此您可以将通常传递给应用程序的所有参数传递给应用程序:

1
2
less复制代码// 配置文件
nodemon [your node app]
  • cross-env是一个跨平台运行设置和使用环境变量的脚本
1
2
3
4
5
6
7
8
json复制代码npm install --save-dev cross-env //安装

//配置文件
{
"scripts": {
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
}
}

2、初始化路由

1.接口开发

  • 初始化路由:根据技术方案,设计出路由
  • 返回假数据:将路由和数据处理分离

五、登陆

1、cookie

  1. 什么是cookie
  • 存储在浏览器的一段字符串(最大5kb)
  • 跨域不共享
  • 格式如k1=k1;k2=k2;k3=k3;因此可以存储为结构化数据
  • 每次发送http亲求,会将请求域的cookie一起发给server
  • server可以修改cookie,并返回给浏览器
  • 浏览器中也可以通过JavaScript修改cookie(修改有限制)
  1. 客户端操作cookie
  • 查看cookie
  • 新增cookie (只能新增,不能修改)

cookie查看有三种方式:http请求/返回中;Application下;Console中执行document.cookie查看
cookie新增: 执行document.cookie = 'k2=200'
3. Server端操作cookie

  • 查看cookie
  • 修改cookie
  • 实现登陆验证
  1. 关于cookie的总结文字
  • 知道cookie的定义和特点;
  • 前后端如何查看和修改cookie;
  • 如何使用cookie实现登陆验证
  1. 缺点
    会暴露username,很危险

2、session

为什么使用session?

  • 原因:因为直接用cookie传值,会暴漏username等信息,很危险。
  • 解决办法:cookie中存储userid,server端对应username

使用session(session是一种登陆注册的通用解决方案),即server端存储用户信息
session解决方案.png

3、redis

  1. window下安装redis
    可参考安装步骤www.runoob.com/redis/redis…
1
2
bash复制代码redis-server.exe redis.windows.conf //window下启动redis
redis-cli.exe -h 127.0.0.1 -p 6379 //执行redis语句(set/get)

设置键值对:set myKey abc
取出键值对:get myKey

  1. 上一步的session使用的是js变量,放在node.js的进程内存中
    直接放到node.js进程中的缺陷:
  • 进程内存有限,访问量过大,内存暴增怎么办?
  • 正式线上运行时多进程,进程之间内存无法共享

进程内存模型.png
3. redis特点

  • webServer常用的缓存数据库,数据存放在内存中
  • 相比mysql,访问速度快(内存和硬盘不是一个级别的)
  • 成本高,可存储的数据量小
  1. redis和MySQL

4、nginx

  1. window下安装nginx
    可参考安装步骤nginx.org/en/download…
    nginx目录.png
  2. 修改conf/nginx.conf配置文件,设置代理指向
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码server {
#*********nginx监听的端口在这里设置**********
listen 8088;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;

# *********这里是修改代理指向***********
#location / {
# root html;
# index index.html index.htm;
#}
location / {
proxy_pass http://localhost:8001;
}
location /api/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
}
}
  1. nginx 操作命令

window系统通过dos窗口,进入根目录,执行命令

1
2
makefile复制代码D:\soft\nginx>cd nginx-1.16.0
D:\soft\nginx\nginx-1.16.0>
  • 测试配置文件格式是否正确:nginx -t
  • 启动:nginx
  • 重启:nginx -s reload
  • 停止:nginx -s stop
  1. nginx启动报错,查看端口是否被占用
  • tasklist
    查看所有任务
  • 查看某个端口是否被监听的情况
1
2
3
4
yaml复制代码D:\soft\nginx\nginx-1.16.0>netstat -ano |findstr 8080
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 15448 //被进程15448占用
TCP 10.200.22.57:55155 101.89.15.106:8080 ESTABLISHED 2088
TCP 10.200.22.57:55228 59.37.96.203:8080 ESTABLISHED 5812
  • 查看某个进程占用的具体程序是什么
1
2
arduino复制代码D:\soft\nginx\nginx-1.16.0>tasklist |findstr 15448
node.exe 15448 Console 2 30,252 K

六、日志

日志分类:

  • 第一:访问日志
    log``(server端最重要的日志)
    1
    2
    3
    4
    5
    6
    7
    * 第二:自定义日志(包括自定义事件、错误记录)


    #### 1. 操作文件时,node相关api


    * fs.readFile //读取文件

javascript复制代码fs.readFile(fileName, (err, data) => {
if (err) {
console.error(err)
return
}
// data 是二进制类型,需要转换为字符串
console.log(data.toString())
})

1
2

* fs.writeFile //写入文件

javascript复制代码const content = ‘这是新写入的内容\n’
const opt = {
flag: ‘a’ // 追加写入。覆盖用 ‘w’
}
fs.writeFile(fileName, content, opt, (err) => {
if (err) {
console.error(err)
}
})

1
2

* fs.exists // 判断文件是否存在

javascript复制代码fs.exists(fileName, (exist) => {
console.log(‘exist’, exist)
})

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

以上node操作文件的api,都是一次性读完、写完,如果文件过大,会很影响效率。


#### 2. IO操作的性能瓶颈


IO包括“网络IO”和“文件IO”。相比于CPU计算和内存读写,IO的突出特点就是:慢。


#### 3. stream


node已经完成了对stream功能的api实现:**req具有pipe()**,以流的形式,更合理的读文件、写文件。
以下是利用stream,合理的进行IO操作:

ini复制代码const http = require(‘http’)
const fs = require(‘fs’)
const path = require(‘path’)
const fileName1 = path.resolve(__dirname, ‘data.txt’)
const server = http.createServer((req, res) => {
if (req.method === ‘GET’) {
const readStream = fs.createReadStream(fileName1)
readStream.pipe(res)
}
})

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

#### 4. readline


readline 模块提供了一个接口,用于一次一行地读取可读流(例如 process.stdin)中的数据。


**总结:**



> * 日志对server的重要性
> * IO性能瓶颈,使用stream提高性能
> * 使用crontab拆分日志文件,使用readline分析日志内容


七、项目安全
------


* sql注入: 窃取数内容
* XSS攻击:窃取前端的cookie内容
* 密码加密:保障用户信息


#### 1. sql注入


sql是最原始、最简单的攻击,从有了web2.0就有了sql注入攻击。



> 攻击方式:输入一个sql片段,最终拼接成一段攻击代码
>
>
> 预防措施:使用mysql的escape函数处理输入内容即可。
> 执行sql注入攻击的效果:

ini复制代码const sql = select username, realname from users where username=${username} and password=${password}

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

![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/8521348a16ae80bc7fcd27e9fe5263040b836cfeac1dfce202f9c4637b5e98b1)
攻击者可以任意操作数据库。


#### 2. XSS攻击



> 攻击方式:在页面展示内容中参杂js代码,以获取网页信息
>
>
> 预防措施:转换生成JS的特殊字符
> 使用XSS处理输入的内容

css复制代码npm install xss –save

1
2

On Node.js

ini复制代码var xss = require(“xss”);
var html = xss(‘‘);
console.log(html);

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

#### 3. 密码加密


数据库被用户攻破,最不应该泄密的就是用户信息。



> 攻击方式:用户用户名和密码,在去尝试登陆其他系统
>
>
> 预防措施:将密码加密,即便拿到密码,也不知道明文密码


使用md5加密,如下:

javascript复制代码const crypto = require(‘crypto’)
// 密匙
const SECRET_KEY = ‘WJiol_8776#’
// md5 加密
function md5(content) {
let md5 = crypto.createHash(‘md5’)
return md5.update(content).digest(‘hex’)
}
// 加密函数
function genPassword(password) {
const str = password=${password}&key=${SECRET_KEY}
return md5(str)
}

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

#### 4. 项目总结



> * 处理http接口 //rquest,cookie,处理路由等
> * 连接数据库
> * 实现登录 //session,redis,
> * 安全
> * 日志 //stram,readline


**项目总流程图如下:**
![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/dc2395fcb7d624c1eada3a9ca11f8cc47c4928d7859ad517b8e2480b7f15d952)


八、express
---------


#### 1、安装


1. express脚手架安装
链接:[expressjs.com/en/starter/…](https://expressjs.com/en/starter/generator.html)
`npm install -g express-generator`
2. 实现登陆需要两个包


* express-session安装
链接:[github.com/expressjs/s…](https://github.com/expressjs/session)
`npm install express-session`
* connect-redis安装
链接:[github.com/tj/connect-…](https://github.com/tj/connect-redis)
`npm install connect-redis express-session`


#### 2、express的中间件概念

javascript复制代码//登陆校验的中间件
function loginCheck(req, res, next){
setTimeout(()=> {
res.json({
errno: -1,
msg: ‘登陆失败’
})
// next()
})
}

//路由匹配
router.get(‘/‘, loginCheck, function(req, res, next) {
res.send(‘respond with a resource’);
});

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

#### 3、总结


* 初始化代码中,各个插件的作用
* express如何处理路由
* express中间件


九、koa2
------


#### 1. 为什么使用koa2



> express 中间件是异步回调,koa2原生支持async/await
> 新开发的框架和系统,都是基于koa2,例如egg.js
> express虽为过时,但是koa2是未来趋势


#### 2. `async/await` 要点:



> 1.`await` 后面可以追加promise对象,获取`resolve`的值
> 2.`await`必须包裹在`async`函数里面
> 3`.async`函数执行返回的也是一个`promise`对象
> 4.`try-catch`获取`promise`中`reject`的值

javascript复制代码//简单的异步函数demo
async function readFileData(){
try{
const aData = await getFileContent(‘a1.json’)
const bData = await getFileContent(aData.next)
const cData = await getFileContent(bData.next)
}catch (err){
console.error(err)
}
}
readFileData();

1
2

#### 3. koa脚手架安装【koa1和koa2通用】

ruby复制代码$ npm install -g koa-generator

1
2

基于脚手架创建koa1项目:

shell复制代码$ koa2 /tmp/foo && cd /tmp/foo
$ npm install
$ npm start

1
2

基于脚手架创建koa2项目:

shell复制代码$ koa2 /tmp/foo && cd /tmp/foo
$ npm install
$ npm start

1
2

#### 4. 实现登陆需要两个包

css复制代码$ npm install koa-redis koa-generic-session redis –save

1
2
3
4
5

#### 5. 路由


(1)xss 安装: 预防xss攻击

css复制代码npm install xss –save

1
2

(2)MySQL安装

css复制代码npm install mysql –save

1
2
3

(3)morgan和koa-morgan: 打印日志
express支持morgan,不支持koa。koa需要安装中间件,`koa-morgan`

css复制代码npm install –save koa-morgan

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

#### 6. koa2中间件原理


十、上线与配置
-------


* 服务器稳定性
* 充分利用服务器硬件资源
* 线上日志记录


#### 1. PM2


简单介绍:


* 进程守护,系统崩溃自动重启
* 启动多进程,充分利用CPU和内存
* 自带日志记录功能


(1)下载

复制代码npm install pm2 -g

1
2

(2)常用命令

less复制代码pm2 start //启动
pm2 list //查看启动后的所有信息状态
pm2 restart [App name]/[id] // 重启
pm2 stop [App name]/[id] //停止
pm2 delete [App name]/[id] //删除
pm2 info [App name]/[id] //基础信息
pm2 log [App name]/[id] //查看log信息
pm2 monit [App name]/[id] //查看cpu、内存等信息

1
2
3
4
5
6
7
8
9

![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/f2a3317b13b22655b78b4e1f6416e182c2fe641582b170171932e3033e5214b8)
(3)进程守护


* `node app.js` 和`nodemon app.js`,进程崩溃则不能访问
* pm2遇到进程崩溃,会自动重启
(4)PM2配置
pm2的配置文件:

json复制代码{
“apps”: {
“name”: “pm2-test-server”,
“script”: “app.js”,
“watch”: true,
“ignore_watch”: [
“node_modules”,
“logs”
],
“instances”: 4,
“error_file”: “logs/err.log”,
“out_file”: “logs/out.log”,
“log_date_format”: “YYYY-MM-DD HH:mm:ss”
}
}


(5)多进程


使用多进程的理由:


* 为什么用多进程
* redis



> 使用多进程带来的问题:**多进程之间,内存无法共享**。
> 解决方案:多进程访问一个redis,实现数据共享


![](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/b6b30d3c4da932bc65617c6ccb9d769e8ee108c71cd1c5e3cf567d76235c36b0)



**本文转载自:** [掘金](https://juejin.cn/post/6959583458779725860)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

盘点Sharding-JDBC 分库分表

发表于 2021-05-07

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

本来没准备看这部分源码的, 事情的起因是因为之前一直在使用 3.0 的版本 ,这次看到5.0了想试试 ,结果没有跑起来, 官方文档又不给力 ,索性把源码看了一遍 , 把问题找到了…..

二 . 事情的起点

起因是因为使用 shardingsphere-jdbc-core-spring-boot-starter 依赖 , 想直接通过配置文件完成所有的配置 , 但是整个流程出现了不少问题 .

幸运的是之前写过一个 Bean 配置的 , 正好可以对比看看整个流程的问题.

2.1 Java Bean 配置方式

这里先将 Java Bean 配置的整个流程贴上来 , 用于对比 .2者都是使用 JPA 作为持久化框架

Maven 配置

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.0.0-alpha</version>
</dependency>

Config 配置

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
java复制代码@Configuration
public class DatabaseConfig {

/**
* 方式一 : 通过 Bean 配置
*/
@Bean
public DataSource dataSource() {
// 配置真实数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();

// 配置第 1 个数据源
BasicDataSource dataSource1 = new BasicDataSource();
dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://127.0.0.1:3306/database0?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC");
dataSource1.setUsername("root");
dataSource1.setPassword("123456");
dataSourceMap.put("ds0", dataSource1);

// 配置第 2 个数据源
BasicDataSource dataSource2 = new BasicDataSource();
dataSource2.setDriverClassName("com.mysql.jdbc.Driver");
dataSource2.setUrl("jdbc:mysql://127.0.0.1:3306/database1?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC");
dataSource2.setUsername("root");
dataSource2.setPassword("123456");
dataSourceMap.put("ds1", dataSource2);

// 配置 t_order 表规则
ShardingTableRuleConfiguration orderTableRuleConfig = new ShardingTableRuleConfiguration("t_blog", "ds${0..1}.t_blog_${0..1}");

// 配置主键生成策略
KeyGenerateStrategyConfiguration configuration = new KeyGenerateStrategyConfiguration("id", null);
orderTableRuleConfig.setKeyGenerateStrategy(configuration);

// 配置分库策略
orderTableRuleConfig.setDatabaseShardingStrategy(new StandardShardingStrategyConfiguration("column_id", "dbShardingAlgorithm"));

// 配置分表策略
orderTableRuleConfig.setTableShardingStrategy(new StandardShardingStrategyConfiguration("title_id", "tableShardingAlgorithm"));

// 配置分片规则
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTables().add(orderTableRuleConfig);

// 配置分库算法
Properties dbShardingAlgorithmrProps = new Properties();
dbShardingAlgorithmrProps.setProperty("algorithm-expression", "ds${column_id % 2}");
shardingRuleConfig.getShardingAlgorithms().put("dbShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", dbShardingAlgorithmrProps));

// 配置分表算法
Properties tableShardingAlgorithmrProps = new Properties();
tableShardingAlgorithmrProps.setProperty("algorithm-expression", "t_blog_${title_id % 2}");
shardingRuleConfig.getShardingAlgorithms().put("tableShardingAlgorithm", new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps));

DataSource dataSource = null;
try {
dataSource = ShardingSphereDataSourceFactory.createDataSource(dataSourceMap, Collections.singleton(shardingRuleConfig), new Properties());
} catch (SQLException e) {
e.printStackTrace();
}
//logger.info("datasource : {}", dataSource);
return dataSource;
}
}

实体类

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复制代码@Entity
@Table(name = "t_blog")
public class BlogEntity {

@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "title")
private String title;

@Column(name = "title_id")
private Integer titleId;

@Column(name = "author")
private String author;

@Column(name = "date")
private Date date;

@Column(name = "column_id")
private Integer columnId;

//.......................
}

三 . Properties 配置方式

为了避免误导 , 配置文件在最后贴 ,我们从异常开始看起

java.util.NoSuchElementException: No value bound

这是第一个出现的异常 , 看异常栈里面 , 有这样几个重要的提示 :

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复制代码Caused by: java.lang.reflect.InvocationTargetException: null
at org.apache.shardingsphere.spring.boot.util.PropertyUtil.v2(PropertyUtil.java:111) ~[shardingsphere-jdbc-spring-boot-starter-infra-5.0.0-alpha.jar:5.0.0-alpha]
at org.apache.shardingsphere.spring.boot.util.PropertyUtil.handle(PropertyUtil.java:75) ~[shardingsphere-jdbc-spring-boot-starter-infra-5.0.0-alpha.jar:5.0.0-alpha]
at org.apache.shardingsphere.spring.boot.datasource.DataSourceMapSetter.getDataSourceMap(DataSourceMapSetter.java:66) ~[shardingsphere-jdbc-spring-boot-starter-infra-5.0.0-alpha.jar:5.0.0-alpha]

// 可以看到 , 出问题的地方为 org.apache.shardingsphere.spring.boot.util.PropertyUtil.v2

// 往上2步 , 可以跟踪到节点 : getDataSourceMap
// 这里就是我们第一个要看的地方 : 多数据源的加载


// 先看下异常的原因 , 再看看整体逻辑
public static Map<String, DataSource> getDataSourceMap(final Environment environment) {
Map<String, DataSource> result = new LinkedHashMap<>();
Map<String, Object> dataSourceCommonProps = PropertyUtil.handle(environment, COMMON_PREFIX, Map.class);

//...

}


// 基本上可以按出来 , 这里缺少 COMMON_PREFIX 打头的配置项 , 查了相关资料 ,大概是这2句
spring.shardingsphere.datasource.common.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.common.driver-class-name=com.mysql.jdbc.Driver

// 问题解决 , 看看整体逻辑

3.1 多数据源

3.1.1 多数据源的加载

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
java复制代码// 核心的配置加载来自于 org.apache.shardingsphere.spring.boot 包下 , 配置了如下
C10- SpringBootConfiguration
F01- SpringBootPropertiesConfiguration : 该类种只有一个 Properties 属性
F02- Map<String, DataSource> dataSourceMap : 维护了一个 DataSource 的列表
M10_01- shardingSphereDataSource
- ShardingSphereDataSourceFactory.createDataSource : 传入 Map 集合创建 DataSource -> M11_01
M10_02- ShardingTransactionTypeScanner : 事务相关

C11- ShardingSphereDataSourceFactory : Datasource 的工厂类
M11_01- createDataSource(Map<String, DataSource>,Collection<RuleConfiguration>, Properties)
?- 创建一个 DataSource , 可以看到 , 这里传入的是一个 DataSource 的集合 , 盲猜原理是创建一个虚拟的DataSource
?- Map<String, DataSource> -> PS_M11_01_1
- new ShardingSphereDataSource(dataSourceMap, configurations, props) -> C12MC

// M11_01 代码
public static DataSource createDataSource(final DataSource dataSource, final Collection<RuleConfiguration> configurations, final Properties props) throws SQLException {
Map<String, DataSource> dataSourceMap = new HashMap<>(1, 1);
dataSourceMap.put(DefaultSchema.LOGIC_NAME, dataSource);
return createDataSource(dataSourceMap, configurations, props);
}


// 由 M11_01 中构建相关对象
C12- ShardingSphereDataSource
MC- ShardingSphereDataSource
- 生成一个 DatabaseType -> M12_01
- 构建一个 SchemaContextsBuilder
- 构建一个 TransactionContexts
M12_01- createDatabaseType(final Map<String, DataSource> dataSourceMap)
FOR- 对 dataSourceMap 循环 , 分别调用 createDatabaseType -> M12_02
- 返回的是最后一个 DataSource 的 DatabaseType
?- 这里的 FOR 循环目的是什么 ? 既然返回的一定是最后一个 , 那循环那么多干嘛
M12_02- createDatabaseType(final DataSource dataSource)
?- 注意 , 这里是2个参数不同的方法
- dataSource.getConnection() : 获取 Connect
- 生成 DatabaseType

// M12_02伪代码 : 通过 DatabaseTypeRegistry 获取 DatabaseType , 此处拿到的是 MySQLDatabaseType
private DatabaseType createDatabaseType(final DataSource dataSource) throws SQLException {
if (dataSource instanceof ShardingSphereDataSource) {
return ((ShardingSphereDataSource) dataSource).schemaContexts.getDatabaseType();
}
try (Connection connection = dataSource.getConnection()) {
return DatabaseTypeRegistry.getDatabaseTypeByURL(connection.getMetaData().getURL());
}
}

PS_M11_01_1 : 传入的资源依赖于 spring.shardingsphere.datasource.names 属性

image-20210427141008081.png

可以看到 , 尽管我的配置中配置了 3 个 datasource , 但是最终使用的依赖于属性 spring.shardingsphere.datasource.names.

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
java复制代码
// 此处为 dataSourceMap , 用于扫描配置中的数据源 ,起点逻辑为 :

// PS : 该方法为 EnvironmentAware 接口的实现方法 , 会在ApplicationContextAwareProcessor 中默认调用
public final void setEnvironment(final Environment environment) {
dataSourceMap.putAll(DataSourceMapSetter.getDataSourceMap(environment));
}


// 扫描具体的 DataSource
C- DataSourceMapSetter
F- private static final String PREFIX = "spring.shardingsphere.datasource.";
F- private static final String COMMON_PREFIX = "spring.shardingsphere.datasource.common.";
M1- getDataSourceMap
- 获取 Common 属性 : spring.shardingsphere.datasource.common.
- 获取 Srouce 名称集合 -> M2
- 依次生成 DataSource 放入集合 -> M3
M2- getDataSourceNames : 获取 datasource Name , 名称为拼接出来的
- 优先获取 spring.shardingsphere.datasource.name
- 没有则获取 spring.shardingsphere.datasource.names
M3- getDataSource
- Map<String, Object> dataSourceProps = mergedDataSourceProps(...) : 生成属性
- Preconditions.checkState(...) : 校验参数是否合法
- DataSourceUtil.getDataSource(...) : 创建 DataSource
- 后面是一个语法糖 , 用于注入属性 -> PS_M3_001

// PS_M3_001 : 最终执行语句就是 HikariDataSourcePropertiesSetter # propertiesSet
// 会获取 Environment 中 spring.shardingsphere.datasource.ds1.data-source-properties 前缀 , 设置相关属性
DataSourcePropertiesSetterHolder.getDataSourcePropertiesSetterByType(dataSourceProps.get(DATA_SOURCE_TYPE).toString()).ifPresent(propsSetter -> propsSetter.propertiesSet(environment, prefix, dataSourceName, result));

3.1.2 多数据源总结

起点 : SpringBootConfiguration

处理核心 : ShardingSphereDataSourceFactory#createDataSource

构建单元 : ShardingSphereDataSource

扫描类 : DataSourceMapSetter

一句话总结 :

  • SpringBootConfiguration 实现了EnvironmentAware 方法 , 则在容器加载的同时 , 会默认调用 SpringBootConfiguration 中 setEnvironment 方法
  • setEnvironment 方法会扫描配置文件中的 Datasourse 配置 ,放入 一个集合 dataSourceMap
  • SpringBootConfiguration 在使用 shardingSphereDataSource 加载多数据源时 , 使用该 Map
  • shardingSphereDataSource 方法调用 ShardingSphereDataSourceFactory 创建数据源
  • ShardingSphereDataSourceFactory 最终会构建出 ShardingSphereDataSource , 标识一个数据源

3.2 分库分表策略配置

配置跑通后 , 又出现了问题 :

java.sql.SQLException: Parameter index out of range (1 > number of parameters, which is 0).

运行时抛出如上异常 , 虽然说的时 SQL 问题 , 但是直觉告诉我 , 这还是配置出现了偏差

于是按照如下步骤进行排查 :

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
java复制代码// Step 1 : PreparedStatement  # checkBounds
// 异常栈第一步就是这里 , 发现校验出现问题 , 抛出了异常
if ((paramIndex < 1)) {
throw SQLError.createSQLException(........);
} else if (paramIndex > this.parameterCount) {
throw SQLError.createSQLException(.......);
}........


// Step 2 : 排查 PreparedStatement 初始化方法
C- PreparedStatement
M-initializeFromParseInfo()
- 观察 staticSqlStrings 发现 , 参数是错误的 , 只有一个数组
insert into org.apache.shardingsphere.sharding.rewrite.token.pojo.TableToken@3df8c40 (author, column_id, date, title, title_id, id) values org.apache.shardingsphere.sharding.rewrite.token.pojo.ShardingInsertValuesToken@c1b5afe
// 此处发现这个值也是错误的 , 猜测应该是 分表时 , table 处理出现异常


// Step 3 : 排查分表的逻辑
C79- ShardingRouteEngine
M79_05- route0
- routeDataSources 获取策略判断的 DataSource 节点
- routeTables 获取判断的 table 表

// StandardShardingStrategy # doSharding
target = shardingAlgorithm.doSharding(availableTargetNames, new PreciseShardingValue(shardingValue.getTableName(), shardingValue.getColumnName(), each));

// 这里 debug 发现 shardingValue.getTableName() 名称不对 -------> dt_blog_1 , 应该是t_blog_1
// 我的天.....这一看就是配置问题了 , 检查一下
spring.shardingsphere.rules.sharding.sharding-algorithms.db-algorithm.props.algorithm-expression=dt_blog_$->{title_id % 2}

// 改为
spring.shardingsphere.rules.sharding.sharding-algorithms.db-algorithm.props.algorithm-expression=t_blog_$->{title_id % 2}

// 一个通过源码排查异常的流程旧结束了 , 重新

这其实就是一个配置问题 , 但是 Sharding 返回的异常信息是看不出来原因的 .

总结

主要注意其中 C79- ShardingRouteEngine / M79_05- route0 方法 , 改方法中有2个主要的方法 , 分库分表的核心判断就在该方法中

这里把策略的加载路径记录一下:

虽然是 properties ,但是使用的还是 yml 的加载类 , 不冲突

具体却什么配置 ,按照 YamlShardingRuleConfiguration 类去反推就大概知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
// 配置映射
C- YamlShardingStrategyConfiguration
?- 数据配置在上一步被配置到当前的 Configuration , 可以通过该类中的属性进行直接配置

// Rule 规则
C- ShardingRuleSpringBootConfiguration

C- ShardingRuleAlgorithmProviderConfigurationYamlSwapper

C- ShardingTableRuleConfigurationYamlSwapper

// algorithm-expression 的注入位置
C- ShardingRule
M- ShardingRule(final AlgorithmProvidedShardingRuleConfiguration config, final Collection<String> dataSourceNames)

3.3 分库分表逻辑

很好 , 项目这里总算跑起来了 , 但是分库分表的功能并没有实现 , 运行时 , 四个表均创建了

Bebug 发现 , ShardingSpherePreparedStatement 的 result 中返回了四个数据

PS:M74_02_01

sharding-jdbc-executionContext.jpg

于是按照以下流程过了一遍分库分表的流程 :

可以看到 , 第一个入口类是 ShardingSpherePreparedStatement

Step 1 : ShardingSpherePreparedStatement

该类是一个入口类 , 相关的操作均可以从该类向下查找 :

  • 核心一 : M74_01中获取的StatementExecuteUnit即为最终会执行的SQL , 这其中已经包含了需要运行的库(详见上图) , 所以 createExecutionContext 时 , 相关数据已经生成
  • 核心二 : M74_01中preparedStatementExecutor.executeUpdate 发起执行
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复制代码C74- ShardingSpherePreparedStatement
M74_01- executeUpdate() : 执行插入操作
?- 这个里面有3个核心操作 , 获取需要执行的 statement + update 执行语句
- createExecutionContext 创建一个 ExecutionContext -> M74_03
?- 核心逻辑 , 生成需要执行的对象 ExecuteUnit
- getInputGroups 获取一个 StatementExecuteUnit 集合 -> M74_02
- preparedStatementExecutor.executeUpdate 执行当前语句
M74_02- getInputGroups()
?- 核心逻辑就是通过相关的 rule 规则生成对应的执行语句
- 构建一个 PreparedStatementExecuteGroupEngine
- 调用 PreparedStatementExecuteGroupEngine generate 方法获取数据
?- 参数一 : executionContext.getRouteContext()
?- 参数二 : executionContext.getExecutionUnits() -> PS:M74_02_01
M74_03- createExecutionContext()
- kernelProcessor.generateExecutionContext 生成 ExecutionContext -> M75_01
- findGeneratedKey 创建主键


// M74_02 代码
private Collection<InputGroup<StatementExecuteUnit>> getInputGroups() throws SQLException {
// 最大连接数
int maxConnectionsSizePerQuery = schemaContexts.getProps().<Integer>getValue(ConfigurationPropertyKey.MAX_CONNECTIONS_SIZE_PER_QUERY);
//
return new PreparedStatementExecuteGroupEngine(maxConnectionsSizePerQuery, connection, statementOption,
schemaContexts.getDefaultSchema().getRules()).generate(executionContext.getRouteContext(), executionContext.getExecutionUnits());
}

知道了源头在 M74_01 中 , 继续向下找 -> M75_01

Step 2 : Rule 的主要处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码// 该类中只有一个方法
C75- KernelProcessor
M75_01- generateExecutionContext
1- 获取 ShardingSphereRule 集合
2- 创建一个 SQLRouteEngine
3- 获取一个 SQLStatementContext : 这里可以映射为一个数据库操作
4- 获取 RouteContext : 这个对象决定将会执行几个数据库/数据表 -> M76_01

// M75_01
public ExecutionContext generateExecutionContext(final LogicSQL logicSQL, final ShardingSphereSchema schema, final ConfigurationProperties props) {
// PS:M75_01_01 rule 规则对象
Collection<ShardingSphereRule> rules = schema.getRules();
SQLRouteEngine sqlRouteEngine = new SQLRouteEngine(rules, props);
SQLStatementContext<?> sqlStatementContext = logicSQL.getSqlStatementContext();

// 核心语句 : 获取 RouteContext : 这个对象决定将会执行几个数据库/数据表 -> M76_01
RouteContext routeContext = sqlRouteEngine.route(logicSQL, schema);
SQLRewriteEntry rewriteEntry = new SQLRewriteEntry(schema.getMetaData().getSchemaMetaData().getConfiguredSchemaMetaData(), props, rules);
SQLRewriteResult rewriteResult = rewriteEntry.rewrite(logicSQL.getSql(), logicSQL.getParameters(), sqlStatementContext, routeContext);
Collection<ExecutionUnit> executionUnits = ExecutionContextBuilder.build(schema.getMetaData(), rewriteResult, sqlStatementContext);
return new ExecutionContext(sqlStatementContext, executionUnits, routeContext);
}

PS:M75_01_01 Rule 结构

可以看到 , 包括多数据源 , 分片规则 ,都已经在里面了

image.png

剩下的就简单了 , 找到之前处理的地方就行了

Step 3 : 一步步 Debug

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
java复制代码C76- SQLRouteEngine
M76_01- route :
- 创建一个 SQLRouteExecutor
?- 参数 : new PartialSQLRouteExecutor(rules, props) , 这里就将所有的 Rule 注入到体系中了
- executor.route 调用执行器执行 rule -> M77_01


C77- PartialSQLRouteExecutor
M77_01- route : 终于找到了 , 这里通过 Rule 规则生成相关的 SQLRouter
- entry.getValue().createRouteContext(logicSQL, schema, entry.getKey(), props) : 核心语句 -> M78_01

C78- ShardingSQLRouter
M78_01- createRouteContext
- ShardingRouteEngineFactory 构建一个 RouteEngine 并且调用对应 route 方法 -> M79_01

C79- ShardingRouteEngine
M79_01- route(RouteContext routeContext, ShardingRule shardingRule)
- getDataNodes 获取 Collection<DataNode> : -> M79_03
?- 此处的 Node 会用于下面生成 RouteUnit -> PS:M79_01_02
FOR- 循环获取的 Nodes , 创建 RouteUnit
M79_02- getDataNodes
- createShardingStrategy 创建策略
M79_03- routeByShardingConditions
?- 这里会通过 Condition 不同分别调用2个方法
- route0 -> M79_05
- routeByShardingConditionsWithCondition -> M79_04
M79_04- routeByShardingConditionsWithCondition : 核心逻辑1
For- 循环所有的 Condition , 获取策略和 ShardingValues 值 , 调用 route0
M79_05- route0
- routeDataSources 获取策略判断的 DataSource 节点
- routeTables 获取判断的 table 表
M79_06- routeTables
- 这里就会调用对应的 Strategy 策略类

// PS : 如果这里发现 TableRule 存在问题 , 可以查看方法 是否存在问题
private Collection<DataNode> getDataNodes(final ShardingRule shardingRule, final TableRule tableRule) {
ShardingStrategy databaseShardingStrategy = createShardingStrategy(shardingRule.getDatabaseShardingStrategyConfiguration(tableRule), shardingRule.getShardingAlgorithms());
ShardingStrategy tableShardingStrategy = createShardingStrategy(shardingRule.getTableShardingStrategyConfiguration(tableRule), shardingRule.getShardingAlgorithms());
if (isRoutingByHint(shardingRule, tableRule)) {
return routeByHint(tableRule, databaseShardingStrategy, tableShardingStrategy);
}
if (isRoutingByShardingConditions(shardingRule, tableRule)) {
return routeByShardingConditions(shardingRule, tableRule, databaseShardingStrategy, tableShardingStrategy);
}
return routeByMixedConditions(shardingRule, tableRule, databaseShardingStrategy, tableShardingStrategy);
}


// M79_03 源代码
private Collection<DataNode> routeByShardingConditions(final ShardingRule shardingRule, final TableRule tableRule,
final ShardingStrategy databaseShardingStrategy, final ShardingStrategy tableShardingStrategy) {
return shardingConditions.getConditions().isEmpty()
? route0(tableRule, databaseShardingStrategy, Collections.emptyList(), tableShardingStrategy, Collections.emptyList())
: routeByShardingConditionsWithCondition(shardingRule, tableRule, databaseShardingStrategy, tableShardingStrategy);
}


C80- StandardShardingStrategy
M80_01- doSharding

这里也不详细说了 , 一步步debug 下来 , 发现是 M79_04- routeByShardingConditionsWithCondition 中缺少相关的策略 , 配置文件写法问题 , 补齐后 , 一切正常

最终配置文件

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
properties复制代码server.port=8085
##Jpa配置
spring.jpa.database=mysql
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
# 开启配置
spring.shardingsphere.enabled=true
# 配置真实数据源 ds0,ds1,ds2
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.common.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.common.driver-class-name=com.mysql.jdbc.Driver
# 配置第 1 个数据源
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://127.0.0.1:3306/database0?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=123456
# 配置第 2 个数据源
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://127.0.0.1:3306/database1?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=UTC
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=123456
# 配置表策略 -- ShardingTableRuleConfiguration
spring.shardingsphere.rules.sharding.tables.t_blog.actual-data-nodes=ds$->{0..1}.t_blog_$->{0..1}
# 配置主键策略 -- KeyGenerateStrategyConfiguration
spring.shardingsphere.rules.sharding.tables.t_blog.key-generate-strategy.column=id
spring.shardingsphere.rules.sharding.tables.t_blog.key-generate-strategy.key-generator-name=snowflake
# 配置分表策略 StandardShardingStrategyConfiguration
spring.shardingsphere.rules.sharding.tables.t_blog.binding-tables=t_blog
spring.shardingsphere.rules.sharding.tables.t_blog.table-strategy.standard.sharding-column=title_id
spring.shardingsphere.rules.sharding.tables.t_blog.table-strategy.standard.sharding-algorithm-name=db-algorithm
# 配置分库策略 StandardShardingStrategyConfiguration
spring.shardingsphere.rules.sharding.tables.t_blog.database-strategy.standard.sharding-column=column_id
spring.shardingsphere.rules.sharding.tables.t_blog.database-strategy.standard.sharding-algorithm-name=table-algorithm
# ================================
# =====================配置默认策略
# 默认插入类型
spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-column=id
spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-algorithm-name=database_inline
# 指定 algorithms
spring.shardingsphere.rules.sharding.sharding-algorithms.database-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.database-inline.props.algorithm-expression=ds$->{id % 2}
# 默认算法
spring.shardingsphere.rules.sharding.key-generators.snowflake.type=SNOWFLAKE
spring.shardingsphere.rules.sharding.key-generators.snowflake.props.worker-id=123
# 切分策略
spring.shardingsphere.rules.sharding.sharding-algorithms.db-algorithm.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.db-algorithm.props.algorithm-expression=t_blog_$->{title_id % 2}
spring.shardingsphere.rules.sharding.sharding-algorithms.table-algorithm.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.table-algorithm.props.algorithm-expression=ds$->{column_id % 2}


# !!!!! 对应表 t_blog_0 , t_blog_1 , 已经上面2个库

总结

Sharding-JDBC 分析中最麻烦的就是 lombok 表达式了 , debug 起来叫一个麻烦 , 还要一个个去配置 , 想想之前改掉了这个习惯还是比较庆幸的 .

整个过程中主要有3个节点 :

  • SpringBootConfiguration
  • ShardingRouteEngine
  • StandardShardingStrategy

头尾中间都在这里了 , 以这三个节点去debug ,基本上问题就出来了

其中需要明白的是 : sharding-jdbc 会在策略执行完成后 , 生成多个 ExecuteUnit , 每一个 ExecuteUnit 即为一个数据库处理对象 , 会在对应的数据库/数据表中执行

后续再来看看他的分布式事务和读写分离的主要逻辑 , 这里先记这么多

本文转载自: 掘金

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

Kafka 不再需要 ZooKeeper

发表于 2021-05-07

img

Kafka 2.8.0 出炉了,此版本有一项重大改进:

实现了 Raft 分布式一致性机制,意味着可以脱离 ZooKeeper 独立运行了。

ZooKeeper 在 Kafka 中扮演着重要的角色,用来存储 Kafka 的元数据。

ZooKeeper 存储着 Partition 和 Broker 的元数据 ,同时也负责 Kafka Controller 的选举工作。

对于 Kafka 来讲,ZooKeeper 是一套外部系统,要想部署一套 Kafka 集群,就要同时部署、管理、监控 ZooKeeper。

ZooKeeper 有自己的配置方式、管理工具,和 Kafka 完全不一样,所以,一起搞两套分布式系统,自然就提升了复杂度,也更容易出现问题。有时工作量还会加倍,例如要开启一些安全特性,Kafka 和 ZooKeeper 中都需要配置。

除了复杂度,外部存储也会降低系统效率。

例如 Kafka 集群每次启动的时候,Controller 必须从 ZooKeeper 加载集群的状态信息。

再比如选举出一个新的 Controller 之后也会比较麻烦,因为需要加载元数据,而此时元数据的量可能已经非常大了,这就产生了效率问题。

所以,ZooKeeper 带来的复杂度、系统效率这两个问题已经成为 Kafka 的痛点,Kafka 团队一直在努力去除对 ZooKeeper 的依赖。Kafka 2.8.0 这个版本终于实现了。

使用 Raft 模式之后,元数据、配置信息都会保存在 @metadata 这个 Topic 中,自动在集群中复制。这样 Kafka 就会简单轻巧很多。

但需要注意的是,Zookeeper-less Kafka 还属于早期版本,并不完善,所以,现在不要应用在线上产品环境中。

本文转载自: 掘金

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

Spring Boot项目优雅的全局异常处理方式(全网最新)

发表于 2021-05-07

前言

在日常项目开发中,异常是常见的,但是如何更高效的处理好异常信息,让我们能快速定位到BUG,是很重要的,不仅能够提高我们的开发效率,还能让你代码看上去更舒服,SpringBoot的项目已经对有一定的异常处理了,但是对于我们开发者而言可能就不太合适了,因此我们需要对这些异常进行统一的捕获并处理。


一、全局异常处理方式一

SpringBoot中,@ControllerAdvice 即可开启全局异常处理,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。

1.1 自定义全局异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* @description: 自定义异常处理
* @author: DT
* @date: 2021/4/19 21:17
* @version: v1.0
*/
@ControllerAdvice
public class MyExceptionHandler {

@ExceptionHandler(value =Exception.class)
@ResponseBody
public String exceptionHandler(Exception e){
System.out.println("全局异常捕获>>>:"+e);
return "全局异常捕获,错误原因>>>"+e.getMessage();
}
}

1.2 手动抛出异常

1
2
3
4
5
6
java复制代码 @GetMapping("/getById/{userId}")
public CommonResult<User> getById(@PathVariable Integer userId){
// 手动抛出异常
int a = 10/0;
return CommonResult.success(userService.getById(userId));
}

1.3 测试打印

在这里插入图片描述
在这里插入图片描述

很显然这样的用户体验效果是极差的,虽然这种能够让我们知道异常的原因,但是在很多的情况下来说,可能还是不够人性化,不符合我们的要求。

二、全局异常处理方式二

2.1 定义基础接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* @description: 服务接口类
* @author: DT
* @date: 2021/4/19 21:39
*/
public interface BaseErrorInfoInterface {

/**
* 错误码
* @return
*/
String getResultCode();

/**
* 错误描述
* @return
*/
String getResultMsg();
}

2.2 定义枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码/**
* @description: 异常处理枚举类
* @author: DT
* @date: 2021/4/19 21:41
* @version: v1.0
*/
public enum ExceptionEnum implements BaseErrorInfoInterface{

// 数据操作错误定义
SUCCESS("2000", "成功!"),
BODY_NOT_MATCH("4000","请求的数据格式不符!"),
SIGNATURE_NOT_MATCH("4001","请求的数字签名不匹配!"),
NOT_FOUND("4004", "未找到该资源!"),
INTERNAL_SERVER_ERROR("5000", "服务器内部错误!"),
SERVER_BUSY("5003","服务器正忙,请稍后再试!");

/**
* 错误码
*/
private final String resultCode;

/**
* 错误描述
*/
private final String resultMsg;

ExceptionEnum(String resultCode, String resultMsg) {
this.resultCode = resultCode;
this.resultMsg = resultMsg;
}

@Override
public String getResultCode() {
return resultCode;
}

@Override
public String getResultMsg() {
return resultMsg;
}
}

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
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
java复制代码/**
* @description: 自定义异常类
* @author: DT
* @date: 2021/4/19 21:44
* @version: v1.0
*/
public class BizException extends RuntimeException{

private static final long serialVersionUID = 1L;

/**
* 错误码
*/
protected String errorCode;
/**
* 错误信息
*/
protected String errorMsg;

public BizException() {
super();
}

public BizException(BaseErrorInfoInterface errorInfoInterface) {
super(errorInfoInterface.getResultCode());
this.errorCode = errorInfoInterface.getResultCode();
this.errorMsg = errorInfoInterface.getResultMsg();
}

public BizException(BaseErrorInfoInterface errorInfoInterface, Throwable cause) {
super(errorInfoInterface.getResultCode(), cause);
this.errorCode = errorInfoInterface.getResultCode();
this.errorMsg = errorInfoInterface.getResultMsg();
}

public BizException(String errorMsg) {
super(errorMsg);
this.errorMsg = errorMsg;
}

public BizException(String errorCode, String errorMsg) {
super(errorCode);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}

public BizException(String errorCode, String errorMsg, Throwable cause) {
super(errorCode, cause);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}


public String getErrorCode() {
return errorCode;
}

public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}

public String getErrorMsg() {
return errorMsg;
}

public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}

@Override
public Throwable fillInStackTrace() {
return this;
}
}

2.4 自定义数据传输

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
java复制代码/**
* @description: 自定义数据传输
* @author: DT
* @date: 2021/4/19 21:47
* @version: v1.0
*/
public class ResultResponse {
/**
* 响应代码
*/
private String code;

/**
* 响应消息
*/
private String message;

/**
* 响应结果
*/
private Object result;

public ResultResponse() {
}

public ResultResponse(BaseErrorInfoInterface errorInfo) {
this.code = errorInfo.getResultCode();
this.message = errorInfo.getResultMsg();
}

public String getCode() {
return code;
}

public void setCode(String code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public Object getResult() {
return result;
}

public void setResult(Object result) {
this.result = result;
}

/**
* 成功
*
* @return
*/
public static ResultResponse success() {
return success(null);
}

/**
* 成功
* @param data
* @return
*/
public static ResultResponse success(Object data) {
ResultResponse rb = new ResultResponse();
rb.setCode(ExceptionEnum.SUCCESS.getResultCode());
rb.setMessage(ExceptionEnum.SUCCESS.getResultMsg());
rb.setResult(data);
return rb;
}

/**
* 失败
*/
public static ResultResponse error(BaseErrorInfoInterface errorInfo) {
ResultResponse rb = new ResultResponse();
rb.setCode(errorInfo.getResultCode());
rb.setMessage(errorInfo.getResultMsg());
rb.setResult(null);
return rb;
}

/**
* 失败
*/
public static ResultResponse error(String code, String message) {
ResultResponse rb = new ResultResponse();
rb.setCode(code);
rb.setMessage(message);
rb.setResult(null);
return rb;
}

/**
* 失败
*/
public static ResultResponse error( String message) {
ResultResponse rb = new ResultResponse();
rb.setCode("-1");
rb.setMessage(message);
rb.setResult(null);
return rb;
}

@Override
public String toString() {
return JSONObject.toJSONString(this);
}

}

2.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
java复制代码/**
* @description: 自定义异常处理
* @author: DT
* @date: 2021/4/19 21:51
* @version: v1.0
*/
@ControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/**
* 处理自定义的业务异常
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = BizException.class)
@ResponseBody
public ResultResponse bizExceptionHandler(HttpServletRequest req, BizException e){
logger.error("发生业务异常!原因是:{}",e.getErrorMsg());
return ResultResponse.error(e.getErrorCode(),e.getErrorMsg());
}

/**
* 处理空指针的异常
* @param req
* @param e
* @return
*/
@ExceptionHandler(value =NullPointerException.class)
@ResponseBody
public ResultResponse exceptionHandler(HttpServletRequest req, NullPointerException e){
logger.error("发生空指针异常!原因是:",e);
return ResultResponse.error(ExceptionEnum.BODY_NOT_MATCH);
}

/**
* 处理其他异常
* @param req
* @param e
* @return
*/
@ExceptionHandler(value =Exception.class)
@ResponseBody
public ResultResponse exceptionHandler(HttpServletRequest req, Exception e){
logger.error("未知异常!原因是:",e);
return ResultResponse.error(ExceptionEnum.INTERNAL_SERVER_ERROR);
}
}

2.6 测试代码

1
2
3
4
5
6
7
8
java复制代码@PostMapping("/add")
public boolean add(@RequestBody User user) {
//如果姓名为空就手动抛出一个自定义的异常!
if(user.getName()==null){
throw new BizException("-1","用户姓名不能为空!");
}
return true;
}

在这里插入图片描述

1
2
3
4
5
6
7
java复制代码 @PutMapping("/update")
public boolean update(@RequestBody User user) {
//这里故意造成一个空指针的异常,并且不进行处理
String str = null;
str.equals("111");
return true;
}

在这里插入图片描述

1
2
3
4
5
6
java复制代码 @DeleteMapping("/delete")
public boolean delete(@RequestBody User user) {
//这里故意造成一个异常,并且不进行处理
Integer.parseInt("abc123");
return true;
}

在这里插入图片描述
如果我们想捕获这个类型转换异常,是不是再添加一个遗产处理方法就可了。
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* 处理类型转换异常
* @param req
* @param e
* @return
*/
@ExceptionHandler(value = NumberFormatException.class)
@ResponseBody
public ResultResponse exceptionHandler(HttpServletRequest req, NumberFormatException e){
logger.error("发生类型转换异常!原因是:",e);
return ResultResponse.error(ExceptionEnum.PARAMS_NOT_CONVERT);
}
1
java复制代码PARAMS_NOT_CONVERT("4002","类型转换不对!"),

在这里插入图片描述

自定义全局异常处理除了可以处理上述的数据格式之外,也可以处理页面的跳转,只需在新增的异常方法的返回处理上填写该跳转的路径并不使用ResponseBody 注解即可。

总结

异常处理,能够减少代码的重复度和复杂度,有利于代码的维护,并且能够快速定位到BUG,大大提高我们的开发效率。

本文转载自: 掘金

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

Python中的魔法属性

发表于 2021-05-07

魔法属性

在Python中,所有以 __ 双下划线包起来的方法,都统称为 Magic Method,例如类的初始化方法 __init__() ,实例对象创造方法 __new__()等。

魔法属性和方法是Python内置的一些属性和方法,有着特殊的含义。命名时前后加上两个下划线,在执行系统特定操作时,会自动调用。

常见的魔法属性

__doc__

表示类的描述信息

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码
# __doc__
class Foo:
""" 描述类信息,这是用于测试的类 """

def func(self):
pass


# ipython 测验
In [2]: Foo.__doc__
Out[2]: ' 描述类信息,这是用于测试的类 '

__module__ 和 __class__

  • __module__ 表示当前操作的对象在那个模块
  • __class__ 表示当前操作的对象的类是什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码
# __module__、__class__
# oop.py
class Student(object):

def __init__(self, name):
self.name = name


# main.py
from oop import Student

s = Student()
print(s.__module__) # 输出 oop 即:输出模块
print(s.__class__) # 输出 <class 'oop.Student'> 即:输出类

__init__ 、__new__

__init__() 初始化方法 和 __new__(),通过类创建对象时,自动触发执行。__new__ 是用来创建类并返回这个类的实例,而 __init__ 只是将传入的参数来初始化该实例。

  • __new__() 创建对象时调用,会返回当前对象的一个实例
  • __init__() 创建完对象后调用,对当前对象的一些实例初始化,无返回值
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
python复制代码
# __init__ 、 __new__
class Student(object):

def __init__(self, name, age):
print('__init__() called')
self.name = name
self.age = age

def __new__(cls, *args, **kwargs):
print('__new__() called')
print(cls, args, kwargs)
return super().__new__(cls)


# ipython 测验
In [26]: s1 = Student('hui', age=21)
__new__() called
<class '__main__.Student'> ('hui',) {'age': 21}
__init__() called

In [27]: s2 = Student('jack', age=20)
__new__() called
<class '__main__.Student'> ('jack',) {'age': 20}
__init__() called

__del__

当对象在内存中被释放时,自动触发执行。

注:此方法一般无须定义,因为Python是一门高级语言,有 内存管理、垃圾回收机制,程序员在使用时无需关心内存的分配和释放,因为此工作都是交给Python解释器来执行,所以,__del__ 的调用是由解释器在进行垃圾回收时自动触发执行的。

1
2
3
4
5
6
7
8
9
10
11
python复制代码# __del__
class Foo:
def __del__(self):
print('__del__() called')


# ipython 测验
In [29]: f = Foo()

In [30]: del f
__del__() called

__call__

让类的实例的行为表现的像函数一样,你可以调用它们,将一个函数当做一个参数传到另外一个函数中等等。这是一个非常强大的特性,其让Python编程更加舒适甜美。对象后面加括号,触发执行。

注:__init__ 方法的执行是由创建对象触发的,即:对象 = 类名() ;而对于 __call__ 方法的执行是由对象后加括号触发的,即:对象() 或者 类()()

__call__ 在那些 类的实例经常改变状态的时候会非常有效。调用这个实例是一种改变这个对象状态的直接和优雅的做法。用一个实例来表达最好不过了:

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
python复制代码# __call__
class Rect(object)
"""
调用实例对象来改变矩形的位置
"""

def __init__(self, x, y):
# x, y代表矩形坐标
self.x, self.y = x, y

def __call__(self, x, y):
# 改变实体的位置
self.x, self.y = x, y


# ipython 测验
In [33]: r = Rect(10, 10)

In [34]: r.x, r.y
Out[34]: (10, 10)

In [35]: r(0, 0)

In [36]: r.x, r.y
Out[36]: (0, 0)

In [37]: r(100, 100)

In [38]: r.x, r.y
Out[38]: (100, 100)

__dict__

类或对象中的所有属性

类的实例属性属于对象;类中的类属性和方法等属于类,即:

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
python复制代码# __dict__
class Student(object):

def __init__(self, name, age):
self.name = name
self._age = age

@property
def age(self):
return self._age


# ipython 测验
In [47]: # 获取类属性

In [48]: Student.__dict__
Out[48]:
mappingproxy({'__module__': '__main__',
'__init__': <function __main__.Student.__init__(self, name, age)>,
'age': <property at 0x210e2a005e8>,
'__dict__': <attribute '__dict__' of 'Student' objects>,
'__weakref__': <attribute '__weakref__' of 'Student' objects>,
'__doc__': None})

In [49]: # 获取实例对象的属性

In [50]: s = Student('hui', 21)

In [51]: s.__dict__
Out[51]: {'name': 'hui', '_age': 21}

In [52]: s2 = Student('jack', 20)

In [53]: s2.__dict__
Out[53]: {'name': 'jack', '_age': 20}

__str__

如果一个类中定义了__str__方法,那么在打印 对象 时,默认输出该方法的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码In [65]: # __str__
...: class Foo(object):
...: pass
...:

In [66]: f = Foo()

In [67]: print(f)
<__main__.Foo object at 0x00000210E2715608>

In [68]: class Foo(object):
...:
...: def __str__(self):
...: return '< Custom Foo object str >'
...:

In [69]: f = Foo()

In [70]: print(f)
< Custom Foo object str >

__getitem__、__setitem__、__delitem__

用于索引操作,如字典。以上分别表示获取、设置、删除数据。

用于切片操作,如列表。

字典示例

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
python复制代码# __getitem__、__setitem__、__delitem__
class MyDict(object):

def __init__(self):
self.my_dict = dict()

def __getitem__(self, key):
print('__getitem__() ', key)
return self.my_dict.get(key, None)

def __setitem__(self, key, value):
print('__setitem__() ', key, value)
self.my_dict.update(key=value)

def __delitem__(self, key):
print('__delitem__() ', key)
del self.my_dict[key]


# ipython 测验
In [33]: mdict = MyDict()

In [34]: print(mdict['name'])
__getitem__() name
None

In [35]: # 新增

In [36]: mdict['name'] = 'hui'
__setitem__() name hui

In [37]: mdict['age'] = 21
__setitem__() age 21

In [38]: mdict['name']
__getitem__() name
Out[38]: 'hui'

In [39]: mdict['age']
__getitem__() age
Out[39]: 21

In [40]: # 更新

In [41]: mdict['name'] = 'jack'
__setitem__() name jack

In [42]: mdict['name']
__getitem__() name
Out[42]: 'jack'

In [43]: # 删除

In [44]: del mdict['age']
__delitem__() age

In [45]: print(mdict['age'])
__getitem__() age
None

列表示例

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
python复制代码# 切片操作
class MyList(object):

def __init__(self):
self.mlist = list()

def __getitem__(self, index):
print('__getitem__() called')
print(index)
if isinstance(index, slice):
return self.mlist[index]

def __setitem__(self, index, value):
print('__getitem__() called')
print(index, value)
if isinstance(index, slice):
self.mlist[index] = value

def __delitem__(self, index):
print('__delitem__() called')
if isinstance(index, slice):
del self.mlist[index]


# ipython 测验
In [70]: mlist = MyList()

In [71]: mlist[0]
__getitem__() called
0

In [72]: mlist[0:-1]
__getitem__() called
slice(0, -1, None)
Out[72]: []

In [73]: mlist[:] = [1,2,3]
__getitem__() called
slice(None, None, None) [1, 2, 3]

In [74]: mlist[:]
__getitem__() called
slice(None, None, None)
Out[74]: [1, 2, 3]

In [75]: mlist[0:2]
__getitem__() called
slice(0, 2, None)
Out[75]: [1, 2]

In [76]: mlist[::-1]
__getitem__() called
slice(None, None, -1)
Out[76]: [3, 2, 1]

In [77]: mlist[0]
__getitem__() called
0

In [78]: mlist[0:1]
__getitem__() called
slice(0, 1, None)
Out[78]: [1]

In [79]: del mlist[0:1]
__delitem__() called

In [80]: mlist[:]
__getitem__() called
slice(None, None, None)
Out[80]: [2, 3]

注意: 当进行 mlist[0] 操作的时候传递并不是一个 slice 对象,不是一个 int 类型的数字,所以不能把索引为 0 的值取出来,改成 mlist[0, 1] 或者在 __getitem__() 的方法中新增数字判断,大家可以尝试一下。

__enter__、__exit__

with 声明是从 Python2.5 开始引进的关键词。你应该遇过这样子的代码:

1
2
3
python复制代码with open('foo.txt') as bar:
# do something with bar
pass

在 with 声明的代码段中,我们可以做一些对象的开始操作和退出操作,还能对异常进行处理。这需要实现两个魔术方法: __enter__ 和 __exit__。

1
python复制代码__enter__(self):

定义了当使用 with 语句的时候,会话管理器在块被初始创建时要产生的行为。请注意,__enter__ 的返回值与 with 语句的目标或者 as 后的名字绑定。

1
python复制代码__exit__(self, exception_type, exception_value, traceback):

定义了当一个代码块被执行或者终止后,会话管理器应该做什么。它可以被用来处理异常、执行清理工作或做一些代码块执行完毕之后的日常工作。如果代码块执行成功,exception_type,exception_value,和traceback 将会为 None 。否则,你可以选择处理这个异常或者是直接交给用户处理。如果你想处理这个异常的话,请确保__exit__ 在所有语句结束之后返回 True。如果你想让异常被会话管理器处理的话,那么就让其产生该异常。

__copy__、__deepcopy__

  有时候,尤其是当你在处理可变对象时,你可能想要复制一个对象,然后对其做出一些改变而不希望影响原来的对象。这就是Python的copy所发挥作用的地方。

1
python复制代码__copy__(self):

  定义了当对你的类的实例调用 copy.copy() 时所产生的行为。copy.copy() 返回了你的对象的一个浅拷贝——这意味着,当实例本身是一个新实例时,它的所有数据都被引用了——例如,当一个对象本身被复制了,它的数据仍然是被引用的(因此,对于浅拷贝中数据的更改仍然可能导致数据在原始对象的中的改变)。

1
python复制代码__deepcopy__(self, memodict={}):

  定义了当对你的类的实例调用 copy.deepcopy()时所产生的行为。copy.deepcopy() 返回了你的对象的一个深拷贝——对象和其数据都被拷贝了。memodict 是对之前被拷贝的对象的一个缓存——这优化了拷贝过程并且阻止了对递归数据结构拷贝时的无限递归。当你想要进行对一个单独的属性进行深拷贝时,调用copy.deepcopy(),并以 memodict 为第一个参数。

  这些魔术方法的用例看起来很小,并且确实非常实用. 它们反应了关于面向对象程序上一些重要的东西在Python 上,并且总的来说 Python 总是一个简单的方法去找某些事情,即使是没有必要的。这些魔法方法可能看起来不是很有用,但是一旦你需要它们,你会感到庆幸它们的存在。

其他魔法方法

由于魔法属性、方法太多了在这就不一一描述和展示了,其他的就以表格形式呈现吧。

用于比较的魔术方法

方法 作用
__cmp__(self, other) 比较方法里面最基本的的魔法方法
__eq__(self, other) 定义相等符号的行为,==
__ne__(self,other) 定义不等符号的行为,!=
__lt__(self,other) 定义小于符号的行为,<
__gt__(self,other) 定义大于符号的行为,>
__le__(self,other) 定义小于等于符号的行为,<=
__ge__(self,other) 定义大于等于符号的行为,>=

数值计算的魔术方法

单目运算符和函数

方法 作用
__pos__(self) 实现一个取正数的操作
__neg__(self) 实现一个取负数的操作
__abs__(self) 实现一个内建的 abs() 函数的行为
__invert__(self) 实现一个取反操作符(~操作符)的行为
__round__(self, n) 实现一个内建的 round() 函数的行为
__floor__(self) 实现 math.floor() 的函数行为
__ceil__(self) 实现 math.ceil() 的函数行为
__trunc__(self) 实现 math.trunc() 的函数行为

双目运算符或函数

方法 作用
__add__(self, other) 实现一个加法
__sub__(self, other) 实现一个减法
__mul__(self, other) 实现一个乘法
__floordiv__(self, other) 实现一个 // 操作符产生的整除操作
__div__(self, other) 实现一个 / 操作符代表的除法操作
__truediv__(self, other) 实现真实除法
__mod__(self, other) 实现一个 % 操作符代表的取模操作
__divmod__(self, other) 实现一个内建函数 divmod()
__pow__(self, other) 实现一个指数操作( ****** 操作符)的行为
__lshift__(self, other) 实现一个位左移操作**(<<)**的功能
__rshift__(self, other) 实现一个位右移操作**(>>)**的功能
__and__(self, other) 实现一个按位进行与操作**(&)**的行为
__or__(self, other) 实现一个按位进行或操作的行为
__xor__(self, other) 异或运算符相当于 ^

增量运算

方法 作用
__iadd__(self, other) 加法赋值
__isub__(self, other) 减法赋值
__imul__(self, other) 乘法赋值
__ifloordiv__(self, other) 整除赋值,地板除,相当于 //= 运算符
__idiv__(self, other) 除法赋值,相当于 /= 运算符
__itruediv__(self, other) 真除赋值
__imod_(self, other) 模赋值,相当于 %= 运算符
__ipow__(self, other) 乘方赋值,相当于 **= 运算符
__ilshift__(self, other) 左移赋值,相当于 <<= 运算符
__irshift__(self, other) 左移赋值,相当于 >>= 运算符
__iand__(self, other) 与赋值,相当于 &= 运算符
__ior__(self, other) 或赋值
__ixor__(self, other) 异或运算符,相当于 ^= 运算符

类型转换

方法 作用
__int__(self) 转换成整型
__long__(self) 转换成长整型
__float__(self) 转换成浮点型
__complex__(self) 转换成 复数型
__oct__(self) 转换成八进制
__hex__(self) 转换成十六进制
__index__(self) 如果你定义了一个可能被用来做切片操作的数值型,你就应该定义__index__
__trunc__(self) 当 math.trunc(self) 使用时被调用 __trunc__ 返回自身类型的整型截取
__coerce__(self, other) 执行混合类型的运算

源代码

源代码已上传到 Gitee PythonKnowledge: Python知识宝库,欢迎大家来访。

✍ 码字不易,还望各位大侠多多支持❤️。

公众号

新建文件夹X

大自然用数百亿年创造出我们现实世界,而程序员用几百年创造出一个完全不同的虚拟世界。我们用键盘敲出一砖一瓦,用大脑构建一切。人们把1000视为权威,我们反其道行之,捍卫1024的地位。我们不是键盘侠,我们只是平凡世界中不凡的缔造者 。

本文转载自: 掘金

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

1…673674675…956

开发者博客

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