grpc 框架教程

gRPC 是一个高性能、跨平台、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C/C++、Java、Python、Ruby、C#、PHP、Node.js、Go 语言等版本,几乎你想到的语言都支持了.

gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

下面先介绍grpc相关概念

1.grpc是什么?

在 gRPC 里客户端应用可以像调用本地方法一样直接调用另一台机器上服务端应用的方法,这样我们就很容易创建分布式应用和服务。跟其他 RPC 系统类似,gRPC 也是基于以下理念:首先定义一个服务,定义能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个方法,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根,这个存根就是长得像服务端一样的方法(但是没有具体实现),客户端通过这个存根调用服务端的方法。

grpc工作原理,如下图:

img

2.grpc使用的协议

gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据的序列化机制,当然也可以使用其他数据格式如 JSON,不过通常都使用protocol buffers这种灵活、高效的数据格式,如果不了解protobuf语法,点击这里学习protocol buffers入门教程

3.服务定义

使用gprc,首先需要定义服务, 指定其可以被远程调用的方法及其参数和返回类型。

服务,你可以理解成服务端api接口的集合,对外提供一些功能。

通过protobuf定义服务的例子:

// 定义一个叫HelloService的服务
service HelloService {
// 定义一个叫SayHello的方法,这个方法接受HelloRequest消息作为参数,返回HelloResponse消息
rpc SayHello (HelloRequest) returns (HelloResponse);
}

// 定义HelloRequest消息
message HelloRequest {
required string greeting = 1;
}

// 定义HelloResponse消息
message HelloResponse {
required string reply = 1;
}

如果你把service和message关键词当成class,是不是跟类定义很像!

提示:本节主要介绍grpc的一些关键概念,具体不同的开发语言如何使用,后面的教程会讲解。

gRPC 允许你定义四类服务方法,下面分别介绍如何定义,以及客户端和服务端的交互方式。

3.1. 单向RPC

即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

rpc SayHello(HelloRequest) returns (HelloResponse){
}

3.2. 服务端流式 RPC

即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。

通俗的讲就是客户端请求一次,服务端就可以源源不断的给客户端发送消息。

// 注意stream关键词在什么地方
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}

3.3. 客户端流式 RPC

即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

通俗的讲就是请求一次,客户端就可以源源不断的往服务端发送消息。

// 注意stream关键词在什么地方
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}

3.4. 双向流式 RPC

即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

类似tcp通信,客户端和服务端可以互相发消息。

// 注意stream关键词在什么地方
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

提示:本序列教程主要翻译gRPC官方文档。

go语言 grpc入门教程

本教程提供了 Go 程序员如何使用 gRPC 的指南。

前置知识点:

go语言使用grpc的步骤:

  1. 安装go语言grpc包

  2. 安装protobuf编译器

  3. 例子目录结构

  4. 定义服务

  5. 使用protobuf编译器,编译proto协议文件,生成go代码。

  6. 实现服务端代码

  7. 实现客户端代码

提示:go版本要求:1.11版本以上,如果你的版本低于1.11版本,可以升级下版本。

1.安装go语言grpc包

go get -u google.golang.org/grpc

2.安装protobuf编译器

2.1. 安装protoc编译器

protobuf编译器就叫protoc

到下面github地址,根据自己的系统版本选择下载,解压缩安装即可。

https://github.com/protocolbuffers/protobuf/releases

例如3.9.2版本压缩包介绍:

  • protoc-3.9.2-win64.zip - windows 64版本

  • protoc-3.9.2-osx-x86_64.zip - mac os 64版本

  • protoc-3.9.2-linux-x86_64.zip - linux 64版本

解压缩安装包之后,将 [安装目录]/bin 目录,添加到PATH环境变量。

2.2. 安装编译器go语言插件

因为目前的protoc编译器,默认没有包含go语言代码生成器,所以需要单独安装插件。

go get -u github.com/golang/protobuf/protoc-gen-go

安装go语言插件后,需要将 $GOPATH/bin 路径加入到PATH环境变量中。

3.例子目录结构

本教程例子的目录结构如下:

helloworld/
├── client.go - 客户端代码
├── go.mod  - go模块配置文件
├── proto     - 协议目录
│   ├── helloworld.pb.go - rpc协议go版本代码
│   └── helloworld.proto - rpc协议文件
└── server.go  - rpc服务端代码

