上一篇文章 中我们快速搭建了一个 http API 服务,并且我们可以看到,对外提供了 URL query 和 application/json 两种服务模式。那么实际上,我们到底实现了什么、并且能够做些什么?读者可能还是没有直观的感受,因此必要先来简单 review 一下。就让我们先放下敲代码的小手,一起看看刚刚写出来的都是些什么玩意儿吧。

系列文章

先说说内部版和开源版的 tRPC

首先要说明的是,腾讯内部使用的 tRPC 与开源版的 tRPC,虽然并不是 100% 相同,但大部份的代码和基本的功能是基本一致。官方对开源版的 PR 比较谨慎,在我提出的 PR 中维护者也提及了这一点,这为的就是尽量保持内部与外部版本的尽量一致性。

笔者虽然是腾讯员工,但并不是 tRPC 团队的开发者,而只是内部版和开源版双边的使用者。撰此系列文章,我的资料主要来源于以下这些:

  1. 对开源版的代码阅读
  2. 内部版和开源版逻辑一致的、可脱敏的资料
  3. 内部版本的一些使用经验和我们团队的经验
  4. 个人喜好和观点(当然我的个人观点多少也是会影响第 3 条的团队决策的哈哈)

所以,也还请读者不要将笔者视作 tRPC 官方,就当作是一名普通程序员就行了~~


从 proto 桩代码说起

业务代码与 trpc 服务的绑定

我们看看例子中的 service:

service HelloWorld {
  rpc Hello(HelloRequest) returns (HelloResponse); // @alias=/demo/Hello
}

平平无奇的一个 service,经过 trpc 工具编译后,生成了 echo.pb.goecho.trpc.go 两个文件。前者和使用 gRPC 的 proto-gen-go 工具生成的差别不大,我们就不用讲了。我们看看后面那个。

echo.trpc.go 文件中,trpc 工具生成了一个服务端接口:

type HelloWorldService interface {
    Hello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) // @alias=/demo/Hello
}

// ......

func RegisterHelloWorldService(s server.Service, svr HelloWorldService) {
    if err := s.Register(&HelloWorldServer_ServiceDesc, svr); err != nil {
        panic(fmt.Sprintf("HelloWorld register error:%v", err))
    }
}

很好理解,RegisterHelloWorldService 函数将 trpc 服务和我们的具体业务实现绑定在了一起,启动服务就对接上了业务逻辑。这跟 gRPC 的思路是一致的。

alias 关键字的作用

继续往下看,可以留意到下面这段代码:

var HelloWorldServer_ServiceDesc = server.ServiceDesc{
    ServiceName: "demo.simplest.HelloWorld",
    HandlerType: ((*HelloWorldService)(nil)),
    Methods: []server.Method{
        {
            Name: "/demo/Hello",
            Func: HelloWorldService_Hello_Handler,
        },
        {
            Name: "/demo.simplest.HelloWorld/Hello",
            Func: HelloWorldService_Hello_Handler,
        },
    },
}

这里其实就是前文 @alias 的作用了。显然,trpc 默认是使用 package/method 的格式定义一个接口方法的路径;而 alias 的作用则是在这基础上再额外注册了一个路径。上面的这两个路径,都可以直接通过 http 访问到。


trpc_go.yaml 配置

上文提到,trpc 服务启动需要搭配一个 yaml 配置文件。tRPC 的文档会告诉你默认使用与工作目录同级的 trpc_go.yaml 文件,但实际上考虑到在 Kubernetes 中挂载的需要,我们往往会将配置文件独立在一个目录下,而可执行文件在另一个目录下,再配合日志(也需要挂载,方便日志采集),这就构成了这样的一个结构:

  • 工作目录

    • bin/ - 可执行文件,包括服务程序和一些启动脚本
    • conf/ - 服务配置文件,主要就是 trpc_go.yaml
    • log/ - 日志文件。这个我后文再讲

由于配置文件与可执行文件或工作目录不在同一个路径下,因此我们启动服务的时候经常需要 -conf 参数制定配置文件的路径。

接下来,我们看一下配置文件中的内容:

server:
  service:
    - name: demo.simplest.HelloWorld
      nic: eth0
      # ip: 127.0.0.1
      port: 8000
      network: tcp
      protocol: http
      timeout: 1800

可以看明白的是:我们注册了一个叫做 demo.simplest.HelloWorld 的服务,监听端口 8000,服务工作在 tcp 协议上,应用层采用 http 协议,超时时间是 1800 毫秒。这几个参数我们都是需要好好说道说道的。

name

trpc 的文档中,并没有详细说明这个 name 应该取什么值。一般而言,这个 name 值等于你 proto 中定义的 package 名 + 服务名。

