Rumah  >  Artikel  >  Java  >  Cara menggunakan Spring Boot+gRPC untuk membina dan menggunakan perkhidmatan mikro

Cara menggunakan Spring Boot+gRPC untuk membina dan menggunakan perkhidmatan mikro

WBOY
WBOYke hadapan
2023-05-22 20:13:591366semak imbas

1. Kenapa guna Istio?

Pada masa ini, untuk tindanan teknologi Java, pilihan terbaik untuk membina perkhidmatan mikro ialah Spring Boot, dan Spring Boot biasanya digunakan dengan rangka kerja mikro perkhidmatan Spring Cloud, yang mempunyai banyak kes pelaksanaan.

Spring Cloud nampaknya sempurna, tetapi selepas pembangunan sebenar, mudah untuk mendapati Spring Cloud mempunyai masalah serius berikut:

  • Logik berkaitan tadbir urus perkhidmatan wujud dalam SDK seperti sebagai Spring Cloud Netflix dan digandingkan rapat dengan kod perniagaan.

  • SDK terlalu mengganggu kod perniagaan Apabila SDK dinaik taraf dan tidak boleh serasi ke belakang, kod perniagaan mesti ditukar untuk menyesuaikan diri dengan peningkatan SDK - walaupun perniagaan. logik tidak Tiada yang berubah.

  • Pelbagai komponen mempesonakan, kualitinya juga tidak sekata, kos pembelajaran terlalu tinggi, dan sukar untuk menggunakan semula kod sepenuhnya antara komponen SDK hanya untuk melaksanakan logik tadbir urus Bukan pilihan yang sangat baik.

  • terikat kepada timbunan teknologi Java Walaupun bahasa lain​​​boleh disambungkan, logik yang berkaitan dengan tadbir urus perkhidmatan mesti dilaksanakan secara manual, yang tidak mematuhi prinsip. microservices "boleh dibangunkan dalam pelbagai bahasa" .

  • Spring Cloud hanyalah rangka kerja pembangunan dan tidak melaksanakan penjadualan perkhidmatan, peruntukan sumber dan fungsi lain yang diperlukan untuk perkhidmatan mikro Keperluan ini mesti dilengkapkan dengan bantuan platform seperti Kubernetes. Spring Cloud dan Kubernetes mempunyai fungsi yang bertindih, dan ciri yang bercanggah menimbulkan kesukaran dalam kerjasama yang lancar antara kedua-duanya.

Adakah terdapat sebarang alternatif kepada Spring Cloud? mempunyai! Ia adalah Istio.

Istio mengasingkan sepenuhnya logik tadbir urus daripada kod perniagaan dan melaksanakan proses bebas (Sidecar). Semasa penggunaan, Sidecar dan kod perniagaan wujud bersama dalam Pod yang sama, tetapi kod perniagaan sama sekali tidak mengetahui kewujudan Sidecar. Ini mencapai sifar pencerobohan logik tadbir urus ke dalam kod perniagaan - sebenarnya, bukan sahaja kod itu tidak mengganggu, tidak ada gandingan antara kedua-duanya semasa masa jalan. Ini membolehkan perkhidmatan mikro yang berbeza dibangunkan menggunakan bahasa dan susunan teknologi yang berbeza tanpa perlu risau tentang isu tadbir urus perkhidmatan Ia boleh dikatakan bahawa ini adalah penyelesaian yang sangat elegan.

Jadi, persoalan "Mengapa menggunakan Istio" mudah diselesaikan - kerana Istio menyelesaikan masalah kesakitan perkhidmatan mikro tradisional seperti gandingan logik perniagaan dan logik tadbir urus perkhidmatan, dan ketidakupayaan untuk melaksanakan merentas bahasa dengan baik. Dan sangat mudah digunakan. Mempelajari cara menggunakan Istio tidak sukar apabila anda menguasai Kubernetes.

1.1. Mengapa menggunakan gRPC sebagai rangka kerja komunikasi?

