Skip to main content

Command Palette

Search for a command to run...

Rpc 理论基础

Updated
•2 min read•View as Markdown
  • 为什么需要使用RPC,减少我们自己写网络编程的复杂逻辑,让我们感受不到底层的网络甚至设备的转换

  • 使用RPC可以自然的进行多语言混搭,这样就不需要比如一个程序多次编写,或是在进行编程语言的转换的时候,会进行语言的转换,丢失掉一部分性能,而RPC就可以避免这些情况

什么是拆包 粘包问题

这是一个非常经典的网络编程问题!如果把 TCP 协议比作“快递运输”,那这个问题就好比快递公司为了省事,把你的东西乱装一通。

我们在图片中看到的红线部分说:“TCP是无边界的字节流形式”,这句话是罪魁祸首。

让我用最通俗的**“喝水”和“快递”**的例子来解释什么是 粘包(Stickiness) 和 拆包(Splitting/Fragmentation)。


1. 根源:TCP 是“水流”,不是“包裹”

UDP 协议像是寄信:你寄一封,对面收一封。界限很清楚。 TCP 协议像是接水管:你在这头倒水,水流到那头。

  • 你倒了三杯水(发了三个数据包)。

  • 但在管道里,这三杯水可能汇成了一股水流。

  • 对面接水时,他根本不知道你原来是分三次倒的,还是一次倒的。

这就是所谓的**“面向字节流”**(无边界)。


2. 什么是“粘包”?(黏在一起了)

场景: 你要给服务器发送两条指令:

  1. "Open" (打开)

  2. "Close" (关闭)

发生粘包: 因为这两个指令很短,发送得又很快,TCP 为了效率(就像攒够一车快递再发车),把它俩合并成了一个包发送出去了。

接收端看到的是: "OpenClose"

后果: 服务器傻了。它去查字典,发现没有叫 "OpenClose" 的指令,于是报错。它根本不知道应该在 n 和 C 之间切开。


3. 什么是“拆包”?(被打散了)

场景: 你要给服务器发送一篇 5000 字的长篇小说。

发生拆包: TCP 的卡车(MTU,最大传输单元)一次只能拉 1500 个字。或者接收端的缓冲区(仓库)满了,装不下这么多。 于是,TCP 只能把这 5000 字切碎,分几次发过去。

接收端看到的是: 第一次收到:"从前有座山,山里有..." (只有一半) 第二次收到:"...个庙,庙里有个老和尚。" (剩下的)

后果: 如果服务器只收到了前半段就急着处理,程序就会崩溃,因为它读到的数据是不完整的。


4. 怎么解决?(RPC 框架必须做的事)

既然 TCP 这么“笨”,分不清界限,那**应用层(也就是写 RPC 框架的人)**必须自己想办法。

最通用的解决办法就是:“消息头 + 消息体” (Length-Field Based Frame)

这就好比我们在每段水流前,先放一块牌子,写上“接下来的水有 500 毫升”。

解决流程:

  1. 发送端(你): 在发送真实数据(ProtoBuf 序列化后的二进制)之前,先发一个固定的 4 字节整数,代表数据的长度。

    • 格式变成:[长度:4][Open] ... [长度:5][Close]
  2. 接收端(服务器):

    • 先读 4 个字节。解析出来是数字 4。

    • 心里有数了:好,接下来我必须读够 4 个字节,才算读完这一个包。

    • 读完这 4 个字节后,剩下的不管还有没有数据,我都先停下来,处理这一个完整的包。

总结

  • 粘包:发送方发得太快,两条消息粘成一条了(AB 变成了 AB)。

  • 拆包:发送方发得太大,一条消息被切成两半了(A 变成了 A1 + A2)。

  • 为什么图片里说要自己处理? 因为 TCP 只要保证数据到了就行,不管数据长啥样。RPC 框架必须利用“长度头”或者“特殊分隔符”来把这股水流重新切分成一个个正确的请求。

