Jason Pan

MSDKv5 Router代码笔记

黄杰 / 2021-01-07


MSDK v5版本的Router接收HTTP请求,对请求中的URL和POST消息体做解析,会校验签名、消息体有效性等,根据不同维度进行频率限制;根据Path路由到不同的逻辑服务器;将解析结果填充到Protobuf中,与逻辑服务器进行通信,会将一些信息上报日志以及调用跟踪。

框架说明

Router使用的是SPP的异步框架CAsyncFrame,而逻辑层大多使用是同步框架。

spp_handle_开头的函数

spp_handle_init()中需要:

  1. 注册几个固定状态的回调,在“7.4.3.4 注册回调函数”一节中有介绍
  2. 添加请求处理所需要的所有状态IState接口(7.4.3.5)

spp_handle_input() 会切分HTTP包,0表示收到的包不完整,>0表示已接收完整的报的长度,<0 错误。

spp_handle_process()中的blob中有完整的包,flow是请求包标志,可以用来串起后边的处理消息的日志。

没有使用第三方库解析HTTP,能够支持Content-Lengthchunked两种组包方式。支持返回Gzip的压缩方式。

主动关闭连接可以发一个blob.data = NULL的包。

三种消息

根据HTTP解析结果,创建不同的异步消息。在spp_handle_process()中调用CAsyncFrame::Instance()->Process(msg);开始调用处理逻辑,由框架去调度。

CommonMsg继承自CMsgBase,而有三个类继承自CommonMsg,分别是:ErrorMsgItopMsgDecryptMsg,各自的作用是返回错误消息、处理正常请求、解密命令字处理,每个请求只会创建其中的一种Msg。

框架相关的Set都是CMsgBase基类的实现的一些成员函数,比如SetFlow(),所以每个消息体中都能获得到flowcommubase等内容。

状态转移

因为使用的异步框架,所以涉及到状态转移,在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

  1. 构造函数中读取配置并设置
  2. HandleEncode()中创建CActionInfo;设置对应的动作CheckLimitAction,并指明该动作使用的服务器信息和超时;将创建的CActionInfo加入到CActionSet
  3. 当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

  1. 框架调用HandleEncode()解析框架中传入的msg,设置CheckLimitReq对应的属性gameidchannelid等;序列化之后的CheckLimitReq拷贝到buf中;
  2. 框架在收到回包之后,调用HandleInput()检查回包的完整性
  3. 当收包完整之后,调用HandleProcess()函数来处理回包;在回包处理中判断是否达到了限频,或者有其他错误;将处理结果设置到框架传入的msg中

Action支持设置IP和Port,也支持使用L5(函数名SetRouteID()

功能分析

除了限频功能外,主要Router的主要逻辑在http_pack.cpp和state_itop_req.h/cpp文件中。

http_pack.cpp文件中http_request_decode()

  1. 解析HTTP请求中URL和Body,检查其中的必填参数
  2. 生成流水号
  3. 过滤特殊来源请求(如安平扫描接口)
  4. 获取CMDID和路由信息,几种错误返回:没有CGI、没有路由、没有下游
  5. 统计请求中的相关信息
  6. 参数有效性校验:channel_id、os、sig(可以配置是否校验NeedCheckSig())
  7. 检查业务是否有访问接口权限
  8. 是否需要解压缩
  9. 创建消息,设置消息的众多属性,解密或处理请求
  10. 支持转发cookie到逻辑层
  11. 可以保持长连接keep-alive
  12. 支持返回进行压缩
  13. 设置调用跟踪

state_itop_req.h/cpp文件中状态StateItopReq和动作ItopReqAction

  1. 调用DNSLocalCache使用域名获取IP地址、或使用L5、或使用IP
  2. 序列化逻辑层请求的Protobuf到字符串,并设置buf和len的值
  3. 处理逻辑层回包并组包,或处理错误/超时返回;
  4. 处理逻辑层返回结果,解析部分参数
  5. 判断是否需要进行重试
  6. 设置一些参数,比如下游信息(因为重试可能导致有多个logic server,从GetActionSet中获取)
  7. 补全调用跟踪日志
  8. 上报统计信息
  9. 上报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 的构造可以使用初始化列表

成员命名不一致: sLogStringrequest_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_没有很好的表达含义。可以做如下修改以优化可读性:

  1. [已存在] ErrorMsgnext_state_id_构造时设置为STATE_ID_FINISHED
  2. ItopMsgDecryptMsgnext_state_id_构造时设置为STATE_ID_ITOP_PROCESS
  3. HTTP解析的时候如果判断需要限频,则将next_state_id_设置为STATE_ID_FREQ_LIMIT
  4. Init()中都是返回msg->next_state_id_
  5. 在其他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()

其次是个线程(会创建一个线程),周期性更新刷新所有曾经解析过的域名。

思考几个问题

  1. gethostbynamegetaddrinfo函数的区别是什么?
  2. 另外线程中调用GetIp()是否还可能卡住线程?
  3. 如何写程序测试

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;