从零开始实现一个C++高性能服务器框架----Hook模块

此项目是根据sylar框架实现,是从零开始重写sylar,也是对sylar丰富与完善
项目地址:https://gitee.com/lzhiqiang1999/server-framework

简介

项目介绍:实现了一个基于协程的服务器框架,支持多线程、多协程协同调度;支持以异步处理的方式提高服务器性能;封装了网络相关的模块,包括socket、http、servlet等,支持快速搭建HTTP服务器或WebSokcet服务器。
详细内容:日志模块,使用宏实现流式输出,支持同步日志与异步日志、自定义日志格式、日志级别、多日志分离等功能。线程模块,封装pthread相关方法,封装常用的锁包括(信号量,读写锁,自旋锁等)。IO协程调度模块,基于ucontext_t实现非对称协程模型,以线程池的方式实现多线程,多协程协同调度,同时依赖epoll实现了事件监听机制。定时器模块,使用最小堆管理定时器,配合IO协程调度模块可以完成基于协程的定时任务调度。hook模块,将同步的系统调用封装成异步操作(accept, recv, send等),配合IO协程调度能够极大的提升服务器性能。Http模块,封装了sokcet常用方法,支持http协议解析,客户端实现连接池发送请求,服务器端实现servlet模式处理客户端请求,支持单Reator多线程,多Reator多线程模式的服务器。

Hook模块

  • hook实际上就是对系统调用API进行一次封装,将其封装成一个与原始的系统调用API同名的接口,应用在调用这个接口时,会先执行封装中的操作,再执行原始的系统调用API。
  • hook的目的是将socket的IO操作都转换为异步,为对于用户来讲是用同步的方式编写代码。
  • hook和IO协程调度是密切相关的,如果不使用IO协程调度器,那hook没有任何意义。考虑IOManager要在一个线程上按顺序调度以下协程:
    • 协程1:sleep(2)睡眠2s后返回
    • 协程2:在socket fd1 上send100k数据
    • 协程3:在socket fd2 上recv直到数据接收成功
    1. 情况1在未hook的情况下,IOManager要调度上面的协程,流程是下面这样的:
      • 调度协程1,协程阻塞在sleep上,等2秒后返回,这两秒内调度线程是被协程1占用的,其他协程无法在当前线程上调度。
      • 调度协徎2,协程阻塞send100k数据上,这个操作一般问题不大,因为send数据无论如何都要占用时间,但如果fd迟迟不可写,那send会阻塞直到套接字可写,同样,在阻塞期间,其他协程也无法在当前线程上调度。
      • 调度协程3,协程阻塞在recv上,这个操作要直到recv超时或是有数据时才返回,期间调度器也无法调度其他协程。
      • 显然,整个过程是同步的,都需要发生阻塞。
    2. 情况2hook的情况下
    • 调度协程1,检测到协程sleep,那么先添加一个2秒的定时器(定时器回调函数是在调度器上继续调度本协程),接着协程back,等定时器超时。
    • 因为上一步协程1已经back了,所以协徎2并不需要等2秒后才可以执行,而是立刻可以执行。同样,调度器检测到协程send,由于不知道fd是不是马上可写,所以先在IOManager上给fd注册一个写事件(回调函数是让当前协程call并执行实际的send操作),然后当前协程back,等可写事件发生。
    • 上一步协徎2也back了,可以马上调度协程3。协程3与协程2类似,也是给fd注册一个读事件(回调函数是让当前协程call并继续recv),然后本协程back,等事件发生。
    • 等2秒超时后,执行定时器回调函数,将协程1call以便继续执行。
    • 等协程2的fd可写,一旦可写,调用写事件回调函数将协程2 call以便继续执行send
    • 等协程3的fd可读,一旦可读,调用回调函数将协程3 call以便继续执行recv
    • 显然,整个过程是异步的,每次协程发生阻塞都会注册对应的事件或定时器,然后退出当前协程,等事件触发或定时器到期,又会回到协程继续完成操作。

1. 主要功能

  • 对socket常用的IO函数进行了hook,配合IO协程调度模块,可以实现异步操作。

2. 功能演示

  • 举例:以下有两个协程任务,协程任务1,协程任务2
    • 协程任务1会注册一个定时器2s,然后back。
    • 协程任务2此时执行,输出“fiber 2”,结束。
    • 进入idle,epoll_wait等待2s,执行定时任务,回到任务协程1,输出“sleep 2”,结束。
    • 整个过程是异步的,并没有因为sleep就让整个线程阻塞。
johnsonli::IOManager iom(1);

// 任务协程1
iom.schedule([](){
    sleep(2);
    LOG_INFO(g_logger) << "sleep 2";
});

