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;
