4

第一篇博客,记录下最近在看的一个开源库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里去了,就去处理吧。
第一篇就这样吧


  1. :

程某有一计
28 声望7 粉丝

本科机械,半路转行C++,在掉头发的边缘疯狂试探。