Dalam seni bina perkhidmatan mikro, komunikasi antara perkhidmatan merupakan masalah yang agak besar, dan umumnya dilaksanakan menggunakan RPC atau RESTful API.

Spring Boot boleh menggunakan RestTemplate untuk memanggil perkhidmatan jauh, tetapi kaedah ini tidak intuitif dan kodnya kompleks juga merupakan masalah besar gRPC daripada rangka kerja Java RPC biasa seperti Dubbo . Ia ringan, mudah digunakan, mempunyai kebolehbacaan kod yang tinggi, dan boleh disepadukan dengan baik dengan Istio dan Kubernetes Dengan sokongan Protobuf dan HTTP2, prestasinya juga baik, jadi kali ini gRPC dipilih untuk menyelesaikan mikropemproses Spring Boot. masalah komunikasi antara perkhidmatan. Selain itu, walaupun gRPC tidak mempunyai penemuan perkhidmatan, pengimbangan beban dan keupayaan lain, Istio sangat berkuasa dalam hal ini, dan kedua-duanya membentuk hubungan pelengkap yang sempurna.

Memandangkan pelbagai grpc-spring-boot-starter mungkin mempunyai kesan sampingan yang tidak diketahui pada penyepaduan Spring Boot dan Istio, saya tidak menggunakan mana-mana grpc-spring-boot-starter kali ini Spring Boot terus ditulis tangan. Jika anda tidak mahu menggunakan rangka kerja pihak ketiga untuk menyepadukan gRPC dan Spring Boot, anda boleh merujuk kepada kaedah pelaksanaan mudah saya.

1.2. Menulis kod perniagaan

Mula-mula gunakan Spring Initializr untuk mencipta projek induk spring-boot-istio dan memperkenalkan kebergantungan gRPC. Fail pom adalah seperti berikut:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <modules>
        <module>spring-boot-istio-api</module>
        <module>spring-boot-istio-server</module>
        <module>spring-boot-istio-client</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId> 
       <artifactId>spring-boot-starter-parent</artifactId> 
       <version>2.2.6.RELEASE</version>
       <relativePath/>
     </parent>
    <groupId>site.wendev</groupId>
    <artifactId>spring-boot-istio</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-istio</name>
    <description>Demo project for Spring Boot With Istio.</description>
    <packaging>pom</packaging>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-all</artifactId>
                <version>1.28.1</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

Kemudian buat modul pergantungan awam spring-boot-istio-api Fail pom adalah seperti berikut, terutamanya beberapa kebergantungan gRPC:

<?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">
    <parent>
        <artifactId>spring-boot-istio</artifactId>
        <groupId>site.wendev</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>spring-boot-istio-api</artifactId>
    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-all</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version> 
       </dependency>
    </dependencies> 
   <build>
        <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.11.3:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.28.1:exe:${os.detected.classifier}</pluginArtifact>
                    <protocExecutable>/Users/jiangwen/tools/protoc-3.11.3/bin/protoc</protocExecutable>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Cipta. folder src/main/proto , cipta hello.proto dalam folder ini dan tentukan antara muka antara perkhidmatan seperti berikut:

syntax = "proto3";
option java_package = "site.wendev.spring.boot.istio.api";
option java_outer_classname = "HelloWorldService";
package helloworld;

service HelloWorld {
    rpc SayHello (HelloRequest)
 returns (HelloResponse) {}
}

message HelloRequest {
    string name = 1;
}
message HelloResponse {
    string message = 1;
}

Ia sangat mudah, hanya hantar nama dan kembalikan mesej dengan nama.

Kemudian jana kod pelayan dan klien dan letakkannya dalam folder java. Untuk bahagian ini, sila rujuk kepada dokumentasi rasmi gRPC.

Setelah modul API tersedia, penyedia perkhidmatan (pelayan) dan pengguna perkhidmatan (pelanggan) boleh dibangunkan. Di sini kami memberi tumpuan kepada cara mengintegrasikan gRPC dan Spring Boot.

1) Bahagian pelayan

Kod perniagaan adalah sangat mudah:

