cpp-httplib 源码剖析


前言

之前实现自己的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;
  }
  if (svr_sock_ != INVALID_SOCKET) {
    detail::close_socket(svr_sock_);
    ret = false;
  } else {
    ; // The server socket was closed by user.
  }
  break;
}

如果 accept() 失败,则根据错误码进行相应处理。如果错误码是 EMFILE,表示打开文件描述符的数量达到了系统设置的最大值,此时暂停一段时间后继续尝试。如果错误码是 EINTREAGAIN,表示该操作被中断或者资源暂时不可用,需要继续重试。如果发生其他错误,则关闭服务器 socket,并将返回值设为 false,表示函数执行失败。此时跳出循环,结束监听。

{
#ifdef _WIN32
  auto timeout = static_cast<uint32_t>(read_timeout_sec_ * 1000 +
                                       read_timeout_usec_ / 1000);
  setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,
             sizeof(timeout));
#else
  timeval tv;
  tv.tv_sec = static_cast<long>(read_timeout_sec_);
  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(read_timeout_usec_);
  setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv, sizeof(tv));
#endif
}
{
#ifdef _WIN32
  auto timeout = static_cast<uint32_t>(write_timeout_sec_ * 1000 +
                                       write_timeout_usec_ / 1000);
  setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout,
             sizeof(timeout));
#else
  timeval tv;
  tv.tv_sec = static_cast<long>(write_timeout_sec_);
  tv.tv_usec = static_cast<decltype(tv.tv_usec)>(write_timeout_usec_);
  setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, (char *)&tv, sizeof(tv));
#endif
}

为新的客户端 socket sock 设置读超时和写超时。

task_queue->enqueue([this, sock]() { process_and_close_socket(sock); });

sock 加入 task_queue,并用 process_and_close_socket() 函数来处理这个连接。

task_queue->shutdown();

循环结束后,调用 task_queue->shutdown() 关闭任务队列。

最后返回变量 ret,表示函数执行结果。如果 ret 是 true,则表示函数执行成功,否则表示失败。

四、路由

添加路由

使用正则表达式来指定URL的模式,并注册不同类型请求的处理函数。
比如说Get函数用于注册GET请求的处理器。将匹配的URL模式(pattern)和处理函数(handler)作为键值对添加到get_handlers_列表中。


inline Server &Server::Get(const std::string &pattern, Handler handler) {
  get_handlers_.push_back(
      std::make_pair(std::regex(pattern), std::move(handler)));
  return *this;
}

inline Server &Server::Post(const std::string &pattern, Handler handler) {
  post_handlers_.push_back(
      std::make_pair(std::regex(pattern), std::move(handler)));
  return *this;
}

inline Server &Server::Post(const std::string &pattern,
                            HandlerWithContentReader handler) {
  post_handlers_for_content_reader_.push_back(
      std::make_pair(std::regex(pattern), std::move(handler)));
  return *this;
}

inline Server &Server::Put(const std::string &pattern, Handler handler) {
  put_handlers_.push_back(
      std::make_pair(std::regex(pattern), std::move(handler)));
  return *this;
}

inline Server &Server::Put(const std::string &pattern,
                           HandlerWithContentReader handler) {
  put_handlers_for_content_reader_.push_back(
      std::make_pair(std::regex(pattern), std::move(handler)));
  return *this;
}

路由

主要函数有routing以及dispatch_request。
dispatch_request方法用于根据请求的路径(path)匹配相应的处理器(handler),并执行该处理器来处理请求。

inline bool Server::dispatch_request(Request &req, Response &res,
                                     const Handlers &handlers) {
  for (const auto &x : handlers) {
    const auto &pattern = x.first;
    const auto &handler = x.second;
    if (std::regex_match(req.path, req.matches, pattern)) {
      handler(req, res);
      return true;
    }
  }
  return false;
}

routing根据请求的方法和内容,将请求分发给相应的处理函数进行处理,并生成对应的响应

主要功能如下:

  1. 首先,如果存在预处理器(pre_routing_handler_)且该预处理器处理了请求(req)并返回HandlerResponse::Handled,则直接返回true,表示请求已被处理完成。
  2. 接着,判断请求的方法(method)是否为"GET"或"HEAD",如果是,则调用handle_file_request函数处理文件请求,并返回true,表示请求已被处理完成。
  3. 如果请求中包含内容,则进入内容读取器(ContentReader)的处理流程。首先,创建ContentReader对象,该对象根据请求的方法选择相应的处理逻辑。具体处理逻辑包括:
    • 对于"POST"方法,调用dispatch_request_for_content_reader函数分发请求到post_handlers_for_content_reader_列表中的处理器。
    • 对于"PUT"方法,调用dispatch_request_for_content_reader函数分发请求到put_handlers_for_content_reader_列表中的处理器。
    • 对于"PATCH"方法,调用dispatch_request_for_content_reader函数分发请求到patch_handlers_for_content_reader_列表中的处理器。
    • 对于"DELETE"方法,调用dispatch_request_for_content_reader函数分发请求到delete_handlers_for_content_reader_列表中的处理器。
  4. 如果请求中包含内容,在上述步骤完成后,继续读取请求的内容,并将内容存储到req.body中。
  5. 最后,根据请求的方法分发请求到相应的处理器列表中去处理请求,并返回处理结果。如果请求方法未被识别,或者没有找到匹配的处理器,则设置响应的状态码为400,并返回false。

