1. 应用

使用.thrift可以实现功能接口的跨语言调用,客户端可以通过连接RPCclient实现调用服务端的实现接口
以Go为服务端,Python为客户端

1. thrift:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
service DevelopManagementService {
// Create a work ticket for deployment
CreateTicketResponse CreateTicket(1: CreateTicketRequest req)
}

// Ticket API Begin
struct CreateTicketRequest {
1: required string Branch // RD - Git DevBranch
2: required string EntityURI
3: required bool Hotfix
4: required string Operator
5: optional string DraftID
6: optional i64 Flow


255: optional base.Base Base
}

struct CreateTicketResponse {
1: required i64 TicketID // work ticket id (auto_generated by system)
255: optional base.BaseResp BaseResp
}

2. 服务端Go接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package release_handler

import (
"context"

"code.byted.org/tiktok-tns-eng/tns_ecosystem/kitex_gen/tns/ecosystem/pc"
"code.byted.org/tiktok-tns-eng/tns_ecosystem/kitex_gen/tns/ecosystem/ubom"
releaseService "code.byted.org/tiktok-tns-eng/tns_ecosystem/services/pc/domain/release_service"
"code.byted.org/tiktok-tns-eng/tns_ecosystem/services/pc/handler"
"code.byted.org/tiktok-tns-eng/tns_ecosystem/shared/pkg/errors"
)

type CreateTicket struct {
releaseService *releaseService.ReleaseServiceImpl
}

func NewCreateTicket(releaseService *releaseService.ReleaseServiceImpl) handler.IHandler[*pc.CreateTicketRequest, *pc.CreateTicketResponse] {
return &CreateTicket{
releaseService: releaseService,
}
}

func (h *CreateTicket) CheckParam(ctx context.Context, req *pc.CreateTicketRequest) error {
if req == nil {
return errors.New(errors.ParameterErr, "req is empty")
}

if !IsValidTicketType(req.Type) {
return errors.New(errors.ParameterErr, "Type not Valid")
}
if req.DraftId == 0 {
return errors.New(errors.ParameterErr, "DraftId is empty")
}
if req.Operator == "" {
return errors.New(errors.ParameterErr, "Operator is empty")
}
return nil
}

func (h *CreateTicket) CheckPermission(ctx context.Context, req *pc.CreateTicketRequest) error {
return nil
}

func (h *CreateTicket) Handle(ctx context.Context, req *pc.CreateTicketRequest, resp *pc.CreateTicketResponse) error {
// userInfo := utils.GetUserInfoFromCtx(ctx)
// operator := userInfo.UserName
var newReq = &ubom.CreateTicketReq{
Type: ubom.TicketType(req.Type),
Operator: req.Operator,
DraftID: req.DraftId,
DeploymentType: (*ubom.DeploymentType)(req.DeploymentType),
}
newResp, err := h.releaseService.CreateTicketUbom(ctx, newReq)
if newResp != nil {
resp.TicketId = newResp.TicketID
}
return err
}

3. 客户端python调用

先通过编译thrift代码,生成一个类似注册的东西,后连接RPCClient,之后封装好请求的request包通过client.createTicket(request)实现调用,


2. Thrift 在客户端和服务端中间具体连接方法

1. IDL 阶段

编写 ticket.thrift:

1
2
3
service TicketService {
string createTicket(1:string title, 2:string content)
}

编译后得到:

  • 客户端 stub(比如 TicketService.Client)
  • 服务端 skeleton(比如 TicketService.Iface + TicketService.Processor)

2. 调用链路(一次 client → server 调用)

客户端(Python 调用 createTicket):
res = client.createTicket(“bug”, “xxx 出错了”)
这一行背后发生了:

  • 本地代理方法被调用
    TicketService.Client.createTicket 不是你写的,而是 Thrift 自动生成的。
    它会把函数名 createTicket 和参数 (“bug”, “xxx 出错了”) 交给 Thrift runtime。
  • 序列化(Serialization)
    • Thrift 用协议(Protocol,比如 Binary/Compact/JSON)把参数转成字节流。
    • 格式化内容大概是:
    • method: “createTicket”
      seqid: 1
      args: [ “bug”, “xxx 出错了” ]
  • 传输(Transport)
    • 通过 Transport(如 TCP Socket、HTTP、内存 buffer)把字节流发给服务端。

服务端(Go 收到请求):

  • 监听 socket 收到字节流
    • Thrift 服务端 Processor 解包,读到方法名 createTicket。
  • 反序列化(Deserialization)
    • 把字节流解码回 Go 的参数类型:title = “bug”, content = “xxx 出错了”。
  • 调用逻辑
    • Processor 找到 Go 里实现的 createTicket 方法,并执行:
    • func (s *TicketServiceHandler) CreateTicket(title string, content string) (string, error) {
      return “TicketID-12345”, nil
      }
  • 结果序列化
    • 把返回值 “TicketID-12345” 编码成字节流。
  • 返回传输
    • 把结果通过 Transport 写回客户端。

客户端(Python 收到响应):

  • 读取返回字节流
    • TicketService.Client 内部等待响应。
  • 反序列化结果
    • 把字节流转成 Python 字符串 “TicketID-12345”。
  • 返回给调用者
    • 最后赋值到 res,拿到的就是个普通的 Python 字符串。