不过你不用记这个规则。实际上我们直接以生成的 trpc.go 文件为准。我们打开之前我们生成的 echo.trpc.go,搜索 Register,很快可以找打下面的代码段:

// RegisterHelloWorldService registers service.
func RegisterHelloWorldService(s server.Service, svr HelloWorldService) {
    if err := s.Register(&HelloWorldServer_ServiceDesc, svr); err != nil {
        panic(fmt.Sprintf("HelloWorld register error:%v", err))
    }
}

找到 s.Register 的第一个参数 HelloWorldServer_ServiceDesc 的定义,对,就是前面我们讲 alias 关键字的时候看到的那个结构体中的 ServiceName 字段,字段的值就是我们应该填在配置的 name 字段中的值。

此外, trpc 的官方文档会告诉你,如果当前进程仅定义了一个 service(这是绝大部分微服务的情况),那么实际上这个 service name 字段定义成什么都没关系,因为 trpc 会自动寻找这唯一的 service 配置,并且自动适配它。尽管如此,笔者依然不建议你因为有了这一条 feature,就随便写,咱们还是按规范的好。后面这个规范在作为客户端的时候也需要遵从,等用到了笔者再提吧。

nic、ip 和 port

ip 就是表示服务应该监听在哪一个 ip 上;而 nic 是 network interface card 的缩写,表示监听哪一个网卡。在生产环境中,应该是 nic 参数用得比较多,而在开发 / 测试的时候,当服务并不是部署在 Kubernetes 上的时候,ip 则提供了更大的自由度。

port 很好理解,表示监听的端口

network 和 protocol

网络类型参数就是 tcpudp 两类。两者能够支持的应用层协议不同。除非是极为极致的性能和高并发需求,否则我们一般还是固定使用 tcp。至于 protocol 参数,一般就是在 httptrpc 这两者之间选。当然也支持 grpc,读者可以试一下,笔者没有实际用过。

在前面的例子中,我们部署了一个 HTTP 服务,因此这里我们填写的是 http。如果填写 trpcgrpc,那么无需修改任何业务逻辑, 框架会自动字改为配置所指定的服务协议。

timeout

毫秒级的超时时间。这个参数会影响在 context 中的时间,如果配置了超时时间,trpc 框架在调用业务逻辑的时候,会给 context 加上这个 timeout。


tRPC 的 HTTP 服务模式

上一篇文章我们分别是用了两种模式来调用 Hello 方法,通过这个例子我们可以知道,trpc 服务框架会自动适配前端不同的调用方式、解析数据并调用业务逻辑。这一点对我们来说是非常舒服的,这让开发者们不用去关心业务无关的东西,一切都交给协议和框架解决。

我们对外提供的 HTTP 服务格式,常见的是以下三种:

  1. application/json: 前端可以使用 GET 或 POST 方式,在 body 中放置 JSON 数据作为入参
  2. application/proto: 与前面一样,不同的是 body 中是 protobuf 编码后的字节流
  3. 如果没有指定 body 格式,那么 trpc 也会尝试从 url query 参数中寻找协议所需的参数

总而言之,如果是定在最前面的 HTTP API,最好跟前端同学约定,一般情况下就使用 POST application/json 把,毕竟 URL query 遇到复杂数据类型比较麻烦,而 proto 前端不太处理了。


下一步

本文,我们简单介绍了上一篇文章中提到的 hello world 服务的各种参数。至此,我们开启了一个最简单的服务,对前端提供了最简单的响应。

然而,当我引入文中的这些概念之后,聪明的读者们肯定有更多的问题。而这些功能,也只不过是 tRPC-Go 众多功能的冰山一角上的一粒冰片。

接下来,我会带领大家开始拉起一个真正的微服务,构建一个完整的系统。我也会列出各种 trpc 服务仓库的目录组织形式,这个目录组织形式也方便对微服务进行单体化构建。与 trpc 官方给出的建议、和 trpc 工具自动生成的不同,这也就是为什么我不使用 trpc 工具的默认行为。

同时,trpc 的周边服务生态也是必不可少的一环,必然需要一并讲述。

此外,我也会详细说明我是如何在 tRPC-Go 中践行 微服务+单体 架构的,比起之前我文章中干巴巴的描述,上代码会来得更清楚。


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,原文发布于腾讯云开发者社区,也是本人的博客。欢迎转载,但请注明出处。

原文标题:《手把手 tRPC-Go 教学——(2)trpc HTTP 能力》

发布日期:2024-01-16

原文链接:https://cloud.tencent.com/developer/article/2379587

CC BY-NC-SA 4.0 DEED.png


amc
927 声望228 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发