在后面的文章之中咱们介绍了基于Kubernetes及Istio如何一步一步把Service Mesh微服务架构玩起来!在该文章中,咱们演示了一个十分贴近实战的案例,这里回顾下该案例的构造,如下图所示:
该案例所演示的就是咱们日常应用微服务架构开发时,服务间最广泛的通信场景。在Spring Cloud微服务体系中,服务间能够通过Fegin+Ribbon组合的形式,实现服务间负载平衡形式的Http接口调用;但在Service Mesh架构中,服务发现及负载平衡等治理逻辑曾经由SideCar代理,如果还心愿连续Spring Cloud场景下服务间接口调用的代码体验,个别能够通过改写Feign组件,去掉其中对于服务治理的逻辑,只保留简略的接口申明式调用逻辑来实现。
上述案例中“micro-api->micro-order”之间的服务通信调用,就是基于该形式实现的(可参考之前的文章)。但在微服务架构中除了采纳Http协定通信外,对于某些对性能有着更高要求的零碎来说,采纳通信效率更高的RPC协定往往是更适合的抉择!
在基于Spring Cloud框架的微服务体系中,服务之间也能够通过RPC协定通信,但因为服务治理的须要,也须要一套相似于Fegin+Ribbon组合的SDK反对。例如gRPC框架就有针对Spring Boot框架的“grpc-client-spring-boot-starter”依赖反对!该我的项目是一个 gRPC 的 Spring Boot 模块,能够在 Spring Boot 中内嵌一个 gRPC Server 对外提供服务,并反对 Spring Cloud 的服务发现、注册、链路跟踪等等。
那么在Service Mesh微服务体系下,服务间基于gRPC框架的通信应该怎么实现呢?接下来,我将以案例中“micro-order->micro-pay”之间的服务调用为例,演示在Service Mesh微服务架构下实现服务间的gRPC通信调用,并将案例中Http+gRPC服务间通信的残缺场景串起来!
gRPC概述
在演示Service Mesh微服务架构下的gRPC通信场景之前,咱们先简略介绍下RPC协定及gRPC框架的基本知识。
RPC(Remote Procedure Call),又称近程过程调用,是一种通过掩藏底层网络通信复杂性,从而屏蔽近程和本地调用区别的通信形式。相比于Http协定,RPC协定属于一种自定义的TCP协定,从而在实现时防止了一些Http协定信息的臃肿问题,实现了更高效率的通信。
在支流实现RPC协定的框架中,比拟驰名的有Dubbo、Thrift及gRPC等。因为目前支流的容器公布平台Kubernetes,以及Service Mesh开源平台Istio都是通过gRPC协定来实现外部组件之间的交互,所以在Service Mesh微服务架构中,服务间通信采纳gRPC协定,从某种角度上说会更具备原生劣势。况且在此之前,gRPC框架曾经在分布式、多语言服务场景中失去了大量利用,因而能够预测在Service Mesh微服务架构场景下,基于gRPC框架的微服务通信形式会逐渐成为支流。
gRPC是Google公布的基于HTTP/2.0传输层协定承载的高性能开源软件框架,提供了反对多种编程语言的、对网络设备进行配置和纳管的办法。因为是开源框架,通信的单方能够进行二次开发,所以客户端和服务器端之间的通信会更加专一于业务层面的内容,缩小了对由gRPC框架实现的底层通信的关注。
接下来的内容就具体演示在Service Mesh微服务架构下,实现微服务“micro-order->micro-pay”的gRPC通信调用!
构建gRPC服务端程序(micro-pay)
首先从gRPC服务端的角度,在微服务micro-pay我的项目中集成gRPC-Java,并实现一个gRPC服务端程序。具体如下:
1、构建Spring Boot根本工程(micro-pay/micro-pay-client)
应用Spring Boot框架构建根本的Maven工程,为了工程代码的复用,这里独自形象一个micro-pay-client工程,并定义micro-pay微服务gRPC服务接口的protobuf文件(*/proto/paycore.proto),代码如下:
syntax = "proto3";
package com.wudimanong.pay.client;
option java_multiple_files = true;
option java_package = "com.wudimanong.micro.pay.proto";
service PayService {
//定义领取rpc办法
rpc doPay (PayRequest) returns (PayResponse);
}
message PayRequest {
string orderId = 1;
int32 amount=2;
}
message PayResponse {
int32 status = 1;
}
如上所示,创立了一个基于protobuf协定的领取接口定义文件,其中定义了领取服务PayService及其中的doPay领取rpc办法,并定义了其申请和返回参数对象,具体的语法遵循“proto3”协定。
为了可能失常编译和生成protobuf文件所定义服务接口的代码,须要在我的项目pom.xml文件中引入jar包依赖及Maven编译插件配置,代码如下:
<?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">
....
<dependencies>
....
<!--gRPC通信类库(截止目前的最新版本)-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.36.1</version>
</dependency>
</dependencies>
<build>
<!--引入gRpc框架proto文件编译生产插件-->
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.36.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这是独自对于gRPC接口proto文件定义的工程,定义后编译工程,maven就会依据后面定义的paycore.proto文件生成gRPC服务端/客户端相干代码。
实现后,持续构建micro-pay微服务的spring boot工程代码,并在其pom.xml文件中引入上述gRPC协定文件定义的依赖,例如:
<!--引入领取服务gRPC ProtoBuf定义依赖-->
<dependency>
<groupId>com.wudimanong</groupId>
<artifactId>micro-pay-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在micro-pay-client工程中所引入的gRPC相干的依赖及插件配置会主动继承至micro-pay工程!
2、编写gRPC领取服务代码
在micro-pay代码工程中创立一个PayCoreProvider接口代码,用于示意领取gRPC服务的入口(相似于Controller),其代码如下:
package com.wudimanong.micro.pay.provider;
import com.wudimanong.micro.pay.proto.PayRequest;
import com.wudimanong.micro.pay.proto.PayResponse;
import com.wudimanong.micro.pay.proto.PayServiceGrpc;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class PayCoreProvider extends PayServiceGrpc.PayServiceImplBase {
/**
* 实现ProtoBuf中定义的服务办法
*
* @param request
* @param responseStreamObserver
*/
@Override
public void doPay(PayRequest request, StreamObserver<PayResponse> responseStreamObserver) {
//逻辑解决(简略模仿打印日志)
log.info("解决gRPC领取解决申请,orderId->{};payAmount{}", request.getOrderId(), request.getAmount());
//构建返回对象(构建解决状态)
PayResponse response = PayResponse.newBuilder().setStatus(2).build();
//设置数据响应
responseStreamObserver.onNext(response);
responseStreamObserver.onCompleted();
}
}
上述代码所引入的一些依赖代码如PayServiceGrpc等,就是后面定义paycore.proto文件所生成的桩文件代码!因为只是简略测试,这里仅仅打印了下日志就返回了,如果波及简单业务还是能够依照MVC分层架构思维进行代码拆分!
3、编写gRPC与Spring Boot框架集成配置代码
在Spring Cloud微服务中集成gRPC能够通过后面提到的“grpc-client-spring-boot-starter”来实现,但目前还没有现成的反对Service Mesh架构下的集成SDK,所以这里通过手工配置定义的形式实现集成。先创立一个配置类,代码如下:
package com.wudimanong.micro.pay.config;
import com.wudimanong.micro.pay.provider.PayCoreProvider;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class GrpcServerConfiguration {
@Autowired
PayCoreProvider service;
/**
* 注入配置文件中的端口信息
*/
@Value("${grpc.server-port}")
private int port;
private Server server;
public void start() throws IOException {
// 构建服务端
log.info("Starting gRPC on port {}.", port);
server = ServerBuilder.forPort(port).addService(service).build().start();
log.info("gRPC server started, listening on {}.", port);
// 增加服务端敞开的逻辑
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down gRPC server.");
GrpcServerConfiguration.this.stop();
log.info("gRPC server shut down successfully.");
}));
}
private void stop() {
if (server != null) {
// 敞开服务端
server.shutdown();
}
}
public void block() throws InterruptedException {
if (server != null) {
// 服务端启动后直到利用敞开都处于阻塞状态,不便接管申请
server.awaitTermination();
}
}
}
如上所示,在该配置代码中,通过gRPC-Java依赖所提供的Server对象构建了gRPC服务端启动、进行、阻塞的办法,并在启动时将后面定义的服务端类通过“.addService()”办法进行了退出(可思考封装更优雅的形式)!
为了让该配置类与Spring Boot集成,再定义一个集成类,代码如下:
package com.wudimanong.micro.pay.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class GrpcCommandLineRunner implements CommandLineRunner {
@Autowired
GrpcServerConfiguration configuration;
@Override
public void run(String... args) throws Exception {
configuration.start();
configuration.block();
}
}
上述代码会在Spring Boot利用启动时主动加载,其中的逻辑就是启动gRPC服务,并阻塞期待连贯!
接下来在配置文件中定义服务所开启的gRPC端口,配置如下:
spring:
application:
name: micro-pay
server:
port: 9092
#定义gRPC服务凋谢的端口
grpc:
server-port: 18888
该配置所定义的参数在后面的服务配置类中援用,示意gRPC服务开启的端口,这里定义的是18888!
到这里gRPC服务端工程代码就构建实现了,从整体上看就是Spring Boot+gRPC的集成与整合,这其中没有引入Spring Boot定制的gRPC集成SDK,目标在于防止其中所波及的客户端服务治理逻辑(与后面Http调用不间接引入Open Feign一样)。
构建gRPC客户端程序(micro-order)
接下来咱们革新micro-order微服务,使其成为调用micro-pay微服务的gRPC客户端程序!
1、引入gRPC客户端依赖包
引入后面定义micro-pay gRPC服务时构建的micro-pay-client protobuf工程依赖,代码如下:
<!--引入领取服务gRPC ProtoBuf定义依赖-->
<dependency>
<groupId>com.wudimanong</groupId>
<artifactId>micro-pay-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2、业务逻辑中实现gRPC服务调用
接下来在micro-order逻辑中调用gRPC领取服务,代码示例如下:
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
/**
* 引入gRPC客户端配置依赖
*/
@Autowired
GrpcClientConfiguration gRpcClent;
@Override
public CreateOrderBO create(CreateOrderDTO createOrderDTO) {
log.info("当初开始解决下单申请.....");
//生成订单号
String orderId = String.valueOf(new Random(100).nextInt(100000) + System.currentTimeMillis());
//构建领取申请(gRPC调用)
PayRequest payRequest = PayRequest.newBuilder().setOrderId(orderId).setAmount(createOrderDTO.getAmount())
.build();
//应用stub发送申请到服务端
PayResponse payResponse = gRpcClent.getStub().doPay(payRequest);
log.info("pay gRpc response->" + payResponse.toString());
return CreateOrderBO.builder().orderId(orderId).status(payResponse.getStatus()).build();
}
}
如上所示,该业务逻辑在接管micro-api通过Http调用的申请后,会在逻辑实现过程中通过gRPC协定拜访领取服务,其中波及的接口定义代码,由protobuf文件所定义!
3、gRPC客户端配置
上述逻辑是通过定义“GrpcClientConfiguration”gRPC客户端配置类来实现gRPC服务调用的,该配置类代码如下:
@Slf4j
@Component
public class GrpcClientConfiguration {
/**
* 领取gRPC Server的地址
*/
@Value("${server-host}")
private String host;
/**
* 领取gRPC Server的端口
*/
@Value("${server-port}")
private int port;
private ManagedChannel channel;
/**
* 领取服务stub对象
*/
private PayServiceGrpc.PayServiceBlockingStub stub;
public void start() {
//开启channel
channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
//通过channel获取到服务端的stub
stub = PayServiceGrpc.newBlockingStub(channel);
log.info("gRPC client started, server address: {}:{}", host, port);
}
public void shutdown() throws InterruptedException {
//调用shutdown办法后期待1秒敞开channel
channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
log.info("gRPC client shut down successfully.");
}
public PayServiceGrpc.PayServiceBlockingStub getStub() {
return this.stub;
}
}
如上所示配置代码,通过依服务配置文件指定的gRPC服务端地址+端口,实现对gRPC客户端的配置,其中次要包含启动和进行办法,并在启动的过程中初始化gRPC服务客户端的桩代码的实例(可思考更优雅地实现)。
在该配置类中所依赖的gRPC服务端地址+端口配置,依赖于服务配置文件的定义,代码如下:
spring:
application:
name: micro-order
server:
port: 9091
#领取微服务Grpc服务地址、端口配置
server-host: ${grpc_server_host}
server-port: ${grpc_server_port}
如果是本地测试能够间接指定grpc_server_host及端口的值,但在Service Mesh微服务架构中,间接在利用的配置文件中指定其余微服务的地址及端口可能并不是很灵便,这个配置信息将在公布Kubernetes集群时,通过Kubernetes公布文件注入!
为了让gRPC客户端配置与Spring Boot集成,这里也须要定义一个Spring Boot加载类,代码如下:
@Component
@Slf4j
public class GrpcClientCommandLineRunner implements CommandLineRunner {
@Autowired
GrpcClientConfiguration configuration;
@Override
public void run(String... args) throws Exception {
//开启gRPC客户端
configuration.start();
//增加客户端敞开的逻辑
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
configuration.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
}
}
该代码将在Spring Boot利用主动时主动加载!到这里micro-order gRPC客户端配置就实现了!
将部署服务至Service Mesh架构环境
后面基于“micro-order->micro-pay”微服务间的gRPC调用场景,别离将两个微服务革新成了gRPC服务端/客户端。但此时从代码上是很难看进去它们二者之间应该怎么实现调用!而这也恰好就印证了Service Mesh架构的劣势,服务的发现、及负载平衡调用之类的服务治理逻辑,曾经齐全不必微服务本人管了!
在Istio中,它们是基于Kubernetes的Service发现机制+Istio-proxy(SideCar代理)来实现的。而具体的操作就是通过微服务Kubernetes服务公布文件的定义,接下来别离定义micro-order及micro-pay的Kubernetes公布文件。
先看下作为gRPC服务端的micro-pay的公布文件(micro-pay.yaml),代码如下:
apiVersion: v1
kind: Service
metadata:
name: micro-pay
labels:
app: micro-pay
service: micro-pay
spec:
type: ClusterIP
ports:
- name: http
#容器裸露端口
port: 19092
#指标利用端口
targetPort: 9092
#设置gRPC端口
- name: grpc
port: 18888
targetPort: 18888
selector:
app: micro-pay
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-pay-v1
labels:
app: micro-pay
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: micro-pay
version: v1
template:
metadata:
labels:
app: micro-pay
version: v1
spec:
containers:
- name: micro-pay
image: 10.211.55.2:8080/micro-service/micro-pay:1.0-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 19092
#指定服务gRPC端口
- name: grpc
protocol: TCP
containerPort: 18888
如上所示k8s公布文件,次要是定义了Service服务拜访资源及Deployment容器编排资源,这两种资源都是Kubernetes的资源类型,在容器编排资源和服务资源中别离定义了gRPC的拜访端口,通过这种设置,后续gRPC客户端通过Service资源拜访服务时,就可能进行端口映射了!
而其余配置则是根本的Kubernetes公布部署逻辑,其中波及的镜像,须要在公布之前,通过构建的形式对我的项目进行Docker镜像打包并上传公有镜像仓库(如果有疑难,能够参考本号之前的文章)。
接下来持续看看作为gRPC客户端的micro-order微服务的k8s公布文件(micro-order.yaml),代码如下:
apiVersion: v1
kind: Service
metadata:
name: micro-order
labels:
app: micro-order
service: micro-order
spec:
type: ClusterIP
ports:
- name: http
#此处设置80端口的起因在于革新的Mock FeignClient代码默认是基于80端口进行服务调用
port: 80
targetPort: 9091
selector:
app: micro-order
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-order-v1
labels:
app: micro-order
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: micro-order
version: v1
template:
metadata:
labels:
app: micro-order
version: v1
spec:
containers:
- name: micro-order
image: 10.211.55.2:8080/micro-service/micro-order:1.0-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 19091
#环境参数设置(设置微服务返回gRPC服务端的地址+端口)
env:
- name: GRPC_SERVER_HOST
value: micro-pay
- name: GRPC_SERVER_PORT
value: "18888"
在该公布文件中,须要阐明的次要就是通过容器env环境参数的设置,指定了之前gRPC客户端服务配置中所依赖的参数变量“GRPC_SERVER_HOST及GRPC_SERVER_PORT”,其中服务地址就是micro-pay微服务在Kubernetes中Service资源定义的名称,端口则是gRPC服务端所开启的端口。
这样在gRPC客户端在Kubernetes集群中依据Service名称发动微服务调用时,Kubernetes集群本身的服务发现逻辑就能主动将申请映射到相应的Pod资源了!这其实就是Service Mesh微服务架构服务发现的根本逻辑!
接下来将微服务进行公布,这里假如你曾经部署了一套Kubernetes集群并装置了基于Istio的Service Mesh微服务架构环境,最终的部署成果如下所示:
root@kubernetes:/opt/istio/istio-1.8.4# kubectl get pods
NAME READY STATUS RESTARTS AGE
micro-api-6455654996-9lsxr 2/2 Running 2 43m
micro-order-v1-744d469d84-rnqq8 2/2 Running 0 6m28s
micro-order-v1-744d469d84-vsn5m 2/2 Running 0 6m28s
micro-pay-v1-7fd5dd4768-txq9d 2/2 Running 0 43s
micro-pay-v1-7fd5dd4768-wqw6b 2/2 Running 0 43s
如上所示,能够看到案例所波及的微服务都被部署了,并且对应的SideCar代理(istio-proxy)也被失常启动了!为了演示负载平衡成果,这里micro-order及micro-pay都别离被部署了两个正本!
微服务多正本负载平衡调用演示
如果环境都没啥问题,此时能够通过调用Istio Gateway来拜访micro-api服务,而后micro-api服务会通过Http的形式拜访micro-order服务,之后micro-order服务通过gRPC协定调用micro-pay服务。
通过curl命令拜访Istio Gateway网关服务,成果如下:
curl -H "Content-Type:application/json" -H "Data_Type:msg" -X POST --data '{"businessId": "202012102", "amount": 100, "channel": 2}' http://10.211.55.12:30844/api/order/create
如果失常返回响应后果,则阐明上述调用链路走通了!此时别离通过观察服务的业务日志和istio-proxy代理日志来加以观测!
其中micro-pay两个实例(PodA~PodB)业务日志信息:
//领取微服务接口拜访日志(POD-A)
root@kubernetes:~# kubectl logs micro-pay-v1-7fd5dd4768-txq9d micro-pay
....
2021-04-01 14:46:15.818 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : Starting gRPC on port 18888.
2021-04-01 14:46:18.859 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : gRPC server started, listening on 18888.
2021-04-01 15:07:36.709 INFO 1 --- [ault-executor-0] c.w.micro.pay.provider.PayCoreProvider : 解决gRPC领取解决申请,orderId->1617289656289;payAmount100
//领取微服务接口拜访日志(POD-B)
root@kubernetes:~# kubectl logs micro-pay-v1-7fd5dd4768-wqw6b micro-pay
...
2021-04-01 15:34:59.673 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : Starting gRPC on port 18888.
2021-04-01 15:35:06.175 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : gRPC server started, listening on 18888.
2021-04-01 15:40:22.019 INFO 1 --- [ault-executor-0] c.w.micro.pay.provider.PayCoreProvider : 解决gRPC领取解决申请,orderId->1617291624127;payAmount100
2021-04-01 15:44:31.630 INFO 1 --- [ault-executor-2] c.w.micro.pay.provider.PayCoreProvider : 解决gRPC领取解决申请,orderId->1617291867537;payAmount100
能够看到,屡次拜访接口,基于gRPC的微服务调用也实现了负载平衡调用!接下来别离看下这两个微服务的istio-proxy(SideCar代理)的日志,具体如下:
--istio-proxy代理日志(POD-A)
root@kubernetes:~# kubectl logs micro-pay-v1-7fd5dd4768-txq9d istio-proxy
...
2021-04-01T15:34:48.009972Z info Envoy proxy is ready
[2021-04-01T15:40:26.240Z] "POST /com.wudimanong.pay.client.PayService/doPay HTTP/2" 200 - "-" 22 7 498 477 "-" "grpc-java-netty/1.36.1" "8eb318e5-ac09-922d-9ca7-603a5c14bdd5" "micro-pay:18888" "127.0.0.1:18888" inbound|18888|| 127.0.0.1:57506 10.32.0.10:18888 10.32.0.12:36844 outbound_.18888_._.micro-pay.default.svc.cluster.local default
2021-04-01T15:45:18.377555Z info xdsproxy disconnected
...
[2021-04-01T15:45:34.885Z] "POST /com.wudimanong.pay.client.PayService/doPay HTTP/2" 200 - "-" 22 7 1200 171 "-" "grpc-java-netty/1.36.1" "c08d540e-db46-9228-b381-0808ac08377e" "micro-pay:18888" "127.0.0.1:18888" inbound|18888|| 127.0.0.1:33218 10.32.0.10:18888 10.32.0.2:42646 outbound_.18888_._.micro-pay.default.svc.cluster.local default
...
2021-04-01T15:52:49.825955Z info xdsproxy connecting to upstream XDS server: istiod.istio-system.svc:15012
如上所示,能够看到istio-proxy代理日志中显示了通过post形式转发gRPC服务的状况,而且能够看出gRRPC是采纳Http/2实现的!
后记
本文通过实战案例,演示了在Service Mesh微服务架构下,服务间通过gRPC协定实现通信调用的场景!
欢送大家关注我的公众号【惊涛骇浪如码】,海量Java相干文章,学习材料都会在外面更新,整顿的材料也会放在外面。
感觉写的还不错的就点个赞,加个关注呗!点关注,不迷路,继续更新!!!
发表回复