3. 整体过程图(简化版)

Python Client 网络传输(TCP) Go Server| 调用 client.createTicket() |
| → 序列化(args) |
| → 发送字节流 ————————> | 接收字节流
| | → 反序列化(args)| | → 调用 handler.createTicket()| | → 序列化(result)| ← 返回字节流 ————————- |
| ← 反序列化(result)| 返回 Python 对象


4. 关键点

  • Stub(客户端代理类):帮你屏蔽网络细节,看起来像本地调用。
  • Processor(服务端调度器):根据方法名找到实现的逻辑并执行。
  • Protocol & Transport:保证数据能被跨语言理解和传输。

3. 全过程和原理

RPC即远程过程调用,就是本机通过序列化、网络通信等手段调用远程服务的某个方法,并获取到对应的响应,一般来说,RPC底层包括了代码生成、序列化、网络通信等,主流的微服务框架也会提供服务治理相关的能力

调用的原理:

  1. Client构造请求参数,发起调用
  2. Client通过服务发现、负载均衡等的带服务器实例地址,并建立链接
  3. Client将请求序列化成二进制数据,通过网络将数据发送给服务端
  4. Server接收数据并反序列化出请求参数
  5. Server Handler处理请求并将返回结果序列化为二进制数据
  6. Server通过网络将数据返回给Client
  7. Client接受数据并反序列化,得到调用结果
    其中步骤二包含的流程为【服务治理】,包括但不限于服务发现、负载均衡、ACL、熔断等功能

开发流程

  1. 编写IDL, 定义service接口
  2. 使用thrift生成客户端、服务端的支持代码
  3. 服务端开发者编写handler,即请求的处理逻辑
  4. 服务端开发者运行服务, 监听对应窗口、处理请求
  5. 客户端开发者编写客户端程序, 经过服务发现链接上服务端程序,发起请求并响应请求

4. 为什么有HTTP协议,还要使用RPC

HTTP 和 RPC 的定位不同

  • HTTP:应用层通用协议,强调的是“资源”导向(RESTful),核心是:

    • URL 定位资源

    • 方法(GET/POST/PUT/DELETE)表示操作

    • 报文体用 JSON、XML 等通用格式,方便跨语言和跨平台

  • RPC(Remote Procedure Call):远程过程调用,强调的是“像调用本地函数一样调用远程服务”,核心是:

    • 你只关心“调用哪个函数、传什么参数”,而不关心报文结构

    • 使用高效的二进制序列化(例如 Protobuf、Thrift、MessagePack)

    • 对开发者屏蔽了通信细节,提供方法级别的调用抽象


为什么 HTTP 不够?

HTTP 本身是基于文本的请求/响应模式,虽然通用,但有几个缺点:

  1. 性能开销大

    • HTTP 头冗余(User-Agent、Cookie、Cache-Control …),业务真正需要的部分往往很少。

    • JSON 序列化/反序列化慢、体积大(数字、布尔值都要转成字符串)。

    • 对高并发场景,浪费网络和 CPU。

  2. 表达能力不足

    • HTTP API 是“资源 + 动作”抽象,但有时我们希望的是“调用函数”,带多个复杂参数,返回复杂对象。

    • 比如 transfer(from, to, amount),如果用 HTTP,可能要自己定义一个 POST body schema。

  3. 协议特性有限

    • HTTP/1.1 是短连接+队头阻塞(虽然有 Keep-Alive,但仍有限制)。

    • HTTP/2/3 改进了多路复用,但应用层序列化仍然是开发者要处理的。

    • RPC 框架一般内置负载均衡、重试、超时控制、服务发现等功能。


RPC 的优势

  1. 高性能

    • 使用二进制协议(Protobuf、Thrift、gRPC 默认 Protobuf),序列化速度快,数据量小。

    • 自定义 TCP 长连接,减少握手开销。

  2. 透明性

    • 写代码像调本地函数一样:UserService.getUser(id)

    • 不需要自己拼 URL、写 JSON。

  3. 功能丰富(很多 RPC 框架内置)

    • 服务注册与发现(结合 Consul、Etcd、Nacos)

    • 负载均衡

    • 超时、重试、熔断、限流

    • Streaming 调用(双向流式通信,HTTP1.1 不支持,HTTP/2 才支持)


实际应用场景

  • 互联网公司内部微服务:更偏向 RPC(gRPC、Dubbo、Thrift),因为追求性能和方法级别抽象。

  • 对外 API:更偏向 HTTP/RESTful 或 GraphQL,因为通用、跨语言、学习成本低。

  • 混合:很多 RPC 框架底层也用 HTTP/2,比如 gRPC 就是跑在 HTTP/2 上的。


  • HTTP:强调通用性、跨平台、易接入(适合开放 API)。

  • RPC:强调高性能、服务内调用、开发体验(适合微服务内部通信)。

所以不是“有了 HTTP 为什么还要 RPC”,而是它们应用场景不同。
RPC 更像是“在 HTTP/TCP 之上进一步封装,让调用远程方法像本地方法一样方便,还顺带提升性能”