这就是为什么你在看 ProtoBuf 或 Muduo 源码时,总能看到类似 Buffer、readInt32、peek 这种操作,它们就是在干“切分水流”这个脏活累活。

什么是代理模式?

既然你刚刚学了 RPC,那理解 代理模式 (Proxy Pattern) 简直是易如反掌。

实际上,RPC 就是代理模式最经典的顶级应用。

用一句话概括:代理模式就是给某个对象找一个“经纪人”或“秘书”。 你不直接找本人,而是找这个经纪人,由经纪人来决定要不要把你的请求转告给本人,或者在转告前后做点手脚。


1. 通俗类比:找明星签名

假设你想找周杰伦(RealSubject,真实对象)签个名。

  • 没有代理模式:你直接冲进周杰伦家里找他。

    • 风险:周杰伦可能在洗澡,可能在美国,或者根本不想理你。你直接访问非常不安全且低效。
  • 有代理模式:你只能联系周杰伦的经纪人(Proxy,代理对象)。

    • 你:“我想找杰伦签名。”

    • 经纪人(也就是代理):

      1. 权限控制:先看看你是不是私生饭?(安全检查)

      2. 前置处理:确认杰伦现在有没有空。(状态检查)

      3. 转发请求:如果一切OK,经纪人把照片递给周杰伦,周杰伦签好。

      4. 后置处理:经纪人把签好名的照片包装一下,寄给你。(结果封装)

对你(客户端)来说,你感觉你在和周杰伦交互,但实际上你是和经纪人交互。


2. 为什么要搞个“中间商”?(核心作用)

在编程里,我们直接 new 一个对象调用不行吗?非要搞个代理? 通常有以下几种情况,必须用代理:

A. 远程代理 (Remote Proxy) —— 就是 RPC!

  • 场景:对象根本不在你的电脑上,而在美国的服务器上。

  • 代理的作用:

    • 你在本地调用的 UserService 其实只是一个空壳(Stub)。

    • 这个空壳(代理)负责把你的参数打包(ProtoBuf),发网络请求,等结果回来。

    • 你以为你在调本地函数,其实代理帮你跑了一趟美国。

B. 虚拟代理 (Virtual Proxy) —— 省资源/懒加载

  • 场景:你要加载一个巨大的高清图片(1GB),或者创建一个极其消耗内存的数据库对象。

  • 代理的作用:

    • 先给你一个“占位符”(Proxy)。网页上先显示一个“Loading...”的小图标。

    • 只有当你真的滚动屏幕看到这张图时,代理才真正去加载那个 1GB 的数据。

    • 核心:“非必要不初始化”,为了性能。

C. 保护代理 (Protection Proxy) —— 保安

  • 场景:你是普通用户,不能删除数据库。

  • 代理的作用:

    • 代理检查你的身份(Token)。

    • 如果是管理员,放行,调用真实对象的删除方法。

    • 如果是普通用户,直接驳回,根本不打扰真实对象。


3. 代码长啥样? (C++ 伪代码)

代理模式的精髓在于:代理类和真实类,继承同一个接口(基类)。 这样客户端根本分不清谁是谁。

// 1. 接口:大家都要遵守的契约
class ISubject {
public:
    virtual void Request() = 0;
};

// 2. 真实对象:周杰伦本人
class RealJayChou : public ISubject {
public:
    void Request() override {
        cout << "周杰伦:哎哟,不错哦,给你签个名。" << endl;
    }
};

// 3. 代理对象:经纪人
class AgentProxy : public ISubject {
private:
    RealJayChou* jay; // 经纪人手里得有周杰伦的联系方式

public:
    AgentProxy() { jay = new RealJayChou(); }

    void Request() override {
        // --- 代理的“私货”开始 ---
        cout << "[经纪人]:先检查一下粉丝有没有买票..." << endl;
        if (!FullPayment()) {
            cout << "[经纪人]:钱没给够,不让见。" << endl;
            return;
        }
        // --- 代理的“私货”结束 ---

        // 一切正常,才让杰伦干活
        jay->Request(); 

        cout << "[经纪人]:签名拿好,慢走不送。" << endl;
    }