通过这个路由方法,服务器可以根据请求的方法和内容,将请求分发给相应的处理函数进行处理,并生成对应的响应。

inline bool Server::routing(Request &req, Response &res, Stream &strm) {
  if (pre_routing_handler_ &&
      pre_routing_handler_(req, res) == HandlerResponse::Handled) {
    return true;
  }

  // File handler
  bool is_head_request = req.method == "HEAD";
  if ((req.method == "GET" || is_head_request) &&
      handle_file_request(req, res, is_head_request)) {
    return true;
  }

  if (detail::expect_content(req)) {
    // Content reader handler
    {
      ContentReader reader(
          [&](ContentReceiver receiver) {
            return read_content_with_content_receiver(
                strm, req, res, std::move(receiver), nullptr, nullptr);
          },
          [&](MultipartContentHeader header, ContentReceiver receiver) {
            return read_content_with_content_receiver(strm, req, res, nullptr,
                                                      std::move(header),
                                                      std::move(receiver));
          });

      if (req.method == "POST") {
        if (dispatch_request_for_content_reader(
                req, res, std::move(reader),
                post_handlers_for_content_reader_)) {
          return true;
        }
      } else if (req.method == "PUT") {
        if (dispatch_request_for_content_reader(
                req, res, std::move(reader),
                put_handlers_for_content_reader_)) {
          return true;
        }
      } else if (req.method == "PATCH") {
        if (dispatch_request_for_content_reader(
                req, res, std::move(reader),
                patch_handlers_for_content_reader_)) {
          return true;
        }
      } else if (req.method == "DELETE") {
        if (dispatch_request_for_content_reader(
                req, res, std::move(reader),
                delete_handlers_for_content_reader_)) {
          return true;
        }
      }
    }

    // Read content into `req.body`
    if (!read_content(strm, req, res)) { return false; }
  }

  // Regular handler
  if (req.method == "GET" || req.method == "HEAD") {
    return dispatch_request(req, res, get_handlers_);
  } else if (req.method == "POST") {
    return dispatch_request(req, res, post_handlers_);
  } else if (req.method == "PUT") {
    return dispatch_request(req, res, put_handlers_);
  } else if (req.method == "DELETE") {
    return dispatch_request(req, res, delete_handlers_);
  } else if (req.method == "OPTIONS") {
    return dispatch_request(req, res, options_handlers_);
  } else if (req.method == "PATCH") {
    return dispatch_request(req, res, patch_handlers_);
  }

  res.status = 400;
  return false;
}

五、处理接受请求

process_and_close_socket() 函数处理连接,其中最核心的是process_server_socket_core函数以及process_request函数。

process_server_socket_core

process_server_socket_core函数用于在服务器套接字上处理连接,并根据保持活动的最大次数和超时时间执行回调函数。在每次循环中,会根据剩余的保持活动次数判断是否需要关闭连接,并将回调函数的执行结果保存在变量ret中。函数最终返回回调函数的执行结果。

template <typename T>
inline bool
process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
                           size_t keep_alive_max_count,
                           time_t keep_alive_timeout_sec, T callback) {
  assert(keep_alive_max_count > 0);
  auto ret = false;
  auto count = keep_alive_max_count;
  while (svr_sock != INVALID_SOCKET && count > 0 &&
         keep_alive(sock, keep_alive_timeout_sec)) {
    auto close_connection = count == 1;
    auto connection_closed = false;
    ret = callback(close_connection, connection_closed);
    if (!ret || connection_closed) { break; }
    count--;
  }
  return ret;
}

process_request

