RPC常见问题回答
项目流程和架构设计
1.服务端的功能:
1.提供rpc调用对应的函数
2.完成服务注册 服务发现 上线/下线通知
3.提供主题的操作 (创建/删除/订阅/取消订阅) 消息的发布
2.服务的模块划分
1.网络通信模块 net 底层套用的moude库
2.应用层通信协议模块 1.序列化 反序列化数据 解决tcp中的粘包问题。2.客户端/服务端对消息进行处理,就需要设置一个oMessage回调函数对收到的数据进行应用层协议处理。
3.消息分发模块 Dispatcher,收到消息 根据不同的消息类型去调用不同的回调函数
4.远程调用路由模块 RpcRouter 提供Rpc请求的处理回调函数 并返回执行完的结果
5.服务注册/发现模块 Register—Discovery 针对 服务请求进行处理
6.发布订阅模块 Publish--subcribel 针对发布订阅请求进行处理 并提供一个回调函数给Dis模块
设计项目原因
对底层网络通信和分布式架构感兴趣,为了对客户端-服务端通信机制更了解 就实现了这个支持注册中心 发布订阅 异步调用功能的JsonRPC框架。
RPC 调用流程简要总结(简洁专业版)
客户端通过统一接口call进行rpc调用,把函数名method+参数(参数名:值)封装进RpcRequest并用JSON:Value格式存储,再进行序列化把JOSN:Value转化为string对象,最后通过应用层通信协议LVProtocol添加上报头,通过TCP发送到服务端。
rpc服务端接收到数据后,先根据LV协议从缓冲区中获取完整的报文,去掉报头 获取正文body字段,再进行反序列化转为JSON:Value格式,根据里面的method字段 路由找本地对应的服务描述对象 进行参数检测 没有问题进行函数调用,返回的结果也是JSON:Value类型,序列化 添加报头 TCP返回给客户端。
一、项目背景与整体架构
你这个 RPC 框架项目的初衷是什么?你为什么要做这个项目?
主要是想了解一下网络通信和分布式架构,为了对服务端-客户端的通信机制更了解就实现了这个rpc项目。
你的 RPC 框架支持哪些核心功能模块?请简要介绍每个模块的作用和之间的协作流程。
主要有三个大的功能,1.rpc调用模块 2.服务发现/注册模块 3.主题发布与订阅模块。
1.rpc模块对内本地维护了一张表,函数名method对应服务描述对象(包含method 参数类型 返回值类型 回调函数 参数的检测check()),给Dispathcer消息派发模块提供处理rpc请求的回调函数,根据请求中的method函数中找到对应的服务描述对象进行调用,并返回结果应答。
2.服务发现/注册模块 整个rpc的流程不是客户端直接向服务端发送rpc请求。而是1.服务提供者也就是rpc服务端先向注册中心发送注册请求,让注册中心知道我能提供什么服务。2.客户端要先向注册中心发起服务发现请求,注册中心再返回能提供该服务的主机地址列表,这里会有一个轮询服务 来选择其中一个主机地址 让客户端进行rpc调用。给Dispatcher提供服务发现/注册请求的回调处理函数。
3.主题操作模块 我们这个rpc框架会提供关于主题的操作,我们可以创建主题/取消主题/订阅/取消订阅 主题消息发布。对内会维护好两种表,1.map<topic_name,topic>主题名 主题间的映射 2.map<conn,subcribe>连接 订阅者,这样就知道每个主题的订阅者有哪些,每个订阅者的订阅主题有哪些。进行主题消息发布时,根据topic中的订阅者列表给订阅者主动推送消息进行广播。给Disathcer提供回调函数处理主题操作/发布请求
你为什么选择自定义 JSON-RPC 协议?和 gRPC 或 Thrift 相比,你的设计有什么优势或不足?
用JSON的话,JSON反序列化和序列化操作更简单 其他的没有了解过。
二、客户端与服务端机制
客户端是如何发起一个 RPC 调用的?整个请求从发起到响应的流程是怎样的?
1.首先要先获取rpc服务端的地址,有两种情况1.通过服务发现进行获取服务端地址。具体流程是 rpc客户端里面是有个服务发现客户端向注册中心进行服务发现,注册中心返回能提供该服务的主机地址,服务发现客户端再根据负载均衡策略 也就简单的轮询 选择其中的一个给rpc客户端进行rpc调用。2.或者rpc不启动服务发现,直接传入服务端地址 进行rpc调用。
2.创建服务端和客户端连接,先从连接池中有没有对应的连接,有获取 没有创建连接,进行rpc调用,结束后放入连接池。
3.创建rpc请求并发送,rpc客户端给外部提供call函数 里面是通过Requestor模块的send()函数进行发送rpc请求的。
4.Dispatcher模块进行消息派发,Dispathcer模块消息回调函数onMessage 获取消息,根据消息的类型,进行派发调用对应服务端提供的回调函数,也就是rpc服务端rpc_router向Dispatcher模块注册的onRpcRequest()函数.
5.rpc服务端进行本地路由,根据请求消息里面的method函数 找对应的服务描述对象,进行参数检测并调用,生成结果响应并返回。
6.Dispatcher派发应答,Requstor请求消息发送的模块提供onResponse()处理返回的应答,根据消息唯一的ID找对应的请求消息,并设置结果/进行回调。
你在服务端如何注册一个服务方法?具体在哪个模块处理服务注册?
1.首先rpc服务端模块 里面会包含一个服务注册的客户端,当服务端本地新上线了一个服务 除了本地进行记录,还得向服务注册中心通过服务注册客户端发送服务注册消息。
2.服务注册中心会像Dispathcer模块注册onServiceRequest()回调函数处理服务注册/发现的请求消息,后续操作就是跟新该服务的服务提供者列表,并给发现过该服务的发现者进行服务上线通知。
客户端怎么实现“同步调用”?你是怎么保证调用阻塞等待返回的?用了哪些 C++ 特性?
1.rpc客户端会提供3中call函数 其中同步call里面创建msg消息并调用Requestor模块中的同步send,但这个同步send里面就是调用异步send+外部等待get()进行阻塞,等结果响应返回过来 在请求描述中的promise对象进行set_value设置结果后 外部get()解除阻塞 从而达到同步的效果。
2.这里我是采用了promise+future来完成阻塞等待返回的,future对象get()进行阻塞等待 直到promise对象set_value才会停止阻塞。
3.同步send()调用异步send()体现函数重载 C++多态特性
三、消息处理与分发机制
Dispatcher 模块的作用是什么?你是如何实现“消息类型到回调函数”的映射的?
1.Dispathcer模块 简单来说是消息分发处理模块,根据消息的类型调用对应模块注册的回调函数进行派发处理。
2.这里我们用一共哈希表map完成消息类型到回调函数的映射的,但这里有两中处理方法。
第一种,就是map的value值就是存储的回调函数,但这就需要注册的所有回调函数类型相同才能统一存储在map中。回调函数的参数都是两个,1.BaseConnect::ptr连接可以基类接收 2.消息类型 当然也可以用BaseMessage::ptr基类接收,但实际上传入的对象的对应的子类对象。这样虽然可以用map统一管理,但我们必须在回调函数中进行判断传入的消息对象是不是对应的子类类型,我们必须要去猜是不是,如果不是需要怎么处理,后续新增了其它消息子类是不是还需要更改逻辑处理,这明显不符合开闭原则。
这里项目中采用的第二种方法,即第二个参数就是用对应子类对象指针来接收。但这样函数类型不一样,还这么统一管理?
这里我是通过map存储一个同一个父类指针,通过虚函数调用机制,调用对应的子类对象 里面的回调函数。我先创建一个父类 里面有一个虚函数,用模板类根据消息类型创建对应的子类,这些子类继承于父类 并重写父类里面的虚函数,写入自己回调函数的处理逻辑。
简单来说就是 多态(虚函数调用机制)+模板类+继承 让map统一管理不同类型的回调函数
你是怎么实现类型安全的消息派发机制的?为啥不用函数指针而是用多态+模板?
各个模块在创建时 向Dispatcher模块注册处理消息的回调函数 传入的参数有1.消息类型 2.回调函数。Dispatcher里面通过模板函数根据消息类型 生成不同的子类,里面重写的虚函数再调用传入的回调函数。在map存储子类对象 用它们的父类统一接收。
后面调用的时候,根据类型找到map存储的父类指针根据虚函数调用机制 调用对应子类重写的虚函数 完成回调处理。
为什么不用函数指针,主要还是不想让回调函数用BaseMessgae::ptr接收子类对象,接收了里面还得判断 把父类指针dynamic_pointer_cast转换为子类指针,如果转换失败了 会返回空指针,需额外判断处理。
JsonRequest 和 JsonResponse 有哪些子类?它们的继承结构设计的初衷是什么?
1.JsonRequest 请求类,有三个子类 分别对应三个主要功能。1.RpcRequest rpc调用请求
2.ServiceRequest 服务请求类 3.TopicRequest 消息请求类.
2.同理JsonResponse 应答类,也根据功能分为三个。
这样根据父类生成子类,主要还是让一些共同需要的部分放在基类中,子类再根据自己需要来自己实现函数 成员变量。比如说应答类都需要rcode响应码,就可以放在响应基类中 check()检测响应字段是否正确。如果子类需要根据自己定义的字段实现check()也可以重写。
这样的好处 新增子类类型只需继承父类并实现即可,符合开闭原则。整体的结构也更清晰。
四、注册中心与服务发现机制
服务注册中心是如何工作的?服务端是如何注册自己的服务信息的?
注册中心PDManager可以分为两个部分,1.ProviderManager服务提供者管理 2.DiscovererManager服务发现者管理。完成服务注册 服务发现 服务上线/下线操作。
1.Pro服务提供者管理 有两张表map<method,vector<Pro>>服务方法能被哪些服务提供者提供,map<conn,Pro>连接到提供者。Pro里面有自己能提哪些方法的列表vector<method>。
2.Dis服务发现者管理 也是有两种表map<method,vector<Dis>>该method方法被哪些发现者发现了,map<conn,Dis>连接到发现者。Dis里面有自己发现过哪些方法的列表vector<method>
根据这些结构 ,并给Dispatcher模块注册onServiceRequset()回调函数,处理服务发现/注册请求消息。
1.服务注册(conn给注册中心说自己能提供method方法) 根据conn从map<conn,Pro>找提供者+map<method,vector<Pro>>新增method服务提供者+Pro提供者内部新增提供方法+服务上线通知 给发现过该method方法的发现者进行通知+返回注册成功应答
1.根据conn从map<conn,Pro>找提供者:先找连接的提供者,如果没有就新建并加入map中
2.map<method,vector<Pro>>新增服务提供者:从map<method,vector<Pro>>方法对应的提供者列表中新增服务者
3.Pro提供者内部新增方法:对Pro里面的方法提供列表vector<method>新增方法
4.服务上线通知:Dis服务发现者中1.从map<method,Dis>找该方法被哪些发现者发现了,并从每个Dis发现者客户端发送服务上线通知
5.给服务注册客户端 发送注册成功应答
2.服务发现(conn想发现能提供method方法的提供者地址) 根据conn从map<conn,Dis>找发现者+map<method,vector<Dis>>新增method服务发现者+Dis发现者内部新增发现的方法+返回应答(包含该method方法提供者地址)
1.根据conn从map<conn,Dis>找发现者:先找连接的发现者,如果没有就新建并加入map中
2.map<method,vector<Dis>>新增method服务发现者:从map<method,vector<Dis>>方法对应的发现者列表中新增发现者
3.Dis发现者内部新增发现的方法:对Dis里面的方法发现列表vector<method>新增发现
4.返回应答(包含该method方法提供者地址):根据method从服务提供者管理的map<method,vector<Pro>>找到能提供该method方法的方法提供者,从中获取host地址 组成应答并返回。
3.连接断开(连接断开的回调函数) 分为提供者断开 发现者断开
1.提供者断开:提供者的所有方法都要下线 根据发现者管理中map<method,vector<Dis>>找每个method方法的发现者,进行服务下线通知。最后删除提供者管理中map<conn,Pro>服务提供者的管理。
2.发现者断开:发现者断开连接不需要通知任何人,直接在发现者管理中map<conn,Dis>删除对发现者的管理就行。
如果服务端宕机,注册中心会怎么处理?你做了哪些“心跳检测”或“服务下线”机制?
针对这种情况,我的项目中没有实现相关的处理,但我可以对这种情况处理提个思路,心跳检测机制。
服务端每隔一段时间(5s)会给注册中心发送心跳包 表示自己还可以提供服务,注册中心记录每个服务端最后一次心跳时间,由定时任务(或事件循环)周期性检查,如果超过一定时间(10s)没上报心跳 就将服务端视为异常,注册中心就会断开连接 触发连接断开回调,1.服务端提供的每个method中的提供者列表去除该提供者 2.并删除map<conn,Pro>对该提供者的管理 3.最后给发现该提供者的方法的客户端发送服务下线的通知。
客户端如何进行服务发现?请求发送到注册中心返回的是什么信息?
服务发现客户端 构建服务发现请求消息并通过Requestor模块中同步send进行发送,Dispatcher模块根据消息类型调用 注册中心注册的回调函数onServiceRequest()处理服务发现请求。
请求发送到注册中心返回的是客户端进行发现method方法的提供者host地址列表,后面通过轮询的负载均衡策略选择一个给客户端。
五、主题发布/订阅模块(Pub/Sub)
你的框架实现了“主题发布/订阅”机制,这一块和传统 RPC 调用有什么区别?
1.通信方式:传统的RPC调用 一般是一个服务端对应一个客户端,而主题消息发布是一个服务端对应多个客户端 对订阅该主题的所有客户端进行广播。
2.控制方向:传统RPC是客户端端主动发送请求消息,而服务端是被动返回应答的。而在主题信息发布中服务端是接收到消息发布请求,对订阅主题的客户端是主动进行发送消息。
3.同步 耦合:相比传统的同步RPC请求-应答模型,我们的主题发布/订阅是基于事件驱动模型,异步非阻塞 客户端不需要阻塞等待响应,由服务端主动推送。解耦性强 发布者不需要知道订阅者是谁,只需要向主题服务端发布消息就可以。
对比维度 传统 RPC 调用 主题发布/订阅 控制方向 客户端主动请求 服务端主动推送 通信方式 一对一 一对多 同步性 同步阻塞等待 异步事件驱动 耦合性 紧耦合 松耦合 适用场景 精准调用,事务请求 广播、推送、订阅通知 实现关键 请求封装 + 回调映射 主题管理 + 回调路由
多个客户端订阅同一个主题,你是如何处理每个客户端的回调函数的?
首先客户端订阅一个主题,服务端接收主题消息发布 向订阅者客户端进行广播主动发送消息,如何处理消息需要客户端自己定义。也就是订阅者客户端会向Dispatcher模块注册onPublish()回调函数处理服务端主动推送的消息,客户端本地会有哈希表map<topic_name,cb>维护主题名到对应回调函数的映射,让本地对应的回调函数处理推送的消息。
简单来说,就是每个客户端本地会维护好一张哈希表 主题名->该主题回调函数的映射。
客户端发布主题消息,服务端是如何进行广播的?如何避免回调未注册问题?
1.服务端接收到主题发布请求消息,会对所有订阅该主题的订阅者客户端进行广播。就要先找到订阅该主题的所有订阅者,这就要先说明主题服务端的两种表和两个结构体。
1.订阅者列表map<conn,subcriber::ptr>,对订阅者进行管理。结构体subcriber订阅者中有订阅的主题名称列表vector<topic_name>.
2.主题列表map<topic_name,topic::ptr>,对主题进行管理。结构体topic主题中有订阅该主题的订阅者列表vetor<sub>
提供主题列表map<topic_name,topic>找到对应主题,topic里面就有对应该主题的所有订阅者,遍历订阅者列表直接通过底层send进行发送消息。
2.对应服务端主动推送过来的主题消息发布,既要在Dispathcer模块注册onPuhlish回调接收,还有在本地进行路由调用对应主题的回调消息进行处理。如果我们在订阅后,再注册本地对应主题的处理函数,有可能刚订阅了主题立马推送过来消息,但本地的主题处理函数还没注册成功,导致找不到回调函数处理该主题消息。
所以必须在订阅主题前,注册主题回调处理函数。如果订阅失败可以del删除刚注册的回调函数。
六、网络与协议设计
你使用了 muduo 网络库,它的 Reactor 模型你理解吗?客户端和服务端的 loop 有什么区别?
Reactor 模型是一种基于事件驱动的并发编程模型,它通过事件分发器(如 epoll)监听所有 socket 的 IO 事件,当某个 socket 可读/写时,调用事先注册的回调函数进行处理。提供一种 非阻塞、事件驱动 的结构,用一个或少量线程高效处理大量并发连接;
Muduo 是一个基于 Reactor 模型的高性能 C++ 网络库
1.muduo网络库中服务端中用的是主从Reactor模型,主Reactor是专门监听连接事件的,有客户端想要建立连接 就accpet()获取创建连接并返回套接字socket,后面就直接交给从Reactor,监听连接后续的IO事件。在客户端中也有一个 Reactor(即 EventLoop),用于管理发送请求、接收响应、处理推送等事件。
2.服务端和客户端的loop最大区别是是否在前台阻塞循环运行。1.服务端是直接把loop线程放在前台循环运行的 2.而客户端是后台运行loop,因为客户端需要主动发起连接、请求、注册回调函数等操作,具体操作的执行和返回结果处理都是在循环线程中处理的。如果loop循环线程放在客户端前台,就会导致阻塞 将影响主线程逻辑。而服务端就是为了处理客户端发送的消息,可以循环运行。
你如何解决 TCP 粘包问题?数据接收后如何区分一条完整的 JSON 消息?
针对TCP粘包问题,我是自己定义了应用层通信协议LVProtocol,采用定长头部 + 变长正文的形式。具体来说是 固定头部由三个4字节字段组成:消息长度、类型、ID长度,后两个变长字段 消息ID+body正文。
接收数据后我们会放在缓冲区中,通过访问前4字节获取该报文的长度 再对比当前缓冲区存储数据的大小,如果缓冲区存储数据大小>该报文长度,就认为缓冲区中至少有一条完整消息。
你在客户端和服务端的协议抽象层(如 BaseProtocol)里做了哪些处理?
BaseProtocol里面实现了接口的统一,这样上层具象层不需要管底层实现是怎么样的,直接用BaseProtocol提供的统一接口就可以 具体接口有3个 1.canProcessed判断缓冲区是否有完整报文 2.onMessage从缓冲区中解析完整报文(去报头+反序列化) 3.serialize 准备应答(序列化+添报头)。BaseProtocol底层具体实现是自定义实现的LVProtocol协议,后续换其它协议只需实现对应的 BaseProtocol 子类即可,不会影响上层代码 符合开闭原则。
七、扩展性与项目亮点
你这个框架的扩展性在哪里体现?如果后续想加负载均衡或限流模块,怎么接入?
我的项目解耦性好,新增业务时 不会影响原有业务,把消息的回调处理函数注册给Dispatcher模块就可以,新增消息类型 继承父类并实现自己需要的字段/函数就行。
这个不了解。
标准回答:
一、框架扩展性的体现
我的框架在架构设计上非常注重模块解耦与可扩展性,主要体现在以下几个方面:
✅ 1. Dispatcher 模块实现了消息分发中心
所有模块之间不直接调用彼此的逻辑,而是通过 Dispatcher 注册和派发消息;
每类消息有唯一的
mtype
类型编号;每个模块通过
dispatcher.registerHandler<MessageType>(type, callback)
注册自己的消息处理函数;新增功能只需要:
定义新的消息类型(继承 JsonMessage);
实现处理逻辑;
注册到 Dispatcher 即可,无需改动原有代码。
📌 体现了开闭原则(对扩展开放,对修改封闭)。
✅ 2. 消息继承体系保证扩展性和统一性
所有消息统一继承自
BaseMessage -> JsonMessage
;不同功能子模块(RPC、服务注册、发布订阅)都基于该结构;
如果未来要增加新的子系统(如监控、配置下发),只需要继承 JsonMessage 并实现新的 Request/Response 即可;
统一的编码/解码流程由
BaseProtocol
层抽象,新增协议或更换底层实现对业务层完全无感。
二、如何扩展限流、负载均衡模块?
✅ 1. 负载均衡模块扩展方式
客户端发起 RPC 请求前,会先调用服务发现模块获取服务地址列表(如 3 个提供者 IP)。
当前默认是 轮询策略,在
RpcClient
模块中选择一个服务端。扩展方式:
定义一个策略接口类(如
class LoadBalancer { virtual Address select(const vector<Address>&) = 0; }
);实现不同策略子类(如轮询、最小连接数、权重 hash);
在
RpcClient::call()
或RpcClient::getConnection()
内注入该策略类;实际运行时按配置动态切换策略,甚至可热更新。
✅ 体现策略模式 + 运行时可插拔能力
✅ 2. 限流模块扩展方式
限流属于“请求前过滤控制”,目标是防止系统过载,可通过以下几种方式接入:
接入点:在客户端发送请求前(即
Requestor::send()
之前)判断是否允许发送;实现方式:
可以通过一个
RateLimiter
类控制 QPS;支持令牌桶、漏桶算法;
拒绝请求时立即返回错误消息;
服务端也可以接入限流模块,在 Dispatcher 注册的
onRpcRequest()
中判断当前负载情况是否超出阈值,做保护性熔断。
你在整个开发过程中,最有成就感或最难解决的问题是什么?你是怎么解决的?
1.异步生命周期管理 + 2.客户端连接资源管理
1.Rpc调用模块的客户端中会提供 异步future调用call函数,promise异步设置结果 上层future get()获取结果。在call函数内部用make_shared创建promise对象并用shared_ptr指针进行管理,用输出行参数result返回和promise管理的future对象给上层。由Requestor的send()发送请求并对请求描述对象进行管理。等服务端处理返回应答时,Requestor模块接收到响应并找到请求描述对象,取出里面promise进行set_value设置结果,上层future就绪get()就可以获取结果了。
但问题在于我们的shared_ptr<promise<JSON::Value>>对象是一个局部对象,出了作用域就会析构,后面回调函数给promise对象设置结果时,promise对象为空,这样上层永远获取不到结果。
针对这种情况,我采用的是bind值绑定+外部保存资源 延长生命周期。bind绑定指向promise对象的shared_ptr指针 。本质就是 让一个生命周期更长的shared_ptr指向promise对象,引用计数不为0 promise对象就不会析构,从而达到延长生命周期的效果。
这里要注意两点 1.bind值绑定 2.外部拷贝保存资源
1.bind不要用引用绑定,如果是引用绑定shared_ptr可能会出现悬垂引用,外部还没进行保存就析构了。
2.bind值绑定并不会延长promise对象的生命周期,必须在外部保存一份资源 让一个生命周期更长的shared_ptr指向promise才能延长生命周期。具体就是在Requestor里面保存的请求描述对象中拷贝了promise的shared指针。
因此我们的异步futuer调用call函数不是走异步furtuer的send函数,而是走异步回调函数。应答返回过来时,调用bind绑定的回调函数 在里面set_value设置结果,而不是直接设置。
2.关于客户端连接 选择短链接还是长连接
一开始我是选择短链接的方案,即客户端调用完就关闭。但这就会有一个问题,在rpc调用的时候rpc客户端用Requestor模块send发送完消息 rpc调用结束后,该客户端就会析构。但后面服务端返回应答,Requestor模块的回调函数中虽然找到请求描述对象 设置结果,但客户端没了后续怎么get()获取结果呢?回调函数中设置的结果也就没有意义了。
所以在我的项目中 我选择的方案是长连接,客户端调用完时,连接也不会进行析构。连接不会析构,我们就需要对这些客户端连接进行管理。这里我是用连接池对客户端连接进行管理的,这样的好处是下次再调用时 相同的话可以复用连接,客户端进行rpc调用时 获取到目标主机的地址,不是直接建立于目标主机服务端的连接,而是在连接池中找有没有连接可以进行复用,没有再创建并加入连接池中。后续服务提供者断开连接了,处理服务下线的回调函数中会调用_offline_cb删除连接池中的客户端连接。
客户端发起rpc调用时 把连接加入连接池,客户端处理服务下线时会删除连接池中对应的连接。
综合以上,无论是功能实现还是效率上,选择长连接都是更合适的。
1.能支持异步调用 get()获取结果。
2.可以从连接池中复用之前的连接 不用频繁的创建和释放连接,提高效率。
如果像短连接频繁创建释放,一是会浪费资源 降低效率。二是如果客户端频繁进行rpc调用,客户端连接会出现大量TIME_WAIT状态 占用端口号不释放(等待2MSL时间后才会完全关闭 释放端口号),导致端口号资源耗尽 无法向服务端建立起连接。
项目拓展
1.发布/订阅模块如果先启动发布者,再启动订阅者,能否收到消息?怎么做才能让收到消息?
不能收到消息
因为目前的做法是当我们发布一条消息到服务器之后,服务器会遍历所有的订阅者,将这条消息转发给订阅者,此时订阅者列表为空,当订阅者启动订阅的时候,这条消息已经没有了,本质上是因为我们在服务器没有对消息进行持久化存储。
每个 Topic 绑定一个消息队列,进行消息发布时进行入队操作。每个sub订阅者维护一个消费位置offset 表示读到第几条消息了。进行订阅时 服务端会订阅者的offset开始从队列中取出所有未消费的消息,主动推送给订阅者,topic主题中还保存每个订阅者的offset(map<conn,offset>) 推送消息出队列时更新,订阅者接收到消息时更新offset,这样服务端客户端都保存offset。
简单来说:服务端topic用一个队列存储主题消息,发布时入队,订阅时出队(移动offset表示逻辑出队列)。
2.为什么要有心跳检测机制?
因为rpc调用时 服务端可能会出现故障不再处理请求消息,但注册中心还会认为服务端存在 就导致发送到该主机的请求全部失败或超时。引入心跳检测机制,让服务端每各一定时间就向注册中心发送消息 表示我还在。注册中心维护一张表 记录服务端也就是服务提供者的状况,如果有服务端长时间没有发送心跳检测消息还没断开,注册中心就会认为该服务端异常 断开连接,并给对应发现者进行服务下线通知 以及删除对该服务的管理。
在实际的 RPC 框架运行过程中,服务端可能会因故障、死循环、崩溃等原因停止处理请求,但:
TCP 连接在底层不会立即断开;
注册中心仍然认为该服务节点“在线”;
导致路由模块持续将请求发送到失效节点,最终全部失败或超时,严重影响系统可用性。
解决方案:引入服务心跳机制
我们设计了服务端心跳机制,具体逻辑如下:
服务提供者(服务端)定时向注册中心发送心跳消息,表明自己仍在线且可服务;
注册中心维护一张服务状态表,记录每个服务节点的最后心跳时间;
若某个服务节点超过一定时间(如 15 秒)未发送心跳,注册中心将其判定为“异常离线”;
注册中心会立即通知所有订阅者(发现者)该服务已下线,并从服务发现表中移除该节点,避免继续路由请求到故障节点;
若后续该服务恢复上线,重新注册后即可恢复服务。
相关文章:
RPC常见问题回答
项目流程和架构设计 1.服务端的功能: 1.提供rpc调用对应的函数 2.完成服务注册 服务发现 上线/下线通知 3.提供主题的操作 (创建/删除/订阅/取消订阅) 消息的发布 2.服务的模块划分 1.网络通信模块 net 底层套用的moude库 2.应用层通信协议模块 1.序列化 反序列化数…...
数据分析和可视化:Py爬虫-XPath解析章节要点总结
重要知识点 XPath 概述:XPath 是一门可以在 XML 文件中查找信息的语言,也可用于 HTML 文件。它功能强大,提供简洁明了的路径表达式和多个函数,用于字符串、数值、时间比较等。1999 年成为 W3C 标准,常用于爬虫中抓取网…...
WIFI原因造成ESP8266不断重启的解决办法
一、报错 报错信息如下: 21:37:21.799 -> ets Jan 8 2013,rst cause:2, boot mode:(3,7) 21:37:21.799 -> 21:37:21.799 -> load 0x4010f000, len 3424, room 16 21:37:21.799 -> tail 0 21:37:21.799 -> chksum 0x2e 21:37:21.799 -> loa…...
OSI网络通信模型详解
OSI 模型就是把这整个过程拆解成了 7 个明确分工的步骤,每一层只负责自己那一摊事儿,这样整个系统才能顺畅运转,出了问题也容易找到“锅”在谁那。 核心比喻:寄快递 📦 想象你要把一份重要的礼物(你的数据…...
第五章 中央处理器
5.1 CPU的功能和基本构造 5.1.1 CPU的基本功能 5.1.2 CPU的基本结构 1.运算器 算术逻辑单元ALU 累加寄存器ACC 程序字状态寄存器PSW 计数器CT 暂存寄存器 通用寄存器组 移位器 通用寄存器供用户自由编程,可以存放数据和地址。而指令寄存器是专门用于存放指令的专用寄存器,…...
大模型学习入门——Day3:注意力机制
本系列笔记的教材:快乐学习大模型-DataWhale团队 注意力机制 注意力机制最先源于计算机视觉领域,其核心思想为当我们关注一张图片,我们往往无需看清楚全部内容而仅将注意力集中在重点部分即可。而在自然语言处理领域,我们往往也…...
C++ 学习笔记精要(二)
第一节 特殊类的设计 1. 一个类: 只能在堆上创建对象 关键点:自己控制析构 1.1 方法一: 使用delete禁掉默认析构函数 #include <iostream> using namespace std;class HeapOnly { public:HeapOnly(){_str new char[10];}~HeapOnly() delete;void Destroy(){delete[…...
博士,超28岁,出局!
近日,长沙市望城区《2025年事业引才博士公开引进公告》引发轩然大波——博士岗位年龄要求28周岁及以下,特别优秀者也仅放宽至30周岁。 图源:网络 这份规定让众多"高龄"博士生直呼不合理,并在社交平台掀起激烈讨论。 图源…...
macOS - 根据序列号查看机型、保障信息
文章目录 最近在看 MacBook 二手机,有个咸鱼卖家放个截图 说不清参数,于是想根据 序列号 查看机型。苹果提供了这样的网页: https://checkcoverage.apple.com/ (无需登录) 结果 2025-06-20(五)…...
C/C++ 高频八股文面试题1000题(一)
原作者:Linux教程,原文地址:C/C 高频八股文面试题1000题(一) 在准备技术岗位的求职过程中,C/C始终是绕不开的核心考察点。无论是互联网大厂的笔试面试,还是嵌入式、后台开发、系统编程等方向的岗位,C/C 都…...
C++ map 和 unordered_map 的区别和联系
C map 和 unordered_map 的区别和联系 map 和 unordered_map 都是 C 标准库中关联容器,用于存储键值对。它们的主要区别在于底层实现和性能特性,联系在于它们都提供了键值对的存储和访问功能。 区别: 特性mapunordered_map底层实现红黑树 …...
Sentinel实现原理
Sentinel 是阿里巴巴开源的分布式系统流量控制组件,主要用于服务保护,涵盖流量控制、熔断降级、系统负载保护等功能。 以下是 Sentinel 的实现原理,使用中文简要说明: 1. 总体架构 Sentinel 采用 轻量级 设计,分为 核…...
python打卡day37
疏锦行 知识点回顾: 1. 过拟合的判断:测试集和训练集同步打印指标 2. 模型的保存和加载 a. 仅保存权重 b. 保存权重和模型 c. 保存全部信息checkpoint,还包含训练状态 3. 早停策略 作业:对信贷数据集训练后保存权重…...
MySQL复杂查询优化实战:从多表关联到子查询的性能突破
文章目录 一、复杂查询性能瓶颈分析与优化框架二、多表关联查询的优化策略与实战1. JOIN顺序优化:基于成本估算的表关联策略2. 复合索引与JOIN条件优化3. 大表JOIN的分片处理 三、子查询优化:从嵌套到JOIN的转换艺术1. 标量子查询转换为JOIN2. EXISTS子查…...
LeetCode 680.验证回文串 II
目录 题目: 题目描述: 题目链接: 思路: 核心思路: 思路详解: 代码: C代码: Java代码: 题目: 题目描述: 题目链接: 680. 验证…...
window显示驱动开发—输出合并器阶段
逻辑管道中的最后一步是通过模具或深度确定可见性,以及写入或混合输出以呈现目标,这可以是多种资源类型之一。 这些操作以及输出资源 (呈现目标) 绑定在输出合并阶段定义。 1. 核心功能与管线定位 输出合并是渲染管线的最终固定功能阶段,负…...
单片机开发日志cv MDK-ARM工具链迁移到MAKE
核心经验: STM32H7 多 RAM 区域,外设相关数据段必须放在 AXI SRAM(RAM)区,不能放在 DTCMRAM,否则外设无法访问,程序表面正常但外设全失效。迁移工程时,务必检查链接脚本的内存分布&a…...
大模型与搜索引擎的技术博弈及未来智能范式演进
基于认知革命与技术替代的全景综述 一、大模型对搜索引擎的替代性分析:技术范式与市场重构 (1)技术原理的代际分野 传统搜索引擎遵循 "爬虫抓取 - 索引构建 - 关键词排序" 的三段式架构,其核心是基于 PageRank 算法的…...
Ajax-入门
Ajax: 全称Asynchronous JavaScript And XML,异步的JavaScript和XML。其作用有如下2点: 与服务器进行数据交换:通过Ajax可以给服务器发送请求,并获取服务器响应的数据。 异步交互:可以在不重新加载整个页面的情况下&a…...
FPGA基础 -- Verilog 共享任务(task)和函数(function)
Verilog 中共享任务(task)和函数(function) 的详细专业培训,适合具有一定 RTL 编程经验的工程师深入掌握。 一、任务(task)与函数(function)的基本区别 特性taskfunctio…...
c++set和pair的使用
set是C中的一种关联容器,具有以下特点: 存储唯一元素(不允许重复) 元素自动排序(默认升序) 基于红黑树实现(平衡二叉搜索树) 插入、删除和查找的时间复杂度为O(log n) 前言 在C…...
数据库中间件ShardingSphere5
一、高性能架构模式 数据库集群,第一种方式“读写分离”,第二种方式“数据库分片”。 1.1 读写分离架构 读写分离原理:将数据库读写操作分散到不同的节点上。 读写分离的基本实现: 主库负责处理事务性的增删改操作,…...
window显示驱动开发—使用状态刷新回调函数
用户模式显示驱动程序可以使用 Direct3D 运行时版本 10 State-Refresh回调函数 来实现无状态驱动程序或构建命令缓冲区前导数据。 Direct3D 运行时在调用 CreateDevice (D3D10 ) 函数时,向D3D10DDIARG_CREATEDEVICE结构的 pUMCallbacks 成员指向的D3D10DDI_CORELAY…...
windows11右击恢复为windows10
文章目录 前言一、问题描述二、解决方案 前言 为了解决win11的右击更多选项的问题 一、问题描述 win11的右键更多选项过于繁琐 二、解决方案 在windows11的终端管理员中输入如下代码: reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c…...
基于物联网的智能衣柜系统设计
标题:基于物联网的智能衣柜系统设计 内容:1.摘要 随着物联网技术的飞速发展,智能家居领域迎来了新的变革机遇。本研究的目的在于设计一种基于物联网的智能衣柜系统,以提升用户的衣物管理和使用体验。方法上,通过搭建物联网硬件平台ÿ…...
GM DC Monitor v2.0 卸载教程
以下俩种方法任选一种均可 第一种方法:一键自动卸载 进入到软件安装目录 卸载app 进入到app目录,运行一键卸载脚本:sh uninstall.sh 卸载es 进入到es目录,运行一键卸载脚本:sh uninstall.sh 卸载db 进入到db目录&a…...
C#上位机实现报警语音播报
我们在开发C#上位机时,有时候会需要将报警信息通过语音进行播报,今天跟大家分享一下具体的实现过程。 一、组件安装 首先我们创建好一个Windows窗体项目,然后添加System.Speech库引用。 点击引用,右击添加引用,在程…...
python自助棋牌室管理系统
目录 技术栈介绍具体实现截图系统设计研究方法:设计步骤设计流程核心代码部分展示研究方法详细视频演示试验方案论文大纲源码获取/详细视频演示 技术栈介绍 Django-SpringBoot-php-Node.js-flask 本课题的研究方法和研究步骤基本合理,难度适中…...
榕壹云婚恋相亲系统:ThinkPHP+UniApp打造高效婚配平台
引言 在数字化浪潮下,婚恋相亲行业正加速向线上迁移。榕壹云公司基于市场需求与技术积累,开发一款功能完备、技术开源的婚恋相亲小程序系统,为单身人士提供高效、安全的婚恋平台。本文将围绕系统背景、客户定位、核心技术、功能模块及优势场景展开详细解析,助力开发者与技…...
每日leetcode
2890. 重塑数据:融合 - 力扣(LeetCode) 题目 DataFrame report --------------------- | Column Name | Type | --------------------- | product | object | | quarter_1 | int | | quarter_2 | int | | quarter_3 | i…...
深入理解XGBoost(何龙 著)学习笔记(五)
深入理解XGBoost(何龙 著)学习笔记(五) 本文接上一篇,内容为线性回归,介绍三部分,首先介绍了"模型评估”,然后分别提供了线性回归的模型代码:scikit-learn的Linear…...
SelectDB 在 AWS Graviton ARM 架构下相比 x86 实现 36% 性价比提升
在海量数据分析中,追求高性价比已成为各大企业的主流趋势。ARM 架构凭借其高能效和低成本的特点,逐渐在数据中心崛起,成为理想的高性价比选择。基于 ARM 架构的 AWS Graviton 系列处理器,正是这一趋势的典型代表。Graviton 处理器…...
机器学习流量识别(pytorch+NSL-KDD+多分类建模)
本文主要实现以下功能,会提供完整的可运行的代码以及解释为什么这么设计。文章不会收费,若被限制查看,请私信我。 使用 NSL-KDD 数据集的CSV文件进行流量攻击检测,使用机器学习算法实现流量攻击检测,使用pytorch框架…...
三种经典算法无人机三维路径规划对比(SMA、HHO、GWO三种算法),Matlab代码实现
代码功能 该MATLAB代码用于对比三种元启发式优化算法(SMA、HHO、GWO三种算法, SMA黏菌算法、HHO哈里斯鹰优化算法、GWO灰狼优化算法) 在特定优化问题上的性能,运行环境MATLABR2020b或更高 : 初始化问题模型ÿ…...
FTTR+软路由网络拓扑方案
文章目录 网络拓扑软路由配置FTTR光猫路由器TPLink路由器配置WAN设置LAN设置 参考 网络拓扑 软路由配置 配置静态IP地址:192.168.1.100设置网关指向主路由的IP 设置自定义DNS服务器 开启DHCP 这一步很关键,可以让连上wifi的所有设备自动趴强。 FTTR光猫…...
服务器获取外网IP,并发送到钉钉
服务器获取外网IP,并发送到钉钉 import time import hmac import hashlib import base64 import urllib.parse import requests# 请填入你的钉钉机器人配置 access_token XXXX secret XXXX# 获取公网 IP def get_public_ip():try:response requests.get("…...
解决uni-app发布微信小程序主包大小限制为<2M的问题
一 问题说明 我想用uniapp开发多端应用,引入了uview组件库来美化样式,可发布为微信小程序却提示我代码质量不过关,主包代码量太大了: 二 问题分析 2.1 原生微信小程序开发代码质量限制: 1.主包代码大小不得大于2M&…...
魅族“换血”出牌:手机基本盘站不稳,想靠AI和汽车“改命”
撰稿|何威 来源|贝多财经 被吉利收购后,魅族逐渐转向在AI领域躬身耕作。 自2024年2月以“All in AI”正式宣告转型、喊出不再推出传统智能手机的豪言开始,这家曾以设计见长的手机厂商,将下半场押注在AI终端、AR眼镜与智能座舱系统上&#…...
原点安全入选 Gartner®“数据安全平台”中国市场指南代表厂商
2025年1月7日,全球权威咨询与分析机构 Gartner 发布《中国数据安全平台市场指南》(China Context: ‘Market Guide for Data Security Platforms’),北京原点数安科技有限公司(简称“原点安全”,英文名称&q…...
uni-app-配合iOS App项目开发apple watch app
假设你已经用uni-app开发好了一个iOS端的app,现在想要开发一个配套的apple watch app。改怎么去开发呢?是不是一头雾水,这篇文章就会介绍一些apple watch app开发的知识以及如何在uni-app开发的iOS app基础上去开发配套的watch app。 一、ap…...
如何理解Java反射机制
反射机制原理 反射是Java在运行时动态获取类信息、操作类属性和方法的能力。核心原理是JVM在类加载时创建Class对象,该对象包含类的完整结构信息。 关键类: Class:类的元数据入口 Field:类的成员变量 Method:类的方…...
SM3算法C语言实现(无第三方库,带测试)
一、SM3算法介绍 SM3算法是中国国家密码管理局(OSCCA)于2010年发布的商用密码散列函数标准,属于我国自主设计的密码算法体系之一 ,标准文档下载地址为:SM3密码杂凑算法 。SM3算法输出长度为256位(32字节&a…...
King’s LIMS 系统引领汽车检测实验室数字化转型
随着汽车保有量的持续攀升和车龄的增长,消费者对汽车的需求已悄然转变,从最初对外观和性能的追求,逐渐深化为对安全性、可靠性、耐久性、性能与舒适性以及智能化功能的全方位关注。这无疑让汽车检测行业在保障车辆质量、满足市场需求方面肩负…...
CppCon 2017 学习:Mocking Frameworks Considered
当然可以,下面是对 Fowler 的 Whiskey-Store 示例。 Fowler 的 Whiskey-Store 示例(坏设计) 贴出的类图是 Martin Fowler 在《重构》书中使用的一个教学用反面案例(故意设计得不合理),用来说明如何通过重…...
通过事件过滤器拦截QRadioButton点击事件
通过事件过滤器拦截QRadioButton点击事件 一、事件过滤器完整实现 1. 核心代码扩展(含注释) bool MainWindow::eventFilter(QObject* obj, QEvent* ev) {// 拦截所有QRadioButton的鼠标事件(包括点击、释放、双击)if (ev->ty…...
领码 SPARK 融合平台赋能工程建设行业物资管理革新——数智赋能,重塑中国模式新范式
摘要 工程建设行业正加速迈向数字化与精益化转型,物资管理成为项目成败的关键瓶颈。本文深入解析中国工程企业“项目部-物资部-企业项目管理部”三级协同的独特物资管理体系,聚焦集中采购与零星采购的统筹难题。基于领码 SPARK 融合平台,提出…...
“地标界爱马仕”再启:世酒中菜联袂陈汇堂共筑新会陈皮顶奢产业
“地标界爱马仕”再启战略新篇:世酒中菜联袂陈汇堂,共筑新会陈皮顶奢产业生态 ——中世国际与陈汇堂股权合作签约仪式在国际地理标志服务基地举行 江门市新会区,2025年6月20日——被誉为“地标界爱马仕”的全球顶奢品牌运营商世酒中菜 &…...
.Net Framework 4/C# 数据访问技术(ADO.NET)
一、数据库基础 (一) 数据库简介 数据库是按照数据结构来组织、存储和管理数据的仓库,是存储在一起的相关数据的集合。 (二) SQL 语言简介 SQL 是一种数据库查询和程序设计语言,用于存取数据以及查询,更新和管理关系型数据库系统。在编写 SQL 语句时,SQL 语句各关键字要以…...
北京京东,看看难度
最近由于三大外卖平台“打仗”,优惠券多到数不过来,一日三餐每个平台各点一单哈哈哈,正好最近组织内部还有朋友在北京的京东面试过,分享一下她的面经(Java岗): 1. Kafka消息不丢失问题…...
RPGMZ游戏引擎 如何手动控制文字显示速度
直接上代码 const _Window_Base_prototype_initialize Window_Base.prototype.initialize;Window_Base.prototype.initialize function(rect) {_Window_Base_prototype_initialize.call(this, rect);this.文字速度缓冲 0;}; this.文字速度缓冲 0; 进行缓冲 Window_Base…...