当前位置: 首页 > 工具软件 > Drogon > 使用案例 >

Drogon网络库URL转发过程源码解析

公羊灿
2023-12-01

Drogon是C++开发的后端服务框架,在看示例代码时,简短的语法一下是真的美,于是打算花点时间学习其中的实现细节。看到request解析到响应的业务处理部分,有个地方处理很是巧妙,跟Qt的信号槽机制神似。
请求到后端服务后,首先做HTTP的协议解析,然后根据URL获取访问路径及请求参数,查找匹配的业务处理接口,接口调用拿到响应结果返回给客户端。查找匹配业务接口的过程,库中是这样的处理:

  1. 定义了一套处理接口的注册机制。
    a. 指定匹配的URL模式,URL中所有可填充参数的部分都用占位符表示,类似Python的Print函数格式化字符;
    b. 接收的HTTP方法等,用于描述请求到达该接口的前置过滤条件;
    c. 请求处理暴露出去的接口,也就是实现业务处理的地方;
    示例代码如下:
	    METHOD_ADD(CustomCtrl::hello, //业务处理接口
	               "/{userName}", //URL
	               Get,           //按方法过滤
	               "CustomHeaderFilter");  // path is /customctrl/{arg1}
	    METHOD_LIST_END
  1. 实现了URL解析,并从URL检出所需参数的方法;并且可以将检出参数(string类型)转换为与业务处理接口相匹配的参数类型。
    对于URL的解析和匹配过程,采取的方法是将位置参数替换为带小括号的正则表达式模式字符串,与请求中的URL做正则匹配,若能匹配中,则从匹配结果的组集合中获取各组所包含的字符串。字符串到参数类型的转换,采用了两种方案:
    1. 将字符串转换字符串流,通过流输出操作符,自动转为对应类型的参数;
    2. 基本的数据类型,int、double等通过stoi的接口调用进行转换。
      关键代码段如下:
// SFINAE技法确定当前类型是否有">>"定义
    template <typename T>
    struct CanConvertFromStringStream
    {
      private:
        using yes = std::true_type;
        using no = std::false_type;
        template <typename U>
        static auto test(U *p, std::stringstream &&ss)
            -> decltype((ss >> *p), yes()); //构造的校验结构,通常为表达式
        template <typename>
        static no test(...);
      public:
        static constexpr bool value =
            std::is_same<decltype(test<T>(nullptr, std::stringstream())),
                         yes>::value;
    };

	// 能够通过流输出操作获取,则通过如下方式
    template <typename T>
    typename std::enable_if<CanConvertFromStringStream<T>::value, void>::type
    getHandlerArgumentValue(T &value, std::string &&p)
    {
        if (!p.empty())
        {
            std::stringstream ss(std::move(p));
            ss >> value;
        }
    }
	//  重载同名方法,通过stoi调用来实现

当然其中要解决:
1) 怎么将url的位置参数与接口对应索引位置关联起来?如何拿到要转换的参数类型?
2) 参数指针是如何保存的?在参数转换完成后又是如何调用的?
针对上述问题,关键代码如下:

// 定义类模板,模板参数为函数指针类型,其函数签名与业务处理接口一致;
template <typename FUNCTION>
class HttpBinder : public HttpBinderBase
{
  public:
      // 通过构造函数的入参类型推导FUNCTION的模板实参类型
    HttpBinder(FUNCTION &&func) : func_(std::forward<FUNCTION>(func))
    {
        static_assert(traits::isHTTPFunction,
                      "Your API handler function interface is wrong!");
        handlerName_ = DrClassMap::demangle(typeid(FUNCTION).name());
    }


  private:
    FUNCTION func_; // 接口函数以成员变量方式保存,待匹配参数匹配转换完成后调用
    
