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返回给客户端。

一、项目背景与整体架构
  1. 你这个 RPC 框架项目的初衷是什么?你为什么要做这个项目?

主要是想了解一下网络通信和分布式架构,为了对服务端-客户端的通信机制更了解就实现了这个rpc项目。

  1. 你的 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提供回调函数处理主题操作/发布请求

  1. 你为什么选择自定义 JSON-RPC 协议?和 gRPC 或 Thrift 相比,你的设计有什么优势或不足?

用JSON的话,JSON反序列化和序列化操作更简单 其他的没有了解过。


二、客户端与服务端机制
  1. 客户端是如何发起一个 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. 你在服务端如何注册一个服务方法?具体在哪个模块处理服务注册?

1.首先rpc服务端模块 里面会包含一个服务注册的客户端,当服务端本地新上线了一个服务 除了本地进行记录,还得向服务注册中心通过服务注册客户端发送服务注册消息。

2.服务注册中心会像Dispathcer模块注册onServiceRequest()回调函数处理服务注册/发现的请求消息,后续操作就是跟新该服务的服务提供者列表,并给发现过该服务的发现者进行服务上线通知。

  1. 客户端怎么实现"同步调用"?你是怎么保证调用阻塞等待返回的?用了哪些 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++多态特性


三、消息处理与分发机制
  1. Dispatcher 模块的作用是什么?你是如何实现"消息类型到回调函数"的映射的?

1.Dispathcer模块 简单来说是消息分发处理模块,根据消息的类型调用对应模块注册的回调函数进行派发处理。

2.这里我们用一共哈希表map完成消息类型到回调函数的映射的,但这里有两中处理方法。

第一种,就是map的value值就是存储的回调函数,但这就需要注册的所有回调函数类型相同才能统一存储在map中。回调函数的参数都是两个,1.BaseConnect::ptr连接可以基类接收 2.消息类型 当然也可以用BaseMessage::ptr基类接收,但实际上传入的对象的对应的子类对象。这样虽然可以用map统一管理,但我们必须在回调函数中进行判断传入的消息对象是不是对应的子类类型,我们必须要去猜是不是,如果不是需要怎么处理,后续新增了其它消息子类是不是还需要更改逻辑处理,这明显不符合开闭原则。

这里项目中采用的第二种方法,即第二个参数就是用对应子类对象指针来接收。但这样函数类型不一样,还这么统一管理?

这里我是通过map存储一个同一个父类指针,通过虚函数调用机制,调用对应的子类对象 里面的回调函数。我先创建一个父类 里面有一个虚函数,用模板类根据消息类型创建对应的子类,这些子类继承于父类 并重写父类里面的虚函数,写入自己回调函数的处理逻辑。

简单来说就是 多态(虚函数调用机制)+模板类+继承 让map统一管理不同类型的回调函数

  1. 你是怎么实现类型安全的消息派发机制的?为啥不用函数指针而是用多态+模板?

各个模块在创建时 向Dispatcher模块注册处理消息的回调函数 传入的参数有1.消息类型 2.回调函数。Dispatcher里面通过模板函数根据消息类型 生成不同的子类,里面重写的虚函数再调用传入的回调函数。在map存储子类对象 用它们的父类统一接收。

后面调用的时候,根据类型找到map存储的父类指针根据虚函数调用机制 调用对应子类重写的虚函数 完成回调处理。

为什么不用函数指针,主要还是不想让回调函数用BaseMessgae::ptr接收子类对象,接收了里面还得判断 把父类指针dynamic_pointer_cast转换为子类指针,如果转换失败了 会返回空指针,需额外判断处理。

  1. JsonRequest 和 JsonResponse 有哪些子类?它们的继承结构设计的初衷是什么?

1.JsonRequest 请求类,有三个子类 分别对应三个主要功能。1.RpcRequest rpc调用请求

2.ServiceRequest 服务请求类 3.TopicRequest 消息请求类.

2.同理JsonResponse 应答类,也根据功能分为三个。

这样根据父类生成子类,主要还是让一些共同需要的部分放在基类中,子类再根据自己需要来自己实现函数 成员变量。比如说应答类都需要rcode响应码,就可以放在响应基类中 check()检测响应字段是否正确。如果子类需要根据自己定义的字段实现check()也可以重写。

这样的好处 新增子类类型只需继承父类并实现即可,符合开闭原则。整体的结构也更清晰。


四、注册中心与服务发现机制
  1. 服务注册中心是如何工作的?服务端是如何注册自己的服务信息的?

注册中心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>删除对发现者的管理就行。

  1. 如果服务端宕机,注册中心会怎么处理?你做了哪些"心跳检测"或"服务下线"机制?

针对这种情况,我的项目中没有实现相关的处理,但我可以对这种情况处理提个思路,心跳检测机制。

服务端每隔一段时间(5s)会给注册中心发送心跳包 表示自己还可以提供服务,注册中心记录每个服务端最后一次心跳时间,由定时任务(或事件循环)周期性检查,如果超过一定时间(10s)没上报心跳 就将服务端视为异常,注册中心就会断开连接 触发连接断开回调,1.服务端提供的每个method中的提供者列表去除该提供者 2.并删除map<conn,Pro>对该提供者的管理 3.最后给发现该提供者的方法的客户端发送服务下线的通知。

  1. 客户端如何进行服务发现?请求发送到注册中心返回的是什么信息?

服务发现客户端 构建服务发现请求消息并通过Requestor模块中同步send进行发送,Dispatcher模块根据消息类型调用 注册中心注册的回调函数onServiceRequest()处理服务发现请求。

请求发送到注册中心返回的是客户端进行发现method方法的提供者host地址列表,后面通过轮询的负载均衡策略选择一个给客户端。


