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

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

听说有个程序员从零开始重写了一个C++高性能服务器框架,还搞了个hook模块,听起来有点高级。那这个hook到底是啥东西呢?简单概括,就是给那些慢吞吞的系统调用(比如网络收发、睡眠)换个装,让它们变成可以“边玩手机边等外卖”的异步操作。这样,服务器就不会因为一个耗时操作而卡住,从而变得飞快。

你可能会想,为啥不用传统的多线程?因为多线程搞多了,就像食堂排队一样,每个人都在等着打饭,资源被占得死死的,效率反而更低。而hook的异步模式,就像是在便利店自助结账,一边等收银员处理前面的单子,另一边还能继续逛逛,效率自然就上来了。

那hook具体是怎么工作的?比如一个协程在发信息(send),正常情况下它会傻乎乎地等着数据发送完,但用了hook之后,它会说:“老铁,你给我发信息,我先去忙别的事了,等搞定了再叫我。”于是这个协程就去执行其他任务了,等信息发好了,再自动回来接着处理。这样一来,线程的利用率就大幅提高了。

总之,hook模块就像一个聪明的“任务调度员”,把原本会堵车的系统调用都变成了“预约服务”,让服务器可以一边“煮汤”一边“煎牛排”,轻松实现高并发和高性能。下次再有人问你server怎么弄才能快如闪电,你就跟他们说:兄弟,赶紧上hook!

从零开始实现一个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的读写超时,是否设置非阻塞等信息。<