从零开始实现一个C++高性能服务器框架----Hook模块
Posted johnsonli99
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始实现一个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 上
send
100k数据 - 协程3:在socket fd2 上
recv
直到数据接收成功
- 情况1:在未hook的情况下,IOManager要调度上面的协程,流程是下面这样的:
- 调度协程1,协程阻塞在sleep上,等2秒后返回,这两秒内调度线程是被协程1占用的,其他协程无法在当前线程上调度。
- 调度协徎2,协程阻塞
send
100k数据上,这个操作一般问题不大,因为send数据无论如何都要占用时间,但如果fd迟迟不可写,那send会阻塞直到套接字可写,同样,在阻塞期间,其他协程也无法在当前线程上调度。 - 调度协程3,协程阻塞在
recv
上,这个操作要直到recv超时或是有数据时才返回,期间调度器也无法调度其他协程。 - 显然,整个过程是同步的,都需要发生阻塞。
- 情况2:hook的情况下:
- 调度协程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秒超时后,执行定时器回调函数,将协程1
call
以便继续执行。 - 等协程2的fd可写,一旦可写,调用写事件回调函数将协程2
call
以便继续执行send
。 - 等协程3的fd可读,一旦可读,调用回调函数将协程3
call
以便继续执行recv
。 - 显然,整个过程是异步的,每次协程发生阻塞都会注册对应的事件或定时器,然后退出当前协程,等事件触发或定时器到期,又会回到协程继续完成操作。
- 协程1:
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;
connect
和connect_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,在应用层记录套接字的读写超时,方便协程调度器获取。
从零开始实现一个C++高性能服务器框架----Socket模块
此项目是根据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多线程模式的服务器。
Socket模块
1. 主要功能
- 对Linux下socket相关方法的封装,包括bind、listen、connect、read/write系列等方法。
- 支持快速创建TCP、UDP对应的Socket。
2. 功能演示
- 模拟一个请求百度的客户端,并打印出响应
IPAddress::ptr addr = Address::LookupAnyIPAddress("www.baidu.com:80");
// 创建socket
Socket::ptr socket = Socket::CreateTCP(addr);
// 连接
socket->connect(addr);
//发送数据
const char buf[] = "GET / HTTP/1.1\\r\\n\\r\\n";
int rt = socket->send(buf, sizeof(buf));
if(rt <= 0)
LOG_INFO(g_logger) << "send fail";
return;
//接收数据
std::string buffers;
buffers.resize(4096);
rt = socket->recv(&buffers[0], 4096);
if(rt <= 0)
LOG_INFO(g_logger) << "recv fail";
return;
LOG_INFO(g_logger) << buffers;
3. 模块介绍
3.1 Socket
- 对socket相关方法的封装,包括以下内容
- 创建各种类型的套接字对象的方法(TCP套接字,UDP套接字,Unix域套接字)
- 设置套接字选项,比如超时参数
- bind/connect/listen方法,实现绑定地址、发起连接、发起监听功能
- accept方法,返回连入的套接字对象
- 发送、接收数据的方法
- 获取本地地址、远端地址的方法
- 获取套接字类型、地址类型、协议类型的方法
- 取消套接字读、写的方法
class Socket : public std::enable_shared_from_this<Socket>, Noncopyable
public:
typedef std::shared_ptr<Socket> ptr;
typedef std::weak_ptr<Socket> weak_ptr;
// 创建TCP Socket(满足地址类型)
static Socket::ptr CreateTCP(johnsonli::Address::ptr address);
// 创建UDP Socket(满足地址类型)
static Socket::ptr CreateUDP(johnsonli::Address::ptr address);
// 创建IPv4的TCP Socket
static Socket::ptr CreateTCPSocket();
// 创建IPv4的UDP Socket
static Socket::ptr CreateUDPSocket();
// 创建IPv6的TCP Socket
static Socket::ptr CreateTCPSocket6();
// 创建IPv6的UDP Socket
static Socket::ptr CreateUDPSocket6();
Socket(int family, int type, int protocol = 0);
virtual ~Socket();
int64_t getSendTimeout(); // 获取发送超时时间(毫秒)
void setSendTimeout(int64_t v); // 设置发送超时时间(毫秒)
int64_t getRecvTimeout(); // 获取接受超时时间(毫秒)
void setRecvTimeout(int64_t v); // 设置接受超时时间(毫秒)
// 获取sockopt @see getsockopt
bool getOption(int level, int option, void* result, socklen_t* len);
// 获取sockopt模板 @see getsockopt
template<class T>
bool getOption(int level, int option, T& result)
socklen_t length = sizeof(T);
return getOption(level, option, &result, &length);
// 设置sockopt @see setsockopt
bool setOption(int level, int option, const void* result, socklen_t len);
// 设置sockopt模板 @see setsockopt
template<class T>
bool setOption(int level, int option, const T& value)
return setOption(level, option, &value, sizeof(T));
/**
* @brief 接收connect链接
* @return 成功返回新连接的socket,失败返回nullptr
* @pre Socket必须 bind , listen 成功
*/
virtual Socket::ptr accept();
/**
* @brief 绑定地址
* @param[in] addr 地址
* @return 是否绑定成功
*/
virtual bool bind(const Address::ptr addr);
/**
* @brief 连接地址
* @param[in] addr 目标地址
* @param[in] timeout_ms 超时时间(毫秒)
*/
virtual bool connect(const Address::ptr addr, uint64_t timeout_ms = -1);
virtual bool reconnect(uint64_t timeout_ms = -1);
/**
* @brief 监听socket
* @param[in] backlog 未完成连接队列的最大长度
* @result 返回监听是否成功
* @pre 必须先 bind 成功
*/
virtual bool listen(int backlog = SOMAXCONN);
/**
* @brief 关闭socket
*/
virtual bool close();
/**
* @brief 发送数据
* @param[in] buffer 待发送数据的内存
* @param[in] length 待发送数据的长度
* @param[in] flags 标志字
* @return
* @retval >0 发送成功对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int send(const void* buffer, size_t length, int flags = 0);
/**
* @brief 发送数据
* @param[in] buffers 待发送数据的内存(iovec数组)
* @param[in] length 待发送数据的长度(iovec长度)
* @param[in] flags 标志字
* @return
* @retval >0 发送成功对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int send(const iovec* buffers, size_t length, int flags = 0);
/**
* @brief 发送数据
* @param[in] buffer 待发送数据的内存
* @param[in] length 待发送数据的长度
* @param[in] to 发送的目标地址
* @param[in] flags 标志字
* @return
* @retval >0 发送成功对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int sendTo(const void* buffer, size_t length, const Address::ptr to, int flags = 0);
/**
* @brief 发送数据
* @param[in] buffers 待发送数据的内存(iovec数组)
* @param[in] length 待发送数据的长度(iovec长度)
* @param[in] to 发送的目标地址
* @param[in] flags 标志字
* @return
* @retval >0 发送成功对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int sendTo(const iovec* buffers, size_t length, const Address::ptr to, int flags = 0);
/**
* @brief 接受数据
* @param[out] buffer 接收数据的内存
* @param[in] length 接收数据的内存大小
* @param[in] flags 标志字
* @return
* @retval >0 接收到对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int recv(void* buffer, size_t length, int flags = 0);
/**
* @brief 接受数据
* @param[out] buffers 接收数据的内存(iovec数组)
* @param[in] length 接收数据的内存大小(iovec数组长度)
* @param[in] flags 标志字
* @return
* @retval >0 接收到对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int recv(iovec* buffers, size_t length, int flags = 0);
/**
* @brief 接受数据
* @param[out] buffer 接收数据的内存
* @param[in] length 接收数据的内存大小
* @param[out] from 发送端地址
* @param[in] flags 标志字
* @return
* @retval >0 接收到对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int recvFrom(void* buffer, size_t length, Address::ptr from, int flags = 0);
/**
* @brief 接受数据
* @param[out] buffers 接收数据的内存(iovec数组)
* @param[in] length 接收数据的内存大小(iovec数组长度)
* @param[out] from 发送端地址
* @param[in] flags 标志字
* @return
* @retval >0 接收到对应大小的数据
* @retval =0 socket被关闭
* @retval <0 socket出错
*/
virtual int recvFrom(iovec* buffers, size_t length, Address::ptr from, int flags = 0);
// 输出信息到流中
virtual std::ostream& dump(std::ostream& os) const;
virtual std::string toString() const;
bool cancelRead(); // 取消读
bool cancelWrite(); // 取消写
bool cancelAccept(); // 取消accept
bool cancelAll(); // 取消所有事件
protected:
void initSock(); // 设置socket属性
void newSock(); // 创建socket m_sockfd = socket()
virtual bool init(int sock); // 初始化sock,调用initSock
protected:
int m_sockfd; /// socket句柄
int m_family; /// 协议簇
int m_type; /// 类型
int m_protocol; /// 协议
bool m_isConnected; /// 是否连接
Address::ptr m_localAddress; /// 本地地址
Address::ptr m_remoteAddress; /// 远端地址
;
// 流式输出socket
std::ostream& operator<<(std::ostream& os, const Socket& sock);
以上是关于从零开始实现一个C++高性能服务器框架----Hook模块的主要内容,如果未能解决你的问题,请参考以下文章
从零开始实现一个C++高性能服务器框架----Socket模块