
摘要市面上主流RPC框架如gRPC、brpc大多依赖IDLProtobuf代码生成学习门槛较高。本文基于Muduo网络库实现一套轻量Json‑RPC不使用IDL代码生成器支持同步调用、异步Future、回调调用、服务注册发现、服务上下线通知、发布订阅能力。本文不去逐行讲解代码重点拆解项目背后的设计思想、遇到的问题与模块权衡。目录前言一、开发环境二、技术选型1.1 RPC调用实现方案选型1.2 序列化JSON1.3 网络库Muduo1.4 异步模型C11 future/promise三、项目设计3.1 RPC调用从请求到响应的完整链路3.2 服务注册与发现让客户端找到正确的服务节点3.3 负载均衡多节点下如何挑选目标3.4 发布订阅从点对点通信到主题广播3.5 三个功能的协作关系四、框架设计4.1 应用层协议LV协议解决TCP粘包4.2 Dispatcher消息分发器网络与业务解耦五、服务端模块设计5.1 RpcRouter — RPC路由模块5.2 Registry‑Discovery 注册中心模块5.3 Publish‑Subscribe 发布订阅模块5.4 Server六、客户端模块设计6.1 Requestor 请求管理器6.2 RpcCaller RPC调用对外API层6.3 Registry‑Discovery服务注册与发现客户端6.4 Publish‑Subscribe发布订阅客户端6.5 Client七、开发过程踩过的坑八、项目实现九、后续扩展方向总结前言RPC全称远程过程调用核心目标就是像调用本地函数一样调用部署在远端机器上的函数。当我们调用本地函数直接栈上跳转执行即可调用RPC时客户端要把方法名、参数通过网络发给服务端服务端执行业务逻辑后再把结果通过网络传回客户端。很多同学初学RPC以为RPC就是“网络收发JSON字符串”。真正实现才会发现这里面要解决TCP粘包、异步网络请求响应乱序、服务治理、多调用模式、资源回收等一堆棘手问题。本项目采用C11开发基于muduo事件驱动网络库 jsoncpp序列化库。区别于传统Protobuf‑RPC本项目直接使用JSON作为报文载体省去IDL编译生成stub代码的步骤对初学者更加友好。当然也会付出代价报文体积更大、缺少原生的参数校验需要我们自己补充元信息做参数合法性检查。⼀个完整RPC通信框架大概包含以下内容RPC三种调用模式同步阻塞调用、std::future异步调用、回调callback异步调用服务注册服务发现、服务上线/下线主动推送通知内置轮询负载均衡服务发现获取多服务节点后自动挑选节点发起调用主题式发布‑订阅消息广播分层抽象架构底层组件可插拔替换一、开发环境LinuxUbuntu-22.04VSCodegMakefile二、技术选型1.1 RPC调用实现方案选型方案一传统IDL方案gRPC/brpc编写.proto接口描述文件通过工具生成客户端/服务端stub桩代码客户端直接调用生成好的本地代理函数底层帮我们完成序列化网络发送。缺点需要学习IDL语法代码生成增加理解成本对于学习向项目不够轻量化。方案二本项目采用的方案不做代码生成对外暴露统一的call(method_name, params)接口。传入方法名字符串JSON格式参数服务端根据方法名字符串匹配对应的业务处理函数。优点不需要IDL编译器上手简单缺点没有编译器自动做参数校验需要我们自己维护服务元信息手动校验参数的存在性与类型。1.2序列化JSON对比ProtobufJSON报文可读性好调试抓包可以直接看懂内容非常适合学习项目。劣势是序列化后的报文更大性能相比pb弱本项目定位学习框架性能做出妥协换取开发调试便捷。1.3网络库Muduo放弃手写原生socket epoll手写Reactor模型工作量巨大非常容易引入并发bug放弃Boost.Asio会引入体积庞大的boost第三方依赖。Muduo由陈硕大佬开发是⼀个基于非阻塞IO和事件驱动的C高并发TCP网络编程库。 它是⼀款基于主从Reactor模型的网络库其使用的线程模型是one loop per thread, 所谓one loop per thread指的是⼀个线程只能有⼀个事件循环EventLoop用于响应计时器和IO事件⼀个⽂件描述符只能由⼀个线程进行读写换句话说就是⼀个TCP连接必须归属于某个EventLoop管理连接管理、定时器、读写缓冲区全部已经封装完成我们可以专注开发RPC上层业务不用耗费精力在底层网络细节。1.4 异步模型C11 future/promiseRPC客户端网络是异步的发送请求之后不能阻塞IO线程等待响应。我们利用C11的std::future、std::promise、std::packaged_task在业务线程实现阻塞等待结果IO事件循环线程保持非阻塞同时对外提供同步、future异步、回调三种API。三、项目设计本质上来讲我们要实现的rpc远端调用思想上并不复杂甚至可以说是简单其实就是客户端想要完成某个任务的处理但是这个处理的过程并不自己来完成而是将请求发送到服务器上让服务器来帮其完成处理过程并返回结果客户端拿到结果后返回。然而上图的模型中是一种多对一或一对一的关系一旦服务端掉线则客户端无法进行远端调用且其服务端的负载也会较高因此在rpc实现中我们不仅要实现其基本功能还要再进一步实现分布式架构的rpc。分布式架构简单理解就是由多个节点组成的一个系统这些节点通常指的是服务器将不同的业务或者同一个业务拆分分分布在不同的节点上通过协同工作解决高并发的问题提高系统扩展性和可用性。其实现思想也并不复杂也就是在原来的模型基础上增加一个注册中心基于注册中心不同的服务提供服务器向注册中心进行服务注册相当于告诉注册中心自己能够提供什么服务而客户端在进行远端调用前先通过注册中心进行服务发现找到能够提供服务的服务器然后发起调用。而其次的发布订阅功能则是依托于多个客户端围绕服务端进行消息的转发。不过单纯的消息转发功能并不能满足于大部分场景的需要因此会在其基础上实现基于主题订阅的转发。基于以上功能的合并我们可以得到⼀个实现所有功能的结构图在上图的结构中我们甚至可以让每一个Server作为备用注册中心形成分布式架构一旦一个注册中心下线可以向备用中心进行注册以及请求且在此基础上客户端在请求Rpc服务的时候因为可以有多个rpc‑provider可选因此可以实现简单的负载均衡策略且基于注册中心可以更简便实现发布订阅的功能。项目的三个主要功能rpc调用服务的注册与发现以及服务的下线/上线通知消息的发布订阅下面我们逐一展开这三个核心功能的设计细节理解它们各自要解决的问题以及彼此之间的协作关系。3.1 RPC调用从请求到响应的完整链路RPC调用是整个框架最基础的能力。一次完整的RPC调用从客户端发起请求到拿到最终结果大致要经历以下几个环节客户端封装请求调用方传入方法名字符串和JSON格式的参数框架将其封装为一条携带唯一MID的请求报文。网络传输请求报文通过Muduo网络库发送到服务端服务端按LV协议解析出完整的消息。服务端路由分发Dispatcher根据方法名在本地服务注册表中查找对应的业务处理函数并把JSON参数反序列化后传入。业务执行与结果回传业务函数执行完毕后把返回值序列化为JSON封装成响应报文再通过网络回传给客户端。客户端匹配响应客户端根据响应报文中的MID找到对应的请求把结果交给调用方。这里最关键的一点是整个调用过程对调用方是透明的。调用方只需要像调用本地函数一样传入参数、拿到返回值完全不需要关心底层网络收发、序列化、路由匹配这些细节。这正是RPC框架的核心价值所在。3.2 服务注册与发现让客户端找到正确的服务节点在单机RPC模型中客户端直接连接固定的服务端地址即可。但一旦引入分布式架构服务端节点可能动态上下线客户端就必须有一种机制来感知当前有哪些可用的服务节点这就是服务注册与发现要解决的问题。整个流程可以拆解为三个步骤服务注册服务提供方启动时向注册中心上报自己的服务名、IP、端口等元信息并定期发送心跳维持租约。服务发现客户端发起调用前先向注册中心查询某个服务名下当前有哪些可用节点拿到节点列表。上下线通知当某个服务节点宕机或主动下线时注册中心要能及时感知并主动推送通知给订阅了该服务的客户端让客户端把失效节点从本地缓存中剔除。引入注册中心之后客户端不再硬编码服务端地址而是通过服务名动态获取节点列表。这样即使某个节点宕机客户端也能自动切换到其他健康节点系统的可用性和扩展性都得到了显著提升。3.3 负载均衡多节点下如何挑选目标当服务发现返回多个可用节点时客户端面临一个新的问题到底该把请求发给哪一个节点如果每次都固定发给第一个节点其他节点就会空闲第一个节点反而成为瓶颈。因此需要引入负载均衡策略。本项目内置了最简单的轮询Round Robin策略客户端维护一个节点列表和一个递增的计数器每次发起调用时按顺序轮流挑选一个节点。这样请求会被均匀地分散到各个服务节点上避免单个节点过载。轮询策略实现简单、无额外开销对于学习项目已经足够。如果后续需要更精细的控制也可以在此基础上扩展加权轮询、最少连接数等更复杂的策略这正是分层抽象带来的好处——负载均衡模块可以独立替换不影响其他部分。3.4 发布订阅从点对点通信到主题广播前面介绍的RPC调用和服务注册发现本质上都是点对点的通信模式一个客户端对应一个服务端。但很多业务场景需要一对多的消息广播比如某个服务状态发生变化需要同时通知多个订阅方。这就是发布订阅模式要解决的问题。发布订阅的核心思想是引入主题Topic的概念发布者把消息发送到某个主题上并不关心谁在订阅。订阅者提前向注册中心订阅自己感兴趣的主题一旦该主题有新消息就会收到推送。注册中心负责维护主题与订阅者之间的映射关系收到发布者的消息后把消息转发给所有订阅了该主题的客户端。这种基于主题的转发机制把发布者和订阅者彻底解耦。发布者不需要维护订阅者列表订阅者也不需要知道消息从哪里来双方只通过主题这一层抽象进行交互。相比单纯的消息转发基于主题的订阅机制能覆盖更丰富的业务场景比如服务上下线通知、配置变更广播、状态同步等。3.5 三个功能的协作关系回顾整个项目设计三个核心功能并不是彼此孤立的而是相互配合、层层递进RPC调用是地基解决最基本的远程方法调用问题。服务注册与发现在RPC之上引入注册中心让客户端能够动态找到可用节点支撑分布式部署。负载均衡进一步解决多节点场景下请求如何分配的问题提升整体吞吐。发布订阅则把通信模式从点对点扩展到一对多让框架能覆盖更广泛的消息广播场景。注册中心在其中扮演了枢纽角色它既是服务注册与发现的核心也是发布订阅的消息路由中枢。理解了这三个功能各自的设计意图和协作方式整个项目的骨架就清晰了。四、框架设计如果直接把muduo、json、业务逻辑全部耦合在一起。后续想把muduo替换成asio或者把json替换成protobuf业务代码要大面积修改。为了解耦整个框架划分为抽象层、具象层、业务层。抽象层一堆纯虚基类。只定义接口契约不绑定任何具体实现。业务层全部依赖抽象接口不感知底层用的是什么网络库、什么序列化协议。具象层继承抽象层做具体实现。把muduo网络库做一层适配封装实现LV长度帧协议实现Json消息的序列化反序列化。业务层实现RPC路由、注册中心、发布订阅全部业务逻辑。完全不感知底层网络与序列化实现。设计收益未来想替换底层组件只需要新增一套具象层实现上层业务代码几乎不用改动真正做到组件可插拔。4.1 应用层协议LV协议解决TCP粘包TCP是流式协议只负责字节的传输没有消息边界。一次recv可能读到半条消息、多条消息粘在一起。必须自己设计应用层协议划分消息边界。本项目使用LVLength‑Value自定义帧协议报文格式如下Length4字节整条消息后续全部数据总字节长度用于读取完整一条消息MType消息类型区分RPC请求、RPC响应、主题消息、服务注册消息IDLengthMID消息ID的字节长度MIDUUID生成全局唯一消息ID解决异步网络请求响应乱序BodyJSON字符串存放业务请求/响应的真实数据。重点MID消息ID的意义异步网络下发送多条请求响应回来的顺序不一定和发送顺序完全一致。我们不能按收发顺序匹配请求与响应。每一次请求生成唯一MID响应报文携带同一个MID。客户端依靠MID把响应报文和原始请求一一对应。4.2 Dispatcher消息分发器网络与业务解耦网络层只负责字节的收发解析不处理任何业务逻辑。解析得到完整消息之后交给Dispatcher分发器。Dispatcher内部维护一张哈希表消息类型MType → 业务回调函数。根据报文的MType调用对应的业务回调RPC请求 → 交给RpcRouter服务注册请求 → 交给注册发现模块主题操作请求 → 交给发布订阅模块。网络模块只管收发包业务全部由分发器转发实现网络逻辑和业务逻辑解耦。五、服务端模块设计服务端拆分为多个职责单一的模块模块之间通过Dispatcher完成交互。5.1 RpcRouter — RPC路由模块RpcRouter是RPC核心处理模块解决收到RPC请求找到对应的业务函数执行返回结果。因为我们没有IDL缺少参数元信息。我们设计ServiceDescribe服务描述结构体流程简述服务启动的时候业务向RpcRouter注册服务描述收到RPC请求根据报文中method方法名字查找对应的ServiceDescribe参数校验校验JSON请求中参数字段是否存在字段类型是否符合定义校验失败直接返回错误响应参数合法执行业务回调函数传入JSON参数拿到业务返回结果将结果封装为RPC‑Response带上原始MID返回给客户端。5.2 Registry‑Discovery 注册中心模块单机RPC存在单点故障问题。引入注册中心实现分布式RPC。两个核心管理器ProviderManager服务提供者管理方法‑提供者映射维护服务方法到全部可用服务提供者节点的映射关系。客户端执行服务发现时可以根据方法名查询获取该方法所有可用服务节点。连接‑提供者管理当服务提供者的TCP连接断开判定该主机上全部服务下线找到所有订阅过这些服务的发现者客户端向它们推送服务下线通知。DiscovererManager服务发现者管理方法‑发现者映射客户端执行服务发现操作时记录该客户端与目标服务的订阅关系。后续一旦有新的服务提供者上线就可以向该发现者推送服务上线通知。连接‑发现者管理如果某个服务发现者客户端断开TCP连接则清除该客户端全部订阅关联关系后续不再向该客户端推送任何服务变更通知。流程简述RPC服务提供者启动连接注册中心上报自己可以提供哪些服务RPC调用者向注册中心查询“谁可以提供Add服务”新增一台服务节点上线注册中心主动通知所有调用者某RPC服务节点下线注册中心主动通知所有调用者若服务提供者断开连接注册中心标记该节点全部服务下线向所有调用者推送下线通知客户端收到通知后剔除本地负载均衡列表中的失效节点。若调用者客户端断开连接注册中心清理该客户端所有服务发现记录不再向其推送变更消息。5.3 Publish‑Subscribe 发布订阅模块实现基于主题的消息广播能力。核心TopicManager维护主题与订阅者连接映射支持客户端创建/删除主题、订阅主题、取消订阅、发布消息收到发布消息遍历该主题全部订阅客户端推送消息连接断开当订阅客户端连接断开自动清理该客户端全部订阅关系避免内存泄漏、无效消息推送。5.4 Server当以上的所有功能模块都完成后我们就可以将所有功能整合到⼀起来实现服务端程序了。RpcServerrpc功能模块与网络通信部分结合。RegistryServer服务发现注册功能模块与网络通信部分结合。TopicServer发布订阅功能模块与网络通信部分结合。六、客户端模块设计客户端最大的难点全部网络IO是异步的不能发送之后同步阻塞等待recv同时要对外提供三种调用模式。6.1 Requestor 请求管理器很多手写RPC教程会忽略客户端的设计而Requestor正是客户端灵魂。Requestor模块存在的意义针对客户端的每一条请求进行管理以便于对请求对应的响应做出合适的操作。首先对于客户端来说不同的地方在于更多时候客户端是请求方是主动发起请求服务的一方而在多线程的网络通信中多线程下针对多个请求进行响应可能会存在时序的问题这种情况下则我们无法保证一个线程发送一个请求后接下来接收到的响应就是针对自己这条请求的响应这种情况是非常危险的一种情况。其次类似于Muduo库这种异步IO网络通信库通常IO操作都是异步操作即发送数据就是把数据放入发送缓冲区但是什么时候会发送由底层的网络库来进行协调并且也并不会提供recv接口而是在连接触发可读事件后IO读取数据完成后调用处理回调进行数据处理因此也无法直接在发送请求后去等待该条请求的响应。针对以上问题我们则创建出当前的请求管理模块来解决它的思想也非常简单就是给每一个请求都设定一个请求ID服务端进行响应的时候标识响应针对的是哪个请求也就是响应信息中会包含请求ID因此客户端这边我们不管收到哪条请求的响应将数据存储入一则hash_map中以请求ID作为映射并向外提供获取指定请求ID响应的阻塞接口这样只要在发送请求的时候知道自己的请求ID那么就能获取到自己想要的响应而不会出现异常。针对这个思想我们再进一步可以将每个请求进一步封装描述添加入异步的future控制或者设置回调函数的方式在不仅可以阻塞获取响应也可以实现异步获取响应以及回调处理响应。6.2 RpcCaller RPC调用对外API层封装对外的RPC调用接口隔离底层Requestor细节给上层业务简单易用接口同步调用发起调用后等收到响应结果后返回异步调用发起调用后立即返回在想获取结果的时候进行获取回调调用发起调用的同时设置结果的处理回调收到响应后自动对结果进行回调处理6.3Registry‑Discovery服务注册与发现客户端客户端的服务注册与发现模块逻辑相对复杂内部包含两种角色分别完成不同功能注册者服务提供者角色作为RPC服务的提供方具备向注册中心上报注册服务的能力将自身提供的方法与本机网络地址上报给注册中心。发现者服务调用者角色作为RPC服务调用方主动向注册中心发起服务发现请求获取目标服务对应的全部服务节点地址。拿到节点列表后在本地维护可用节点集合用于后续负载均衡选择服务节点。同时客户端需要监听注册中心推送的服务上线、下线通知收到通知后及时更新本地节点列表把已经下线失效的服务节点剔除保证后续RPC调用只会选择健康可用节点。6.4 Publish‑Subscribe发布订阅客户端Publish‑Subscribe发布订阅客户端存在的意义对外提供发布、订阅相关接口完成推送消息的接收与业务处理。发布订阅模型存在两种角色同一个客户端既可以作为消息的发布者也可以作为消息的订阅者。两种角色都围绕主题完成操作发布消息前需要先创建对应的主题消息最终是发送到指定主题之下。一个订阅者客户端可以同时订阅多个不同主题不同主题到来的消息需要执行各自独立的业务逻辑因此模块需要管理订阅者‑主题‑回调函数的对应关系收到主题消息时执行对应回调完成业务处理。6.5 Client当以上的所有功能模块都完成后我们就可以将所有功能整合到⼀起来实现客户端程序了。RegistryClient服务注册功能模块与网络通信客户端结合DiscoveryClient服务发现功能模块与网络通信客户端结合RpcClientDiscoveryClient RPC功能模块与网络通信客户端结合TopicClient发布订阅功能模块与网络通信客户端结合七、开发过程踩过的坑TCP粘包坑一开始直接发送裸JSON字符串没有做LV帧协议。多条JSON粘在同一个缓冲区直接解析失败。TCP是字节流应用层必须自己划分消息边界。请求响应乱序坑最开始天真以为发送顺序等于响应返回顺序。异步网络下多请求响应顺序完全可能错乱。MIDRequestor哈希表是必须的设计不能省略。连接断开资源泄漏坑TCP连接关闭的时候不能只是简单关闭socket。注册中心需要清理订阅关系、服务提供者信息客户端需要处理下线通知。不处理会内存泄漏还会继续向已经断开的连接发送消息。Dispatcher回调注册手动强转的负担最开始Dispatcher分发模块注册回调统一使用 BaseMessage 基类作为回调入参。在每个业务回调函数内部都需要手动将基类指针强制转换为对应的具体消息子类每新增一类业务消息都要重复写类型转换代码开发繁琐。因此采用模板封装注册接口模板内部自动完成消息类型推导与向下转换业务开发者直接传入接收具体消息子类的处理函数即可不再需要手动强制类型转换消除重复样板代码。八、项目实现代码链接https://gitee.com/sloth-running-away/rpc-project.git九、后续扩展方向本项目是学习型RPC框架还有很多可以拓展的点负载上报系统服务节点定期向注册中心上报自身CPU、内存、连接数等运行负载数据。注册中心汇总各节点负载信息为负载均衡策略提供数据依据实现根据节点真实负载做权重调度。负载均衡增强当前仅实现简单轮询策略。后续可扩展随机、源IP哈希一致性哈希算法结合节点上报的负载指标实现基于CPU、内存、连接数的加权负载均衡。客户端拿到服务节点列表以及对应负载权重根据算法动态选择压力更小的服务节点发起调用提升集群整体吞吐量。服务健康检测引入心跳检测机制服务节点定时向注册中心发送心跳包注册中心维护各个节点的心跳时间戳超过阈值未收到心跳则判定节点僵死故障。自动将故障节点从服务列表剔除并向所有客户端推送节点下线通知客户端更新本地节点列表调用时避开故障节点提升调用可靠性。序列化替换当前使用JSON序列化项目已经对序列化做抽象层封装。后续可以新增Protobuf序列化具象实现上层业务逻辑、消息分发、网络收发代码几乎无需改动。同一套业务代码可以切换不同序列化方案充分体现分层抽象设计带来的可扩展性。发布订阅增强完善发布订阅能力增加消息持久化消息落盘存储新订阅者上线可以消费历史消息支持多种消息转发策略包括广播、组播、单播增加消息重试、死信队列处理消费失败的消息提升发布订阅模块可靠性。超时控制完善全链路超时控制客户端发起RPC调用时设置超时时间超时未收到响应直接终止请求返回超时异常避免请求无限阻塞占用资源服务端增加处理超时检测防止慢请求一直占用线程资源同时支持不同业务接口配置独立超时时间。总结手写RPC并不仅仅是实现网络收发JSON字符串这么简单。一套完整可用的RPC框架是多组件协同配合的整体事件驱动网络库 应用层帧协议解决TCP粘包问题 请求‑响应匹配机制 消息分发调度 RPC路由逻辑 服务治理服务注册、发现、节点上下线管理 多调用模式封装。本项目最大的收获并不只是实现了一套可以跑通的RPC代码而是切身实践了分层抽象、单一职责的软件设计思想。将底层网络通信细节和上层业务逻辑充分解耦每个模块只聚焦自身职责模块之间通过接口完成交互。这样的设计使得后续迭代新功能、替换内部组件、定位修复bug都会更加便捷。通过从零实现RPC框架也对网络异步编程、多线程并发、服务注册发现、消息编解码、事件循环模型有了更加透彻的理解。