// 任务协程2
iom.schedule([](){
    LOG_INFO(g_logger) << "fiber 2";
});

3. 模块介绍

3.1 Hook了哪些方法

  • 只针对soket的IO操作进行了hook,普通文件描述符将继续使用原始系统调用
    • sleep延时系列接口,包括sleep/usleep/nanosleep。只需要给IO协程调度器注册一个定时事件,在定时事件触发后再继续执行当前协程即可。当前协程在注册完定时事件后即可back让出执行权。
    • socket IO系列接口,包括read/write/recv/send…等,connect及accept。这类接口的hook首先需要判断操作的fd是否是socket fd,以及用户是否显式地对该fd设置过非阻塞模式,如果不是socket fd或是用户显式设置过非阻塞模式,那么就不需要hook了,直接调用操作系统的IO接口即可。如果需要hook,那么首先在IO协程调度器上注册对应的读写事件,等事件发生后再继续执行当前协程。当前协程在注册完IO事件即可back让出执行权。
    • socket/fcntl/ioctl/close等接口,这类接口主要处理的是边缘情况,比如分配fd上下文,处理超时及用户显式设置非阻塞问题。
sleep
usleep
nanosleep
socket
connect
accept
read、readv、recv、recvfrom、recvmsg
write、writev、send、sendto、sendmsg
close
fcntl
ioctl
getsockopt
setsockopt

3.2 FdManager

  • 为了对socket的统一管理,设计了一个FdManager类来记录所有分配过的fd的上下文,这是一个单例类,每个socket fd上下文记录了当前fd的读写超时,是否设置非阻塞等信息。
  • FdCtx类在用户态记录了fd的读写超时和非阻塞信息,其中非阻塞包括用户显式设置的非阻塞和hook内部设置的非阻塞,区分这两种非阻塞可以有效应对用户对fd设置/获取NONBLOCK模式的情形。
class FdCtx : public std::enable_shared_from_this<FdCtx>
{
public:
	// 成员方法
private:
    bool m_isInit: 1;           //是否初始化
    bool m_isSocket: 1;         //是否socket
    bool m_sysNonblock: 1;      //是否hook非阻塞
    bool m_userNonblock: 1;     //是否用户主动设置非阻塞
    bool m_isClosed: 1;         //是否关闭
    int m_fd;                   //文件句柄
    uint64_t m_recvTimeout;     //读超时时间毫秒
    uint64_t m_sendTimeout;     //写时间时间毫秒
};


/**
 * @brief 文件句柄管理类
 */
class FdManager {
public:
    typedef RWMutex RWMutexType;

    FdManager();

    /**
     * @brief 获取/创建文件句柄类FdCtx
     * @param[in] fd 文件句柄
     * @param[in] auto_create 是否自动创建
     * @return 返回对应文件句柄类FdCtx::ptr
     */
    FdCtx::ptr get(int fd, bool auto_create = false);

    /**
     * @brief 删除文件句柄类
     * @param[in] fd 文件句柄
     */
    void del(int fd);
private:
    RWMutexType m_mutex;				/// 读写锁
    std::vector<FdCtx::ptr> m_datas;	/// 文件句柄集合
};

typedef Singleton<FdManager> FdMgr;		/// 文件句柄单例

Hook的整体实现

  • hook功能以线程为单位,可自由设置当前线程是否使用hook。默认情况下,协程调度器的调度线程会开启hook,而其他线程则不会开启。
  • 线程局部变量t_hook_enable。用于表示当前线程是否启用hook。各个线程可单独启用或关闭hook。
static thread_local bool t_hook_enable = false;

// 当前线程是否hook
bool is_hook_enable() {
    return t_hook_enable;
}

// 设置当前线程的hook状态
void set_hook_enable(bool flag) {
    t_hook_enable = flag;
}
  • 使用获取被hook的接口的原始地址。
#define HOOK_FUN(XX) \
    XX(sleep) \
    XX(usleep) \
    XX(nanosleep) \
    XX(socket) \
    XX(connect) \
    XX(accept) \
    XX(read) \
    XX(readv) \
    XX(recv) \
    XX(recvfrom) \
    XX(recvmsg) \
    XX(write) \
    XX(writev) \
    XX(send) \
    XX(sendto) \
    XX(sendmsg) \
    XX(close) \
    XX(fcntl) \
    XX(ioctl) \
    XX(getsockopt) \
    XX(setsockopt)
 
extern "C" {
#define XX(name) name ## _fun name ## _f = nullptr;
    HOOK_FUN(XX);
#undef XX
}
 