初始化命令如下:

# 创建项目目录
mkdir helloworld
# 切换到项目目录
cd helloworld
# 创建RPC协议目录
mkdir proto
# 初始化go模块配置,用来管理第三方依赖, 本例子,项目模块名是:tizi365.com/helloworld
go mod init tizi365.com/helloworld

说明:本例子使用go module管理第三方依赖包,如果不了解可以点击学习go语言包管理

4.定义服务

定义服务,其实就是通过protobuf语法定义语言平台无关的接口。

文件: helloworld/proto/helloworld.proto

syntax = "proto3";
// 定义包名
package proto;

// 定义Greeter服务
service Greeter {
  // 定义SayHello方法,接受HelloRequest消息, 并返回HelloReply消息
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定义HelloRequest消息
message HelloRequest {
  // name字段
  string name = 1;
}

// 定义HelloReply消息
message HelloReply {
  // message字段
  string message = 1;
}

Greeter服务提供了一个SayHello接口,请求SayHello接口,需要传递一个包含name字段的HelloRequest消息,返回包含message字段的HelloReply消息。

5.编译proto协议文件

上面通过proto定义的接口,没法直接在代码中使用,因此需要通过protoc编译器,将proto协议文件,编译成go语言代码。

# 切换到helloworld项目根目录,执行命令
protoc -I proto/ --go_out=plugins=grpc:proto proto/helloworld.proto

protoc命令参数说明:

  • -I 指定代码输出目录,忽略服务定义的包名,否则会根据包名创建目录

  • --go_out 指定代码输出目录,格式:--go_out=plugins=grpc:目录名

  • 命令最后面的参数是proto协议文件

编译成功后在proto目录生成了helloworld.pb.go文件,里面包含了,我们的服务和接口定义。

6.实现服务端代码

文件:helloworld/server.go

package main

import (
	"log"
	"net"

	"golang.org/x/net/context"
	// 导入grpc包
	"google.golang.org/grpc"
	// 导入刚才我们生成的代码所在的proto包。
        pb "tizi365.com/helloworld/proto"
	"google.golang.org/grpc/reflection"
)


// 定义server,用来实现proto文件,里面实现的Greeter服务里面的接口
type server struct{}

// 实现SayHello接口
// 第一个参数是上下文参数,所有接口默认都要必填
// 第二个参数是我们定义的HelloRequest消息
// 返回值是我们定义的HelloReply消息,error返回值也是必须的。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	// 创建一个HelloReply消息,设置Message字段,然后直接返回。
	return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
	// 监听127.0.0.1:50051地址
	lis, err := net.Listen("tcp", "127.0.0.1:50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 实例化grpc服务端
	s := grpc.NewServer()

        // 注册Greeter服务
	pb.RegisterGreeterServer(s, &server{})

	// 往grpc服务端注册反射服务
	reflection.Register(s)

        // 启动grpc服务
	if err := s.Serve(lis); err != nil {
	    log.Fatalf("failed to serve: %v", err)
	}
}

运行服务端:

# 切换到项目根目录,运行命令
go run server.go

7.实现客户端代码

文件:helloworld/client.go

package main

import (
	"log"
	"os"
	"time"

	"golang.org/x/net/context"
	// 导入grpc包
	"google.golang.org/grpc"
	// 导入刚才我们生成的代码所在的proto包。
        pb "tizi365.com/helloworld/proto"
)

const (
	defaultName = "world"
)

func main() {
	// 连接grpc服务器
	conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	// 延迟关闭连接
	defer conn.Close()

	// 初始化Greeter服务客户端
	c := pb.NewGreeterClient(conn)

	// 初始化上下文,设置请求超时时间为1秒
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	// 延迟关闭请求会话
	defer cancel()

	// 调用SayHello接口,发送一条消息
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}

	// 打印服务的返回的消息
	log.Printf("Greeting: %s", r.Message)
}

运行

# 切换到项目根目录,运行命令
go run client.go

输出结果:

2019/09/26 00:19:27 Greeting: Hello world

gRPC python快速入门教程

本章主要介绍在python中grpc的基本用法。

在开始之前先安装一些东西。

前置知识点:

1.环境安装

grpc支持Python 2.7 或者 Python 3.4 或者更高的版本。

