第一篇博客,记录下最近在看的一个开源库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里去了,就去处理吧。
第一篇就这样吧
- : ↩
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。