void hook_init() {
    static bool is_inited = false;
    if(is_inited) {
        return;
    }
#define XX(name) name ## _f = (name ## _fun)dlsym(RTLD_NEXT, #name);
    HOOK_FUN(XX);
#undef XX
}

展开后

extern "C" {
    sleep_fun sleep_f = nullptr; \
    usleep_fun usleep_f = nullptr; \
    ....
    setsocketopt_fun setsocket_f = nullptr;
};
 
hook_init() {
    ...
     
    sleep_f = (sleep_fun)dlsym(RTLD_NEXT, "sleep"); \
    usleep_f = (usleep_fun)dlsym(RTLD_NEXT, "usleep"); \
    ...
    setsocketopt_f = (setsocketopt_fun)dlsym(RTLD_NEXT, "setsocketopt");
}

hook_init() 放在一个静态对象的构造函数中调用,这表示在main函数运行之前就会获取各个符号的地址并保存在全局变量中

  • 对sleep/usleep/nanosleep的hook实现。x秒后,又回到该协程。
 unsigned int sleep(unsigned int seconds) 
 {
     //如果没使用hook,就调用原系统函数
     if(!johnsonli::t_hook_enable)
     {
         return sleep_f(seconds);
     }

     //使用了hook,用自己实现的
     johnsonli::Fiber::ptr fiber = johnsonli::Fiber::GetThis();      //获取当前协程
     johnsonli::IOManager* iom = johnsonli::IOManager::GetThis();    //获取当前IOManager

     iom->addTimer(seconds * 1000, [iom, fiber](){
         iom->schedule(fiber);
     });

     johnsonli::Fiber::YieldToHoldBySwap();
     return 0;
 }
  • socket的hook实现。socket用于创建套接字,需要在拿到fd后将其添加到FdManager中
    int socket(int domain, int type, int protocol)
    {
        //不使用hook
        if(!johnsonli::t_hook_enable) {
            return socket_f(domain, type, protocol);
        }

        //出错,直接返回
        int fd = socket_f(domain, type, protocol);
        if(fd == -1) {
            return fd;
        }

        //成功创建,需要存储相关信息
        johnsonli::FdMgr::GetInstance()->get(fd, true);
        return fd;
    }
  • connectconnect_with_timeout的hook实现。先尝试连接,超时,注册定时器,注册事件,退出当前协程。等事件发生,再回到该协程,此时可以连接成功;事件未发生并超时,回到该协程,将返回-1,说明超时。
int connect_with_timeout(int fd, const struct sockaddr* addr, socklen_t addrlen, uint64_t timeout_ms) {
    if(!johnsonli::t_hook_enable) {
        //LOG_INFO(g_logger) << "connect";
        return connect_f(fd, addr, addrlen);
    }

    johnsonli::FdCtx::ptr ctx = johnsonli::FdMgr::GetInstance()->get(fd);

    //不存在
    if(!ctx || ctx->isClose()) {
        errno = EBADF;
        return -1;
    }
    
    //不是socket
    if(!ctx->isSocket()) {
        return connect_f(fd, addr, addrlen);
    }

    //用户已经设置了非阻塞
    if(ctx->getUserNonblock()) {
        return connect_f(fd, addr, addrlen);
    }

    int n = connect_f(fd, addr, addrlen);
    if(n == 0) {
        return 0;	 //连接成功
    }
    else if(n != -1 || errno != EINPROGRESS) {
        return n;
    }

    johnsonli::IOManager* iom = johnsonli::IOManager::GetThis();
    johnsonli::Timer::ptr timer;
    std::shared_ptr<timer_info> tinfo(new timer_info);
    std::weak_ptr<timer_info> winfo(tinfo);

    if(timeout_ms != (uint64_t)-1) {
        timer = iom->addConditionTimer(timeout_ms, [winfo, fd, iom](){
            auto t = winfo.lock();
            if(!t || t->cancelled) {
                return;
            }

            //超时,取消事件,回到HOLD(任务协程)
            t->cancelled = ETIMEDOUT;
            iom->cancelEvent(fd, johnsonli::IOManager::WRITE);
        }, winfo);
    }

    int rt = iom->addEvent(fd, johnsonli::IOManager::WRITE);
    if(rt == 0) {
        johnsonli::Fiber::YieldToHoldBySwap();
        //定时器还有,说明事件触发了,需要取消定时器
        if(timer) {
        	// 此时取消定时器会强制执行定时任务,但是由于只是把定时任务加入到任务协程队列,因此不会马上执行。
            // 必须等退出connect_with_timeout,调度线程再去调度。但是此时weak_ptr已经被释放,条件不成立,定时任务最终不会被执行
            timer->cancel();
        }
        if(tinfo->cancelled) {
            errno = tinfo->cancelled;
            return -1;
        }
    }
    else {
        //超时
        if(timer) {
            timer->cancel();
        }
        LOG_ERROR(g_logger) << "connect addEvent(" << fd << ", WRITE) error";
    }

    int error = 0;
    socklen_t len = sizeof(int);
    //检查有无错误
    if(-1 == getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len)) {       
        return -1;	 // 调用失败
    }
   
    if(!error) {
        return 0;	 	//没有错误
    }else {  			//有错误
        errno = error;
        return -1;
    }
}
  • 这里需要注意:我们添加的是条件定时器,当事件发生,回到该协程,会取消定时器并强制执行定时任务,但是由于只是把定时任务加入到任务协程队列,因此不会马上执行。必须等退出connect_with_timeout,调度线程再去调度。但是此时weak_ptr已经被释放,条件不成立,定时任务最终不会被执行。
  • read、write系列方法和accept都是依赖于do_io模板函数,具体实现和connect_with_timeout类似。
  • close,这里除了要删除fd的上下文,还要取消掉fd上的全部事件,这会让fd的读写事件回调都执行一次。
  • fcntl,这里的O_NONBLOCK标志要特殊处理,因为所有参与协程调度的fd都会被设置成非阻塞模式,所以要在应用层维护好用户设置的非阻塞标志。
  • ioctl,同样要特殊处理FIONBIO命令,这个命令用于设置非阻塞,处理方式和上面的fcntl一样。
  • setsocketopt,这里要特殊处理SO_RECVTIMEO和SO_SNDTIMEO,在应用层记录套接字的读写超时,方便协程调度器获取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/7811.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