回调函数是process_request处理请求。
请求处理步骤如下:

  1. 设置默认的HTTP响应版本号为"HTTP/1.1"。
  2. 将默认的HTTP响应头加入响应对象中,如果响应对象中没有相同的头字段。
  3. 检查请求的URI长度是否超过最大限制,如果超过则返回状态码414(请求URI过长)。
  4. 解析请求行和请求头,如果解析失败则返回状态码400(错误的请求)。
  5. 根据请求的Connection头判断是否需要关闭连接。
  6. 获取客户端和服务器端的IP地址和端口,并添加到请求头中。
  7. 如果请求头包含Range字段,解析Range字段的值,并存储在请求对象的ranges成员中。
  8. 调用用户提供的请求处理回调函数,对请求做进一步处理。
  9. 如果请求头包含Expect字段并且值为"100-continue",则发送100 Continue响应。
  10. 根据路由规则处理请求,并设置相应的状态码。
  11. 如果成功匹配到路由规则,根据请求中的ranges判断是否返回部分内容或全部内容。
  12. 如果无法匹配到路由规则,则返回404 Not Found状态码。
  13. 最后根据routing路由处理结果,将响应发送回客户端。

inline bool
Server::process_request(Stream &strm, bool close_connection,
                        bool &connection_closed,
                        const std::function<void(Request &)> &setup_request) {
  std::array<char, 2048> buf{};

  detail::stream_line_reader line_reader(strm, buf.data(), buf.size());

  // Connection has been closed on client
  if (!line_reader.getline()) { return false; }

  Request req;
  Response res;

  res.version = "HTTP/1.1";

  for (const auto &header : default_headers_) {
    if (res.headers.find(header.first) == res.headers.end()) {
      res.headers.insert(header);
    }
  }

#ifdef _WIN32
  // TODO: Increase FD_SETSIZE statically (libzmq), dynamically (MySQL).
#else
#ifndef CPPHTTPLIB_USE_POLL
  // Socket file descriptor exceeded FD_SETSIZE...
  if (strm.socket() >= FD_SETSIZE) {
    Headers dummy;
    detail::read_headers(strm, dummy);
    res.status = 500;
    return write_response(strm, close_connection, req, res);
  }
#endif
#endif

  // Check if the request URI doesn't exceed the limit
  if (line_reader.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
    Headers dummy;
    detail::read_headers(strm, dummy);
    res.status = 414;
    return write_response(strm, close_connection, req, res);
  }

  // Request line and headers
  if (!parse_request_line(line_reader.ptr(), req) ||
      !detail::read_headers(strm, req.headers)) {
    res.status = 400;
    return write_response(strm, close_connection, req, res);
  }

  if (req.get_header_value("Connection") == "close") {
    connection_closed = true;
  }

  if (req.version == "HTTP/1.0" &&
      req.get_header_value("Connection") != "Keep-Alive") {
    connection_closed = true;
  }

  strm.get_remote_ip_and_port(req.remote_addr, req.remote_port);
  req.set_header("REMOTE_ADDR", req.remote_addr);
  req.set_header("REMOTE_PORT", std::to_string(req.remote_port));

  strm.get_local_ip_and_port(req.local_addr, req.local_port);
  req.set_header("LOCAL_ADDR", req.local_addr);
  req.set_header("LOCAL_PORT", std::to_string(req.local_port));

  if (req.has_header("Range")) {
    const auto &range_header_value = req.get_header_value("Range");
    if (!detail::parse_range_header(range_header_value, req.ranges)) {
      res.status = 416;
      return write_response(strm, close_connection, req, res);
    }
  }

  if (setup_request) { setup_request(req); }

  if (req.get_header_value("Expect") == "100-continue") {
    auto status = 100;
    if (expect_100_continue_handler_) {
      status = expect_100_continue_handler_(req, res);
    }
    switch (status) {
    case 100:
    case 417:
      strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status,
                        detail::status_message(status));
      break;
    default: return write_response(strm, close_connection, req, res);
    }
  }

  // Rounting
  bool routed = false;
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
  routed = routing(req, res, strm);
#else
  try {
    routed = routing(req, res, strm);
  } catch (std::exception &e) {
    if (exception_handler_) {
      auto ep = std::current_exception();
      exception_handler_(req, res, ep);
      routed = true;
    } else {
      res.status = 500;
      std::string val;
      auto s = e.what();
      for (size_t i = 0; s[i]; i++) {
        switch (s[i]) {
        case '\r': val += "\\r"; break;
        case '\n': val += "\\n"; break;
        default: val += s[i]; break;
        }
      }
      res.set_header("EXCEPTION_WHAT", val);
    }
  } catch (...) {
    if (exception_handler_) {
      auto ep = std::current_exception();
      exception_handler_(req, res, ep);
      routed = true;
    } else {
      res.status = 500;
      res.set_header("EXCEPTION_WHAT", "UNKNOWN");
    }
  }
#endif

  if (routed) {
    if (res.status == -1) { res.status = req.ranges.empty() ? 200 : 206; }
    return write_response_with_content(strm, close_connection, req, res);
  } else {
    if (res.status == -1) { res.status = 404; }
    return write_response(strm, close_connection, req, res);
  }
}
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值