插件化多协议网关的 C++ 设计实践
很多系统的对外接入需求是相似的:外部客户端的协议五花八门(HTTP、私有 TCP、之后可能还有 MQTT、WebSocket),内部服务不应该直接暴露给外部,而且每接入一个新服务、每支持一个新协议,都不应该修改网关核心代码再重新编译。本文整理一套满足这些约束的 C++ 网关设计:分层插件化架构,接入端协议与业务逻辑都做成运行时动态加载的插件,核心框架只负责路由、配置与生命周期。文中接口定义与流程图来自一个实际网关项目的设计文档,整理时做了适当抽象,适合准备自研协议网关或 API 接入层的开发者参考。
总体架构
网关分为三层,每层职责单一:
- 接入层(Access Layer):监听端口、解析协议,把外部请求转换为内部统一的 Context/Message;
- 核心框架(Core Framework):配置管理、插件加载、路由分发、线程池等公共能力;
- 业务层(Business Layer):处理具体业务逻辑,与后端服务通信。
两条插件抽象是整个架构的关键:
- AccessPlugin 面向协议差异:一种协议一个插件,负责"外部协议 → 统一 Context"的转换;
- BusinessPlugin 面向业务差异:一类业务一个插件,负责"统一 Context → 后端调用 → 响应填充"。
核心框架不认识任何具体协议和业务,两者只通过 Context 交互。新协议、新业务都以动态库形式在运行期接入,核心代码不动。
统一消息模型:Context 与 Message
所有协议的请求进入网关后,都被封装成统一的 Context + Message:
// 消息体:与协议无关的内容抽象
enum class ContentType { TEXT, BINARY, JSON, XML, PROTOBUF, UNKNOWN };
struct Message {
ContentType contentType; // 内容类型
uint64_t id; // 消息 ID(请求-响应匹配)
unordered_map<string, string> headers; // 消息头
string body; // 消息体
int64_t timestamp; // 时间戳
unordered_map<string, string> metadata; // 元数据
};
// 请求上下文:贯穿整个处理流程的"护照"
class Context {
protected:
string _id; // 上下文唯一标识
ProtocolType _protocol; // 协议类型(HTTP/TCP)
ServiceType _serviceType; // 服务类型(Service/Action/Topic)
string _from; // 请求来源(客户端地址)
string _to; // 请求目标(URL 路径)
atomic_bool _isAlive{true};
unordered_map<string, string> _metadata; // 路由参数等元数据
any _userData; // 自定义数据
int _responseCode;
MessagePtr _requestMessage;
MessagePtr _responseMessage;
public:
// 由协议特定的 Context 子类实现
virtual bool sendResponse(bool isLast = true);
virtual bool sendError(int errorCode, const string& errorMessage = "");
virtual bool publishMessage(); // Topic 模式推送
}; Context 在各模块间的流转是固定的:接入插件创建并填充请求信息,路由器把提取的路径参数写进 _metadata,业务插件读取请求、填充响应,最后通过 sendResponse() 把结果按原协议送回客户端。
sendResponse 的 isLast 参数承担着连接语义:true 表示这是最后一条消息(一次性的请求-响应),false 表示中间消息、连接继续保持(Action 的多次进度反馈、Topic 的持续推送都依赖它)。协议特定的差异被隔离在 Context 子类里:
// HTTP 上下文:短连接一次性返回;长连接走 SSE
class HttpContext : public Context {
bool sendResponse(bool isLast = true) override {
if (isLast) {
response_status(_httpCtx, _responseCode, _responseMessage->body);
_httpCtx->close(); // 请求结束,关闭连接
} else {
_httpCtx->writer->SSEvent(_responseMessage->body); // SSE 推送
}
return true;
}
};
// TCP 上下文:按网关消息协议封包后写回连接
class TcpContext : public Context {
bool sendResponse(bool isLast = true) override {
if (!isSessionValid()) return false; // 连接已断开
proto::Message protoMsg; // Header + Body 结构
auto* header = protoMsg.mutable_header();
header->set_message_id(getMetadata("message_id"));
header->set_is_last(isLast);
header->set_code(_responseCode);
protoMsg.set_body(_responseMessage->body);
string serialized;
protoMsg.SerializeToString(&serialized);
return send(serialized);
}
}; 同一个业务插件不需要感知自己服务的是 HTTP 客户端还是 TCP 客户端——它只管填充 _responseMessage 并调用 sendResponse,协议差异由 Context 子类消化。
接入层:协议适配
AccessPlugin 的接口刻意保持小:
class AccessPlugin {
virtual bool init(const YAML::Node& config,
std::shared_ptr<hv::EventLoop> eventLoop) = 0;
virtual bool start(int port) = 0;
virtual void stop() = 0;
virtual std::string getName() const = 0;
virtual ProtocolType getProtocol() const = 0;
bool routeCtx(const ContextPtr& ctx); // 把请求交给路由层
void setRouteCtxCallback(...); // 框架注入的路由回调
}; 几个设计点:
- 事件循环由框架统一创建、插件复用。
init接收框架的hv::EventLoop(libhv),接入插件不自己起 IO 线程,网络事件集中在同一个事件循环里处理。 - HTTP 插件基于 libhv 的 HttpServer 实现,收到的 HTTP 请求被转换为 Context:URL 路径写入
_to(供路由),header 与 body 装入 Message。 - TCP 插件实现全双工通信,线上报文格式为"4 字节小端长度 + Protobuf 序列化数据"。Protobuf 部分是一个简单的信封结构,头部携带路由所需的最小信息:
message MessageHeader {
string message_id = 1; // 消息唯一标识(响应匹配)
int64 timestamp = 3;
string url = 4; // 请求路径(路由用)
int32 code = 11; // 响应状态码
bool is_last = 13; // 是否最后一条消息
}
message Message {
MessageHeader header = 1;
bytes body = 2;
} 把 URL 放进 TCP 消息头是整个统一模型成立的前提:这样一来,无论 HTTP 还是 TCP,路由依据都是同一个"路径",路由层不需要为不同协议维护两套匹配逻辑。
TCP 侧还需要自己做连接与请求上下文管理:一条连接(channel)上可以承载多个请求上下文,每个上下文有唯一 ID;连接关闭时清理该连接下的全部上下文,响应完成后释放对应上下文。这套生命周期管理在 HTTP 协议里由协议本身承担,在自定义 TCP 协议里就要自己实现。
路由:模式匹配与参数提取
路由规则由配置驱动:
struct RouteRule {
string pattern; // 匹配模式:/api/{namespace}/{service}/{cmd}
string businessName; // 命中后交给哪个业务插件
ServiceType serviceType; // 服务类型过滤(可选)
bool matchAndExtractParams(const ContextPtr& ctx) const;
shared_ptr<regex> _cachedRegex; // 预编译正则缓存
vector<string> _cachedParamNames; // 参数名缓存
}; 模式语法支持三类:精确路径、{param} 路径参数、* 通配符。加载时把模式编译成正则并缓存,{param} 变成捕获组:
/api/{namespace}/{service}/{cmd}
→ ^/api/([^/]+)/([^/]+)/([^/]+)$
参数名列表 = [namespace, service, cmd] 匹配时按优先级从高到低遍历规则:先比对协议类型,再用缓存正则匹配路径,命中后把捕获组的值写入 Context 的 _metadata(如 namespace=core、service=param、cmd=query),然后把请求交给 businessName 指向的业务插件;找不到插件或所有规则都不匹配,直接回错误响应。规则数量不多时线性遍历即可,正则预编译缓存保证了单次匹配只是一次 regex_match 的开销。过滤器链(认证、限流)在架构上是路由的一环,实现上作为预留扩展未随首版落地。
业务层:配置驱动的服务接入
BusinessPlugin 是业务侧唯一的扩展点:
class BusinessPlugin {
virtual std::string getName() const = 0;
virtual bool init(const YAML::Node& config) = 0;
virtual bool process(const ContextPtr& ctx) = 0;
virtual bool stop() = 0;
}; 两个内置插件覆盖了三类通信模式:API 插件处理 Service(一次请求一次响应)与 Action(一次请求、多次进度反馈加一次最终结果),Topic 插件处理 Topic(订阅后持续推送)。API 插件的服务清单完全由配置声明,三层结构 namespace → service → cmd:
business_plugins:
- name: api
path: api_plugin.so
enabled: true
config:
services:
core: # 命名空间
- name: param # 服务名
channel: param_service # 后端服务通道(须与后端注册一致)
type: service # service 或 action
cmd:
- name: query
reqType: ParamMsg.QueryReq # 请求 Protobuf 类型
rspType: ParamMsg.QueryRsp # 响应 Protobuf 类型 这个结构直接映射到路由的路径参数:/api/core/param/query 命中后,插件拿 namespace/service/cmd 三段元数据查找配置,找到对应的后端通道与消息类型。新增一个后端服务只需追加一段配置,不触碰任何代码。
reqType/rspType 用于 JSON 与 Protobuf 的自动互转:HTTP 客户端通常发 JSON,后端走 Protobuf,插件在转发前按 reqType 做运行时转换,收到响应再按 rspType 转回 JSON。转换基于 Protobuf 的运行时反射(描述池)完成,不需要为每种消息生成转换代码。转换与后端调用都是耗时操作,提交到线程池异步执行,process 立即返回,不阻塞接入层的事件循环。
Topic 插件的订阅关系按连接维护,相同 topic 的多个订阅连接共享一次对后端的订阅(引用计数):
断开的连接由定时任务清理,对应的后端订阅在引用计数归零后取消。
插件动态加载
插件以动态库形式交付,核心框架用 Boost.DLL 加载(它封装了 Windows .dll 与 Linux .so 的平台差异)。两层的加载协议完全一致:动态库导出一个 extern "C" 的 createPlugin 工厂函数,框架加载后查符号、调用工厂拿到实例,再按 getName() 的返回值注册:
#define EXPORT_ACCESS_PLUGIN(ClassName) \
extern "C" PLUGIN_EXPORT gateway::AccessPluginPtr createPlugin() \
{ \
return std::make_shared<ClassName>(); \
}
// 插件实现侧只需一行
EXPORT_ACCESS_PLUGIN(HttpAccessPlugin) extern "C" 保证符号名不被 C++ 名字修饰,跨编译器查找符号才可靠;工厂返回智能指针,插件实例的生命周期交给框架管理。加载流程:
PluginManager 同时持有库句柄与插件实例:只存实例不存句柄,库可能在某个 shared_ptr 归零时被卸载,留下悬空代码。单个插件加载失败只记日志跳过,不影响其他插件与网关启动。
请求处理全流程
一次请求的完整路径,把前面的部件串起来:
启动顺序上有一个容易忽略的细节:业务插件先于接入插件初始化。路由命中的终点是业务插件,先让业务层就绪、再开放外部端口,可以避免启动窗口期内请求命中未初始化的插件。
小结
这套设计的核心决策可以归纳为四条:
- 两层插件抽象切开两类变化:协议变化止于 AccessPlugin,业务变化止于 BusinessPlugin,核心框架只面对 Context 与两个抽象接口,扩展永远是"加一个动态库加一段配置"。
- 统一消息模型是解耦的地基:URL 同时作为 HTTP 与 TCP 的路由依据,
isLast统一表达一次性响应与流式推送,业务代码因此与协议无关。 - 配置驱动的服务声明:namespace/service/cmd 三层结构与路由参数一一对应,接入新服务是纯配置变更;JSON 与 Protobuf 的互转交给运行时反射,不为每种消息写转换代码。
- 动态加载的工程细节要闭环:
extern "C"工厂符号、库句柄与实例同生命周期、单插件失败不拖垮网关,这三点缺一不可。
过滤器链、路由热更新、更多协议(MQTT、WebSocket)都留在了扩展点上:接入层再添一个插件即可,这正是插件化架构要兑现的承诺。