求给定集合中好数对的个数

已知一个集合A&#xff0c;对A中任意两个不同的元素求和&#xff0c;若求得的和仍在A内&#xff0c;则称其为好数对。例如&#xff0c;集合A{1 2 3 4}&#xff0c;123&#xff0c;134&#xff0c;则1,2和1,3 是两个好数对。 编写程序求给定集合中好数对的个数。 注&#xff1a;…

AST解混淆

示例&#xff1a; 智慧树 将混淆js复制保存到en.js文本中&#xff0c;语言选择JavaScript。 通过对文件分析得出 _0x1e77这个函数就是这个混淆JS 加密字符串的解密函数 折叠所有视图&#xff08;ctrl0&#xff09; 将未格式化的解密函数复制出来到ob.js文件中 const fs requ…

开源Icon大合集

Remix Icon Remix Icon 是一套面向设计师和开发者的开源图标库&#xff0c;所有的图标均可免费用于个人项目和商业项目。 与拼凑混搭的图标库不同&#xff0c;Remix Icon 的每一枚图标都是由设计师按照统一规范精心绘制的&#xff0c;在拥有完美像素对齐的基础上&#xff0c;…

.net特性(个人笔记)

dui前言 个人理解的特性。对标 简单的AOP编程 就是在对于的 类、方法、属性等上面声明一个标签--->然后利用反射的知识对标签进行解析-->进行某些特殊处理的判断。 实现过程 类/方法声明特性 /// <summary>/// 自定义Custom特性/// </summary>[Attribut…

Qt——实现一个简单的获取文件信息的dialog

实现效果 选择文件&#xff1a; 获取文件信息&#xff1a; 实现 1.创建项目 新建一个项目&#xff0c;随便起一个项目名FileInfo&#xff0c;让FileInfo继承自 QDialog类。 2.项目布局 topLayout使用的是 网格布局&#xff08;QGridLayout&#xff09;&#xff0c;把 属性 …

Windows编程基础

Windows编程基础 Unit1应用程序分类 控制台程序&#xff1a;Console Dos程序&#xff0c;本身没有窗口&#xff0c;通过windows Dos窗口执行 窗口程序 拥有自己的窗口&#xff0c;可以与用户交互 库程序 存放代码、数据的程序&#xff0c;执行文件可以从中取出代码执行和获取…

基于SpringBoot+Vue家乡特色推荐系统

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精…

异步线程池 CompletableFuture 异步编排 【下篇】

1、创建异步对象 CompletableFuture 提供了四个静态方法来创建一个异步操作。 提示 1、runXxxx 都是没有返回结果的&#xff0c;supplyXxx 都是可以获取返回结果的2、可以传入自定义的线程池&#xff0c;否则就用默认的线程池 1.1 不存在返回结果 public class ThreadTestD…

制造业短视频标题文案写作技巧

