MSDKv5 Router代码笔记
潘忠显 / 2021-01-07
MSDK v5版本的Router接收HTTP请求,对请求中的URL和POST消息体做解析,会校验签名、消息体有效性等,根据不同维度进行频率限制;根据Path路由到不同的逻辑服务器;将解析结果填充到Protobuf中,与逻辑服务器进行通信,会将一些信息上报日志以及调用跟踪。
框架说明
Router使用的是SPP的异步框架CAsyncFrame
,而逻辑层大多使用是同步框架。
spp_handle_
开头的函数
spp_handle_init()
中需要:
- 注册几个固定状态的回调,在“7.4.3.4 注册回调函数”一节中有介绍
- 添加请求处理所需要的所有状态IState接口(7.4.3.5)
spp_handle_input()
会切分HTTP包,0
表示收到的包不完整,>0
表示已接收完整的报的长度,<0
错误。
spp_handle_process()
中的blob中有完整的包,flow是请求包标志,可以用来串起后边的处理消息的日志。
没有使用第三方库解析HTTP,能够支持Content-Length
和chunked
两种组包方式。支持返回Gzip的压缩方式。
主动关闭连接可以发一个blob.data = NULL
的包。
三种消息
根据HTTP解析结果,创建不同的异步消息。在spp_handle_process()
中调用CAsyncFrame::Instance()->Process(msg);
开始调用处理逻辑,由框架去调度。
CommonMsg
继承自CMsgBase
,而有三个类继承自CommonMsg
,分别是:ErrorMsg
、ItopMsg
、DecryptMsg
,各自的作用是返回错误消息、处理正常请求、解密命令字处理,每个请求只会创建其中的一种Msg。
框架相关的Set都是CMsgBase基类的实现的一些成员函数,比如SetFlow()
,所以每个消息体中都能获得到flow
、commu
、base
等内容。
状态转移
因为使用的异步框架,所以涉及到状态转移,在consts.h中定义了状态:
// 状态ID
#define STATE_ID_FINISHED 0
#define STATE_ID_ITOP_PROCESS 125
#define STATE_ID_TLOG_REPORT 233 // tlog上报
#define STATE_ID_FREQ_LIMIT 555 // 检查限频
上述过程正常走完的状态流转是:STATE_ID_FREQ_LIMIT
-> STATE_ID_ITOP_PROCESS
-> STATE_ID_FINISHED
并没有状态转移到STATE_ID_TLOG_REPORT
(已经被注释掉),TLog上报是在注册的回调函数Fini()
中进行的。
以限频为例解释
state_check_limit.h/cpp
消息
在绑定回调函数Init()里,有判断msg->is_error_
是否为创建的错误消息(因为解析等原因失败了);也有判断msg->check_limit_
是否需要限频,而这个是在配置中读取的。
状态:StateCheckLimit
- 构造函数中读取配置并设置
HandleEncode()
中创建CActionInfo
;设置对应的动作CheckLimitAction
,并指明该动作使用的服务器信息和超时;将创建的CActionInfo
加入到CActionSet
中- 当Action都执行完成之后,调用
HandleProcess()
,进行状态转移
这里阅读代码的时候有一个疑问:为什么在CheckLimitAction
相关处理函数中没有看到有设置msg->next_state_id_
,而能够跳转到STATE_ID_ITOP_PROCESS
?因为在HTTP解析构造msg的时候就设置了msg->next_state_id_ = STATE_ID_ITOP_PROCESS;
这里可读性不好,也破坏了next_state_id_
应该表达的含义。
动作:CheckLimitAction
- 框架调用
HandleEncode()
解析框架中传入的msg,设置CheckLimitReq
对应的属性gameid
、channelid
等;序列化之后的CheckLimitReq
拷贝到buf中; - 框架在收到回包之后,调用
HandleInput()
检查回包的完整性 - 当收包完整之后,调用
HandleProcess()
函数来处理回包;在回包处理中判断是否达到了限频,或者有其他错误;将处理结果设置到框架传入的msg中
Action支持设置IP和Port,也支持使用L5(函数名SetRouteID()
)
功能分析
除了限频功能外,主要Router的主要逻辑在http_pack.cpp和state_itop_req.h/cpp文件中。
http_pack.cpp文件中http_request_decode()
- 解析HTTP请求中URL和Body,检查其中的必填参数
- 生成流水号
- 过滤特殊来源请求(如安平扫描接口)
- 获取CMDID和路由信息,几种错误返回:没有CGI、没有路由、没有下游
- 统计请求中的相关信息
- 参数有效性校验:channel_id、os、sig(可以配置是否校验
NeedCheckSig()
) - 检查业务是否有访问接口权限
- 是否需要解压缩
- 创建消息,设置消息的众多属性,解密或处理请求
- 支持转发cookie到逻辑层
- 可以保持长连接
keep-alive
- 支持返回进行压缩
- 设置调用跟踪
state_itop_req.h/cpp文件中状态StateItopReq
和动作ItopReqAction
- 调用
DNSLocalCache
使用域名获取IP地址、或使用L5、或使用IP - 序列化逻辑层请求的Protobuf到字符串,并设置buf和len的值
- 处理逻辑层回包并组包,或处理错误/超时返回;
- 处理逻辑层返回结果,解析部分参数
- 判断是否需要进行重试
- 设置一些参数,比如下游信息(因为重试可能导致有多个logic server,从GetActionSet中获取)
- 补全调用跟踪日志
- 上报统计信息
- 上报TLog(
Fini()
中)
配置从哪里来?
检查业务接口权限的时候,有一个开关需要判断:
parse_conf->GetServerCheckPermission()
而parse_conf->server_info_
是从哪里解析出来的?
LoadServerInfo
从XML文件解析并转换成Protobuf的。比如其中的check_permission
是作为server_info
标签的一个属性。
<server_info env="1" cmcc="1" check_limit="1" check_permission="1" default_permission="1" check_sig="0"/>
已经支持的权限控制
Router 可配置是否需要打开权限控制开关。
itop_conf::ParseConf的权限检测,通过全局的cmcc::ITOPRouterPermClient
,实现细节在itop_routerperm_client.h
中。
本质上是匹配配置中心的配置,但是设计上有一定的技巧。
存储结构以source为key存储PermMap_t
,而PermMap_t
则是以PermRS
为key存储PermVal
三级key获取权限,支持默认权限配置。(当前代码中暂时未使用ip
一级权限控制。)
source
-> gameid + channelid + cmdid
-> ip
-> value
source
-> gameid + 0 + cmdid
-> ip
-> value
source
-> gameid + 0 + 0
-> ip
> value
**source2perm_map_
的原始存储是在哪里?**配置实际在数据表tbRouterPerm
中,咋CMCC plugin中itop_routerperm_plugin.cpp
中有拼装详情。
支持非阻塞的DNS解析
可优化点
编程规范
CommonMsg
的构造可以使用初始化列表
成员命名不一致: sLogString
、request_url_
、find_openid()
、SetErrorResponse()
、set_timecost()
函数过长,圈复杂度过高:http_request_decode()
效率
CommonMsg::SendHttpRsp()
中string sRspBuffer = response_body();
可以使用引用接收返回,减少一次copy。
异常处理
SetErrorResponse
没有使用JSON库直接拼字符串,没有转移msg中的特殊字符
如果是Login(11001)命令字,从返回中找openid,否则从请求中找openid。为了做log上报。
Router中的find_openid()
直接根据字符串匹配"\"openid\":\""
是不是不准确?因为部分渠道(手Q、微信PC)登录的过程中,在channel_info的对象中也有openid,如果顺序有调整,将会取到渠道的openid。不过只有调用跟踪日志里边有用到这个字段。
状态转移不清晰
上边有描述说msg->next_state_id_
没有很好的表达含义。可以做如下修改以优化可读性:
- [已存在]
ErrorMsg
的next_state_id_
构造时设置为STATE_ID_FINISHED
ItopMsg
和DecryptMsg
的next_state_id_
构造时设置为STATE_ID_ITOP_PROCESS
- HTTP解析的时候如果判断需要限频,则将
next_state_id_
设置为STATE_ID_FREQ_LIMIT
Init()
中都是返回msg->next_state_id_
- 在其他IActionInfo和IState中正确的进行跳转
待完善(2021-01-07)
parse_conf->GetServerCheckPermission();
check_request_permission(gameid, channelid, source_str, cmdid, check_whitelist, remote_ip);
疑问
cookie转发到逻辑层,需要进行全局的存储?
DNSLocalCache::GetIp()
不会死锁吗?
item->mutex_.lock();
int size = item->ip_list_.size();
if (size == 0) {
err = "Get ip list fail";
item->mutex_.unlock();
return -1;
}
学到点什么
ISO time的utility函数 有没有什么库里边有替换的函数?非线程安全的。单线程也不需要线程安全。
DNSLocalCache
为了防止DNS解析卡住SPP主线程,这里是用了一个单独的线程去更新DNS的解析。
DNSLocalCache
做了哪些事
首先是个单例,外部可以通过Instance()
获得对象,并调用其public
方法,比如GetIp()
。
其次是个线程(会创建一个线程),周期性更新刷新所有曾经解析过的域名。
思考几个问题
gethostbyname
与getaddrinfo
函数的区别是什么?- 另外线程中调用
GetIp()
是否还可能卡住线程? - 如何写程序测试
The getaddrinfo() function combines the functi:onality provided by the gethostbyname(3) and getservbyname(3) functions into a single interface, but unlike the latter functions, getaddrinfo() is reentrant and allows programs to eliminate IPv4-versus-IPv6 dependencies.
编写独立的main()
函数,编译的时候加上对应的include路径,链接的时候加上对应的静态库即可。
// 省去头文件
int main() {
// DNS 解析初始化
DNSLocalCache::getInstance()->Init(60 * 1000);
DNSLocalCache::getInstance()->SetLogHandle(NULL);
DNSLocalCache::getInstance()->start();
while (true) {
printf("[%d] before parsing...\n", time(NULL));
std::string ip, err_msg;
int ret =
DNSLocalCache::getInstance()->GetIp("www.b1aidunnnnn.com", ip, err_msg);
printf("ret: %d, ip: %s, err_msg: %s\n", ret, ip.c_str(), err_msg.c_str());
printf("[%d] after parsing...\n", time(NULL));
usleep(100000); // sleep 100ms
}
return 0;
}
其他线程中调用GetIp()
的时候可能会卡住一段时间。因为其中使用了DNSLocalCache使用getaddrinfo()
,该函数是同步的,其异步版本是带_a
(asynchronous)的getaddrinfo_a()
。
特别的将/etc/resolv.conf
中正常的域名服务器注释掉,写一个10.0.0.1的进去,会造成解析时达到15秒超时的情况。
[1610098186] before parsing...
ret: -1, ip: , err_msg: dns[www.b1aidunnnnn.com] lookup fail:Temporary failure in name resolution
[1610098201] after parsing...
[1610098201] before parsing...
ret: -1, ip: , err_msg: dns[www.b1aidunnnnn.com] lookup fail:Temporary failure in name resolution
[1610098211] after parsing...
[1610098211] before parsing...
ret: -1, ip: , err_msg: dns[www.b1aidunnnnn.com] lookup fail:Temporary failure in name resolution
[1610098221] after parsing...
语言技巧
类的父类可以是使用自己作为形参的类模板生成的类
class DNSLocalCache : public taf::TC_Singleton<DNSLocalCache>,
public taf::TC_Thread,
public taf::TC_ThreadLock {
要声明的类本身,可以作为继承的类模板生成的类的派生。简化成:
class A;
class A : public std::vector<A> {};
可是,什么情况下的会需要这种继承?
typedef typename std::map<PermRS, PermVal, Comp<PermRS> > PermMap_t;