       // 接口信息获取的Traits,主要用于提取接口回调敢兴趣的特性,比如参数个数,
       // 是否为类接口以及类名等。后面展开讲述;
	using traits = FunctionTraits<FUNCTION>;
    template <std::size_t Index>
    using nth_argument_type = typename traits::template argument<Index>;
    static const size_t argument_count = traits::arity;

//url中获取参数集解析逻辑如下,pathArguments中保存了所有的从路径中获取到string类型的参数。
//values为可变参数集,随着递归调用此次越来越深,该可变参数列表越长。
//初始个数为0,终止个数为响应处理接口的入参个数。
template <typename... Values, std::size_t Boundary = argument_count>
    typename std::enable_if<(sizeof...(Values) < Boundary), void>::type run(
        std::deque<std::string> &pathArguments,
        const HttpRequestPtr &req,
        std::function<void(const HttpResponsePtr &)> &&callback,
        Values &&... values)
    {
        // Call this function recursively until parameter's count equals to the
        // count of target function parameters
        static_assert(
            BinderArgTypeTraits<nth_argument_type<sizeof...(Values)>>::isValid,
            "your handler argument type must be value type or const left "
            "reference type or right reference type");

	   //nth_argument_type能够获取到FUNCTION形参列表中,
	   // sizeof...(Values对应索引位置的参数类型。怎么获取到的后面再看。可
	   // 以把他当成特特殊的函数,入参为索引位置n,出参为数据类型。
        using ValueType =
            typename std::remove_cv<typename std::remove_reference<
                nth_argument_type<sizeof...(Values)>>::type>::type;
        ValueType value = ValueType();
        if (!pathArguments.empty())
        { 
            // 从参数集中获取首个,由于后面有弹出操作,所以是按序逐个获取的
            std::string v = std::move(pathArguments.front());
            pathArguments.pop_front();
            try
            {
                if (v.empty() == false)
                    // 这里就是根据实参类型调对应的转换接口了
                    getHandlerArgumentValue(value, std::move(v));
            }
            catch (const std::exception &e)
            {
                handleException(e, req, std::move(callback));
                return;
            }
        }
        else
        {
            try
            {
                value = req->as<ValueType>();
            }
            catch (const std::exception &e)
            {
                handleException(e, req, std::move(callback));
                return;
            }
            catch (...)
            {
                LOG_ERROR << "Exception not derived from std::exception";
                return;
            }
        }

	   // 将新解析的出来的value参数,作为入参追加道变参列表中
        run(pathArguments,
            req,
            std::move(callback),
            std::forward<Values>(values)...,
            std::move(value));
    }

    // sizeof...(Values) == Boundary,即从参数集中拿到了接口调用所需的所有参数
    template <typename... Values,
              std::size_t Boundary = argument_count,
              bool isCoroutine = traits::isCoroutine>
    typename std::enable_if<(sizeof...(Values) == Boundary) && !isCoroutine,
                            void>::type
    run(std::deque<std::string> &,
        const HttpRequestPtr &req,
        std::function<void(const HttpResponsePtr &)> &&callback,
        Values &&... values)
    {
        try
        {
            // 调用保存下来的FUNCTION函数
            callFunction(req, callback, std::move(values)...);
        }
        catch (const std::exception &except)
        {
            handleException(except, req, std::move(callback));
        }
        catch (...)
        {
            LOG_ERROR << "Exception not derived from std::exception";
            return;
        }
    }

至此,url中的参数集到接口实际参数之间的转换和调用过程就看完了。其中还有两个疑问点,FunctionTraits干了啥?假如调用接口为类的静态接口,callFunction又做了些什么?
首先来看FunctionTraits,关键代码如下:

// 通用模板定义
template <typename>
struct FunctionTraits;

… //此处省去非成员函数的特化模板

// 类成员函数的特化模板,通过模板匹配获取到了ClassType,ReturnType和形参列表Arguments
//还是没有看到获取第n个形参类型的实现。由于通过返回值和形参列表又重新构造了基类模板,所
//  以猜测肯定是在非成员函数特化模板中定义的。
template <typename ClassType, typename ReturnType, typename... Arguments>
struct FunctionTraits<ReturnType (ClassType::*)(Arguments...) const>
    : FunctionTraits<ReturnType (*)(Arguments...)> //重新构造非成员函数模板,提取其他特性
{
    static const bool isClassFunction = true;
    static const bool isDrObjectClass =
        std::is_base_of<DrObject<ClassType>, ClassType>::value;
    using class_type = ClassType;
    static const std::string name()
    {
        return std::string("Class Function");
    }
};

// 就是这里,argument就是辅助模板
template <typename ReturnType, typename... Arguments>
struct FunctionTraits<ReturnType (*)(Arguments...)>
{
    using result_type = ReturnType;