五、主题发布/订阅模块(Pub/Sub)
  1. 你的框架实现了"主题发布/订阅"机制,这一块和传统 RPC 调用有什么区别?

1.通信方式:传统的RPC调用 一般是一个服务端对应一个客户端,而主题消息发布是一个服务端对应多个客户端 对订阅该主题的所有客户端进行广播。

2.控制方向:传统RPC是客户端端主动发送请求消息,而服务端是被动返回应答的。而在主题信息发布中服务端是接收到消息发布请求,对订阅主题的客户端是主动进行发送消息。

3.同步 耦合:相比传统的同步RPC请求-应答模型,我们的主题发布/订阅是基于事件驱动模型,异步非阻塞 客户端不需要阻塞等待响应,由服务端主动推送。解耦性强 发布者不需要知道订阅者是谁,只需要向主题服务端发布消息就可以。

对比维度 传统 RPC 调用 主题发布/订阅
控制方向 客户端主动请求 服务端主动推送
通信方式 一对一 一对多
同步性 同步阻塞等待 异步事件驱动
耦合性 紧耦合 松耦合
适用场景 精准调用,事务请求 广播、推送、订阅通知
实现关键 请求封装 + 回调映射 主题管理 + 回调路由
  1. 多个客户端订阅同一个主题,你是如何处理每个客户端的回调函数的?

首先客户端订阅一个主题,服务端接收主题消息发布 向订阅者客户端进行广播主动发送消息,如何处理消息需要客户端自己定义。也就是订阅者客户端会向Dispatcher模块注册onPublish()回调函数处理服务端主动推送的消息,客户端本地会有哈希表map<topic_name,cb>维护主题名到对应回调函数的映射,让本地对应的回调函数处理推送的消息。

简单来说,就是每个客户端本地会维护好一张哈希表 主题名->该主题回调函数的映射。

  1. 客户端发布主题消息,服务端是如何进行广播的?如何避免回调未注册问题?

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删除刚注册的回调函数。


六、网络与协议设计
  1. 你使用了 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循环线程放在客户端前台,就会导致阻塞 将影响主线程逻辑。而服务端就是为了处理客户端发送的消息,可以循环运行。

  1. 你如何解决 TCP 粘包问题?数据接收后如何区分一条完整的 JSON 消息?

针对TCP粘包问题,我是自己定义了应用层通信协议LVProtocol,采用定长头部 + 变长正文的形式。具体来说是 固定头部由三个4字节字段组成:消息长度、类型、ID长度,后两个变长字段 消息ID+body正文。

接收数据后我们会放在缓冲区中,通过访问前4字节获取该报文的长度 再对比当前缓冲区存储数据的大小,如果缓冲区存储数据大小>该报文长度,就认为缓冲区中至少有一条完整消息。

  1. 你在客户端和服务端的协议抽象层(如 BaseProtocol)里做了哪些处理?

BaseProtocol里面实现了接口的统一,这样上层具象层不需要管底层实现是怎么样的,直接用BaseProtocol提供的统一接口就可以 具体接口有3个 1.canProcessed判断缓冲区是否有完整报文 2.onMessage从缓冲区中解析完整报文(去报头+反序列化) 3.serialize 准备应答(序列化+添报头)。BaseProtocol底层具体实现是自定义实现的LVProtocol协议,后续换其它协议只需实现对应的 BaseProtocol 子类即可,不会影响上层代码 符合开闭原则。


七、扩展性与项目亮点
  1. 你这个框架的扩展性在哪里体现?如果后续想加负载均衡或限流模块,怎么接入?

我的项目解耦性好,新增业务时 不会影响原有业务,把消息的回调处理函数注册给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. 你在整个开发过程中,最有成就感或最难解决的问题是什么?你是怎么解决的?

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 连接在底层不会立即断开;

  • 注册中心仍然认为该服务节点"在线";

  • 导致路由模块持续将请求发送到失效节点,最终全部失败或超时,严重影响系统可用性。


解决方案:引入服务心跳机制

我们设计了服务端心跳机制,具体逻辑如下:

  1. 服务提供者(服务端)定时向注册中心发送心跳消息,表明自己仍在线且可服务;

  2. 注册中心维护一张服务状态表 ,记录每个服务节点的最后心跳时间

  3. 若某个服务节点超过一定时间(如 15 秒)未发送心跳,注册中心将其判定为"异常离线"

  4. 注册中心会立即通知所有订阅者(发现者)该服务已下线,并从服务发现表中移除该节点,避免继续路由请求到故障节点;

  5. 若后续该服务恢复上线,重新注册后即可恢复服务。

相关推荐
Code季风15 分钟前
深入实战 —— Protobuf 的序列化与反序列化详解(Go + Java 示例)
java·后端·学习·rpc·golang·go
我是谁谁38 分钟前
在 UniApp 开发中,由于需要跨平台(小程序、H5、App 等),样式兼容性是常见挑战
前端·面试
技术蔡蔡1 小时前
Flutter真实项目中bug解决详解
flutter·面试·android studio
工呈士1 小时前
HTTP 请求方法与状态码
前端·后端·面试
独行soc1 小时前
2025年渗透测试面试题总结-2025年HW(护网面试) 01(题目+回答)
linux·科技·安全·面试·职场和发展·区块链
小毛驴8502 小时前
httpclient实现http连接池
网络·网络协议·http
独行soc2 小时前
2025年渗透测试面试题总结-2025年HW(护网面试) 02(题目+回答)
linux·科技·安全·面试·职场和发展·渗透测试·区块链
2501_916013742 小时前
iOS应用启动时间优化:通过多工具协作提升iOS App性能表现
websocket·网络协议·tcp/ip·http·网络安全·https·udp
伊成3 小时前
Java面试高频面试题【2025最新版】
java·开发语言·面试