:让抽象消息真正落地——JsonMessage、业务消息与 MessageFactory)
目录前言一、一条消息里其实有两类数据二、JsonMessage统一管理 JSON 正文三、为什么还要分成 JsonRequest 和 JsonResponse四、RPC 消息method parameters result4.1 RpcRequest4.2 RpcResponse五、Topic 消息同一种请求不同操作需要的字段不同六、Service 消息服务发现和其他操作不完全一样6.1 ServiceRequest6.2 ServiceResponse七、六种消息都有了为什么还要 MessageFactory7.1 已经知道具体 C 类型7.2 只知道 MType八、unserialize() 和 check() 解决的不是同一个问题8.1 unserialize()JSON 语法能不能解析8.2 check()业务字段是不是符合当前消息规则九、从一次 RPC 请求看消息对象怎样被组织出来写到最后前言系列C RPC 框架从设计到实现第七篇项目源码JSON-RPChttps://gitee.com/kuang-zhenting/json-rpc前面我们已经把框架最基础的公共地基搭起来了。第五篇定义了BaseMessage统一规定一条消息至少需要具备请求 IDRID消息大类MType序列化 / 反序列化消息合法性检查。第六篇又补上了日志宏让后面的模块能够统一输出调试和错误信息。但到这里BaseMessage仍然只是一个抽象接口。它知道“消息应该有哪些公共能力”却不知道真正的业务数据应该怎样保存。例如 RPC 请求需要method parametersTopic 请求需要topic_key optype topic_msg服务注册与发现又需要method optype host这些字段显然不能全部塞进BaseMessage。不同业务消息应该只关心自己真正需要的数据。所以这一篇我们继续实现source/commom/message.hpp当前仓库中的目录实际拼写为commom本文继续按源码保持一致。这一篇的目标只有一个把前面的抽象消息接口真正落成可以承载 RPC、Topic、Service 业务数据的消息对象。最终我们会得到三组请求 / 响应消息并通过MessageFactory把协议中的MType和具体 C 消息类型对应起来。一、一条消息里其实有两类数据先回忆一下BaseMessage中已经保存的两个成员MType _mtype{MType::REQ_RPC}; std::string _rid;这一篇的JsonMessage又会增加Json::Value _body;这三部分不是在重复保存同一份信息。在当前项目里一条完整消息可以先分成两部分其中mtype用来说明这是一条 RPC、Topic 还是 Service 请求 / 响应rid用来标识一次请求后续客户端可以用它匹配对应响应_body才是真正的 JSON 业务正文。例如一个 RPC 请求的正文可能是{ method: Add, parameters: { left: 11, right: 22 } }这里并没有把mtype和rid再塞进 JSON。因为后面的LVProtocol会单独编码MType RID JSON Body所以这一篇的消息类主要负责一件事把原本没有业务语义的Json::Value封装成 RPC、Topic、Service 都能直接使用的消息对象。二、JsonMessage统一管理 JSON 正文项目已经决定使用 JsonCpp 表示业务正文因此最自然的第一层就是class JsonMessage : public BaseMessage当前源码中的核心实现如下class JsonMessage : public BaseMessage { public: using ptr std::shared_ptrJsonMessage; virtual std::string serialize() override { std::string body; bool ret JSON::serialize(_body, body); if (ret false) { ELOG(serialize fail); return std::string(); } return body; } virtual bool unserialize(const std::string msg) override { bool ret JSON::unserialize(msg, _body); if (ret false) { ELOG(unserialize fail); } return ret; } protected: Json::Value _body; };这里真正新增的核心成员只有Json::Value _body;JsonMessage不关心_body里面具体放什么它只统一完成Json::Value ↓ serialize() JSON 字符串以及反方向JSON 字符串 ↓ unserialize() Json::Value具体的业务字段由后面的派生类决定。比如_body[KEY_METHOD]在RpcRequest中代表方法名_body[KEY_TOPIC_KEY]在TopicRequest中代表主题名称。这样第五篇里定义的KEY_METHOD、KEY_PARAMS、KEY_HOST等公共字段就真正开始参与消息组织了。三、为什么还要分成JsonRequest和JsonResponse从JsonMessage往下项目没有直接派生六种业务消息而是先分成JsonRequest JsonResponseJsonRequest很薄class JsonRequest : public JsonMessage { public: using ptr std::shared_ptrJsonRequest; };它目前没有新增字段主要作用是把“请求消息”这一条继承分支单独划出来。真正有公共逻辑的是JsonResponse。大部分响应都会携带统一响应码rcode所以当前源码把它的检查、读取和设置放到了JsonResponseclass JsonResponse : public JsonMessage { public: virtual bool check() override { if (_body[KEY_RCODE].isNull() true) { ELOG(响应中没有状态码!); return false; } if (_body[KEY_RCODE].isIntegral() false) { ELOG(响应状态码类型错误!); return false; } return true; } virtual RCode rcode() { return (RCode)_body[KEY_RCODE].asInt(); } virtual void setRCode(RCode rcode) { _body[KEY_RCODE] (int)rcode; } };为什么不把rcode直接放进JsonMessage因为请求消息根本不需要响应码。所以这里多一层继承并不是为了把类结构写复杂而是为了把公共规则放到真正需要它的那一支里JsonRequest → 请求公共分支JsonResponse → 响应公共分支 rcode。四、RPC 消息method parameters resultRPC 是三类业务里最容易理解的一组消息。4.1RpcRequest一个 RPC 请求至少需要method parameters例如{ method: Add, parameters: { left: 11, right: 22 } }当前RpcRequest::check()要求virtual bool check() override { if (_body[KEY_METHOD].isNull() true || _body[KEY_METHOD].isString() false) { ELOG(RPC请求中没有方法名称或方法名称类型错误); return false; } if (_body[KEY_PARAMS].isNull() true || _body[KEY_PARAMS].isObject() false) { ELOG(RPC请求中没有参数信息或者参数信息错误); return false; } return true; }也就是说当前项目明确约定method必须是字符串parameters必须是 JSON Object。因此{ left: 11, right: 22 }可以作为当前项目的 RPC 参数而直接传[11, 22]虽然也是合法 JSON但不符合当前RpcRequest的字段规则。业务层不需要直接操作_body而是通过接口访问std::string method(); void setMethod(const std::string method_name); Json::Value params(); void setParams(const Json::Value params);4.2RpcResponseRPC 响应除了继承来的rcode还要带上最终结果result例如{ rcode: 0, result: 33 }当前源码的检查逻辑是virtual bool check() override { if (_body[KEY_RCODE].isNull() true || _body[KEY_RCODE].isIntegral() false) { ELOG(响应中没有响应状态码,或状态码类型错误); return false; } if (_body[KEY_RESULT].isNull() true) { ELOG(响应中没有Rpc调用结果,或结果类型错误); return false; } return true; }result没有被限定成int、string或某个固定 C 类型而是继续使用Json::Value这是合理的因为不同 RPC 方法可能返回完全不同的 JSON 数据。消息层只负责承载结果不应该在这里规定所有 RPC 服务只能返回某一种类型。五、Topic 消息同一种请求不同操作需要的字段不同Topic 一共有五种操作TOPIC_CREATE TOPIC_REMOVE TOPIC_SUBSCRIBE TOPIC_CANCEL TOPIC_PUBLISH它们都使用同一个TopicRequest公共字段为topic_key optype但只有真正发布消息时才必须额外携带topic_msg所以check()里有一段条件检查if (_body[KEY_OPTYPE].asInt() (int)TopicOptype::TOPIC_PUBLISH (_body[KEY_TOPIC_MSG].isNull() true || _body[KEY_TOPIC_MSG].isString() false)) { ELOG(主题消息发布请求中没有消息内容字段或消息内容类型错误); return false; }可以简单理解成Topic 操作topic_keyoptypetopic_msg创建必须必须不要求删除必须必须不要求订阅必须必须不要求取消订阅必须必须不要求发布必须必须必须这也是消息校验里很常见的一种情况字段是否合法不一定只看消息大类有时还要继续看这条消息准备执行什么操作。对应的访问接口也比较直接std::string topicKey(); void setTopicKey(const std::string key); TopicOptype optype(); void setOptype(TopicOptype optype); std::string topicMsg(); void setTopicMsg(const std::string msg);TopicResponse则很简单class TopicResponse : public JsonResponse { public: using ptr std::shared_ptrTopicResponse; };当前 Topic 响应没有额外业务字段因此直接复用JsonResponse已经提供的rcode就够了。六、Service 消息服务发现和其他操作不完全一样服务注册与发现这一组消息要稍微复杂一些。当前ServiceOptype包含SERVICE_REGISTRY SERVICE_DISCOVERY SERVICE_ONLINE SERVICE_OFFLINE SERVICE_UNKNOW6.1ServiceRequest服务请求至少需要method optype但host是否必须存在要看操作类型。当前源码的规则是SERVICE_DISCOVERY → 不要求 host其他当前操作 → 要求 host.ip host.port。核心判断如下if (_body[KEY_OPTYPE].asInt() ! (int)(ServiceOptype::SERVICE_DISCOVERY) (_body[KEY_HOST].isNull() true || _body[KEY_HOST].isObject() false || _body[KEY_HOST][KEY_HOST_IP].isNull() true || _body[KEY_HOST][KEY_HOST_IP].isString() false || _body[KEY_HOST][KEY_HOST_PORT].isNull() true || _body[KEY_HOST][KEY_HOST_PORT].isIntegral() false)) { ELOG(服务请求中主机地址信息错误); return false; }为什么发现服务时可以没有host因为客户端真正想问的是谁能提供这个 method它本来就不知道 Provider 地址所以请求里只需要告诉注册中心要找哪个方法。而服务注册、上线、下线时注册中心必须知道当前 Provider 在哪里因此需要一个地址。项目中把地址统一定义成using Address std::pairstd::string, int;也就是IP PortServiceRequest::setHost()会把它写成 JSON Object{ host: { ip: 127.0.0.1, port: 8080 } }6.2ServiceResponse服务发现响应和普通注册响应还有一个明显区别。客户端查询一个方法时注册中心可能找到多个 Provider因此服务发现响应需要返回method host[]例如{ rcode: 0, optype: 1, method: Add, host: [ { ip: 10.0.0.8, port: 8080 }, { ip: 10.0.0.9, port: 8080 } ] }所以这里需要特别区分ServiceRequest host 单个 Address 对象 ServiceResponse服务发现 host Address 数组当前源码提供void setHosts(const std::vectorAddress addrs); std::vectorAddress hosts();用于在std::vectorAddress和 JSON 数组之间转换。这里也要注意一个边界当前ServiceResponse::check()会确认服务发现响应中的method 是字符串host 是数组。但它没有继续逐个检查数组里的每个元素是否都包含合法的ip和port。七、六种消息都有了为什么还要MessageFactory现在我们已经有RpcRequest RpcResponse TopicRequest TopicResponse ServiceRequest ServiceResponse如果业务代码自己创建消息当然可以直接写auto req std::make_sharedRpcRequest();但协议层收包时遇到的问题不一样。它从网络报文中首先得到的是MType mtype;例如REQ_RPC这时协议层需要知道REQ_RPC到底应该创建哪一个 C 消息类如果这套映射关系散落在各个模块里后面会出现大量重复的if / else或switch。所以项目用MessageFactory集中维护当前实现如下class MessageFactory { public: static BaseMessage::ptr create(MType mtype) { switch (mtype) { case MType::REQ_RPC: return std::make_sharedRpcRequest(); case MType::RSP_RPC: return std::make_sharedRpcResponse(); case MType::REQ_TOPIC: return std::make_sharedTopicRequest(); case MType::RSP_TOPIC: return std::make_sharedTopicResponse(); case MType::REQ_SERVICE: return std::make_sharedServiceRequest(); case MType::RSP_SERVICE: return std::make_sharedServiceResponse(); } return BaseMessage::ptr(); } template typename T, typename... Args static std::shared_ptrT create(Args ...args) { return std::make_sharedT(std::forward(args)...); } };这里其实提供了两种创建方式。7.1 已经知道具体 C 类型业务层可以直接auto req MessageFactory::createRpcRequest();例如当前RpcCaller组织 RPC 请求时就是这样做的。7.2 只知道MType协议层收包时则可以BaseMessage::ptr msg MessageFactory::create(MType::REQ_RPC);得到的对象实际是RpcRequest这样“协议中的类型编号”和“C 中的消息对象”之间就有了统一入口。不过有一个地方非常容易误解MessageFactory::create(MType::REQ_RPC)只负责选择并创建正确的消息类。它不会自动执行msg-setMType(MType::REQ_RPC); msg-setId(...);创建对象和填写消息元信息是两件不同的事。后面的LVProtocol收包时会把它们真正接起来。八、unserialize()和check()解决的不是同一个问题现在每个具体消息都提供了check()而JsonMessage又有unserialize()。这两个函数看起来都在“验证消息”但验证层次完全不同。8.1 unserialize()JSON 语法能不能解析例如{method:Add,这连完整 JSON 都不是所以unserialize() false8.2 check()业务字段是不是符合当前消息规则下面这段 JSON 本身完全合法{ method: 123, parameters: {} }JsonCpp 可以正常解析因此unserialize() true但RpcRequest要求method 必须是字符串所以check() false可以把两者简单记成unserialize() → “你是不是合法 JSON”check() → “你是不是合法的这种业务消息”还有一个必须严格按照当前源码说明的事实。目前LVProtocol::onMessage()的收包流程是读取 mtype / rid / body ↓ MessageFactory::create(mtype) ↓ unserialize(body) ↓ setId(rid) ↓ setMType(mtype)它当前没有自动调用msg-check()。check()是消息对象已经提供的业务结构检查能力但“接口已经存在”不代表当前所有调用链都会自动执行它。这一点后面看 Dispatcher 和具体业务模块时要继续以实际源码为准。九、从一次 RPC 请求看消息对象怎样被组织出来当前RpcCaller创建请求时会写出类似这样的代码auto req_msg MessageFactory::createRpcRequest(); req_msg-setId(UUID::uuid()); req_msg-setMType(MType::REQ_RPC); req_msg-setMethod(method); req_msg-setParams(params);这几步正好对应这一篇的整个设计createRpcRequest() ↓ 先创建具体业务消息 setId() ↓ 保存 RID setMType() ↓ 保存消息大类 setMethod() / setParams() ↓ 填写 JSON 业务正文于是一个RpcRequest对象内部同时拥有协议公共信息 ├── rid └── mtype JSON 业务正文 ├── method └── parameters之后req_msg-serialize();得到的是 JSON Body。再下一步LVProtocol才会继续把mtype rid body组织成真正能够发送到 TCP 字节流中的完整协议帧。到这里第五篇里那个看起来还比较抽象的BaseMessage终于有了真正能被 RPC、Topic、Service 使用的具体实现。写到最后这一篇虽然出现了不少类名但真正的主线并不复杂BaseMessage ↓ JsonMessage 统一保存 JSON Body ↓ JsonRequest / JsonResponse 区分请求与响应的公共规则 ↓ RPC / Topic / Service 封装具体业务字段 ↓ MessageFactory 把 MType 映射到具体 C 消息类型现在我们的框架已经能够用统一对象表示不同业务消息也能在“只有MType”的情况下创建正确的消息类型。下一篇就可以正式回到网络主线Muduo Buffer ↓ LVProtocol ↓ 判断完整消息边界 ↓ 读取 Length / MType / IDLength / RID / Body ↓ MessageFactory::create(mtype) ↓ 恢复成具体消息对象也就是说下一步真正要解决的是TCP 给我们的只是连续字节流LVProtocol怎样从 Buffer 中准确取出一条完整消息并把它恢复成这一篇定义的消息对象