RPC 的野心是「让远程调用看起来像本地调用」,代价是把它从「跳一下」变成「打包、发送、等待、 拆包」。 本节实测这两个动作差了三万多倍,然后说明为什么这个数字不影响 RPC 的价值。
这一章讲 SOAP。SOAP 是 RPC 的一种实现,所以先得知道 RPC 在解决什么问题。 ONVIF 的每一次「读设备信息」「改分辨率」,本质都是一次 RPC 调用。
一、本地调用和远程调用,差在哪几步
本地函数调用在汇编层面就是一条 call 指令:把返回地址压栈,跳到目标地址,
执行完再跳回来。整个过程在同一个进程的地址空间里完成,谁也不用问谁。
| 步骤 | 本地调用 | 远程调用 |
|---|---|---|---:|
| ① 准备参数 | 压寄存器/压栈 | 序列化成字节能传输的形式 |
| ② 传递 | call 指令 | 通过 socket 发出去,等对端收完 |
| ③ 执行 | 直接跑 | 对端反序列化,再跑 |
| ④ 取结果 | 读返回值寄存器 | 对端序列化结果,再发一遍回来 |
| ⑤ 出错 | 返回错误码 | 错误也得打包成报文传回来 |
五步里有四步是本地调用根本没有的,而且③④之间还夹着一段谁也无法消除的网络往返时间。 所以「看起来像本地调用」这句话,只在代码写法上成立,在时间上从来不成立。
二、实测:同一件事,差了三万多倍
Demo 做的是同一件事——把两个整数加起来,一次走本地函数,一次走本机的 JSON-RPC。
| 方式 | 次数 | 总耗时 | 每次 |
|---|---|---|---|
| 本地函数调用 | 200000 | 20.2 毫秒 | 0.101 微秒 |
| JSON-RPC(回环) | 2000 | 3448.5 毫秒 | 1724 微秒 |
每次差 17038 倍。 而且这已经是最乐观的情况:回环网络不出网卡、不过交换机、不丢包、不排队。 真走网线还要加上往返时延:同城专线 1 毫秒,就是本地调用的一万倍; 跨省 30 毫秒,是三十万倍。 这个差距不是优化能缩小的,因为其中大部分是光在光纤里跑的时间(每 200 公里约 1 毫秒)。
三、那 RPC 靠什么活下来
因为「跨机器」这件事,本地调用根本做不到。 RPC 卖的不是速度,是「把一次调用搬到另一台机器上」。
| 要解决的问题 | 手段 | 在 ONVIF 里对应什么 |
|---|---|---|
| 两边语言不同 | 用文本格式(XML/JSON) | XML 信封,C 和 C# 都能解 |
| 调用哪个函数 | 请求里带方法名 | GetDeviceInformation 这个元素名 |
| 参数怎么传 | 按约定的结构序列化 | WSDL 里的 types 段 |
| 出错怎么办 | 回包里带错误码 | Fault 元素(06.01.05) |
这四行就是所有 RPC 框架的骨架。
SOAP 只是把「文本格式」选了 XML,并且把「约定」写成了 WSDL 文件(06.01.04)。
换成 gRPC,四行一样成立:格式换成 Protobuf,约定换成 .proto 文件。
四、三代 RPC,一次比一次瘦
| 年代 | 代表 | 格式 | 约定在哪 |
|---|---|---|---|
| 1998 | XML-RPC | XML | 口头约定 |
| 2000 | SOAP/WSDL | XML + 信封 | WSDL 文件 |
| 2015 | gRPC | Protobuf(二进制) | .proto 文件 |
这条演进线只干一件事:减少「包装」的字节数。 XML-RPC 和 SOAP 的差别只是信封更规范,体积反而更大(06.01.06 有实测)。 真正的瘦身发生在 gRPC:Protobuf 是二进制,同样的字段名只传一次(靠编号), 比 XML 小五到十倍。 但监控行业停在第二代——ONVIF 2008 年定下来,存量设备全是 SOAP,改不动了。
五、动手 Demo:把一次函数调用变成一条消息
Demo 用同一个「两数相加」做两遍:一遍直接调函数,一遍包成 JSON-RPC 发给自己。 服务端是本机起的迷你 HTTP 服务,走真实的 socket 回环。 所以量到的是「序列化 + HTTP + 回环」的总开销。
# ① 本地:一条 call 指令的事
for i in range(200000):
acc = add(acc, 1)
# ② 远程:序列化 → 发 HTTP → 等回包 → 反序列化
for i in range(2000):
body = json.dumps({"jsonrpc": "2.0", "id": i, "method": "add",
"params": [i, 1]})
r = _http_post(port, "/rpc", [("Content-Type", "application/json"),
("Content-Length", str(len(body)))], body)
json.loads(r.split("\r\n\r\n", 1)[1])
实跑输出:
--------------------------------------------------------------------
把一次函数调用变成一条消息
--------------------------------------------------------------------
**RPC(远程过程调用)的野心是:让远程调用看起来像本地调用。**
这一节量一下「看起来像」的代价。
**同一件事:把两个数加起来。**
方式 次数 总耗时 每次
----------------------------------------------------------
本地函数调用 200000 19.6 毫秒 0.098 微秒
JSON-RPC(回环) 2000 5805.0 毫秒 2902 微秒
**每次差 29658 倍左右**,而且这还是**本机回环**——真走网线,还要加上往返时延。
**这就是 RPC 的代价:把「跳转」变成了「序列化 + 发字节 + 等回包 + 反序列化」。**
**所以 RPC 的设计目标从来不是「快」,是「跨机器」**:
要解决的问题 手段
------------------------------------------------------
两边语言不同 用文本格式(XML/JSON)
调用哪个函数 请求里带方法名
参数怎么传 按约定的结构序列化
出错怎么办 回包里带错误码
**这四行就是所有 RPC 框架的骨架**,SOAP 只是把「文本格式」选成了 XML,
**并且把「约定」写成了 WSDL 文件。**
**看一眼刚才那条请求长什么样:**
POST /rpc HTTP/1.1
Host: 127.0.0.1:64265
Content-Type: application/json
Content-Length: 68
{"jsonrpc": "2.0", "id": 1999, "method": "add", "params": [1999, 1]}
**第一行是「方法 + 地址 + 版本」,后面是头,空行之后是体。**
**这个格式就是 HTTP,下一节讲它。**
两次的样本量不一样(20 万 vs 2000),因为远程那一次太慢:2000 次已经跑了 3.4 秒, 跑 20 万次得六分钟。 这种量级差别下,样本量不影响结论——差的是三个数量级,不是百分之几。 如果要更公平,本地那次应该跑 2000 次并扣掉循环本身的空转开销, 但那样反而更不准(2000 次的计时会被系统时钟精度吃掉)。
30 秒实验
- 把
n改成 2000、m也改成 2000,看本地那一次的时间还准不准(会抖,因为总量太小)。 - 把 JSON 换成更长的字符串(比如带 100 个字段),看每次耗时涨多少。
- 把服务端改成「收到就不回」,看客户端是不是卡在
recv上直到超时。
常见误区
- 以为 RPC 是「更快的调用方式」。 它比本地调用慢三到五个数量级, 它解决的是「跨机器」,不是「快」。
- 以为本机回环测出来的时间可以代表网络。 回环不过网卡、不丢包、不排队, 真网络至少再叠加一个往返时延。
- 以为差距是「实现不够优化」。 其中一大块是光在光纤里的传播时间,任何软件优化都动不了它。
自测三问(点开对答案)
1. 本地调用和远程调用,本质差在哪一步? —— 差在**参数和返回值要变成字节流**,以及**中间多了一次网络往返**。其他步骤都是这两件事的衍生。 2. 实测差 17038 倍,为什么还说 RPC 有价值? —— 因为它做到的是**本地调用根本做不到的事**:在另一台机器上执行。 **这是「能不能」的问题,不是「快不快」的问题。** 3. SOAP 和 gRPC 都是 RPC,它们的主要区别是什么? —— **序列化格式和约定文件**。SOAP 用 XML + WSDL,gRPC 用 Protobuf + `.proto`。 **四要素(格式、方法名、参数结构、错误码)两边都有。**下一节看那层「跑 RPC 的传输层」——HTTP,用手写 socket 的方式看清它的骨架。
- ☐ (本节完,下一节 06.01.02 HTTP简介)