/**
 * 服务端业务逻辑实现
 * 
 * @author 江文
 * @date 2020/4/12 2:49 下午
 */
@Slf4j
@Component
public class HelloServiceImpl extends HelloWorldGrpc.HelloWorldImplBase {
    @Override
    public void sayHello(HelloWorldService.HelloRequest request,
                         StreamObserver<HelloWorldService.HelloResponse> responseObserver) {
        // 根据请求对象建立响应对象,返回响应信息
        HelloWorldService.HelloResponse response = HelloWorldService.HelloResponse
                .newBuilder()
                .setMessage(String.format("Hello, %s. This message comes from gRPC.", request.getName()))
                .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
        log.info("Client Message Received:[{}]", request.getName());
    }
}

Selain kod perniagaan, kami juga perlu memulakan gRPC di pada masa yang sama apabila aplikasi memulakan Pelayan. Mula-mula, tulis permulaan, penutupan dan logik lain pada bahagian pelayan:

/**
 * gRPC Server的配置——启动、关闭等
 * 需要使用<code>@Component</code>注解注册为一个Spring Bean
 * 
 * @author 江文
 * @date 2020/4/12 2:56 下午
 */
@Slf4j
@Componentpublic class GrpcServerConfiguration {
    @Autowired
    HelloServiceImpl 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();
        }
    }
}

Selepas mentakrifkan permulaan, hentikan dan logik gRPC yang lain, anda boleh menggunakan CommandLineRunner untuk menambahkannya pada permulaan Spring Boot:

/** 
 * 加入gRPC Server的启动、停止等逻辑到Spring Boot的生命周期中
 *
 * @author 江文
 * @date 2020/4/12 3:10 下午
 */
@Component
public class GrpcCommandLineRunner implements CommandLineRunner {
    @Autowired
    GrpcServerConfiguration configuration;

    @Override
    public void run(String... args) throws Exception {
        configuration.start();
        configuration.block();
    }
}

Kami mendaftarkan logik gRPC sebagai Spring Bean kerana kami perlu mendapatkan contohnya dan melaksanakan operasi yang sepadan.

Dengan cara ini, apabila memulakan Spring Boot, disebabkan kewujudan CommandLineRunner, pelayan gRPC boleh dimulakan bersama.

2) Pelanggan

Kod perniagaan juga sangat mudah:

/**
 * 客户端业务逻辑实现
 *
 * @author 江文
 * @date 2020/4/12 3:26 下午
 */
@RestController
@Slf4j
public class HelloController {
    @Autowired
    GrpcClientConfiguration configuration;
    @GetMapping("/hello")
    public String hello(@RequestParam(name = "name", defaultValue = "JiangWen", required = false) String name) {
        // 构建一个请求        HelloWorldService.HelloRequest request = HelloWorldService.HelloRequest
                .newBuilder()
                .setName(name)
                .build();        // 使用stub发送请求至服务端
        HelloWorldService.HelloResponse response = configuration.getStub().sayHello(request);
        log.info("Server response received: [{}]", response.getMessage());
        return response.getMessage();
    }
}

在启动客户端时,我们需要打开gRPC的客户端,并获取到channel和stub以进行RPC通信,来看看gRPC客户端的实现逻辑:

/**
 * gRPC Client的配置——启动、建立channel、获取stub、关闭等
 * 需要注册为Spring Bean
 *
 * @author 江文
 * @date 2020/4/12 3:27 下午
 */
@Slf4j
@Component
public class GrpcClientConfiguration {
    /** gRPC Server的地址 */
    @Value("${server-host}")
    private String host;
    /** gRPC Server的端口 */
    @Value("${server-port}")
    private int port;
    private ManagedChannel channel;
    private HelloWorldGrpc.HelloWorldBlockingStub stub;
    public void start() {
        // 开启channel
        channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
        // 通过channel获取到服务端的stub
        stub = HelloWorldGrpc.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 HelloWorldGrpc.HelloWorldBlockingStub getStub() {
        return this.stub;
    }
}

比服务端要简单一些。

最后,仍然需要一个CommandLineRunner把这些启动逻辑加入到Spring Boot的启动过程中:

/**
 * 加入gRPC Client的启动、停止等逻辑到Spring Boot生命周期中
 *
 * @author 江文
 * @date 2020/4/12 3:36 下午
 */
@Component
@Slf4j
public class GrpcClientCommandLineRunner implements CommandLineRunner {
    @Autowired
    GrpcClientConfiguration configuration;