    bool FullPayment() { return true; }
};

// 4. 客户端:粉丝
int main() {
    // 粉丝以为自己在找杰伦,其实找的是经纪人
    ISubject* star = new AgentProxy();
    star->Request(); 
}

4. 总结

  • 定义:给某一个对象提供一个代理,用来控制对这个对象的访问。

  • 如果你在学 Muduo 和 RPC:

    • 你本地调用的那个 Stub 就是 Proxy。

    • Nginx 这种反向代理,虽然是架构层面的,但思想也是 Proxy(帮后端服务器挡流量、做缓存)。

    • 智能指针 std::shared_ptr 其实也是一种特殊的 Proxy(它代理了裸指针,帮你管理内存引用计数)。

现在再回看 RPC 的“本地发起远程调用”,是不是瞬间明白了?那就是代理模式在帮你“伪装”成本地调用的样子。

More from this blog

Redis学习

1.什么是SSD HDD作为存储传统的数据库? SSD:固态硬盘---使用闪存芯片存储数据 无机械部件 特点:读写速度快(尤其是随机读写)、延迟低、抗震性好,但单位容量成本较高。 HDD:机械硬盘--传统机械硬盘 特点:容量大、成本低,但读写速度较慢(尤其是随机读写),延迟高。 2.什么是非易失性持久化 非易失性持久化 = 靠谱的硬件 (断电不丢数据的 SSD/PMEM) + 严谨的软件策略 (确

Mar 4, 20261 min read

分布式存储系统面试问题解析

共识算法剖析 raft共识算法 1.在你的 C++ 实现中,如果 Leader 发送 AppendEntries 给 Follower,但 Follower 的日志落后太多(比如中间缺了 100 条),Raft 论文建议是一个一个往前找(nextIndex--)。这种做法在高性能存储中显然是不可接受的,你会如何优化这个“回溯”过程? 在工程实现中,我会从两个维度优化日志冲突后的同步效率: 1. 快

Feb 9, 202613 min read

Unix环境编程:杂谈

动态加载 API 进程管理 什么是进程 进程的创建 进程的终止与遗言函数 进程资源回收 进程的映像更新 ps -o pid,ppid,pgrp,comm 这个linux命令是将当前终端上运行的进程打印出来 进程中使用环境变量 子进程环境变量改变 不会更改父进程的环境变量 进程间通信 有名管道不存储数据 其本身大小为 0 当一个进程写入数据进去时 如果没有进程读取数据那么该写进程就会堵塞在哪里 同理 如果读进程想要读取数据 但是却没有数据...

Jan 30, 20263 min read

Unix环境编程:第一章

1.1操作系统的介绍 1.2 计算机系统的分层 1.3 编译过程 ?上一个命令的运行结果 echo 就是输出上一个命令的运行结果 哎linux环境下文件中查找一个元素 即在命令模式下 输入/+你要查找的字符 即/printf 这样 然后找到之后 n表示查找下一条 shift+n表示上一条 file+文件名 查=查看文件是否可运行 1.4 多模块开发 变量的定义是为变量扩充作用域 而变量的声明需要为变量内存空间 gcc -c 文件 检验是否存在语法错误 nm 目标文...

Jan 30, 20261 min read

C++ Stl杂项

迭代器特性 输入流的容器特性 迭代器对算法的影响 仿函数和函数对象 分配器 对于平常的我们内存的使用,直接使用new delete 即可 使用分配器的话还需要记住自己当时申请了多少的内存 很不方便 哈希表深度探索 哈希容器 哈希表在扩充时,会先将自身大小乘以2 然后在选择自身2倍距离最近的一个质数 作为新的大小 哈希函数 以哈希表为底层支撑的适配器 红黑树 容器 list容器 forward_list 对于标准库,其本身提供一个sort排序函数,但某些容器也提供sort服务...

Jan 29, 20261 min read

xianyu-sheng

29 posts