ns模块
ns模块对应sylar/ns
在上一篇文章中我们已经介绍了 sylar 实现的 RPC 框架是基于模块化分发机制实现的服务端对客户端请求的处理。并且介绍了 module.h
中的类 Module
、RockModule
,其中 RockModule
是一个继承自 Module
的抽象类,专门用于处理基于 Rock 协议的模块化服务逻辑。
而今天要介绍的 ns 模块,就是基于 RockModule
实现的一个具体业务模块。也就是说ns 模块本质上是一个支持 Rock 协议的服务端插件,它实现了 handleRockRequest
和 handleRockNotify
等方法,用于处理客户端发来的服务注册、发现、心跳、订阅等一系列 RPC 请求。作为 Sylar 框架中的核心系统模块之一,ns 模块承担着“服务注册中心”的角色,负责维护全局服务节点的元信息,并对外提供统一的服务发现接口。
ns_protocol.h
该文件是构建在 Rock 协议(RPC传输层协议)之上的一组应用层协议定义文件,用于支撑服务注册中心(NameServer,简称 ns 模块)的核心数据结构和操作语义。这个文件不仅定义了服务节点的信息模型,还建立了一整套围绕域名、命令类型(注册、心跳、查询等)的多层次节点分类体系,并通过线程安全机制确保在并发环境下的正确性和性能。
核心类
NSCommand
定义一组预设的命令类型,用于标识网络服务节点管理系统中不同操作的指令。这些命令会配合 .proto 文件中定义的 protobuf 消息(如 ns_protobuf.proto)通过 Rock 协议传输,形成应用层 RPC 协议语义。
enum class NSCommand {
// 注册节点信息
// 当新节点加入系统时,通过此命令将节点信息(如 IP、端口、权重)注册到服务管理模块中。
REGISTER = 0x10001,
// 查询节点信息
// 客户端或其他服务需要查询某个节点的详细信息(如 IP、端口、权重)时使用此命令
QUERY = 0x10002,
// 设置黑名单
// 将某个节点加入黑名单(例如因异常行为被禁用),防止其继续参与服务
SET_BLACKLIST = 0x10003,
// 查询黑名单
// 获取当前黑名单中的节点列表,用于监控或告警
QUERY_BLACKLIST = 0x10004,
// 心跳
// 节点定期发送心跳包,表明自身存活状态,防止被误判为失效节点
TICK = 0x10005
};
NSNotify
定义一组预设的通知类型,用于标识系统中需要广播或处理的特定事件或状态变化
enum class NSNotify {
NODE_CHANGE = 0x10001
};
NSNode
表示网络服务中的节点信息,用于存储和管理节点的基本属性(如IP、端口、权重)及唯一标识。
成员变量
节点的唯一ID(64位无符号整数)。
uint64_t m_id;
节点的IP地址(字符串格式)。
std::string m_ip;
节点的端口号(16位无符号整数)。
uint16_t m_port;
节点的权重值(32位无符号整数)。用于进行负载均衡
uint32_t m_weight;
成员函数
GetID()
根据ip和端口号生成该Node的唯一id,确保同一网络环境下的全局唯一性。
uint64_t NSNode::GetID(const std::string& ip, uint64_t port) {
in_addr_t ip_addr = inet_addr(ip.c_str());
uint64_t v = (((uint64_t)ip_addr) << 32) | port;
return v;
}
对于成员的get/set方法
const std::string& getIp() const { return m_ip; }
uint16_t getPort() const { return m_port; }
uint32_t getWeight() const { return m_weight; }
void setWeight(uint32_t v) { m_weight = v; }
uint64_t getId() const { return m_id; }
NSNodeSet
管理一组 NSNode 节点的集合类,提供线程安全的操作接口。
成员变量
关联的命令类型(如 NSCommand 中的值),用于标识集合的用途。
uint32_t m_cmd;
存储节点的 map,键为节点唯一 ID,值为 NSNode 实例。
std::map<uint64_t, NSNode::ptr> m_datas;
m_cmd
:这个成员表示当前节点集合(NSNodeSet
)所对应的“命令类型”,用于标识该集合的用途。例如,如果 m_cmd
是 REGISTER
,则该集合用于存储注册上来的正常服务节点;如果是 SET_BLACKLIST
,则代表存储的是被列入黑名单的节点。也就是说,它是这个节点集合的“功能标签”。
m_datas:这个成员是实际存储节点数据的容器。它是一个以节点唯一 ID(由 IP 和端口组成)为键、以节点对象(NSNode::ptr
)为值的映射关系,用于快速查找、增删、管理节点信息。该结构确保集合中每个节点具有唯一性,并支持高效访问。
成员函数
提供了对m_datas的增删改查操作
void NSNodeSet::add(NSNode::ptr info) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_datas[info->getId()] = info;
}
NSNode::ptr NSNodeSet::del(uint64_t id) {
NSNode:; ptr rt;
sylar::RWMutex::WriteLock lock(m_mutex);
auto it = m_datas.find(id);
if (it != m_datas.end()) {
rt = it->second;
m_datas.erase(it);
}
return rt;
}
NSNode::ptr NSNodeSet::get(uint64_t id) {
sylar::RWMutex::ReadLock lock(m_mutex);
auto it = m_datas.find(id);
return it == m_datas.end() ? nullptr : it->second;
}
void NSNodeSet::listAll(std::vector<NSNode::ptr>& infos) {
sylar::RWMutex::ReadLock lock(m_mutex);
for (auto& i : m_datas) {
infos.push_back(i.second);
}
}
NSDomain
管理与域名关联的节点集合容器,支持按命令类型分类管理节点集合(NSNodeSet)。
成员变量
关联的域名,用于标识该 NSDomain 管理的节点范围。
std::string m_domain;
存储节点集合的 map,键为命令类型(如 NSCommand 中的值),值为 NSNodeSet 实例。
std::map<uint32_t, NSNodeSet::ptr> m_datas;
m_domain:表示当前 NSDomain
所管理的“域名”。在注册中心中,服务节点会按“服务名”或“业务域名”进行分类管理,m_domain
就是这个分类的依据。比如 "user.service"
或 "order.api"
,用于将属于不同服务的节点隔离开来,避免混淆。
m_datas:这个成员是该域名下所有节点集合的容器,它以命令类型(NSCommand
)为键,对应的节点集合(NSNodeSet
)为值,用于按用途进一步分类管理。
成员函数
提供了对成员的设置与获取
void NSDomain::add(NSNodeSet::ptr info) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_datas[info->getCmd()] = info;
}
NSNodeSet::ptr NSDomain::get(uint32_t cmd) {
sylar::RWMutex::ReadLock lock(m_mutex);
auto it = m_datas.find(cmd);
return it == m_datas.end() ? nullptr : it->second;
}
void NSDomain::add(uint32_t cmd, NSNode::ptr info) {
auto ns = get(cmd);
if (!ns) {
ns.reset(new NSNodeSet(cmd));
add(ns);
}
ns->add(info);
}
void NSDomain::del(uint32_t cmd) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_datas.erase(cmd);
}
NSNode::ptr NSDomain::del(uint32_t cmd, uint64_t id) {
auto ns = get(cmd);
if (!ns) {
return nullptr;
}
auto info = ns->del(id);
if (!ns->size()) {
del(cmd);
}
return info;
}
NSNodeSet::ptr NSDomain::get(uint32_t cmd) {
sylar::RWMutex::ReadLock lock(m_mutex);
auto it = m_datas.find(cmd);
return it == m_datas.end() ? nullptr : it->second;
}
void NSDomain::listAll(std::vector<NSNodeSet::ptr>& infos) {
sylar::RWMutex::ReadLock lock(m_mutex);
for (auto& i : m_datas) {
infos.push_back(i.second);
}
}
NSDomainSet
管理一组 NSDomain 域名集合的容器类,支持按域名分类管理节点集合(NSNodeSet)。
成员变量
std::map<std::string, NSDomain::ptr> m_datas;
m_datas:存储 NSDomain 实例的 map,键为域名(字符串),值为 NSDomain 实例,每个域名对应一个 NSDomain,用于管理该域名下的节点集合(NSNodeSet)。
成员函数
对于m_datas的增删改查
void NSDomainSet::add(NSDomain::ptr info) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_datas[info->getDomain()] = info;
}
void NSDomainSet::del(const std::string& domain) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_datas.erase(domain);
}
NSDomain::ptr NSDomainSet::get(const std::string& domain, bool auto_create) {
{
sylar::RWMutex::ReadLock lock(m_mutex);
auto it = m_datas.find(domain);
if (!auto_create) {
return it == m_datas.end() ? nullptr : it->second;
}
}
sylar::RWMutex::WriteLock lock(m_mutex);
auto it = m_datas.find(domain);
if (it != m_datas.end()) {
return it->second;
}
NSDomain::ptr d(new NSDomain(domain));
m_datas[domain] = d;
return d;
}
void NSDomainSet::del(const std::string& domain, uint32_t cmd, uint64_t id) {
auto d = get(domain);
if (!d) {
return;
}
auto ns = d->get(cmd);
if (!ns) {
return;
}
ns->del(id);
}
void NSDomainSet::listAll(std::vector<NSDomain::ptr>& infos) {
sylar::RWMutex::ReadLock lock(m_mutex);
for (auto& i : m_datas) {
infos.push_back(i.second);
}
}
该文件总共实现了四个类
| 类名 | 说明
| ------------- | ---------------------------------
| `NSNode` | 表示单个节点(ip、port、weight)
| `NSNodeSet` | 某一类节点集合(如注册节点、黑名单节点)
| `NSDomain` | 某个域名下的所有节点分类(每个 cmd 对应一个 NodeSet)
| `NSDomainSet` | 所有域名下的管理(整个系统的节点结构顶层)
这种设计形成了一个域名 -> 命令类型 -> 节点 ID 的 3 层树状结构,非常适合注册中心服务或配置中心中对节点进行分类和查询。NSDomainSet 是顶级容器,通过 std::map<std::string, NSDomain::ptr> 管理多个 NSDomain 实例,每个 NSDomain 对应一个域名;NSDomain 通过 std::map<uint32_t, NSNodeSet::ptr> 管理多个 NSNodeSet 实例,每个 NSNodeSet 对应一个命令类型(如注册、查询);NSNodeSet 通过 std::map<uint64_t, NSNode::ptr> 管理多个 NSNode 实例,每个 NSNode 表示一个节点的 IP、端口和权重等信息。所有层级均通过线程安全的读写锁(sylar::RWMutex)保护数据操作,形成从域名 → 命令类型 → 节点 ID 的多级分类管理结构。系统实现了对分布式节点的高效分类管理。
ns_protobuf.ptoto
这个 .proto
文件定义的是客户端和 NameServer(ns 模块)之间通信时用到的消息格式,主要用于描述服务注册、查询和变更通知等场景下的数据结构。它配合 Rock 协议使用,负责构造和解析传输内容中的 m_body
部分,是整个服务治理过程中客户端和服务端能正常“对话”的基础。
消息类型
Node
表示一个服务节点
message Node {
optional string ip = 1; // ip地址
optional uint32 port = 2; // 端口号
optional uint32 weight = 3; // 节点权重,用于负载均衡策略
}
RegisterInfo
一条注册项,表示某个节点提供了某个域名下的某些命令服务
message RegisterInfo {
optional string domain = 1; //域名
repeated uint32 cmds = 2; // 该服务支持的命令编号
optional Node node = 3; // 提供该服务的节点信息
}
RegisterRequest
客户端向NameServer发送的注册请求,可能包含多个服务注册项
message RegisterRequest {
repeated RegisterInfo infos = 1; // 注册信息列表
}
RegisterRequest {
infos: [
{
domain: "order_service"
cmds: [1001, 1002]
node: {
ip: "192.168.1.100"
port: 8080
weight: 10
}
},
{
domain: "user_service"
cmds: [2001]
node: {
ip: "192.168.1.101"
port: 9000
weight: 8
}
}
]
}
QueryRequest
客户端向NameServer发起的服务查询请求
message QueryRequest {
repeated string domains = 1; // 要查询的服务域名列表
}
QueryRequest {
domains: ["order_service", "user_service"]
}
NodeInfo
NameServer 响应服务查询或变更通知时使用的服务节点结构体
message NodeInfo {
optional string domain = 1; // 服务域名
optional uint32 cmd = 2; // 命令编号
repeated Node nodes = 3; // 提供该服务的节点列表
}
QueryResponse
NameServer 返回给客户端的服务节点查询响应
message QueryResponse {
repeated NodeInfo infos = 1; // 每个查询域名对应的节点信息集合
}
QueryResponse {
infos: [
{
domain: "order_service"
cmd: 1001
nodes: [
{ ip: "192.168.1.100", port: 8080, weight: 10 },
{ ip: "192.168.1.101", port: 8080, weight: 5 }
]
},
{
domain: "user_service"
cmd: 2001
nodes: [
{ ip: "192.168.1.120", port: 9000, weight: 10 }
]
}
]
}
NotifyMessage
NameServer 主动通知客户端服务节点变更(新增、删除)的消息结构
message NotifyMessage {
repeated NodeInfo dels = 1; // 被移除的服务节点(注销)
repeated NodeInfo updates = 2; // 新增或变更的服务节点(注册或更新)
}
NotifyMessage {
dels: [
{
domain: "order_service"
cmd: 1001
nodes: [
{ ip: "192.168.1.101", port: 8080, weight: 5 }
]
}
],
updates: [
{
domain: "order_service"
cmd: 1001
nodes: [
{ ip: "192.168.1.100", port: 8080, weight: 20 } 权重更新
]
}
]
}
ns_client.h
核心类
NSClient
该类是 Sylar 框架中用于连接 NameServer(名称服务中心)的客户端类,它继承自 RockConnection
,通过 Rock 协议与服务端通信。其主要作用是维护客户端所关注的服务域名列表,并支持向 NameServer 主动发起查询请求,获取每个域名下的服务节点信息。同时,它还具备监听服务端变更通知(如节点上线、下线或权重变更)并自动更新本地缓存的能力,确保客户端始终拥有最新的服务节点视图。
成员变量
当前查询的域名集合:存储了客户端想向NameServer查询哪些服务
std::set<std::string> m_queryDomains;
本地缓存的服务节点信息:存储了从NameServer查询得到的结果,是查询到的 服务-命令-节点信息 的本地缓存。
NSDomainSet::ptr m_domains;
请求序列号,用于标识请求/响应
uint32_t m_sn = 0;
定时器:用于周期性任务
sylar::Timer::ptr m_timer;
成员函数
query()
是向服务端(NameServer)发送包含所有查询域名的请求,通过 Rock 协议获取对应的服务节点列表,并将响应结果反序列化后,构造成本地的服务缓存(NSDomainSet
),以便客户端后续进行服务调用和负载均衡选择。该函数是客户端实现动态服务发现机制的核心入口,确保客户端随时拥有最新的服务节点信息。
RockResult::ptr NSClient::query() {
// 创建一个 Rock 协议的请求对象
sylar::RockRequest::ptr req = std::make_shared<sylar::RockRequest>();
// 设置请求序列号(用于唯一标识请求)
req->setSn(sylar::Atomic::addFetch(m_sn, 1));
// 设置命令为 QUERY(服务查询)
req->setCmd((int)NSCommand::QUERY);
// 创建一个 protobuf 消息 QueryRequest,用于携带查询的域名信息
auto data = std::make_shared<sylar::ns::QueryRequest>();
// 加读锁保护 m_queryDomains(当前需要查询的域名集合)
sylar::RWMutex::ReadLock lock(m_mutex);
if (m_queryDomains.empty()) {
// 如果没有任何域名需要查询,返回一个空的响应结构
return std::make_shared<RockRequest>(0, 0, nullptr, nullptr);
}
// 遍历所有待查询的域名,加入 protobuf 消息中
for (auto& i : m_queryDomains) {
data->add_domains(i);
}
lock.unlock(); // 提前释放读锁
// 序列化 QueryRequest 并设置为 RockRequest 的 payload(即 m_body)
req->setAsPB(*data);
// 发送请求并等待服务端响应,超时时间为 1000ms
auto rt = request(req, 1000);
// 处理响应内容
do {
if (!rt->response) {
// 如果没有收到响应,记录错误日志
SYLAR_LOG_ERROR(g_logger) << "query error result=" << rt->result;
break;
}
// 尝试将响应反序列化为 QueryResponse 类型(protobuf)
auto rsp = rt->response->getAsPB<sylar::ns::QueryResponse>();
if (!rsp) {
// 如果反序列化失败,记录错误
SYLAR_LOG_ERROR(g_logger) << "invalid data not QueryResponse";
break;
}
// 临时构造一个新的 NSDomainSet 用于更新本地缓存
NSDomainSet::ptr domains(new NSDomainSet);
// 遍历 QueryResponse 中每条服务信息(NodeInfo)
for (auto& i : rsp->infos()) {
// 如果该域名并不在当前客户端查询列表中,跳过(防止收到多余数据)
if (!hasQueryDomain(i.domain())) {
continue;
}
// 获取(或创建)该域名对应的 NSDomain
auto domain = domains->get(i.domain(), true);
uint32_t cmd = i.cmd();
// 遍历每个节点,构造 NSNode 并加入域名/命令映射
for (auto& n : i.nodes()) {
NSNode::ptr node(new NSNode(n.ip(), n.port(), n.weight()));
// 过滤非法节点(ID 的高 32 位为 0 通常表示 ID 不合法)
if (!(node->getId() >> 32)) {
SYLAR_LOG_ERROR(g_logger) << "invalid node: "
<< node->toString();
continue;
}
// 将该节点添加到 domain->cmd 的节点集合中
domain->add(cmd, node);
}
}
// 更新本地服务节点缓存(用新数据替换旧的 m_domains)
m_domains->swap(*domains);
} while (false);
// 返回请求的结果(包括 response/错误码等)
return rt;
}
onQueryDomainChange()
当客户端的查询域名列表m_queryDomains发生变化(增删改)时,若当前已与 NameServer 保持连接,则立即向服务端重新发起一次查询请求(query()
),以获取最新的服务节点信息。
void NSClient::onQueryDomainChange() {
if (isConnected()) {
query();
}
}
对维护的查询域名容器m_queryDomains进行增删改查操作
// 获取当前客户端正在关注(查询)的所有服务域名集合
const std::set<std::string>& NSClient::getQueryDomains() {
sylar::RWMutex::ReadLock lock(m_mutex);
return m_queryDomains;
}
// 设置客户端当前要查询的服务域名集合,并触发更新逻辑
void NSClient::setQueryDomains(const std::set<std::string>& v) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_queryDomains = v;
lock.unlock();
onQueryDomainChange();
}
// 向查询域名集合中新增一个域名,并触发更新逻辑
void NSClient::addQueryDomain(const std::string& domain) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_queryDomains.insert(domain);
lock.unlock();
onQueryDomainChange();
}
// 判断某个服务域名是否已经在查询列表中
bool NSClient::hasQueryDomain(const std::string& domain) {
sylar::RWMutex::ReadLock lock(m_mutex);
return m_queryDomains.count(domain) > 0;
}
// 从查询域名集合中移除一个域名,并触发更新逻辑
void NSClient::delQueryDomain(const std::string& domain) {
sylar::RWMutex::WriteLock lock(m_mutex);
m_queryDomains.erase(domain);
lock.unlock();
onQueryDomainChange();
}
onTimer()
实现客户端与 NameServer 之间的心跳机制和服务信息定期刷新机制。它会定期构造一个携带 TICK
命令的 RockRequest
并发送给服务端,以表明客户端当前仍然在线、连接仍然有效。如果心跳失败则记录错误日志;紧接着,它还会主动调用一次 query()
,向 NameServer 查询当前所有感兴趣的服务域名的最新节点信息,从而确保客户端缓存中的服务节点始终保持最新。
void NSClient::onTimer() {
sylar::RockRequest::ptr req = std::make_shared<sylar::RockRequest>();
req->setSn(sylar::Atomic::addFetch(m_sn, 1));
req->setCmd((uint32_t)NSCommand::TICK);
auto rt = request(req, 1000);
if (!rt->response) {
SYLAR_LOG_ERROR(g_logger) << "tick error result=" << rt->result;
}
sleep(1);
query();
}
onConnect()
当前NSClient在于NameServer建立连接后执行的回调函数
在连接建立后,先取消旧的定时器(如果有),然后创建一个新的 30 秒周期性定时器定期执行onTimer,定期进行心跳验证并更新客户端缓存的服务节点信息,接着异步调度执行一次服务查询操作 query()
,确保连接建立后尽快同步服务节点信息。
bool NSClient::onConnect(sylar::AsyncSocketStream::ptr stream) {
if (m_timer) {
m_timer->cancel();
}
auto self = std::dynamic_pointer_cast<NSClient>(shared_from_this());
m_timer = m_iomanager->addTimer(30 * 1000, std::bind(&NSClinet::onTimer, self), true);
m_iomanager->schedule(std::bind(&NSClinet::query, self));
return true;
}
onNotify()
处理服务端发来的通知(Notify),当某些服务节点变更(增加或删除)时,更新本地 m_domains 中的服务节点缓存。
bool NSClient::onNotify(sylar::RockNotify::ptr nty, sylar::RockStream::ptr stream) {
do {
// 判断通知类型是否为服务节点变更通知
if (nty->getNotify() == (uint32_t)NSNotify::NODE_CHANGE) {
// 将通知内容反序列化为 NotifyMessage protobuf 对象
auto nm = nty->getAsPB<sylar::ns::NotifyMessage>();
if (!nm) break;
// 处理被移除的节点列表
for (auto& i : nm->dels()) {
// 如果客户端没有订阅该域名,跳过
if (!hasQueryDomain(i.domain())) continue;
// 从本地缓存中获取对应域名的服务节点集合
auto domain = m_domains->get(i.domain());
if (!domain) continue;
int cmd = i.cmd();
// 遍历被移除的节点,逐个从本地缓存中删除
for (auto& n : i.nodes()) {
NSNode::ptr node(new NSNode(n.ip(), n.port(), n.weight()));
domain->del(cmd, node->getId());
}
}
// 处理新增或更新的节点列表
for (auto& i : nm->updates()) {
// 如果客户端没有订阅该域名,跳过
if (!hasQueryDomain(i.domain())) continue;
// 获取或创建对应域名的服务节点集合
auto domain = m_domains->get(i.domain(), true);
if (!domain) continue;
int cmd = i.cmd();
// 遍历新增或更新的节点,逐个添加或更新到本地缓存
for (auto& n : i.nodes()) {
NSNode::ptr node(new NSNode(n.ip(), n.port(), n.weight()));
// 只添加有效的节点(节点ID高32位非0)
if (node->getId() >> 32) {
domain->add(cmd, node);
} else {
SYLAR_LOG_ERROR(g_logger) << "invalid node: " << node->toString();
}
}
}
}
} while (false);
return true;
}
init() && uninit()
init()
用于初始化客户端,绑定连接建立、断开和通知处理的回调函数;
uninit()
则用于清理客户端状态,解绑回调并取消定时器。
void NSClient::init() {
auto self = std::dynamic_pointer_cast<NSClient>(shared_from_this());
setConnectCb(std::bind(&NSClient::onConnect, self, std::placeholders::_1));
setDisconnectCb(std::bind(&NSClient::onDisconnect, self, std::placeholders::_1));
setNotifyHandler(std::bind(&NSClient::onNotify, self
,std::placeholders::_1, std::placeholders::_2));
}
void NSClient::uninit() {
setConnectCb(nullptr);
setDisconnectCb(nullptr);
setNotifyHandler(nullptr);
if(m_timer) {
m_timer->cancel();
}
}
name_server_module.h
核心类
NSClientInfo
表示一个已注册的客户端节点及其所注册的服务信息。NSClientInfo 封装了一个客户端节点的元信息,包括:
* - 基础节点信息(IP、端口、权重)由 `NSNode` 表示;
* - 注册的服务清单:即该客户端在哪些域名下注册了哪些命令(如 REGISTER、QUERY 等)。
这个类用于 NameServer 侧,用来记录当前有哪些客户端节点接入了注册中心、并提供了哪些域名与命令的服务。
注意:此类不对外暴露修改接口,只允许 `NameServerModule` 友元类访问和修改,所以外部代码只能通过 `NameServerModule` 对它进行创建和管理。
class NSClientInfo {
friend class NameServerModule;
public:
typedef std::shared_ptr<NSClientInfo> ptr;
private:
/**
* @brief 客户端节点的基础信息。
*
* `NSNode` 中包含了该节点的 IP、端口、权重,以及唯一 ID(由 IP + 端口生成)。
* 它是客户端节点的标识数据。
*/
NSNode::ptr m_node;
/**
* @brief 域名 -> 命令集合 的映射。
*
* 表示该客户端在什么域名下注册了哪些命令。
*
* 用于后续注销、查询等操作时快速定位该节点在哪些服务下起作用。
*/
std::map<std::string, std::set<uint32_t>> m_domain2cmds;
}; *
* 例如:
* {
* "sylar.top" => {REGISTER, QUERY}
* }
*
表示该客户端节点在域名 sylar.top 下关联了两个命令类型:REGISTER 和 QUERY。
REGISTER 表示这个客户端向 sylar.top 域名注册了服务(它是服务提供者)
QUERY 表示这个客户端也对 sylar.top 域名的服务进行了查询(它是服务消费者)。
*
* 用于后续注销、查询等操作时快速定位该节点在哪些服务下起作用。
*/
std::map<std::string, std::set<uint32_t>> m_domain2cmds;
};
NameServerModule
该类是基于 Rock 协议实现的服务注册中心核心模块,负责管理客户端服务节点的注册、查询、心跳维护和连接管理。它维护了服务域名、命令和节点的组织结构,同时跟踪每个客户端连接对应的注册信息和其关注的服务域名,实现对客户端的服务状态变更通知推送。通过处理来自客户端的请求和通知,该模块实现了服务的动态注册与发现,确保服务节点信息的实时更新和客户端的精准感知。
成员变量
核心数据结构:域名管理器,组织所有域名、命令、节点的三层结构。
NSDomainSet::ptr m_domains;
会话映射:每个连接对应一个客户端信息(IP/端口/注册内容)
std::map<sylar::RockStream::ptr, MSClientInfo::ptr> m_sessions;
记录每个连接(客户端)关注了哪些域名,用于推送通知
std::map<sylar::RockStream::ptr, std::set<std::string>> m_queryDomains;
反向映射:每个域名有哪些连接(客户端)在关注它
std::map<std::string, std::set<sylar::RockStream::ptr>> m_domainToSessions;
-
m_domains
这是核心的数据结构,负责管理整个服务注册中心的服务组织情况。它以域名为一级分类,域名下包含多个命令(服务类型),每个命令对应一组具体的服务节点(IP、端口、权重等)。通过这个三层结构,NameServerModule 能够高效管理和查询所有注册的服务节点信息。 -
m_sessions
该映射维护了所有当前连接的客户端会话信息。键是客户端连接的网络流(RockStream),值是对应客户端的注册信息(包含 IP、端口和它注册或订阅的服务内容)。它用于快速定位和管理每个客户端连接及其服务状态。 -
m_queryDomains
这个映射记录了每个客户端连接所关注的服务域名列表。客户端通过订阅感兴趣的域名来接收对应服务的变更通知,模块通过这个映射知道应当给哪些连接推送相关服务的更新消息。 -
m_domainToSessions
这是上述m_queryDomains
的反向映射,用于根据服务域名快速找到所有关注该域名的客户端连接集合。这样,服务状态发生变化时,模块能够高效定位所有需通知的客户端,实现精准推送。
成员函数
onConnect()
NameServerModule的连接回调函数,当有新的客户端连接到 NameServer 时被触发。
bool NameServerModule::onConnect(sylar::Stream::ptr stream) {
sylar::Atomic::addFetch(s_on_connect, 1);
auto rockstream = std::dynamic_pointer_cast<RockStream>(stream);
if (!rockstream) return false;
// 获取客户端的远程地址信息
auto addr = rockstream->getRemoteAddress();
if (!addr) return false;
return true;
}
get()
获取指定stream的客户端信息
NSClientInfo::ptr NameServerModule::get(sylar::RockStream::ptr rs) {
sylar::RWMutex::ReadLock lock(m_mutex);
auto it = m_sessions.find(rs);
return it == m_sessions.end() ? nullptr : it->second;
}
set()
用于更新指定客户端连接(RockStream
)所对应的服务注册信息。它会对比旧的客户端服务信息和新的服务信息,计算出新增的命令、删除的命令以及未变更的命令,并据此更新内部的域名管理结构(m_domains
),实现服务节点的增删改管理。最后,将最新的客户端信息缓存到 m_sessions
中,或者在连接断开时移除。辅助的 diff
函数则负责计算两个域名-命令映射之间的差异,帮助 set
函数正确识别哪些服务需要添加、删除或保持。
void NameServerModule::set(sylar::RockStream::ptr rs, NSClientInfo::ptr new_value) {
// 如果连接已断开,强制将新值置空,表示清理客户端信息
if (!rs->isConnected()) {
new_value = nullptr;
}
// 获取旧的客户端信息
NSClientInfo::ptr old_value = get(rs);
// 定义旧服务、 新服务、 删除服务、 新增服务、 共有服务 命令集合
std::map<std::string, std::set<uint32_t> > old_v;
std::map<std::string, std::set<uint32_t> > new_v;
std::map<std::string, std::set<uint32_t> > dels;
std::map<std::string, std::set<uint32_t> > news;
std::map<std::string, std::set<uint32_t> > comms;
if (old_value) {
old_v = old_value->m_domain2cmds; // 旧客户端域名-命令映射
}
if (new_value) {
new_v = new_value->m_domain2cmds; // 新客户端域名-命令映射
}
// 计算差异,得到 dels、news、comms 三个集合
diff(old_v, new_v, dels, news, comms);
// 处理删除的服务命令:从域名管理器中删除对应节点和命令的记录
for (auto& i : dels) {
auto d = m_domains->get(i.first);
if (d) {
for (auto& c : i.second) {
d->del(c, old_value->m_node->getId());
}
}
}
// 处理新增的服务命令:添加到域名管理器中
for (auto& i : news) {
auto d = m_domains->get(i.first);
if (!d) {
d.reset(new NSDomain(i.first)); // 新域名则新建管理器
m_domains->add(d);
}
for (auto& c : i.second) {
d->add(c, new_value->m_node);
}
}
// 处理命令交集部分,若节点权重变更,则更新域名管理器中的节点信息
if (!comms.empty()) {
if (old_value->m_node->getWeight() != new_value->m_node->getWeight()) {
for (auto& i : comms) {
auto d = m_domains->get(i.first);
if (!d) {
d.reset(new NSDomain(i.first));
m_domains->add(d);
}
for (auto& c : i.second) {
d->add(c, new_value->m_node);
}
}
}
}
// 更新或删除 m_sessions 中的客户端信息缓存
sylar::RWMutex::WriteLock lock(m_mutex);
if(new_value) {
m_sessions[rs] = new_value;
} else {
m_sessions.erase(rs);
}
}
// 计算旧值和新值的差异,分别填充 dels(删除命令), news(新增命令), comms(共有命令)
void diff(const std::map<std::string, std::set<uint32_t> >& old_value,
const std::map<std::string, std::set<uint32_t> >& new_value,
std::map<std::string, std::set<uint32_t> >& dels,
std::map<std::string, std::set<uint32_t> >& news,
std::map<std::string, std::set<uint32_t> >& comms) {
// 遍历旧值,找出已删除和共有命令
for (auto& i : old_value) {
auto it = new_value.find(i.first);
if (it == new_value.end()) {
dels.insert(i); // 旧值有,新值没有,标记为删除
continue;
}
for (auto& n : i.second) {
auto iit = it->second.find(n);
if (iit == it->second.end()) {
dels[i.first].insert(n);
continue;
}
comms[i.first].insert(n); // 命令交集
}
}
// 遍历新值,找出新增命令
for (auto& i : new_value) {
auto it = old_value.find(i.first);
if (it == old_value.end()) {
news.insert(i); // 新值有,旧值没有,标记为新增
continue;
}
for (auto& n : i.second) {
auto iit = it->second.find(n);
if (iit == it->second.end()) {
news[i.first].insert(n);
continue;
}
}
}
}
setQueryDomain()
更新指定客户端连接(rs
)关注的服务域名列表。当客户端希望订阅或取消订阅某些域名的服务通知时,会调用此函数。它首先获取该连接之前关注的域名集合,若新旧域名集合相同则不做操作;否则更新映射关系,先将旧域名对应的连接记录移除,再将新域名加入到对应的关注列表中,最终更新 m_queryDomains
中该连接的关注域名集合。这样保证服务端能准确维护每个连接关注的域名,实现精准的通知推送。
void NameServerModule::setQueryDomain(sylar::RockStream::ptr rs, const std::set<std::string>& ds) {
std::set<std::string> old_ds;
{
sylar::RWMutex::ReadLock lock(m_mutex);
auto it = m_queryDomains.find(rs);
if (it != m_queryDomains.end()) {
if (it->second == ds) {
return;
}
old_ds = it->second;
}
}
sylar::RWMutex::WriteLock lock(m_mutex);
if (!rs->isConnected()) return;
for (auto& i : old_ds) {
m_domainToSessions[i].erase(rs);
}
for (auto& i : ds) {
m_domainToSessions[i].insert(rs);
}
if (ds.empty()) {
m_queryDomains.erase(rs);
} else {
m_queryDomains[rs] = ds;
}
}
onDisconnect()
这个函数在客户端连接断开时被调用,用于执行断开连接后的清理工作。通过 set
函数将该连接对应的客户端信息清空,并通过 setQueryDomain
函数清除该连接关注的所有服务域名,确保服务端内部状态一致,避免无效的连接信息和订阅残留。
bool NameServerModule::onDisconnect(sylar::Stream::ptr stream) {
sylar::Atomic::addFetch(s_on_disconnect, 1);
auto rockstream = std::dynamic_pointer_cast<RockStream>(stream);
if (!rockstream) return false;
auto addr = rockstream->getRemoteAddress();
if (addr) {
SYLAR_LOG_INFO(g_logger) << "onDisconnect: " << *addr;
}
set(rockstream, nullptr);
setQueryDomain(rockstream, {});
return true;
}
doNotify()
将服务节点的变更通知发送给所有关注相关域名的客户端连接。当某些服务节点发生注册、下线或权重更新等变化时,该函数会根据变更涉及的域名,查找出订阅了这些域名的客户端连接,并通过 Rock 协议将封装好的 NotifyMessage
主动推送给它们,从而实现服务发现的实时性和动态性。
void NameServerModule::doNotify(std::set<std::string>& domains, std::shared_ptr<NotifyMessage> nty) {
// 创建一个 RockNotify 消息实例,作为要发送的通知消息。
RockNotify::ptr notify(new RockNotify());
// 设置通知类型为 NODE_CHANGE,表示服务节点发生变更(新增、删除、更新)
notify->setNotify((int)NSNotify::NODE_CHANGE);
// 将 NotifyMessage protobuf 对象序列化为二进制并写入 notify 的消息体中
notify->setAsPB(*nty);
// 遍历所有涉及的域名
for (auto& i : domains) {
// 获取订阅了该域名的所有客户端连接(RockStream)
auto ss = getStream(i);
// 向每一个连接发送该通知消息
for (auto& n : ss) {
n->sendMessage(notify);
}
}
}
handleTick()
处理客户端发送的心跳请求(TICK),用于维持连接的活跃状态,只是简单地返回 true
,没有实际处理逻辑。
bool NameServerModule::handleTick(sylar::RockRequest::ptr request
,sylar::RockResponse::ptr response
,sylar::RockStream::ptr stream) {
return true;
}
handleQuery()
用于响应客户端服务发现请求的核心逻辑,它会解析客户端发来的 QueryRequest
,根据请求中包含的域名,在服务端缓存的 NSDomainSet
中查找对应的服务节点信息,并构造 QueryResponse
返回给客户端。这个过程使得客户端能够实时获取目标域名下每个命令对应的可用服务节点列表,实现服务的动态发现与路由。
bool NameServerModule::handleQuery(sylar::RockRequest::ptr request,
sylar::RockResponse::ptr response,
sylar::RockStream::ptr stream) {
// 将 RockRequest 中携带的 protobuf 字节流反序列化为 QueryRequest(客户端查询请求)
auto qreq = request->getAsPB<QueryRequest>();
if (!qreq) {
// 如果反序列化失败,说明请求格式非法,记录日志并返回失败
SYLAR_LOG_ERROR(g_logger) << "invalid query request from: "
<< stream->getRemoteAddressString();
return false;
}
// 如果请求中没有任何域名,说明查询内容为空,也视为非法请求
if (!qreq->domains_size()) {
SYLAR_LOG_ERROR(g_logger) << "invalid query request from: "
<< stream->getRemoteAddressString()
<< " domains is null";
}
// 用于收集客户端请求中有效的 NSDomain 实例(即服务端已维护的域名)
std::set<NSDomain::ptr> domains;
// 记录客户端请求中所有域名(即使某些域名服务端并不存在)
std::set<std::string> ds;
// 遍历客户端请求的域名列表
for (auto& i : qreq->domain()) {
auto d = m_domains->get(i); // 在本地注册表中查找是否存在该域名
if (d) {
domains.insert(d); // 如果存在,将其加入有效域名集合
}
ds.insert(i); // 所有请求的域名统一加入 ds(后续可用于绑定订阅)
}
// 创建响应对象:QueryResponse,用于封装返回给客户端的数据
auto qrsp = std::make_shared<QueryResponse>();
// 遍历每个有效的 NSDomain,获取该域名下所有命令类型的节点集合
for (auto& i : domains) {
std::vector<NSNodeSet::ptr> nss;
i->listAll(nss); // 获取当前域名下所有 NSNodeSet,每个 NSNodeSet 表示一个 cmd 类型
// 遍历每个命令类型对应的节点集合
for (auto& n : nss) {
// 向响应中新增一个 NodeInfo 条目
auto item = qrsp->add_infos();
item->set_domain(i->getDomain()); // 设置域名
item->set_cmd(n->getCmd()); // 设置命令编号
std::vector<NSNode::ptr> ns;
n->listAll(ns); // 获取该命令下所有服务节点
// 将节点信息逐个添加到响应中
for (auto& x : ns) {
auto node = item->add_nodes();
node->set_ip(x->getIp());
node->set_port(x->getPort());
node->set_weight(x->getWeight());
}
}
}
// 填充响应对象:表示处理成功
response->setResult(0);
response->setResultStr("ok");
// 将 QueryResponse 作为 protobuf 数据序列化进 RockResponse 的 m_body 部分
response->setAsPB(*qrsp);
// 返回 true,表示成功处理了查询请求
return true;
}
handleRegister()
用于处理客户端服务注册请求的核心逻辑。它会从请求中解析出客户端节点注册的服务信息(包括服务所属域名、支持的命令编号、节点 IP/端口/权重等),校验数据的合法性与一致性,并将这些注册信息记录到服务治理结构中(即将节点加入对应的域名和命令类型下的注册表中)。如果该连接之前已经注册过其他服务,函数还会对比旧信息和新注册信息的差异,并据此更新整体的服务拓扑结构,最终回复客户端注册成功。
bool NameServerModule::handleRegister(sylar::RockRequest::ptr request,
sylar::RockResponse::ptr response,
sylar::RockStream::ptr stream) {
// 尝试将请求反序列化为 protobuf 的 RegisterRequest 对象
auto rr = request->getAsPB<RegisterRequest>();
if (!rr) {
SYLAR_LOG_ERROR(g_logger) << "invalid register request from: "
<< stream->getRemoteAddressString();
return false;
}
// 获取当前连接对应的旧的注册信息(如果存在)
NSClientInfo::ptr old_value = get(stream);
NSClientInfo::ptr new_value;
// 遍历请求中的所有注册项(每个 info 表示一个服务节点的注册信息)
for (int i = 0; i < rr->infos_size(); ++i) {
auto& info = rr->infos(i); // 第 i 个注册信息
#define XX(info, attr) \
if (!info.has_##attr()) { \
SYLAR_LOG_ERROR(g_logger) \
<< "invalid register request from: " \
<< stream->getRemoteAddressString() \
<< " " #attr " is null"; \
return false; \
}
// 校验域名和节点是否存在
XX(info, node);
XX(info, domain);
// 校验命令集非空
if (info.cmds_size() == 0) {
SYLAR_LOG_ERROR(g_logger) << "invalid register request from: "
<< stream->getRemoteAddressString()
<< " cmds is null";
return false;
}
// 提取 Node 信息(IP、端口、权重)
auto& node = info.node();
XX(node, ip);
XX(node, port);
XX(node, weight);
// 构造内部节点对象 NSNode(包含唯一 ID 信息)
NSNode::ptr ns_node(new NSNode(node.ip(), node.port(), node.weight()));
// 校验 Node ID 是否合法(要求高 32 位非 0)
if (!(ns_node->getId() >> 32)) {
SYLAR_LOG_ERROR(g_logger) << "invalid register request from: "
<< stream->getRemoteAddressString()
<< " ip=" << node.ip() << " invalid";
return false;
}
// 如果客户端之前注册过服务,验证其注册 ID 是否一致
if (old_value) {
if (old_value->m_node->getId() != ns_node->getId()) {
SYLAR_LOG_ERROR(g_logger) << "inconsistent node id from: "
<< stream->getRemoteAddressString()
<< " old.ip=" << old_value->m_node->getIp()
<< " old.port=" << old_value->m_node->getPort()
<< " cur.ip=" << ns_node->getIp()
<< " cur.port=" << ns_node->getPort();
return false;
}
}
// 如果 new_value 已创建,则校验其 ID 是否一致
if (new_value) {
if (new_value->m_node->getId() != ns_node->getId()) {
SYLAR_LOG_ERROR(g_logger) << "mismatched node in same request: "
<< stream->getRemoteAddressString()
<< " new.ip=" << new_value->m_node->getIp()
<< " new.port=" << new_value->m_node->getPort()
<< " cur.ip=" << ns_node->getIp()
<< " cur.port=" << ns_node->getPort();
return false;
}
} else {
// 否则首次初始化 new_value
new_value.reset(new NSClientInfo);
new_value->m_node = ns_node;
}
// 将命令加入 new_value 的域名-命令映射表中
for (auto& cmd : info.cmds()) {
new_value->m_domain2cmds[info.domain()].insert(cmd);
}
}
// 将新的客户端注册信息绑定到 stream 上,并同步更新服务节点管理结构
set(stream, new_value);
// 设置响应内容
response->setResult(0);
response->setResultStr("ok");
return true;
}
handleRockRequest()
这个函数是 NameServerModule
的核心入口,用于统一处理客户端基于 Rock 协议发送的请求。它会根据请求中的命令类型(如注册、查询、心跳)将请求分发给对应的处理函数(handleRegister
、handleQuery
、handleTick
)。这个函数本质上就是一个分发器,确保客户端发来的不同类型请求能够被正确处理,是整个名称服务模块响应逻辑的调度中心。
bool NameServerModule::handleRockRequest(sylar::RockRequest::ptr request
,sylar::RockResponse::ptr response
,sylar::RockStream::ptr stream) {
sylar::Atomic::addFetch(s_request_count, 1);
switch (request->getCmd()) {
case (int)NSCommand::REGISTER:
return handleRegister(request, response, stream);
case (int)NSCommand::QUERY:
return handleQuery(request, response, stream);
case (int)NSCommand::TICK:
return handleTick(request, response, stream);
default:
SYLAR_LOG_WARN(g_logger) << "invalid cmd=0x" << std::hex << request->getCmd();
break;
}
return true;
}
此时再结合RockServer::handleClient来看
Sylar 的 RPC 框架基于 Rock 协议,采用模块化设计实现请求的分发和处理。服务器端为每个客户端连接创建一个 RockSession
会话对象,并为该会话设置请求和通知的处理回调。当客户端发起请求时,RockSession
会触发请求处理逻辑,框架通过 ModuleMgr
遍历所有注册的 ROCK 类型模块,并调用它们的 handleRequest
方法对请求进行处理。
我们实现的核心业务模块是 NameServerModule
,它继承自 RockModule
,并重写了 handleRockRequest
方法,根据请求的命令类型(如 REGISTER、QUERY、TICK)将请求分发到对应的处理函数中完成具体的业务逻辑。如果某个模块成功处理了请求,会立即返回,阻止请求继续传递给其他模块,从而实现高效的请求分发和模块解耦。
此外,框架还会在连接建立和断开时,依次调用所有 ROCK 模块的 onConnect
和 onDisconnect
回调,用于维护连接状态和资源清理。整个流程由 RockServer
负责管理,确保网络事件和消息能够正确分发到对应的业务模块,实现灵活且高效的 RPC 模块分发机制。
bool RockModule::handleRequest(sylar::Message::ptr req
,sylar::Message::ptr rsp
,sylar::Stream::ptr stream) {
auto rock_req = std::dynamic_pointer_cast<sylar::RockRequest>(req);
auto rock_rsp = std::dynamic_pointer_cast<sylar::RockResponse>(rsp);
auto rock_stream = std::dynamic_pointer_cast<sylar::RockStream>(stream);
return handleRockRequest(rock_req, rock_rsp, rock_stream);
}
void RockServer::handleClient(Socket::ptr client) {
SYLAR_LOG_DEBUG(g_logger) << "handleClient " << *client;
sylar::RockSession::ptr session(new sylar::RockSession(client));
session->setWorker(m_worker);
// 通知所有 ROCK 类型模块有连接建立
ModuleMgr::GetInstance()->foreach(Module::ROCK,
[session](Module::ptr m) {
m->onConnect(session);
});
// 设置连接断开时的回调
session->setDisconnectCb([](AsyncSocketStream::ptr stream) {
ModuleMgr::GetInstance()->foreach(Module::ROCK,
[stream](Module::ptr m) {
m->onDisconnect(stream);
})
});
// 设置请求处理函数
// 当客户端发来一个 RockRequest(ROCK协议的请求包)时,调用这个 lambda。
// 遍历所有注册的 ROCK 模块:
// 调用每个模块的 handleRequest() 来处理请求。
// 如果有一个模块返回了 true(表示处理了该请求),就不再继续调用其他模块(短路逻辑)。
session->setRequestHandler([](sylar::RockRequest::ptr req, sylar::RockResponse::ptr rsp, sylar::RockStream::ptr conn)->bool {
bool rt = false;
ModuleMgr::GetInstance()->forearch(Module::ROCK, [&rt, req, rsp, conn](Module::ptr m) {
if (rt) {
return;
}
rt = m->handleRequest(req, rsp, conn);
});
return rt;
})
// 设置通知处理函数
session->setNotifyHandler([](sylar::RockNotify::ptr nty, sylar::RockStream::ptr conn)->bool {
SYLAR_LOG_INFO(g_logger) << "handleNty " << nty->toString()
<< " body=" << nty->getBody();
bool rt = false;
ModuleMgr::GetInstance()->foreach(Module::ROCK, [&rt, nty, conn](Module::ptr m) {
if (rt) return;
rt = m->handleNotify(nty, conn);});
return rt;
}
);
// 启动会话,开始异步读写
// 会在设置的m_worker中持续进行
session->start();
}
更详细的sylar注释: jinkezhen/sylar: for self study