Rpc 理论基础
为什么需要使用RPC,减少我们自己写网络编程的复杂逻辑,让我们感受不到底层的网络甚至设备的转换
使用RPC可以自然的进行多语言混搭,这样就不需要比如一个程序多次编写,或是在进行编程语言的转换的时候,会进行语言的转换,丢失掉一部分性能,而RPC就可以避免这些情况
什么是拆包 粘包问题
这是一个非常经典的网络编程问题!如果把 TCP 协议比作“快递运输”,那这个问题就好比快递公司为了省事,把你的东西乱装一通。
我们在图片中看到的红线部分说:“TCP是无边界的字节流形式”,这句话是罪魁祸首。
让我用最通俗的**“喝水”和“快递”**的例子来解释什么是 粘包(Stickiness) 和 拆包(Splitting/Fragmentation)。
1. 根源:TCP 是“水流”,不是“包裹”
UDP 协议像是寄信:你寄一封,对面收一封。界限很清楚。 TCP 协议像是接水管:你在这头倒水,水流到那头。
你倒了三杯水(发了三个数据包)。
但在管道里,这三杯水可能汇成了一股水流。
对面接水时,他根本不知道你原来是分三次倒的,还是一次倒的。
这就是所谓的**“面向字节流”**(无边界)。
2. 什么是“粘包”?(黏在一起了)
场景: 你要给服务器发送两条指令:
"Open"(打开)"Close"(关闭)
发生粘包: 因为这两个指令很短,发送得又很快,TCP 为了效率(就像攒够一车快递再发车),把它俩合并成了一个包发送出去了。
接收端看到的是: "OpenClose"
后果: 服务器傻了。它去查字典,发现没有叫 "OpenClose" 的指令,于是报错。它根本不知道应该在 n 和 C 之间切开。
3. 什么是“拆包”?(被打散了)
场景: 你要给服务器发送一篇 5000 字的长篇小说。
发生拆包: TCP 的卡车(MTU,最大传输单元)一次只能拉 1500 个字。或者接收端的缓冲区(仓库)满了,装不下这么多。 于是,TCP 只能把这 5000 字切碎,分几次发过去。
接收端看到的是: 第一次收到:"从前有座山,山里有..." (只有一半) 第二次收到:"...个庙,庙里有个老和尚。" (剩下的)
后果: 如果服务器只收到了前半段就急着处理,程序就会崩溃,因为它读到的数据是不完整的。
4. 怎么解决?(RPC 框架必须做的事)
既然 TCP 这么“笨”,分不清界限,那**应用层(也就是写 RPC 框架的人)**必须自己想办法。
最通用的解决办法就是:“消息头 + 消息体” (Length-Field Based Frame)
这就好比我们在每段水流前,先放一块牌子,写上“接下来的水有 500 毫升”。
解决流程:
发送端(你): 在发送真实数据(ProtoBuf 序列化后的二进制)之前,先发一个固定的 4 字节整数,代表数据的长度。
- 格式变成:
[长度:4][Open]...[长度:5][Close]
- 格式变成:
接收端(服务器):
先读 4 个字节。解析出来是数字
4。心里有数了:好,接下来我必须读够
4个字节,才算读完这一个包。读完这 4 个字节后,剩下的不管还有没有数据,我都先停下来,处理这一个完整的包。
总结
粘包:发送方发得太快,两条消息粘成一条了(
AB变成了AB)。拆包:发送方发得太大,一条消息被切成两半了(
A变成了A1+A2)。为什么图片里说要自己处理? 因为 TCP 只要保证数据到了就行,不管数据长啥样。RPC 框架必须利用“长度头”或者“特殊分隔符”来把这股水流重新切分成一个个正确的请求。
这就是为什么你在看 ProtoBuf 或 Muduo 源码时,总能看到类似 Buffer、readInt32、peek 这种操作,它们就是在干“切分水流”这个脏活累活。
什么是代理模式?
既然你刚刚学了 RPC,那理解 代理模式 (Proxy Pattern) 简直是易如反掌。
实际上,RPC 就是代理模式最经典的顶级应用。
用一句话概括:代理模式就是给某个对象找一个“经纪人”或“秘书”。 你不直接找本人,而是找这个经纪人,由经纪人来决定要不要把你的请求转告给本人,或者在转告前后做点手脚。
1. 通俗类比:找明星签名
假设你想找周杰伦(RealSubject,真实对象)签个名。
没有代理模式:你直接冲进周杰伦家里找他。
- 风险:周杰伦可能在洗澡,可能在美国,或者根本不想理你。你直接访问非常不安全且低效。
有代理模式:你只能联系周杰伦的经纪人(Proxy,代理对象)。
你:“我想找杰伦签名。”
经纪人(也就是代理):
权限控制:先看看你是不是私生饭?(安全检查)
前置处理:确认杰伦现在有没有空。(状态检查)
转发请求:如果一切OK,经纪人把照片递给周杰伦,周杰伦签好。
后置处理:经纪人把签好名的照片包装一下,寄给你。(结果封装)
对你(客户端)来说,你感觉你在和周杰伦交互,但实际上你是和经纪人交互。
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 的“本地发起远程调用”,是不是瞬间明白了?那就是代理模式在帮你“伪装”成本地调用的样子。