首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从零实现RPC框架:我花了一个周末,写出了能跨进程“调函数”的玩具

从零实现RPC框架:我花了一个周末,写出了能跨进程“调函数”的玩具

原创
作者头像
闪学it点com
发布2026-08-25 13:26:50
发布2026-08-25 13:26:50
870
举报

一个常见的困惑

刚学分布式时,我被“远程过程调用(RPC)”这个名字唬住了——听起来像魔法:在A机器上调用B机器上的一个函数,就像调用本地函数一样。

但拆开来看,本质上就三件事:

  1. 把函数名和参数打包成数据(序列化)
  2. 通过网络发给服务端(网络传输)
  3. 服务端执行后把结果传回来(反序列化)

明白了这个,我决定自己写一个最简版本,不依赖任何现成RPC框架。

第一步:协议设计——用什么格式传递信息?

我选择用JSON,因为人类可读、调试方便。通信协议定义为一个字典:

代码语言:javascript
复制
# 请求格式
request = {
    "method": "add",      # 要调用的函数名
    "params": [3, 5],     # 参数列表
    "id": 1               # 请求编号,用于匹配响应
}

# 响应格式
response = {
    "result": 8,          # 执行结果
    "error": None,        # 错误信息
    "id": 1               # 对应请求的编号
}

这是RPC框架中最核心的设计决策——双方必须看懂同一套“语言”

第二步:服务端——暴露函数

服务端要做的事情很简单:启动一个Socket监听,收到请求后解析JSON,找到对应的函数执行,再返回结果。

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

# 注册可调用的函数
functions = {
    "add": lambda a, b: a + b,
    "sub": lambda a, b: a - b
}

def handle_request(data):
    req = json.loads(data)
    func = functions.get(req["method"])
    if not func:
        return {"error": "method not found", "id": req["id"]}
    result = func(*req["params"])
    return {"result": result, "id": req["id"]}

# 启动TCP服务
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("localhost", 8888))
server.listen(1)

while True:
    conn, _ = server.accept()
    data = conn.recv(1024).decode()
    resp = handle_request(data)
    conn.send(json.dumps(resp).encode())
    conn.close()

服务端代码不到30行,核心逻辑就是查表->执行->返回

第三步:客户端——伪装成本地调用

客户端的职责是:让程序员感觉像在调用普通函数,实际上背后发起了网络请求。

我用了一个代理类来实现“魔法”:

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

class RPCClient:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.id = 0
    
    def __getattr__(self, method):
        # 动态拦截属性访问,返回一个可调用的函数
        def caller(*args):
            self.id += 1
            req = {"method": method, "params": args, "id": self.id}
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect((self.host, self.port))
            sock.send(json.dumps(req).encode())
            resp = json.loads(sock.recv(1024).decode())
            sock.close()
            if resp.get("error"):
                raise Exception(resp["error"])
            return resp["result"]
        return caller

使用的时候,代码简洁得不像网络编程:

代码语言:javascript
复制
client = RPCClient("localhost", 8888)

# 看起来就像在调用本地函数
print(client.add(3, 5))   # 输出: 8
print(client.sub(10, 4))  # 输出: 6

Python的__getattr__魔法方法在这里发挥了关键作用——它拦截了client.add这样的访问,动态构造网络请求。

这个玩具还缺什么?

我故意省略了很多生产级RPC框架的必要组件:

  • 序列化:JSON性能差,生产常用Protobuf/Thrift
  • 服务注册发现:没有服务端地址硬编码,生产需要Nacos/Consul
  • 负载均衡:单节点,生产需要多节点+路由策略
  • 超时重试:网络故障就崩溃,生产需要容错机制
  • 连接池:每次新建Socket开销大,生产需要长连接复用

但这些“缺失”恰恰是我想展示的——RPC的核心只有那几十行代码,其余全是工程优化

一点感悟

写完这个玩具后,再看Dubbo、gRPC的文档,我不再感到陌生。因为我知道它们在解决什么问题:

当你的服务有100个节点、每秒处理10万请求时,连接管理、序列化效率、服务发现才成为难题。但如果你只是调用另一个进程的函数,一个TCP端口+JSON就足够了。

技术框架往往被包装得很复杂,但剥开外壳,底层原理往往朴素得惊人。如果你也想理解RPC,别急着读源码——先花半小时写一个最小的版本,胜过看三天文档。


后记:这个框架后来被我用来做跨语言通信(Python服务端调用Go客户端函数),只需要两边约定相同的JSON协议即可。协议是RPC的“宪法”,比代码更重要。

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

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

目录
  • 一个常见的困惑
  • 第一步:协议设计——用什么格式传递信息?
  • 第二步:服务端——暴露函数
  • 第三步:客户端——伪装成本地调用
  • 这个玩具还缺什么?
  • 一点感悟
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档