    @Override
    public void run(String... args) {
        // 开启gRPC客户端
        configuration.start();
                // 添加客户端关闭的逻辑
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                configuration.shutdown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));
    }
}

1.3、 编写Dockerfile

业务代码跑通之后,就可以制作Docker镜像,准备部署到Istio中去了。

在开始编写Dockerfile之前,先改动一下客户端的配置文件:

server:
  port: 19090
spring:
  application:
      name: spring-boot-istio-clientserver-host: ${server-host}server-port: ${server-port}

接下来编写Dockerfile:

1) 服务端:

FROM openjdk:8u121-jdk
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
  && echo &#39;Asia/Shanghai&#39; >/etc/timezone
ADD /target/spring-boot-istio-server-0.0.1-SNAPSHOT.jar /ENV
 SERVER_PORT="18080" ENTRYPOINT java -jar /spring-boot-istio-server-0.0.1-SNAPSHOT.jar

主要是规定服务端应用的端口为18080,并且在容器启动时让服务端也一起启动。

2) 客户端:

FROM openjdk:8u121-jdk
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
  && echo &#39;Asia/Shanghai&#39; >/etc/timezoneADD /target/spring-boot-istio-client-0.0.1-SNAPSHOT.jar /ENV GRPC_SERVER_HOST="spring-boot-istio-server"ENV GRPC_SERVER_PORT="18888"ENTRYPOINT java -jar /spring-boot-istio-client-0.0.1-SNAPSHOT.jar \ --server-host=$GRPC_SERVER_HOST \ --server-port=$GRPC_SERVER_PORT

可以看到这里添加了启动参数,配合前面的配置,当这个镜像部署到Kubernetes集群时,就可以在Kubernetes的配合之下通过服务名找到服务端了。

同时,服务端和客户端的pom文件中添加:

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <executable>true</executable>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.spotify</groupId> 
               <artifactId>dockerfile-maven-plugin</artifactId>
                <version>1.4.13</version>
                <dependencies>
                    <dependency>
                        <groupId>javax.activation</groupId>
                        <artifactId>activation</artifactId>
                        <version>1.1</version>
                    </dependency>
                </dependencies>
                <executions>
                    <execution>
                        <id>default</id> 
                       <goals>
                            <goal>build</goal>
                            <goal>push</goal>
                       </goals>
                    </execution>
                </executions>
                <configuration>
                    <repository>wendev-docker.pkg.coding.net/develop/docker/${project.artifactId}</repository>
                    <tag>${project.version}</tag>
                    <buildArgs>
                        <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
                    </buildArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

这样执行mvn clean package时就可以同时把docker镜像构建出来了。

2. 编写部署文件

有了镜像之后,就可以写部署文件了:

1) 服务端:

apiVersion: v1
kind:
 Servicemetadata:
  name: spring-boot-istio-server
spec:
  type: ClusterIP
  ports:
    - name: http 
      port: 18080
      targetPort: 18080
    - name: grpc
      port: 18888
      targetPort: 18888
  selector:
    app: spring-boot-istio-server

---apiVersion: apps/v1
kind:
 Deploymentmetadata:
  name: spring-boot-istio-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-boot-istio-server
  template:
    metadata:
      labels:
        app: spring-boot-istio-server
    spec:
      containers:
        - name: spring-boot-istio-server
          image: wendev-docker.pkg.coding.net/develop/docker/spring-boot-istio-server:0.0.1-SNAPSHOT
          imagePullPolicy: Always
          tty: true
          ports:
            - name: http
              protocol: TCP
              containerPort: 18080 
            - name: grpc
              protocol: TCP
              containerPort: 18888