随着短视频的普及&#xff0c;越来越多的企业开始利用短视频来宣传产品、服务和品牌。一个好的短视频标题文案可以吸引用户的关注&#xff0c;提高点击率&#xff0c;从而达到宣传效果。那么&#xff0c;企业短视频标题文案如何制作呢&#xff1f;下面将从几个方面进行介绍。  …

测试碎碎念:selenium

一、什么是自动化 生活中的例子: 在早期的时候,园丁在给花园浇水的时候,都是亲手的去操作;然而在现在,更常见的是在花园里安装了洒水装置,定时定期的洒水,这就是自动化~ 原来,也是自己亲自的在家里打扫卫生;现在,出现了更多的扫地机器人来帮助完成,这也是自动化~ .…

sql语法:详解DDL

Mysql版本&#xff1a;8.0.26 可视化客户端&#xff1a;sql yog 目录一、DDL是什么&#xff1f;二、和数据库相关的DDL2.1 创建数据库2.2 删除数据库2.3 查看所有的数据库&#xff0c;当前用户登录后&#xff0c;可以看到哪些数据库2.4 查看某个数据库的详细定义2.5 修改数据库…

【云原生】Kubernetes(k8s)之容器的探测

Kubernetes&#xff08;k8s&#xff09;之容器的探测一、探测类型及使用场景1.1、startupProbe&#xff08;启动探测&#xff09;1.2、readinessProbe&#xff08;就绪探测&#xff09;1.3、livenessProbe&#xff08;存活探测&#xff09;二、检查机制三、探测结果四、容器探测…

日撸 Java 三百行day14-16

文章目录说明day14 栈1.栈的特点2.代码day15 栈的应用-括号匹配1.思路2.代码day16 递归1.递归特点2.代码说明 闵老师的文章链接&#xff1a; 日撸 Java 三百行&#xff08;总述&#xff09;_minfanphd的博客-CSDN博客 自己也把手敲的代码放在了github上维护&#xff1a;https…

全面带你了解AIGC的风口

前言 一、AIGC的介绍 二、AIGC 的几个主要作用 三、实现AIGC过程的步骤 四、科技新赛道AIGC开始火了 五、AIGC对世界产生广泛的影响 六、AIGC技术的主要风口 &#x1f618;一、AIGC的介绍 AIGC (AI Generated Content) 是指通过人工智能技术生成的各种类型的内容&#xff0c;…

软件测试,自学3个月出来就是高薪工作?你以为还是2019年以前?

朋友&#xff0c;作为一个曾经从机械转行到IT的行业的过来人&#xff0c;已在IT行业工作4年&#xff0c;分享一下我的经验&#xff0c;供你参考。 讲真&#xff0c;现在想通过培训班培训几个月就进入IT行业&#xff0c;越来越来难了&#xff1b;如果是在2018年以前&#xff0c;…

mysql基本语法

mysql基本语法ddl语句基础一、查询数据库二、创建数据库三、删除数据库四、表操作五、修改表六、删除表七、dql基本查询1.查询多个字段2.设置别名3.去除重复记录4.批量添加数据5.update更新八、删除表总结ddl语句基础 一、查询数据库 show databases; 查询当前数据库 select…

ST-GCN 论文解读

论文名称&#xff1a;Spatial Temporal Graph Convolutional Networks for Skeleton-Based Action Recognition论文下载&#xff1a;https://arxiv.org/pdf/1801.07455.pdf论文代码&#xff1a;https://github.com/yysijie/st-gcn 论文&#xff1a;基于骨骼动作识别的时空图卷…

EDAS投稿系统的遇到的问题及解决办法

问题1&#xff1a; gutter: Upload failed: The gutter between columns is 0.2 inches wide (on page 1), but should be at least 0.2 inches 解决&#xff1a; 在\begin{document}前添加\columnsep 0.201 in&#xff08;0.2in也会报错&#xff0c;建议填大一点点&#xff09…

ToBeWritten之物联网Zigbee协议

也许每个人出生的时候都以为这世界都是为他一个人而存在的&#xff0c;当他发现自己错的时候&#xff0c;他便开始长大 少走了弯路&#xff0c;也就错过了风景&#xff0c;无论如何&#xff0c;感谢经历 转移发布平台通知&#xff1a;将不再在CSDN博客发布新文章&#xff0c;敬…

Systemverilog中interprocess间synchronization和communication的记录

1. 同步和通讯机制的种类 systemverilog提供了三种方式&#xff1a;named event type(->, )、semaphore、mailbox。其中semaphores和mailbox虽然是built-in type&#xff0c;但它们是class&#xff0c;且可以作为base classes被扩展为更高level的class。这些built-in class…
最新文章