之前实现自己的http库的时候感觉有一些设计的不是很好,这几天对cpp-httplib 源码进行剖析,对如何设计http库有了更深入的认识。(主要是对server类进行拆解分析)
cpp-httplib是一个c++封装的http开源库,仅包含一个头文件,不过代码行数达到8000多行。
cpp-httplib 服务端采用select IO多路复用模型,工作线程池的处理方式,主要包含的类Server、Client、Request、Response。
server类的工作流程基本如下:
1、 搭建tcp服务,注册资源路径与处理方法,并开启监听。
2、 创建工作线程,select循环监听,当收到客户端消息,放入jobs中,通知工作线程处理。
3、工作线程从jobs中取出待处理请求。
4、 处理请求,包含校验、协议解析、根据请求方式分发,最终调用用户注册方法处理上层业务。
绑定和监听是由Servert中两个成员函数bind_internal和listen_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;
}
}
首先是对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
,表示打开文件描述符的数量达到了系统设置的最大值,此时暂停一段时间后继续尝试。如果错误码是 EINTR
或 EAGAIN
,表示该操作被中断或者资源暂时不可用,需要继续重试。如果发生其他错误,则关闭服务器 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根据请求的方法和内容,将请求分发给相应的处理函数进行处理,并生成对应的响应
主要功能如下:
req.body
中。通过这个路由方法,服务器可以根据请求的方法和内容,将请求分发给相应的处理函数进行处理,并生成对应的响应。
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函数用于在服务器套接字上处理连接,并根据保持活动的最大次数和超时时间执行回调函数。在每次循环中,会根据剩余的保持活动次数判断是否需要关闭连接,并将回调函数的执行结果保存在变量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处理请求。
请求处理步骤如下:
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);
}
}