主要是暴露服务端的端口:18080和gRPC Server的端口18888,以便可以从Pod外部访问服务端。

2) 客户端:

apiVersion: v1
kind:
 Servicemetadata:
  name:
 spring-boot-istio-client
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 19090
      targetPort: 19090
  selector:
    app: spring-boot-istio-client

---apiVersion: apps/v1
kind:
 Deploymentmetadata:
  name: spring-boot-istio-client
spec:
  replicas: 1
  selector:
    matchLabels:
      app: spring-boot-istio-client
  template:
    metadata:
      labels:
        app: spring-boot-istio-client
    spec:
      containers:
        - name: spring-boot-istio-client
          image: wendev-docker.pkg.coding.net/develop/docker/spring-boot-istio-client:0.0.1-SNAPSHOT
          imagePullPolicy: Always
          tty: true
          ports:
            - name: http 
             protocol: TCP
              containerPort: 19090

主要是暴露客户端的端口19090,以便访问客户端并调用服务端。

如果想先试试把它们部署到k8s可不可以正常访问,可以这样配置Ingress:

apiVersion: networking.k8s.io/v1beta1
kind:
 Ingressmetadata:
  name: nginx-web
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/use-reges: "true"
    nginx.ingress.kubernetes.io/proxy-connect-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: dev.wendev.site
      http:
        paths:
          - path: /
            backend:
              serviceName: spring-boot-istio-client
              servicePort: 19090

Istio的网关配置文件与k8s不大一样:

apiVersion: networking.istio.io/v1alpha3
kind:
 Gatewaymetadata:
  name: spring-boot-istio-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - "*"

---apiVersion: networking.istio.io/v1alpha3
kind: Virtual
Servicemetadata:
  name: spring-boot-istio
spec:
  hosts:
    - "*"
  gateways:
    - spring-boot-istio-gateway
  http:
    - match:
        - uri:
            exact: /hello
      route:
        - destination:
            host: spring-boot-istio-client
            port:
              number: 19090

主要就是暴露/hello这个路径,并且指定对应的服务和端口。

3. 部署应用到Istio

首先搭建k8s集群并且安装istio。我使用的k8s版本是1.16.0,Istio版本是最新的1.6.0-alpha.1,使用istioctl命令安装Istio。建议跑通官方的bookinfo示例之后再来部署本项目。

注:以下命令都是在开启了自动注入Sidecar的前提下运行的

我是在虚拟机中运行的k8s,所以istio-ingressgateway没有外部ip:

$ kubectl get svc istio-ingressgateway -n istio-system
NAME                   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S) 
                                                                                                                                     
AGEistio-ingressgateway   NodePort   10.97.158.232   <none>        15020:30388/TCP,80:31690/TCP,443:31493/TCP,15029:32182/TCP,15030:31724/TCP,15031:30887/TCP,15032:30369/TCP,31400:31122/TCP,15443:31545/TCP   26h

所以,需要设置IP和端口,以NodePort的方式访问gateway:

export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=&#39;{.spec.ports[?(@.name=="http2")].nodePort}&#39;)
export SECURE_INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath=&#39;{.spec.ports[?(@.name=="https")].nodePort}&#39;)
export INGRESS_HOST=127.0.0.1export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT

这样就可以了。

接下来部署服务:

$ kubectl apply -f spring-boot-istio-server.yml
$ kubectl apply -f spring-boot-istio-client.yml
$ kubectl apply -f istio-gateway.yml

必须要等到两个pod全部变为Running而且Ready变为2/2才算部署完成。

接下来就可以通过

curl -s http://${GATEWAY_URL}/hello

访问到服务了。如果成功返回了Hello, JiangWen. This message comes from gRPC.的结果,没有出错则说明部署完成。

Atas ialah kandungan terperinci Cara menggunakan Spring Boot+gRPC untuk membina dan menggunakan perkhidmatan mikro. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam