首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从零实现RPC框架:当“本地调用”跨过网络那道坎

从零实现RPC框架:当“本地调用”跨过网络那道坎

原创
作者头像
闪 学it
发布2026-08-26 18:12:54
发布2026-08-26 18:12:54
1740
举报

2026年的今天,微服务、分布式系统早已是行业标配。但拨开Spring Cloud、Dubbo、gRPC这些繁杂的生态,追问一个最朴素的问题:远程过程调用(RPC)的核心,究竟是什么?

答案远比想象中简单——它不过是想让开发者像调用本地函数一样,调用另一台机器上的函数。而“从零实现”的意义,正是为了撕开优雅封装的外衣,亲眼看看网络通信、序列化、代理和注册这四块积木,是如何咬合成现代分布式基石的。


一、抽象之美:我们到底在“屏蔽”什么?

假设你在写一个计算器服务,本地调用是:

代码语言:javascript
复制
result = add(3, 5)  # 8,瞬时完成

但如果 add 函数运行在千里之外的服务器上呢?你必须手动处理:

  1. 网络传输:把“请计算 3+5”这个意图变成字节,扔到网线里。
  2. 协议约定:对方怎么知道你要调用哪个函数?参数是什么?
  3. 序列化:内存里的整数、对象,如何变成可传输的二进制流?
  4. 结果取回:服务器算完,怎么把“8”原样送回调用方?

RPC框架的本质,就是把这四步封装成一个“本地代理”。调用者面对代理对象,浑然不觉背后是Socket在收发数据。而我们要从零搭建的,正是这个“魔术帽子”。


二、极简核心:四段代码,拆解RPC骨架

为了避开第三方重量级库,我们用最原生的 Python socket + JSON 实现一个迷你RPC。它虽简陋,却五脏俱全。

1. 网络传输层(最朴素的“信使”)

我们将请求和响应都封装成字典,用 JSON 序列化后通过 TCP 发送。注意,真实框架需处理粘包问题,此处为清晰仅做简易收发。

代码语言:javascript
复制
import socket
import json

def send_request(host, port, request_dict):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((host, port))
        sock.sendall(json.dumps(request_dict).encode('utf-8'))
        response = sock.recv(1024).decode('utf-8')  # 简易场景,假设数据包足够小
        return json.loads(response)
2. 服务端核心(注册表 + 调度器)

服务端需要维护一个“函数名 → 函数实体”的字典,收到请求后,根据方法名查表、反射调用、返回结果。

代码语言:javascript
复制
class RpcServer:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.functions = {}  # 服务注册表

    def register(self, func):
        """将函数按名称注册到表中"""
        self.functions[func.__name__] = func
        return func  # 方便装饰器用法

    def run(self):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.bind((self.host, self.port))
            sock.listen(5)
            print(f"RPC Server running on {self.host}:{self.port}")
            while True:
                conn, _ = sock.accept()
                data = json.loads(conn.recv(1024).decode())
                # 查表并调用
                func = self.functions.get(data['method'])
                if func:
                    result = func(*data['params'])
                    conn.sendall(json.dumps({'result': result}).encode())
                else:
                    conn.sendall(json.dumps({'error': 'Method not found'}).encode())
                conn.close()
3. 客户端代理(“魔术”发生之地)

这是RPC最精妙的一环——利用 Python 的 __getattr__ 拦截未定义属性的访问,动态构造请求并发送。

代码语言:javascript
复制
class RpcClient:
    def __init__(self, host, port):
        self.host = host
        self.port = port

    def __getattr__(self, name):
        """当调用 client.add 时,此处被触发,name 即为 'add'"""
        def wrapper(*args):
            request = {'method': name, 'params': args}
            return send_request(self.host, self.port, request)
        return wrapper
4. 跑通一切:现场演示

把三块拼起来,奇迹发生了:

代码语言:javascript
复制
# ---------- 服务端 ----------
server = RpcServer('localhost', 8888)

@server.register
def add(a, b):
    return a + b

@server.register
def greet(name):
    return f"Hello, {name}!"

# 在终端启动服务(示意,实际运行需开新线程)
# server.run()

# ---------- 客户端(另一个进程) ----------
client = RpcClient('localhost', 8888)

# 宛如本地函数!
print(client.add(100, 200))   # 输出: 300
print(client.greet("World"))  # 输出: Hello, World!

短短几十行,一个完整的RPC骨架已然立起。 客户端不知道网络,服务端不知道谁在调用——抽象的胜利,莫过于此。


三、冰山之下:当我们越过这“从零”的边界

如果你的老板说“把它上线,扛住百万QPS”,上面的代码会在瞬间崩塌。因为现实世界的残酷,恰恰隐藏在“从零到生产”的巨大鸿沟中:

  1. 致命的粘包/拆包recv(1024) 极其脆弱。实际框架必须设计协议头(Header) 指示载荷长度,或使用特殊的界定符。
  2. 序列化之痛:JSON 无法处理日期、自定义类,且效率低下。工业界转向 Protobuf(gRPC)或 Hessian(Dubbo),兼顾性能与跨语言。
  3. 同步阻塞的瓶颈:每个请求独占一个线程,高并发下线程爆炸。成熟的框架会引入 Reactor 模型(如 Netty)或 异步非阻塞 IO
  4. 服务治理的缺失:没有注册中心,服务地址写死在客户端。一旦服务迁移或扩容,只能干瞪眼——这正是 ZooKeeper、Nacos 登场的理由。
  5. 超时与重试的困局:网络闪断时,客户端是傻等还是重试?重试会导致幂等性问题,如何保证业务数据不错乱?

你看,从零实现让我们获得了极致的透明感,但也让我们深刻理解:分布式领域没有银弹。每一层优雅的封装背后,都沉淀着数十年的工程妥协。


四、重新审视“调用”这件事

当我们敲下 client.add(3,5) 时,底层其实发生了这些事:

  • 客户端代理捕获参数,打包成 {"method":"add","params":[3,5]}
  • 数据穿越 OS 内核协议栈,经过网卡、路由器、防火墙,抵达服务器。
  • 服务端反序列化、查表、压栈执行,再将结果按原路返回。
  • 这一路上,数据包可能丢失、乱序、延迟,但最终(在理想情况下)回到了调用者的栈帧中,仿佛一切从未发生。

RPC 不仅仅是一项技术,更是一种宏大的编程哲学——它试图让程序员遗忘“网络”这个不可靠的物理事实,坚信计算机之间可以像大脑神经元一样,无缝协作。

而我们亲手写出的这几段代码,就是对这种哲学最朴素、最诚实的注脚。下次当你使用 Dubbo 注解或 gRPC 拦截器时,愿你能回想起那个由 socketjson__getattr__ 构建的微小世界——那里没有复杂的 SPI 机制,没有冗长的 XML 配置,只有函数跨越进程边界时,那一声清脆的呼唤与回响。

(全文核心代码仅4段,总计约60行,却勾勒出了RPC从诞生到工业演进的完整脉络。)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、抽象之美:我们到底在“屏蔽”什么?
  • 二、极简核心:四段代码,拆解RPC骨架
    • 1. 网络传输层(最朴素的“信使”)
    • 2. 服务端核心(注册表 + 调度器)
    • 3. 客户端代理(“魔术”发生之地)
    • 4. 跑通一切:现场演示
  • 三、冰山之下:当我们越过这“从零”的边界
  • 四、重新审视“调用”这件事
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档