第一篇博客,记录下最近在看的一个开源库cpp-httplib。
起因:要做一个设备的本地服务端,因为要调用一个本地的动态库(windows平台),就选择了这个库作为网络库。
优势:header only,讲人话就是只需要包含一个头文件就行了,方便。也可以使用它自带的python脚本把它劈开成头文件和源文件,避免“强迫症”觉得头文件到处展开不好。然后对于常用的操作 get post put delete option patch 都进行了一个封装,只需要定义一个处理函数就行了,加上c11的lambda表达式,让人觉得比PHP还天下第一😉
using namespace httplib; Server svr; svr.Get("/hi", [](const Request& req, Response& res) { res.set_content("Hello World!", "text/plain"); }); svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) { auto numbers = req.matches[1]; res.set_content(numbers, "text/plain"); });
嗯,上面就是一个Server的用法,Client也很简单
httplib::Client cli("localhost", 1234); auto res = cli.Get("/hi"); if (res && res->status == 200) { std::cout << res->body << std::endl; }
其他的示例可以看上面的官方文档,有一个问题就是我使用成员函数指针作为回调时,用std::bind 绑定对象,总会出现一个错误,开发环境是vs2015,所以只能在lambda 里面去调用这个类的成员函数,看着比较low,如果有解决方法的烦请在评论区告知在下。
发现问题是我对bind有误解,我以为bind只需要绑定参数即可,结果还需要占位符如 _1 ,_2
来表示它有几个参数……果然对于新特性的了解我还有待提高啊。
然后代码解析就先从最简单的Client::Get 作为突破口。
然后又从最简单的单参数重载版本入手
然后Client的成员就自己下载一下看,贴上来感觉有凑字数的嫌疑……
std::shared_ptr<Response> Client::Get(const char *path) { return Get(path, Headers(), Progress()); }
解读:调用了另一个重载版本,使用Progress为默认构造functional,可以使用判断时类似nullptr 作为要给false。
std::shared_ptr<Response> Client::Get(const char *path, const Headers &headers, Progress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.progress = std::move(progress); auto res = std::make_shared<Response>(); return send(req, *res) ? res : nullptr; }
解读:Request 中定义了请求的方法,路径,还有 header,而header的类型是mulitmap<string,stirng> 的,然后此处使用了一个send函数,内部对request response 形成了一个封装,还有就是url中的params 是自己添加到path里面的,当然还有使用了params的重载类型,这里就不讲了。
bool Client::send(const Request &req, Response &res) { auto sock = create_client_socket(); if (sock == INVALID_SOCKET) { return false; } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT if (is_ssl() && !proxy_host_.empty()) { bool error; if (!connect(sock, res, error)) { return error; } } #endif return process_and_close_socket( sock, 1, [&](Stream &strm, bool last_connection, bool &connection_close) { return handle_request(strm, req, res, last_connection, connection_close); }); }
解读:handle_request是关键,其他里面使用了creat_client_socket 这个就涉及到系统API了,进入了可以看见还是重载,重载里面判断了是否使用代理,但是调用了同一个外部函数
socket_t create_client_socket(const char *host, int port, time_t timeout_sec, const std::string &intf) { return create_socket( host, port, [&](socket_t sock, struct addrinfo &ai) -> bool { if (!intf.empty()) { auto ip = if2ip(intf); if (ip.empty()) { ip = intf; } if (!bind_ip_address(sock, ip.c_str())) { return false; } } set_nonblocking(sock, true); auto ret = ::connect(sock, ai.ai_addr, static_cast<socklen_t>(ai.ai_addrlen)); if (ret < 0) { if (is_connection_error() || !wait_until_socket_is_ready(sock, timeout_sec, 0)) { close_socket(sock); return false; } } set_nonblocking(sock, false); return true; }); }
解读:这个函数里面就是具体的系统API调用了,intf 字面意思是网络接口,我的理解就是某个网卡,不指定网卡使用默认网卡时,就不用bind,否则还需要bind。然后就是平平无奇的connect,跟我认知不同的是,当它connect 失败时,会调用wait_until_socket_is_ready 我们再看一下它的代码
bool wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { #ifdef CPPHTTPLIB_USE_POLL struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLIN | POLLOUT; auto timeout = static_cast<int>(sec * 1000 + usec / 1000); if (poll(&pfd_read, 1, timeout) > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { int error = 0; socklen_t len = sizeof(error); return getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast<char *>(&error), &len) >= 0 && !error; } return false; #else fd_set fdsr; FD_ZERO(&fdsr); FD_SET(sock, &fdsr); auto fdsw = fdsr; auto fdse = fdsr; timeval tv; tv.tv_sec = static_cast<long>(sec); tv.tv_usec = static_cast<decltype(tv.tv_usec)>(usec); if (select(static_cast<int>(sock + 1), &fdsr, &fdsw, &fdse, &tv) > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { int error = 0; socklen_t len = sizeof(error); return getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast<char *>(&error), &len) >= 0 && !error; } return false; #endif }
解读:不使用ssl的版本,就是调用了一个select函数,然后查看了一下这个api,是看这个socket是否处于可读可写的状态,继续查资料就发现windows的select是一种IO模型,意思就是我一条线程里面可以处理多个socket,然后通过select轮询各个socket是否处于可读写的状态。那么也就是说windows下还有其他的IO模型。所以这个可以作为后续文章的标题,这里只是简单的了解一下,然后参考资料如下:
windows select 用法
windows select 模型
总之这个是一种验证方法,至于为什么肯定是有更底层的原因的。这篇文章就不深究了,知道有这么一种检测socket ready的方式即可。至于getsockopt 就是获取socket的各种状态信息,这里是异常状态。接下来就是一个setnonblock 函数,设置socket 为飞阻塞,这样recv 没有接收到数据的时候就不会干等着了
void set_nonblocking(socket_t sock, bool nonblocking) { #ifdef _WIN32 auto flags = nonblocking ? 1UL : 0UL; **ioctlsocket(sock, FIONBIO, &flags);** #else auto flags = fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); #endif }
解读:就是一个ioctlsocket 第二个参数是控制它的某个状态,第三个参数是它的某个状态值,这里就是设置为非阻塞。
然后创建客户端的socket 就说完了。你细品,是不是就一个connect,检测,设置状态。比熟悉的connect 等待服务器接收不同于加了安全判定。用起来更加丝滑。
创建完了就是数据处理数据
bool Client::process_and_close_socket( socket_t sock, size_t request_count, std::function<bool(Stream &strm, bool last_connection, bool &connection_close)> callback) { request_count = (std::min)(request_count, keep_alive_max_count_); return detail::process_and_close_socket(true, sock, request_count, read_timeout_sec_, read_timeout_usec_, callback); }
解读:这里第三个参数复杂点,是一个functional 对象,可以理解为函数指针,这里的Stream 是对socket 进行了一层封装,使其读写变得更加规范。然后套娃一样的,调用了一个外部的同名函数procees_and_close_socket,将第一个参数为true就进行了一个甩锅般的调用
template <typename T> bool process_and_close_socket(bool is_client_request, socket_t sock, size_t keep_alive_max_count, time_t read_timeout_sec, time_t read_timeout_usec, T callback) { auto ret = process_socket(is_client_request, sock, keep_alive_max_count, read_timeout_sec, read_timeout_usec, callback); close_socket(sock); return ret; }
解读:一个模板函数,模板对象是之前那个回调functional,也就是functional 类型不同,这里还不需要深究它,因为它又调用了一个process 函数,然后进行了一个close操作。所以需要深究的是process_socket 函数 和那个回调函数
template <typename T> bool process_socket(bool is_client_request, socket_t sock, size_t keep_alive_max_count, time_t read_timeout_sec, time_t read_timeout_usec, T callback) { assert(keep_alive_max_count > 0); auto ret = false; if (keep_alive_max_count > 1) { auto count = keep_alive_max_count; while (count > 0 && (is_client_request || select_read(sock, CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND, CPPHTTPLIB_KEEPALIVE_TIMEOUT_USECOND) > 0)) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec); auto last_connection = count == 1; auto connection_close = false; ret = callback(strm, last_connection, connection_close); if (!ret || connection_close) { break; } count--; } } else { // keep_alive_max_count is 0 or 1 SocketStream strm(sock, read_timeout_sec, read_timeout_usec); auto dummy_connection_close = false; ret = callback(strm, true, dummy_connection_close); } return ret; }
解读:在这里构建了Stream,供回调函数使用。然后发现使用了callback的时候,明明是知道了函数类型的,也就是说它的函数签名是一样的,只有可能是返回值不同?然后虽然keep_alive_max_count 不是1,但是可以理解为1 走下面这个路,也就是说调用了回调。然后回到我们的send 函数 中的handle_request
bool Client::handle_request(Stream &strm, const Request &req, Response &res, bool last_connection, bool &connection_close) { if (req.path.empty()) { return false; } bool ret; if (!is_ssl() && !proxy_host_.empty()) { auto req2 = req; req2.path = "http://" + host_and_port_ + req.path; ret = process_request(strm, req2, res, last_connection, connection_close); } else { ret = process_request(strm, req, res, last_connection, connection_close); } if (!ret) { return false; } if (300 < res.status && res.status < 400 && follow_location_) { ret = redirect(req, res); } return ret; } //这个是重点 bool Client::process_request(Stream &strm, const Request &req, Response &res, bool last_connection, bool &connection_close) { // Send request if (!write_request(strm, req, last_connection)) { return false; } // Receive response and headers if (!read_response_line(strm, res) || !detail::read_headers(strm, res.headers)) { return false; } if (res.get_header_value("Connection") == "close" || res.version == "HTTP/1.0") { connection_close = true; } if (req.response_handler) { if (!req.response_handler(res)) { return false; } } // Body if (req.method != "HEAD" && req.method != "CONNECT") { ContentReceiver out = [&](const char *buf, size_t n) { if (res.body.size() + n > res.body.max_size()) { return false; } res.body.append(buf, n); return true; }; if (req.content_receiver) { out = [&](const char *buf, size_t n) { return req.content_receiver(buf, n); }; } int dummy_status; if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(), dummy_status, req.progress, out)) { return false; } } // Log if (logger_) { logger_(req, res); } return true; }
解读: 使用了write_request写socket,然后就是对response 也就是接收到的数据进行处理了,先看写
bool Client::write_request(Stream &strm, const Request &req, bool last_connection) { detail::BufferStream bstrm; // Request line const auto &path = detail::encode_url(req.path); bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str()); // Additonal headers Headers headers; if (last_connection) { headers.emplace("Connection", "close"); } if (!req.has_header("Host")) { if (is_ssl()) { if (port_ == 443) { headers.emplace("Host", host_); } else { headers.emplace("Host", host_and_port_); } } else { if (port_ == 80) { headers.emplace("Host", host_); } else { headers.emplace("Host", host_and_port_); } } } if (!req.has_header("Accept")) { headers.emplace("Accept", "*/*"); } if (!req.has_header("User-Agent")) { headers.emplace("User-Agent", "cpp-httplib/0.5"); } if (req.body.empty()) { if (req.content_provider) { auto length = std::to_string(req.content_length); headers.emplace("Content-Length", length); } else { headers.emplace("Content-Length", "0"); } } else { if (!req.has_header("Content-Type")) { headers.emplace("Content-Type", "text/plain"); } if (!req.has_header("Content-Length")) { auto length = std::to_string(req.body.size()); headers.emplace("Content-Length", length); } } if (!basic_auth_username_.empty() && !basic_auth_password_.empty()) { headers.insert(make_basic_authentication_header( basic_auth_username_, basic_auth_password_, false)); } if (!proxy_basic_auth_username_.empty() && !proxy_basic_auth_password_.empty()) { headers.insert(make_basic_authentication_header( proxy_basic_auth_username_, proxy_basic_auth_password_, true)); } detail::write_headers(bstrm, req, headers); // Flush buffer auto &data = bstrm.get_buffer(); strm.write(data.data(), data.size()); // Body if (req.body.empty()) { if (req.content_provider) { size_t offset = 0; size_t end_offset = req.content_length; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) { auto written_length = strm.write(d, l); offset += static_cast<size_t>(written_length); }; data_sink.is_writable = [&](void) { return strm.is_writable(); }; while (offset < end_offset) { req.content_provider(offset, end_offset - offset, data_sink); } } } else { strm.write(req.body); } return true; }
解读:encode_url 对url中的特殊字符进行替换操作,Buffer实质是个对string的封装,write_format是个变参函数,是父类的模板成员函数,然后调用的也是snprintf这个变参函数(写入一个临时buffer,再调用write 写入成员buffer形成重载)。然后对req header字段一通处理之后,调用了wirte_header,所以它是先写入header 再写入body的。
template <typename T> ssize_t write_headers(Stream &strm, const T &info, const Headers &headers) { ssize_t write_len = 0; for (const auto &x : info.headers) { if (x.first == "EXCEPTION_WHAT") { continue; } auto len = strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); if (len < 0) { return len; } write_len += len; } for (const auto &x : headers) { auto len = strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); if (len < 0) { return len; } write_len += len; } auto len = strm.write("\r\n"); if (len < 0) { return len; } write_len += len; return write_len; }
没错,没啥好看的,就是写键值对然后加个\r\n 如此朴实无华,再把组装好的数据写入socket 接下来看写body部分(反思:我自己封装的话,可以直接起个名字叫做proccess_header 然后 wirte_header write_body 这样岂不是代码更加清晰?代码清晰不清晰我不知道,至少思路会很清晰。就是可能看上去b格不高,属于没必要的封装可能。)
// Body if (req.body.empty()) { if (req.content_provider) { size_t offset = 0; size_t end_offset = req.content_length; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) { auto written_length = strm.write(d, l); offset += static_cast<size_t>(written_length); }; data_sink.is_writable = [&](void) { return strm.is_writable(); }; while (offset < end_offset) { req.content_provider(offset, end_offset - offset, data_sink); } } } else { strm.write(req.body); }
解读:由于我们的入口body是空的,content_povider也是空的,所以呢是压根不写入数据的。后面我会继续沿着含有content_provider 做一个分析。这个入口的写操作到这里就完成了。下来进行接收操作。忘了刚才代码的跳转
if (!read_response_line(strm, res) || !detail::read_headers(strm, res.headers)) { return false; }
用了一个中断,如果连第一个都是false 第二个是不回执行的直接返回false。 也就是一开始会读一行,之后会读整个头,那么它是怎么就刚好只读一行的呢,我们接着看😂
bool Client::read_response_line(Stream &strm, Response &res) { std::array<char, 2048> buf; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); if (!line_reader.getline()) { return false; } const static std::regex re("(HTTP/1\\.[01]) (\\d+?) .*\r\n"); std::cmatch m; if (std::regex_match(line_reader.ptr(), m, re)) { res.version = std::string(m[1]); res.status = std::stoi(std::string(m[2])); } return true; }
将读取到的行进行正则,分离出http 版本和statu
关键在于一个stream_line_reader的类,它的关键函数又是getline这个函数。
bool getline() { fixed_buffer_used_size_ = 0; glowable_buffer_.clear(); for (size_t i = 0;; i++) { char byte; auto n = strm_.read(&byte, 1); if (n < 0) { return false; } else if (n == 0) { if (i == 0) { return false; } else { break; } } append(byte); if (byte == '\n') { break; } } return true; }
原来它是一个字节一个自己的读,读到\n 就不读了,奇怪了,不应该是\r\n吗。也对,\r\n是两个字符,终究还是\n结尾。那么read_header 推理应该是读到空行结尾咯
bool read_headers(Stream &strm, Headers &headers) { const auto bufsiz = 2048; char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); for (;;) { if (!line_reader.getline()) { return false; } // Check if the line ends with CRLF. if (line_reader.end_with_crlf()) { // Blank line indicates end of headers. if (line_reader.size() == 2) { break; } } else { continue; // Skip invalid line. } // Skip trailing spaces and tabs. auto end = line_reader.ptr() + line_reader.size() - 2; while (line_reader.ptr() < end && (end[-1] == ' ' || end[-1] == '\t')) { end--; } // Horizontal tab and ' ' are considered whitespace and are ignored when on // the left or right side of the header value: // - https://stackoverflow.com/questions/50179659/ // - https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html static const std::regex re(R"(([^:]+):[\t ]*(.+))"); std::cmatch m; if (std::regex_match(line_reader.ptr(), end, m, re)) { auto key = std::string(m[1]); auto val = std::string(m[2]); headers.emplace(key, val); } } return true; }
是的,读取到空行的标准就是它只有\r\n ,然后这个end操作就很有意思了,居然可以使用负数下标往回跑……不过想想也是只是自己没有用过而已。去除掉非空字符,然后用正则表达式取出key和value。这个正则的话使用的R string 里面第一个括号是固定的,第二个括号里面的1 其中的^是非的意思,也就是取到非冒号的部分,然后冒号和空白字符。然后就是之后的任意部分。这个connct 字段。然后就回到了读取body的部分,这里构造了一个content_reciver
ContentReceiver out = [&](const char *buf, size_t n) { if (res.body.size() + n > res.body.max_size()) { return false; } res.body.append(buf, n); return true;
因为是默认的嘛,就判断了一下能不能塞得下,塞得下就塞到response 的body里去。初始化的时候res.body.size 是0
if (!detail::read_content(strm, res, (std::numeric_limits<size_t>::max)(), dummy_status, req.progress, out)) { return false; }
使用out函数
template <typename T> bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, Progress progress, ContentReceiver receiver) { ContentReceiver out = [&](const char *buf, size_t n) { return receiver(buf, n); }; if (x.get_header_value("Content-Encoding") == "gzip") { status = 415; return false; } auto ret = true; auto exceed_payload_max_length = false; if (is_chunked_transfer_encoding(x.headers)) { ret = read_content_chunked(strm, out); } else if (!has_header(x.headers, "Content-Length")) { ret = read_content_without_length(strm, out); } else { auto len = get_header_value_uint64(x.headers, "Content-Length", 0); if (len > payload_max_length) { exceed_payload_max_length = true; skip_content_with_length(strm, len); ret = false; } else if (len > 0) { ret = read_content_with_length(strm, len, progress, out); } } if (!ret) { status = exceed_payload_max_length ? 413 : 400; } return ret; }
然后会进入一个read_content_with_length,这里需要注意的是out 使用的就是外层的recvicer,感觉挺多此一举的。
bool read_content_with_length(Stream &strm, uint64_t len, Progress progress, ContentReceiver out) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; uint64_t r = 0; while (r < len) { auto read_len = static_cast<size_t>(len - r); auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); if (n <= 0) { return false; } if (!out(buf, static_cast<size_t>(n))) { return false; } r += static_cast<uint64_t>(n); if (progress) { if (!progress(r, len)) { return false; } } } return true; }
嗯,终于结束了,就是它了,先读到一个buffer里面,再写入out里面也就是contentRecevier里面,循环写知道写完。然后它就被放到里面去了response的body里去了,就去处理吧。
第一篇就这样吧