文章目录
前言
之前实现自己的http库的时候感觉有一些设计的不是很好,这几天对cpp-httplib 源码进行剖析,对如何设计http库有了更深入的认识。(主要是对server类进行拆解分析)
一、cpp-httplib 是什么?
cpp-httplib是一个c++封装的http开源库,仅包含一个头文件,不过代码行数达到8000多行。
cpp-httplib 服务端采用select IO多路复用模型,工作线程池的处理方式,主要包含的类Server、Client、Request、Response。
二、Server类整体架构
server类的工作流程基本如下:
1、 搭建tcp服务,注册资源路径与处理方法,并开启监听。
2、 创建工作线程,select循环监听,当收到客户端消息,放入jobs中,通知工作线程处理。
3、工作线程从jobs中取出待处理请求。
4、 处理请求,包含校验、协议解析、根据请求方式分发,最终调用用户注册方法处理上层业务。
三、绑定和监听
绑定和监听是由Servert中两个成员函数bind_internal和listen_internal的实现的。
bind_internal
bind internal函数的作用是绑定服务器的地址和端口。函数首先检查服务器套接字是否有效,如果无效则返回-1。然后调用create_server_socket函数创建服务器套接字,并将其赋值给成员变量svr_sock.。如果创建套接字失败,则返回-1。接下来,根据传入的ort参数判断是否为0。如果为0,则通过调用getsockname函数获取绑定的地址和端口,并返回解析得到的端口号。否则,直接返回传入的port参数。
inline int Server::bind_internal(const std::string &host, int port,
int socket_flags) {
if (!is_valid()) {
return -1; }
svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_);
if (svr_sock_ == INVALID_SOCKET) {
return -1; }
if (port == 0) {
struct sockaddr_storage addr;
socklen_t addr_len = sizeof(addr);
if (getsockname(svr_sock_, reinterpret_cast<struct sockaddr *>(&addr),
&addr_len) == -1) {
return -1;
}
if (addr.ss_family == AF_INET) {
return ntohs(reinterpret_cast<struct sockaddr_in *>(&addr)->sin_port);
} else if (addr.ss_family == AF_INET6) {
return ntohs(reinterpret_cast<struct sockaddr_in6 *>(&addr)->sin6_port);
} else {
return -1;
}
} else {
return port;
}
}
listen_internal
首先是对Select的封装:
创建了一个 fd_set 对象并将要监听的套接字加入其中,然后设置超时时间,最后调用 select 函数进行阻塞等待可读事件或超时。通过这种方式可以同时监听多个套接字,并在有可读事件发生时进行处理。
inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(sock, &fds);
timeval tv;
tv.tv_sec = static_cast<long>(sec);
tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec);
return handle_EINTR([&]() {
return select(static_cast<int>(sock + 1), &fds, nullptr, nullptr, &tv);
});
}
Server::listen_internal()
函数,它用于监听连接并处理客户端请求。
auto ret = true;
is_running_ = true;
auto se = detail::scope_exit([&]() {
is_running_ = false; });
首先将返回值 ret
设为 true,表示函数执行成功。将服务器运行状态 is_running_
设为 true,并创建 detail::scope_exit
对象,在函数退出时将 is_running_
设置为 false。
std::unique_ptr<TaskQueue> task_queue(new_task_queue());
创建一个 TaskQueue
的智能指针 task_queue
,用于保存待处理的任务。
while (svr_sock_ != INVALID_SOCKET) {
#ifndef _WIN32
if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) {
#endif
auto val = detail::select_read(svr_sock_, idle_interval_sec_,
idle_interval_usec_);
if (val == 0) {
// Timeout
task_queue->on_idle();
continue;
}
#ifndef _WIN32
}
#endif
socket_t sock = accept(svr_sock_, nullptr, nullptr);
// ...
}
进入主循环,只要服务器 socket svr_sock_
是有效的,就继续监听连接。在循环中,先调用 detail::select_read()
函数等待读事件,如果超时则调用 task_queue->on_idle()
处理空闲状态,并继续等待下一个连接。如果有新的连接到达,调用 accept()
函数接受客户端连接,并返回一个新的客户端 socket sock
。
if (sock == INVALID_SOCKET) {
if (errno == EMFILE) {
// The per-process limit of open file descriptors has been reached.
// Try to accept new connections after a short sleep.
std::this_thread::sleep_for(std::chrono::milliseconds(1));
continue;
} else if (errno == EINTR || errno == EAGAIN) {
continue;