📹 第六章 SOAP及gSOAP实战
📄 一句话概括

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 秒实验

  1. 把 n 改成 2000、m 也改成 2000,看本地那一次的时间还准不准(会抖,因为总量太小)。
  2. 把 JSON 换成更长的字符串(比如带 100 个字段),看每次耗时涨多少。
  3. 把服务端改成「收到就不回」,看客户端是不是卡在 recv 上直到超时。

常见误区

  1. 以为 RPC 是「更快的调用方式」。 它比本地调用慢三到五个数量级, 它解决的是「跨机器」,不是「快」。
  2. 以为本机回环测出来的时间可以代表网络。 回环不过网卡、不丢包、不排队, 真网络至少再叠加一个往返时延。
  3. 以为差距是「实现不够优化」。 其中一大块是光在光纤里的传播时间,任何软件优化都动不了它。
自测三问(点开对答案) 1. 本地调用和远程调用,本质差在哪一步? —— 差在**参数和返回值要变成字节流**,以及**中间多了一次网络往返**。其他步骤都是这两件事的衍生。 2. 实测差 17038 倍,为什么还说 RPC 有价值? —— 因为它做到的是**本地调用根本做不到的事**:在另一台机器上执行。 **这是「能不能」的问题,不是「快不快」的问题。** 3. SOAP 和 gRPC 都是 RPC,它们的主要区别是什么? —— **序列化格式和约定文件**。SOAP 用 XML + WSDL,gRPC 用 Protobuf + `.proto`。 **四要素(格式、方法名、参数结构、错误码)两边都有。**

下一节看那层「跑 RPC 的传输层」——HTTP,用手写 socket 的方式看清它的骨架。

相关笔记