首先确认pip的版本大于9.0.1

$ python -m pip install --upgrade pip

如果因为某些原因无法直接升级pip, 可以选择在virtualenv中运行grpc例子。

$ python -m pip install virtualenv
$ virtualenv venv
$ source venv/bin/activate
$ python -m pip install --upgrade pip

1.1. 安装gRPC

$ python -m pip install grpcio

在mac系统中可能会出现下面的错误:

$ OSError: [Errno 1] Operation not permitted: '/tmp/pip-qwTLbI-uninstall/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/six-1.4.1-py2.7.egg-info'

可以使用下面的命令解决问题:

$ python -m pip install grpcio --ignore-installed

1.2. 安装gRPC工具

gRPC工具包括protocol buffer编译器(protoc)和 python代码生成插件,python代码生成插件通过.proto服务定义文件,生成python的grpc服务端和客户端代码。

$ python -m pip install grpcio-tools

2.下载例子代码

我们直接从grpc的github地址,下载grpc代码,里面包含了python的例子。

$ # 将grpc代码clone到本地
$ git clone -b v1.23.0 https://github.com/grpc/grpc

$ # 切换到python的helloworld例子目录。
$ cd grpc/examples/python/helloworld

3.运行grpc应用

首先运行服务端

$ python greeter_server.py

打开另外一个命令窗口,运行客户端

$ python greeter_client.py

到目前为止,我们已经运行了服务端和客户端的例子。

4.更新rpc服务

现在我们添加一个新的rpc接口,看看python的服务端和客户端代码怎么修改。

grpc的接口是通过protocol buffers定义的,通常都保存在.proto文件中。

如果你打开,前面例子的服务端和客户端代码看,你会发现有一个SayHello的方法,这个方法接受HelloRequest参数,并返回HelloReply参数,下面看看对应的.proto文件,是如何定义rpc接口的。

打开:examples/protos/helloworld.proto 协议文件。

