trpc.group/trpc-go/trpc-go@v1.0.3/docs/basics_tutorial.zh_CN.md (about)

     1  [English](./basics_tutorial.md) | 中文
     2  
     3  ## 基础教程
     4  
     5  在[快速开始](./quick_start.zh_CN.md)中,你已经成功地运行了 tRPC-Go helloworld。但是,我们忽略了很多细节。这章中,你将更加细致地了解 tRPC-Go 服务开发流程。我们将依次介绍:
     6  - 如何通过 protobuf 定义 tRPC 服务?
     7  - `trpc_go.yaml` 要如何配置?
     8  - tRPC-Go 具有哪些扩展能力?
     9  - tRPC-Go 支持的各种能力。
    10  
    11  我们服务依赖 Protocol Buffer v3,你可以参考它 Go 语言的[官方文档](https://protobuf.dev/getting-started/gotutorial/)。
    12  
    13  ### 定义服务
    14  
    15  为了定义一个新服务,我们首先需要在 protobuf 中声明它。下面的示例声明了一个名为 `MyService` 的服务。
    16  ```protobuf
    17  service MyService {
    18    // ...
    19  }
    20  ```
    21  
    22  一个服务有各种各样的方法,它们需要声明在 service 内部。比如,下面的示例中,我们为 `Greeter` 声明了一个方法 `Hello`,它以 `HelloReq` 作为参数,返回 `HelloRsp`。
    23  ```protobuf
    24  service Greeter {
    25    rpc Hello(HelloReq) returns (HelloRsp) {}
    26    // ...
    27  }
    28  
    29  message HelloReq {
    30    // ...
    31  }
    32  
    33  message HelloRsp {
    34    // ...
    35  }
    36  ```
    37  
    38  注意,`Method` 最后有一个 `{}`,其内部也是可以有内容的。我们将在后面看到。
    39  
    40  ### 编写客户端和服务端代码
    41  
    42  protobuf 给出的是一个语言无关的服务定义,我们还要用 [trpc 命令行工具](https://github.com/trpc-group/trpc-cmdline)将它翻译成对应语言的桩代码。你可以通过 `$ trpc create -h` 查看它支持的各种选项。你可以参考快速开始的 [helloworld](/examples/helloworld/pb/Makefile) 项目来快速创建你自己的桩代码。
    43  
    44  桩代码主要分为 client 和 server 两部分。  
    45  下面是生成的部分 client 代码。在[快速开始](./quick_start.zh_CN.md)中,我们通过 `NewGreeterClientProxy` 来创建一个 client 实例,并调用了它的 `Hello` 方法:
    46  ```go
    47  type GreeterClientProxy interface {
    48      Hello(ctx context.Context, req *HelloReq, opts ...client.Option) (rsp *HelloRsp, err error)
    49  }
    50  
    51  var NewGreeterClientProxy = func(opts ...client.Option) GreeterClientProxy {
    52      return &GreeterClientProxyImpl{client: client.DefaultClient, opts: opts}
    53  }
    54  ```
    55  
    56  下面是生成的部分 server 代码,`GreeterService` 约定了你需要实现的接口。`RegisterGreeterService` 会将你的实现注册到框架中。在[快速开始](./quick_start.zh_CN.md)中,我们先通过 `s := trpc.NewServer()` 创建了一个 tRPC-Go 实例,然后把实现了业务逻辑的 `Greeter` 结构体注册到了 `s` 中。
    57  ```go
    58  type GreeterService interface {
    59      Hello(ctx context.Context, req *HelloReq) (*HelloRsp, error)
    60  }
    61  
    62  func RegisterGreeterService(s server.Service, svr GreeterService) { /* ... */ }
    63  ```
    64  
    65  ### 框架配置
    66  
    67  也许你已经注意到了客户端和服务端的些许不同。在客户端,我们通过 `client.WithTarget` 指定了服务端的地址,但是在服务端,我们并没有在代码中找到对应的地址,实际上它配置在 `./server/trpc_go.yaml` 中。  
    68  这是 tRPC-Go 的支持的 yaml 配置能力。几乎所有 tRPC-Go 框架能力都可以通过文件配置进行定制化。当你执行 tRPC-Go 时,框架会在当前目录下寻找 `trpc_go.yaml` 文件,并加载相关配置。这使得你可以在不重新编译服务的前提下,就更改程序的行为。  
    69  下面是一些本教程所需的必要配置,完整配置请参考[框架配置](/docs/user_guide/framework_conf.zh_CN.md)。
    70  ```yaml
    71  server:  # 服务端配置
    72    service:  # 可以配置多个 service
    73      - name: helloworld  # 服务名
    74        ip: 127.0.0.1  # 服务监听的 IP
    75        port: 8000  # 服务监听的端口
    76        protocol: trpc  # 服务使用的协议
    77  ```
    78  
    79  ### 拦截器和插件
    80  
    81  tRPC-Go 有着丰富的可扩展性,你可通过拦截器在请求执行流程中注入各种新能力,插件工厂则允许你方便地集成新功能。
    82  
    83  #### 拦截器
    84  
    85  [拦截器](/filter)像一颗洋葱,当 tRPC-Go 处理请求时,会依次经过洋葱的每一层。你可以通过拦截器定制这颗洋葱模型。
    86  
    87  客户端拦截器定义如下:
    88  ```go
    89  type ClientFilter func(ctx context.Context, req, rsp interface{}, next ClientHandleFunc) error
    90  type ClientHandleFunc func(ctx context.Context, req, rsp interface{}) error
    91  ```
    92  当实现你自己的拦截器时:
    93  ```go
    94  func MyFilter(ctx context.Context, req, rsp interface{}, next ClientHandleFunc) error {
    95      // 前置流程
    96      err := next(ctx, req, rsp)
    97      // 后置流程
    98      return err
    99  }
   100  ```
   101  `next` 前后的代码代会在实际请求之前和之后分别执行,即前置流程和后置流程。你可以实现很多拦截器,通过 [`client.WithFilters`](/client/options.go) 在调用时使用,框架会自动将这些拦截器串成一个链。
   102  
   103  服务端拦截器的签名与客户端略有不同:
   104  ```go
   105  type ServerFilter func(ctx context.Context, req interface{}, next ServerHandleFunc) (rsp interface{}, err error)
   106  type ServerHandleFunc func(ctx context.Context, req interface{}) (rsp interface{}, err error)
   107  ```
   108  `rsp` 在返回值中,而不是参数中。在使用,需要通过 [`server.WithFilters`](/server/options.go) 注入到框架中。框架会自动将这些拦截器串成一个链。
   109  
   110  除了上面提到的通过代码直接添加拦截器,你也可以通过配置文件加载拦截器。
   111  ```yaml
   112  client:
   113    filter:  # 指定客户端全局拦截器
   114      - client_filter_name_1
   115      - client_filter_name_2
   116    service:
   117      - name: xxx
   118        filter:  # 指定 xxx 客户端特有的拦截器,它们会追加在全局拦截器之后
   119          - client_filter_name_3
   120  server:
   121    filter:  # 指定服务端全局拦截器
   122      - server_filter_name_1
   123      - server_filter_name_2
   124    service:
   125      - name: yyy
   126        filter:  # 指定 yyy 服务端特有的拦截器,它们会追加在全局拦截器之后
   127          - server_filter_name_3
   128  ```
   129  这些拦截器需要提前通过 [`filter.Register`](/filter/filter.go) 注册在框架中。在执行 `trpc.NewServer` 时,框架会自动加载它们。  
   130  注意,当代码和配置文件同时存在时,代码指定的拦截器会先执行,然后再执行配置文件指定的拦截器。
   131  
   132  你可以在[这里](/examples/features/filter)看到拦截器的使用示例。
   133  
   134  #### 插件
   135  
   136  [插件](/plugin)是 tRPC-Go 基于 yaml 配置文件设计的一套自动化模块加载机制。它的接口定义如下:
   137  ```go
   138  package plugin
   139  
   140  type Factory interface {
   141      Type() string
   142      Setup(name string, dec Decoder) error
   143  }
   144  
   145  type Decoder interface {
   146      Decode(cfg interface{}) error
   147  }
   148  ```
   149  `Type` 返回插件的类型,`Setup` 时会传入插件名和一个 `Decoder`,用于解析 yaml 的内容。yaml 来自 `trpc_go.yaml` 配置:
   150  ```yaml
   151  plugins:
   152    __type:
   153      __name:
   154        # plugin contents
   155  ```
   156  其中 `__type` 应替换为 `Factory.Type()` 返回的值,`__name` 应替换为 `plugin.Register` 的第一个参数。
   157  
   158  在实现 `plugin` 时,你应该创建一个 `func init()` 函数,通过 `Register` 注册你的插件。这样别人用你的插件时,只需要在代码中匿名 import 你的包即可。当调用 `trpc.NewServer()` 时,插件就会调用 `Factory.Setup` 函数进行初始化。
   159  
   160  插件经常和拦截器配合,比如在 `Factory.Setup` 函数中调用 `filter.Register` 注册拦截器。框架保证插件初始化在拦截器加载之前完成。这样,你就可以通过修改 `trpc_go.yaml` 来配置拦截器的行为。
   161  
   162  ### 多协议支持
   163  
   164  [快速开始](./quick_start.zh_CN.md)中介绍的是普通的一应一答式 RPC。tRPC-Go 还支持流式 RPC、HTTP 等。
   165  
   166  #### 流式 RPC
   167  
   168  流式 RPC 支持客户端与服务端之间进行更加灵活的交互。它可以分为三种模式:客户端流式、服务端流式和双向流式。  
   169  客户端流式允许客户端依次发送多个包,服务端全收到后,再返回一个包。它是多对一关系。  
   170  服务端流式允许服务端为一个客户端请求生成多次回包。它是一对多关系。  
   171  双向流式允许客户端服务端可以并行地给对方发送请求,并且是保序的,就像两个交谈中的人一样。它是多对多关系。
   172  
   173  本小节代码基于 [`example/stream`](/examples/features/stream)。
   174  
   175  与普通 RPC 不同,在 protobuf 中声明流式 RPC 需要使用 `stream` 关键字。
   176  ```protobuf
   177  service TestStream {
   178    rpc ClientStream (stream HelloReq) returns (HelloRsp);
   179    rpc ServerStream (HelloReq) returns (stream HelloRsp);
   180    rpc BidirectionalStream (stream HelloReq) returns (stream HelloRsp);
   181  }
   182  ```
   183  当 `stream` 只出现在方法请求前时,该方法为客户端流式;当 `stream` 只出现在方法返回值前时,该方法为服务端流式;当 `steam` 同时出现在方法的请求和返回值前时,该方法为双向流式。
   184  
   185  流式生成的桩代码与普通 RPC 有很大区别,以客户端流式为例:
   186  ```go
   187  type TestStreamService interface {
   188      ClientStream(TestStream_ClientStreamServer) error
   189      // ...
   190  }
   191  
   192  type TestStream_ClientStreamServer interface {
   193      SendAndClose(*HelloRsp) error
   194      Recv() (*HelloReq, error)
   195      server.Stream
   196  }
   197  
   198  type TestStreamClientProxy interface {
   199      ClientStream(ctx context.Context, opts ...client.Option) (TestStream_ClientStreamClient, error)
   200  }
   201  
   202  type TestStream_ClientStreamClient interface {
   203      Send(*HelloReq) error
   204      CloseAndRecv() (*HelloRsp, error)
   205      client.ClientStream
   206  }
   207  ```
   208  从上面的桩代码,大概可以猜到业务代码的编写方式。客户端通过 `TestStream_ClientStreamClient.Send` 多次发送请求,最后通过 `TestStream_ClientStreamClient.CloseAndRecv` 关闭流并等待回包。服务端则通过 `TestStream_ClientStreamServer.Recv` 接收客户端的流式请求,如果它返回了 `io.EOF`,说明客户端调用了 `CloseAndRecv` 并正在等待回包,它最后再通过 `TestStream_ClientStreamServer.SendAndClose` 发送回包并确认关闭流。注意,客户端流式是由客户端主动结束的,`CloseAndRecv` 表明客户端先关闭,再等待回包,`SendAndClose` 表明服务端先发送回包,再关闭。
   209  
   210  服务端流式与客户端流式相反。只要 `TestStreamService.ServerStream` 函数退出,就表示服务端已经完成了流的发送。客户端通过多次调用 `TestStream_ServerStreamClient.Recv` 来获取流式回包,当收到 `io.EOF` 错误时,表明服务端已完成流式回包。
   211  
   212  双向流式是前两者的结合。它们的发送和读取可以交叉起来,就像两个人交谈一样,可以实现更加复杂的交互逻辑。
   213  
   214  流式 RPC 的配置和普通 RPC 没有任何不同。
   215  
   216  #### 标准 HTTP 服务
   217  
   218  tRPC-Go 支持将 Go 标准库的 HTTP 服务注册到框架中。假如你需要在 8080 端口监听你的 HTTP 服务,首先,在 `trpc_go.yaml` 中加入如下 service 配置:
   219  ```yaml
   220  server:
   221    service:
   222      - name: std_http
   223        ip: 127.0.0.1
   224        port: 8080
   225        protocol: http
   226  ```
   227  再加入下面的代码即可:
   228  ```go
   229  import thttp trpc.group/trpc-go/trpc-go/http
   230  
   231  func main() {
   232      s := trpc.NewServer()
   233      thttp.RegisterNoProtocolServiceMux(s.Service("std_http"), your_http_handler) 
   234      log.Info(s.Serve())
   235  }
   236  ```
   237  
   238  注意,与普通 RPC 不同,yaml 中的 `protocol` 字段需要改成 `http`。`thttp.RegisterNoProtocolServiceMux` 方法的第一个参数需要指定 yaml 中的 service 名,即 `s.Service("std_http")`。
   239  
   240  #### 将 RPC 快速转为 HTTP 服务
   241  
   242  在 tRPC-Go 中,将 `trpc_go.yaml` 中的 `server.service[i].protocol` 从 `trpc` 改为 `http`,就可以将普通 tRPC 协议的服务转换为 HTTP 协议。在调用时,HTTP url 对应生成的桩代码中的[方法名](/examples/helloworld/pb/helloworld.trpc.go)。
   243  比如,如果将[快速开始](./quick_start.zh_CN.md)里的 RPC 改为 HTTP,在调用时,你需要用下面的 curl 命令:
   244  ```bash
   245  $ curl -XPOST -H"Content-Type: application/json" -d'{"msg": "world"}' 127.0.0.1:8000/trpc.helloworld.Greeter/Hello
   246  {"msg":"Hello world!"}
   247  ```
   248  
   249  如果你需要更改 HTTP url,可以在 protobuf 中声明服务时,加上下面的扩展:
   250  ```protobuf
   251  import "trpc/proto/trpc_options.proto";
   252  
   253  service Greeter {
   254    rpc Hello (HelloRequest) returns (HelloReply) {
   255      option (trpc.alias) = "/alias/hello";
   256    }
   257  }
   258  ```
   259  curl 命令如下:
   260  ```bash
   261  $ curl -XPOST -H"Content-Type: application/json" -d'{"msg": "world"}' 127.0.0.1:8000/alias/hello
   262  {"msg":"Hello world!"}
   263  ```
   264  
   265  #### RESTful HTTP
   266  
   267  将 RPC 快速转为 HTTP 虽然很方便,但是却无法定制 HTTP url/parameter/body 和 RPC Request 的映射关系。RESTful 提供了更加灵活的 RPC 到 HTTP 的转换。
   268  
   269  你可以参考 [RESTful example](/examples/features/restful) 快速了解 RESTful。更多细节请参考 RESTful 的[文档](/restful)。
   270  
   271  ### 进一步阅读
   272  
   273  tRPC-Go 还支持其他丰富的功能特性,你可以查看它们的[文档](/main/docs/user_guide)了解更多细节。
   274