RPC
1. 应用
使用.thrift可以实现功能接口的跨语言调用,客户端可以通过连接RPCclient实现调用服务端的实现接口
以Go为服务端,Python为客户端
1. thrift:
1 | service DevelopManagementService { |
2. 服务端Go接口:
1 | package release_handler |
3. 客户端python调用
先通过编译thrift代码,生成一个类似注册的东西,后连接RPCClient,之后封装好请求的request包通过client.createTicket(request)实现调用,
2. Thrift 在客户端和服务端中间具体连接方法
1. IDL 阶段
编写 ticket.thrift:1
2
3service 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底层包括了代码生成、序列化、网络通信等,主流的微服务框架也会提供服务治理相关的能力
调用的原理:
- Client构造请求参数,发起调用
- Client通过服务发现、负载均衡等的带服务器实例地址,并建立链接
- Client将请求序列化成二进制数据,通过网络将数据发送给服务端
- Server接收数据并反序列化出请求参数
- Server Handler处理请求并将返回结果序列化为二进制数据
- Server通过网络将数据返回给Client
- Client接受数据并反序列化,得到调用结果
其中步骤二包含的流程为【服务治理】,包括但不限于服务发现、负载均衡、ACL、熔断等功能
开发流程
- 编写IDL, 定义service接口
- 使用thrift生成客户端、服务端的支持代码
- 服务端开发者编写handler,即请求的处理逻辑
- 服务端开发者运行服务, 监听对应窗口、处理请求
- 客户端开发者编写客户端程序, 经过服务发现链接上服务端程序,发起请求并响应请求
4. 为什么有HTTP协议,还要使用RPC
HTTP 和 RPC 的定位不同
HTTP:应用层通用协议,强调的是“资源”导向(RESTful),核心是:
URL 定位资源
方法(GET/POST/PUT/DELETE)表示操作
报文体用 JSON、XML 等通用格式,方便跨语言和跨平台
RPC(Remote Procedure Call):远程过程调用,强调的是“像调用本地函数一样调用远程服务”,核心是:
你只关心“调用哪个函数、传什么参数”,而不关心报文结构
使用高效的二进制序列化(例如 Protobuf、Thrift、MessagePack)
对开发者屏蔽了通信细节,提供方法级别的调用抽象
为什么 HTTP 不够?
HTTP 本身是基于文本的请求/响应模式,虽然通用,但有几个缺点:
性能开销大
HTTP 头冗余(User-Agent、Cookie、Cache-Control …),业务真正需要的部分往往很少。
JSON 序列化/反序列化慢、体积大(数字、布尔值都要转成字符串)。
对高并发场景,浪费网络和 CPU。
表达能力不足
HTTP API 是“资源 + 动作”抽象,但有时我们希望的是“调用函数”,带多个复杂参数,返回复杂对象。
比如
transfer(from, to, amount),如果用 HTTP,可能要自己定义一个 POST body schema。
协议特性有限
HTTP/1.1 是短连接+队头阻塞(虽然有 Keep-Alive,但仍有限制)。
HTTP/2/3 改进了多路复用,但应用层序列化仍然是开发者要处理的。
RPC 框架一般内置负载均衡、重试、超时控制、服务发现等功能。
RPC 的优势
高性能
使用二进制协议(Protobuf、Thrift、gRPC 默认 Protobuf),序列化速度快,数据量小。
自定义 TCP 长连接,减少握手开销。
透明性
写代码像调本地函数一样:
UserService.getUser(id)不需要自己拼 URL、写 JSON。
功能丰富(很多 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 之上进一步封装,让调用远程方法像本地方法一样方便,还顺带提升性能”