// 定义Greeter,你可以当成类
service Greeter {
  // 定义一个rpc方法SayHello,接受HelloRequest消息,返回HelloReply消息
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定义请求参数,HelloRequest消息
message HelloRequest {
  string name = 1;
}

// 定义响应参数,HelloReply消息
message HelloReply {
  string message = 1;
}

现在我们添加一个新的rpc方法,现在.proto定义变成这样。

// 定义Greeter,你可以当成类
service Greeter {
  // SayHello方法
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // 定义一个SayHelloAgain方法,接受HelloRequest消息,返回HelloReply消息
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

// 定义请求参数,HelloRequest消息
message HelloRequest {
  string name = 1;
}

// 定义响应参数,HelloReply消息
message HelloReply {
  string message = 1;
}

修改后记得保存.proto文件。

5.生成grpc代码

现在我们需要根据新定义的.proto文件,生成新的python代码(这里其实只是生成一个类库,相当于更新类库,我们的业务代码实现刚才定义的rpc方法即可)

切换到examples/python/helloworld目录,运行命令:

$ python -m grpc_tools.protoc -I../../protos --python_out=. --grpc_python_out=. ../../protos/helloworld.proto

命令说明:

  • -I proto协议文件目录

  • --python_out和--grpc_python_out 生成python代码的目录

  • 命令最后面的参数是proto协议文件名

命令执行后生成helloworld_pb2.py文件和helloworld_pb2_grpc.py文件。

  • helloworld_pb2.py - 主要包含proto文件定义的消息类。

  • helloworld_pb2_grpc.py - 包含服务端和客户端代码

6.更新并运行应用程序

现在我们已经根据proto文件,生成新的python类库,但是我们还没实现新定义的rpc方法,下面介绍服务端和客户端如果升级代码。

6.1. 更新服务端代码

在同样目录打开greeter_server.py文件,实现类似如下代码。

class Greeter(helloworld_pb2_grpc.GreeterServicer):
  # 实现SayHello方法
  def SayHello(self, request, context):
    return helloworld_pb2.HelloReply(message='Hello, %s!' % request.name)
  # 实现SayHelloAgain方法
  def SayHelloAgain(self, request, context):
    return helloworld_pb2.HelloReply(message='Hello again, %s!' % request.name)
...

6.2. 更新客户端代码

在同样的目录打开greeter_client.py文件,实现代码如下:

def run():
  # 配置grpc服务端地址
  channel = grpc.insecure_channel('localhost:50051')
  stub = helloworld_pb2_grpc.GreeterStub(channel)
  # 请求服务端的SayHello方法
  response = stub.SayHello(helloworld_pb2.HelloRequest(name='you'))
  print("Greeter client received: " + response.message)
  # 请求服务端的SayHelloAgain方法
  response = stub.SayHelloAgain(helloworld_pb2.HelloRequest(name='you'))
  print("Greeter client received: " + response.message)

6.3. 运行代码

在examples/python/helloworld目录下面,首先运行服务端:

$ python greeter_server.py

运行客户端

$ python greeter_client.py

grpc node入门教程

本章主要介绍在node.js中grpc的基本用法。

前置知识点:

 

1.环境要求

在开始之前先安装一些东西。

  • node版本大于4.0.0

大家可以直接到node官网下载最新的版本安装。

2.下载例子代码

我们直接从grpc的github地址下载演示代码。

$ # 下载v1.24.0版本的代码,当然你可以下载其他版本
$ git clone -b v1.24.0 https://github.com/grpc/grpc

$ # 切换到node演示代码目录
$ cd grpc/examples/node/dynamic_codegen

$ # 安装下node的依赖包
$ npm install

3.运行grpc应用

首先运行服务端

$ node greeter_server.js

另外打开一个新的命令窗口,运行客户端

$ node greeter_client.js

ok, 这个时候你就看到客户端请求服务端的输出结果,后面我们演示下怎么添加一个新的rpc方法。

4.更新rpc服务

grpc的接口是通过protocol buffers定义的,通常都保存在.proto文件中。

如果你打开,前面例子的服务端和客户端代码看,你会发现有一个SayHello的方法,这个方法接受HelloRequest参数,并返回HelloReply参数,下面看看对应的.proto文件,是如何定义rpc接口的。

打开:examples/protos/helloworld.proto 协议文件。

// 定义Greeter,你可以当成类
service Greeter {
  // 定义一个rpc方法SayHello,接受HelloRequest消息,返回HelloReply消息
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定义请求参数,HelloRequest消息
message HelloRequest {
  string name = 1;
}

// 定义响应参数,HelloReply消息
message HelloReply {
  string message = 1;
}

现在我们添加一个新的rpc方法,现在.proto定义变成这样。

// 定义Greeter,你可以当成类
service Greeter {
  // SayHello方法
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // 定义一个SayHelloAgain方法,接受HelloRequest消息,返回HelloReply消息
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

// 定义请求参数,HelloRequest消息
message HelloRequest {
  string name = 1;
}

// 定义响应参数,HelloReply消息
message HelloReply {
  string message = 1;
}

5.生成grpc代码

node.js支持动态编译proto协议文件,可以不用手动将.proto文件转换成javascript代码。

6.更新并运行应用程序

6.1. 更新服务端代码

下面是完整的服务端代码,文件:greeter_server.js

// 我们定义的.proto协议文件路径
var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';

// 加载grpc包
var grpc = require('grpc');
var protoLoader = require('@grpc/proto-loader');
// 下面是处理动态加载.proto协议文件
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

/**
 * 实现 SayHello RPC 方法.
 */
function sayHello(call, callback) {
  callback(null, {message: 'Hello ' + call.request.name});
}

/**
 * 实现 SayHelloAgain RPC 方法.
 */
function sayHelloAgain(call, callback) {
  callback(null, {message: 'Hello again, ' + call.request.name});
}

/**
 * 启动 RPC server
 */
function main() {
  var server = new grpc.Server();
  // 注册我们实现的rpc方法
  server.addService(hello_proto.Greeter.service,
                         {sayHello: sayHello, sayHelloAgain: sayHelloAgain});
  // 设置rpc服务端绑定的地址
  server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
  // 启动服务端
  server.start();
}

main();

6.2. 更新客户端代码

完成客户端代码,文件:greeter_client.js

// proto协议文件路径
var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';

var grpc = require('grpc');

// 下面是处理动态加载.proto协议文件
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function main() {
  // 设置rpc服务端连接地址
  var client = new hello_proto.Greeter('localhost:50051',
                                       grpc.credentials.createInsecure());
  var user;
  if (process.argv.length >= 3) {
    user = process.argv[2];
  } else {
    user = 'world';
  }
  // 请求服务端的sayHello方法
  client.sayHello({name: user}, function(err, response) {
    console.log('Greeting:', response.message);
  });
 
  // 请求服务端的sayHelloAgain方法
  client.sayHelloAgain({name: 'you'}, function(err, response) {
    console.log('Greeting:', response.message);
  });
}

main();

重新运行服务端和客户端即可。

grpc java入门教程

本章主要介绍在java中grpc的基本用法。

前置知识点:

1.环境要求

  • JDK 7 或者更高的版本

2.下载例子代码

我们直接从grpc的github地址下载演示代码。

$ # 下载v1.23.0版本的代码,你也可以直接下载最新的版本
$ git clone -b v1.23.0 https://github.com/grpc/grpc-java

$ # 切换到Java的演示代码目录
$ cd grpc-java/examples

3.运行grpc应用

首先编译服务端和客户端代码

$ ./gradlew installDist

运行grpc服务端

$ ./build/install/examples/bin/hello-world-server

另外打开一个命令行创建,运行客户端

$ ./build/install/examples/bin/hello-world-client

ok, 这个时候你就看到客户端请求服务端的输出结果,后面我们演示下怎么添加一个新的rpc方法。

4.更新rpc服务

grpc的接口是通过protocol buffers定义的,通常都保存在.proto文件中。

如果你打开,前面例子的服务端和客户端代码看,你会发现有一个SayHello的方法,这个方法接受HelloRequest参数,并返回HelloReply参数,下面看看对应的.proto文件,是如何定义rpc接口的。

打开:src/main/proto/helloworld.proto 协议文件。

// 定义Greeter,你可以当成类
service Greeter {
  // 定义一个rpc方法SayHello,接受HelloRequest消息,返回HelloReply消息
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定义请求参数,HelloRequest消息
message HelloRequest {
  string name = 1;
}

// 定义响应参数,HelloReply消息
message HelloReply {
  string message = 1;
}

现在我们添加一个新的rpc方法,现在.proto定义变成这样。

// 定义Greeter,你可以当成类
service Greeter {
  // SayHello方法
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // 定义一个SayHelloAgain方法,接受HelloRequest消息,返回HelloReply消息
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

// 定义请求参数,HelloRequest消息
message HelloRequest {
  string name = 1;
}

// 定义响应参数,HelloReply消息
message HelloReply {
  string message = 1;
}

5.生成grpc代码

重新编译客户端和服务端代码。

$ ./gradlew installDist

这个时候会生成GreeterGrpc.java源码文件,包含了我们在.proto定义的服务和方法。

6.更新并运行应用程序

6.1. 更新服务端代码

打开源码:src/main/java/io/grpc/examples/helloworld/HelloWorldServer.java,下面是关键代码。

// 定义GreeterImpl类型,实现我们在.proto定义的服务
private class GreeterImpl extends GreeterGrpc.GreeterImplBase {

  // 实现sayHello方法
  @Override
  public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    // 初始化HelloReply消息
    HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
    // 返回消息
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }

  // 实现sayHelloAgain方法
  @Override
  public void sayHelloAgain(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + req.getName()).build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }
}
...

6.2. 更新客户端代码

打开src/main/java/io/grpc/examples/helloworld/HelloWorldClient.java文件,调用rpc方法的关键代码如下:

public void greet(String name) {
  logger.info("Will try to greet " + name + " ...");
  // 创建请求消息
  HelloRequest request = HelloRequest.newBuilder().setName(name).build();
  // 定义响应消息
  HelloReply response;
  try {
    // 调用服务端的sayHello方法
    response = blockingStub.sayHello(request);
  } catch (StatusRuntimeException e) {
    logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
    return;
  }
  logger.info("Greeting: " + response.getMessage());
  try {
    // 调用服务端的sayHelloAgain方法
    response = blockingStub.sayHelloAgain(request);
  } catch (StatusRuntimeException e) {
    logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
    return;
  }
  logger.info("Greeting: " + response.getMessage());
}

完整的代码,请参考源码文件的内容。

需要重新编译在运行程序即可。

posted on 2022-08-10 13:53  root-123  阅读(423)  评论(0编辑  收藏  举报