       // 借助标注库的tuple,获取指定索引位置的形参类型。其实也可以理解为另一个特殊函数的调用:
      //  入参为Index形参列表,输出为形参类型
    template <std::size_t Index>
    using argument =
        typename std::tuple_element<Index, std::tuple<Arguments...>>::type;
    static const std::size_t arity = sizeof...(Arguments);
    using class_type = void;
    using return_type = ReturnType;
    static const bool isHTTPFunction = false;
    static const bool isClassFunction = false;
    static const bool isDrObjectClass = false;
    static const bool isCoroutine = false;
    static const std::string name()
    {
        return std::string("Normal or Static Function");
    }
};

至此,获取第n个位置的形参问题已经解决了。再看第二个函数调用问题,关键代码如下:

  // 有了FunctionTraits,各种函数特性皆可认为已有了。这里只看类成员函数的场景
    template <typename... Values,
              bool isClassFunction = traits::isClassFunction,
              bool isDrObjectClass = traits::isDrObjectClass,
              bool isNormal = std::is_same<typename traits::first_param_type,
                                           HttpRequestPtr>::value>
    typename std::enable_if<isClassFunction && isDrObjectClass && isNormal,
                            typename traits::return_type>::type
    callFunction(const HttpRequestPtr &req, Values &&... values)
    {
        // 这里根据类型获取对象,可以理解为获取该类型的单例对象,至于该单例对象是
               //  如何获取管理的又是如何获取的,后面再展开看。至此我们已经看清了调用过程的全貌
        static auto objPtr =
            DrClassMap::getSingleInstance<typename traits::class_type>();
        return (*objPtr.*func_)(req, std::move(values)...);
    }

整个从url参数集到业务接口调用的过程我们已经撕完了,那么还有就是回调对象的注册管理问题了。
在Drogon库中可以通过继承HTTPController,增加业务处理接口,并注册到框架的方式来完成url到处理接口注册过程。看个典型代码:

class User : public drogon::HttpController<User>
{
  public:
    METHOD_LIST_BEGIN
    //use METHOD_ADD to add your custom processing function here;
    METHOD_ADD(User::getInfo, "/{id}", Get);                  //path is /api/v1/User/{arg1}
    METHOD_ADD(User::getDetailInfo, "/{id}/detailinfo", Get);  //path is /api/v1/User/{arg1}/detailinfo
    METHOD_ADD(User::newUser, "/{name}", Post);                 //path is /api/v1/User/{arg1}
    METHOD_LIST_END
    //your declaration of processing function maybe like this:
    void getInfo(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback, int userId) const;
    void getDetailInfo(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback, int userId) const;
    void newUser(const HttpRequestPtr &req, std::function<void(const HttpResponsePtr &)> &&callback, std::string &&userName);
  public:
    User()
    {
        LOG_DEBUG << "User constructor!";
    }
};

#define METHOD_ADD(method, pattern, ...) \
    // 这里有一点宏定义使用的小技巧,变参是宏参数可以在函数调用时自动将部分参数
       //   检出用作构造函数的初始化列表。
    registerMethod(&method, pattern, {__VA_ARGS__}, true, #method)

想必getSingleInstance获取到的就是这里的User单例对象。METHOD_ADD自然是要创建HttpBinder对象并保存起来,然后在请求到来时根据path进行查找,然后就是刚讲完的那一套调用过程。HttpBinder的保存和查找过程不是我们所感兴趣的。我们感兴趣的是HttpController和getObject之间有什么关联。


template <typename T, bool AutoCreation = true>
class HttpController : public DrObject<T>, public HttpControllerBase
{
…

namespace drogon
{
/**
 * @brief The base class for all drogon reflection classes.
 *
 */
class DROGON_EXPORT DrObjectBase
{
  public:
    /**
     * @brief Get the class name
     *
     * @return const std::string& the class name
     */
    virtual const std::string &className() const
    {
        static const std::string name{"DrObjectBase"};
        return name;
    }
    /**
     * @brief Return true if the class name is 'class_name'
     */
    virtual bool isClass(const std::string &class_name) const
    {
        return (className() == class_name);
    }
    virtual ~DrObjectBase()
    {
    }
};

/**
 * a class template to
 * implement the reflection function of creating the class object by class name
 */
template <typename T>
class DrObject : public virtual DrObjectBase
{
  public:
    virtual const std::string &className() const override
    {
        return alloc_.className();
    }
    static const std::string &classTypeName()
    {
        return alloc_.className();
    }
    virtual bool isClass(const std::string &class_name) const override
    {
        return (className() == class_name);
    }
  protected:
    // protect constructor to make this class only inheritable
    DrObject() = default;
    ~DrObject() override = default;
  private:
    class DrAllocator
    {
      public:
        DrAllocator()
        {
            // 构造函数中完成T的注册逻辑
            registerClass<T>();
        }
        const std::string &className() const
        {  
            // typeid(T).name()获取到T的类名,如"class test::MyClass"
            static std::string className =
                DrClassMap::demangle(typeid(T).name());
            return className;
        }
        template <typename D>
        typename std::enable_if<std::is_default_constructible<D>::value,
                                void>::type
        registerClass()
        {
            // 以类名为key,将创建单例的回调函数,向DrClassMap进行注册
            DrClassMap::registerClass(className(),
                                      []() -> DrObjectBase * { return new T; });
        }
        template <typename D>
        typename std::enable_if<!std::is_default_constructible<D>::value,
                                void>::type
        registerClass()
        {
        }
    };
    // use static val to register allocator function for class T;
    static DrAllocator alloc_; // 静态成员,该静态对象构造时完成T的注册
};
template <typename T>
typename DrObject<T>::DrAllocator DrObject<T>::alloc_;
}  // namespace drogon

// 注册到map表中
void DrClassMap::registerClass(const std::string &className,
                               const DrAllocFunc &func)
{
    LOG_TRACE << "Register class:" << className;
    getMap().insert(std::make_pair(className, func));
}

// 根据classname找到map表中单例对象,有则返回无则创建
const std::shared_ptr<DrObjectBase> &DrClassMap::getSingleInstance(
    const std::string &className)
{
    auto &mtx = internal::getMapMutex();
    auto &singleInstanceMap = internal::getObjsMap();
    {
        std::lock_guard<std::mutex> lock(mtx);
        auto iter = singleInstanceMap.find(className);
        if (iter != singleInstanceMap.end())
            return iter->second;
    }
    auto newObj = std::shared_ptr<DrObjectBase>(newObject(className));
    {
        std::lock_guard<std::mutex> lock(mtx);
        auto ret = singleInstanceMap.insert(
            std::make_pair(className, std::move(newObj)));
        return ret.first->second;
    }
}

// 调用注册时创建对象的回调函数,创建对象
DrObjectBase *DrClassMap::newObject(const std::string &className)
{
    auto iter = getMap().find(className);
    if (iter != getMap().end())
    {
        return iter->second();
    }
    else
        return nullptr;
}

DrObject的实现为反射机制的简单实现,即通过类名获取构造指定类型的对象,类名到类型的转换过程即为反射。这种机制在java中用的比较多,库作者自己简单实现了一套反射机制,主要做单例对象的管理用。
该部分代码,库作者对模板技术的应用也是看着很爽,算得上是模板应用的实例教程了。

 类似资料: